uppdate:页面

This commit is contained in:
ltx 2026-02-12 21:38:29 +08:00
parent 6af6191d1c
commit 75c7c83876
22 changed files with 6644 additions and 9 deletions

View File

@ -0,0 +1,6 @@
{
"ExpandedNodes": [
""
],
"PreviewInSolutionExplorer": false
}

BIN
.vs/slnx.sqlite Normal file

Binary file not shown.

View File

@ -21,6 +21,7 @@
"echarts": "^5.4.2",
"echarts-gl": "^2.0.9",
"echarts-wordcloud": "^2.1.0",
"element-china-area-data": "^6.1.0",
"element-plus": "^2.3.3",
"js-cookie": "^3.0.5",
"js-table2excel": "^1.0.3",

View File

@ -142,7 +142,7 @@ const props = withDefaults(defineProps<ProTableProps>(), {
requestAuto: true,
pagination: true,
initParam: {},
border: true,
border: false,
toolButton: true,
rowKey: 'id',
searchCol: () => ({ xs: 1, sm: 2, md: 2, lg: 3, xl: 4 }),

View File

@ -62,8 +62,126 @@ export const dynamicRoutes: Array<RouteRecordRaw> = [
icon: 'iconfont icon-shouye',
},
},
{
path: '/match',
name: '/match',
component: () => import('/@/layout/index.vue'),
redirect: '/',
meta: {
isKeepAlive: true,
},
children: [
{
path: '/match/manage',
name: 'manage',
component: () => import('/@/views/match/manage/index.vue'),
meta: {
title: '比赛管理',
isLink: '',
isHide: false,
isKeepAlive: true,
isAffix: false,
isIframe: false,
roles: ['admin', 'common'],
icon: 'iconfont icon-shouye',
},
},
{
path: '/match/project',
name: 'project',
component: () => import('/@/views/match/project/index.vue'),
meta: {
title: '项目管理',
isLink: '',
isHide: false,
isKeepAlive: true,
isAffix: false,
isIframe: false,
roles: ['admin', 'common'],
icon: 'iconfont icon-shouye',
},
},
{
path: '/match/shop',
name: 'shop',
component: () => import('/@/views/match/shop/index.vue'),
meta: {
title: '商户管理',
isLink: '',
isHide: false,
isKeepAlive: true,
isAffix: false,
isIframe: false,
roles: ['admin', 'common'],
icon: 'iconfont icon-shouye',
},
},
{
path: '/match/order',
name: 'order',
component: () => import('/@/views/match/order/index.vue'),
meta: {
title: '订单管理',
isLink: '',
isHide: false,
isKeepAlive: true,
isAffix: false,
isIframe: false,
roles: ['admin', 'common'],
icon: 'iconfont icon-shouye',
},
},
{
path: '/match/user',
name: 'user',
component: () => import('/@/views/match/user/index.vue'),
meta: {
title: '用户管理',
isLink: '',
isHide: false,
isKeepAlive: true,
isAffix: false,
isIframe: false,
roles: ['admin', 'common'],
icon: 'iconfont icon-shouye',
},
},
{
path: '/match/book',
name: 'book',
component: () => import('/@/views/match/book/index.vue'),
meta: {
title: '证书管理',
isLink: '',
isHide: false,
isKeepAlive: true,
isAffix: false,
isIframe: false,
roles: ['admin', 'common'],
icon: 'iconfont icon-shouye',
},
},
{
path: '/match/coupon',
name: 'coupon',
component: () => import('/@/views/match/coupon/index.vue'),
meta: {
title: '优惠券管理',
isLink: '',
isHide: false,
isKeepAlive: true,
isAffix: false,
isIframe: false,
roles: ['admin', 'common'],
icon: 'iconfont icon-shouye',
},
},
],
}
],
},
];
/**

View File

@ -24,7 +24,7 @@
--next-color-seting-header: #b3c0d1;
--el-text-color-regular: rgba(0, 0, 0, 0.99) !important;
// --el-text-color-primary: #409eff;
--el-table-tr-bg-color: #f5f7fa;
--el-table-header-bg-color: #f5f7fa;
}

View File

@ -223,7 +223,7 @@
/* Steps 步骤条
------------------------------- */
.el-step__icon-inner {
font-size: 30px !important;
// font-size: 30px !important;
font-weight: 400 !important;
}
.el-step__title {
@ -245,7 +245,7 @@
margin: 0 auto !important;
position: absolute;
.el-dialog__body {
padding: 20px !important;
// padding: 20px !important;
}
}
}
@ -257,14 +257,14 @@
}
.el-dialog__title{
font-size: 14px;
color: #FFF;
font-size: 18px;
// color: #FFF;
font-weight: 700;
}
.el-dialog__header {
margin-right: 0;
padding-top: 10px;
background: var(--el-color-primary);
// background: var(--el-color-primary);
}
.el-dialog__footer {
border-top: 1px solid var(--el-color-info-light-7);
@ -274,7 +274,7 @@
height: 32px;
font-weight: 900;
.el-dialog__close{
color: #FFF;
color: #000;
}
}
.el-dialog__headerbtn:hover .el-dialog__close{

View File

@ -0,0 +1,241 @@
<template>
<div class="system-menu-container layout-padding">
<ProTable ref="tableRef" :request-api="SysMenuApi.page" :columns="columns" :pagination="false"
:tool-button="false">
<template #tools>
<el-button v-auth="'sysmenu:add'" type="primary" icon="ele-Plus" @click="onOpenMenu"> 新增 </el-button>
</template>
<template #index="scope">
{{ scope.$index + 1 }}
</template>
<template #name="scope">
<span class="ml10">{{ $t(scope.row.name) }}</span>
</template>
<template #img="scope">
<img :src="scope.row.img" class="img" />
</template>
<template #status="scope">
<el-tag :type="scope.row.status === 0 ? 'success' : 'danger'"> {{ scope.row.status === 0 ? '启用' : '禁用'
}}</el-tag>
</template>
<template #action="scope">
<el-button v-auth="'sysmenu:edit'" icon="ele-Edit" size="small" text type="primary"
@click="onOpenMenu(scope.row.id)"> 编辑 </el-button>
<el-popconfirm title="确认删除吗?" @confirm="onDeleteMenu(scope.row.id)">
<template #reference>
<el-button v-auth="'sysmenu:delete'" icon="ele-Delete" size="small" text type="danger"> 删除
</el-button>
</template>
</el-popconfirm>
</template>
</ProTable>
<!-- 弹窗 -->
<el-dialog title="新增" v-model="isShowDialog" width="60%">
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="200px"
style="width: 80%;margin-bottom: 30px;max-width: 600px" v-if="isShowDialog">
<!-- 证书名称 -->
<el-form-item label="证书名称" prop="projectName" >
<el-input v-model="formData.projectName" placeholder="请输入项目名称"></el-input>
</el-form-item>
<!-- 证书模板 -->
<el-form-item label="证书模板" prop="fee">
<el-upload class="avatar-uploader" action=""
:show-file-list="false" :on-success="handleAvatarSuccess" :before-upload="beforeAvatarUpload">
<img v-if="formData.fee" :src="formData.fee" class="avatar" />
<el-icon v-else class="avatar-uploader-icon">
<Plus />
</el-icon>
</el-upload>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="onCancel">取消</el-button>
<el-button type="primary" @click="onSubmit">确定</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup name="sysMenu">
import { defineAsyncComponent, ref, reactive } 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';
// 定义表单引用类型
const formRef = ref();
const formData = ref({
projectName: '30秒单摇',
projectType: 'personal_speed',
participantCount: '',
description: '',
fee: '100',
durationMinutes: '',
phone: '30'
})
const formRules = ref({
projectName: [
{ required: true, message: '请输入证书名称', trigger: 'blur' }
],
fee: [
{ required: true, message: '请上传证书模版', trigger: 'blur' }
],
});
const columns = ref([
{
prop: 'index',
label: '序号',
align: 'center',
},
{
prop: 'name',
label: '证书名称',
align: 'center',
search: {
el: 'input',
},
},
{ prop: 'img', label: '证书' },
{ prop: 'action', label: '操作', align: 'center', fixed: 'right', isShow: auths(['sysmenu:delete', 'sysmenu:edit']) },
]);
const isShowDialog = ref(false);
const dialogTitle = ref('新增');
const onOpenMenu = (id) => {
dialogTitle.value = id ? '编辑' : '新增';
formData.value = {
projectName: '',
projectType: '',
participantCount: '',
description: '',
fee: '',
durationMinutes: '',
phone: ''
};
isShowDialog.value = true;
};
//删除菜单
const onDeleteMenu = async (id) => {
const { succeeded, errors } = await SysMenuApi.delete({ id });
if (succeeded) {
ElMessage.success('删除成功');
} else {
ElMessage.error(errors);
}
};
const handleAvatarSuccess = ( response,
uploadFile) => {
formData.value.fee = URL.createObjectURL(uploadFile.raw)
}
const beforeAvatarUpload = () => {
// if (rawFile.type !== 'image/jpeg') {
// ElMessage.error('Avatar picture must be JPG format!')
// return false
// } else if (rawFile.size / 1024 / 1024 > 2) {
// ElMessage.error('Avatar picture size can not exceed 2MB!')
// return false
// }
return true
}
// 表单提交
const onSubmit = () => {
if (!formRef.value) return;
formRef.value.validate(async (valid) => {
if (!valid) return;
// try {
// let res;
// if (formData.value.id) {
// res = await SysMenuApi.add(formData.value);
// } else {
// res = await SysMenuApi.add(formData.value);
// }
// if (res.succeeded) {
// ElMessage.success(dialogTitle.value + '成功');
// isShowDialog.value = false;
// } else {
// ElMessage.error(res.errors);
// }
// } catch (error) {
// console.error(error);
// }
});
};
// 取消
const onCancel = () => {
isShowDialog.value = false;
};
</script>
<style scoped lang="scss">
.dialog-footer {
margin-top: 80px;
}
/* 时长组合框样式 */
.duration-row {
display: flex;
align-items: center;
gap: 8px;
}
.duration-row .el-input {
width: 120px;
}
.duration-separator {
color: #606266;
font-size: 14px;
}
.duration-label {
color: #909399;
font-size: 14px;
}
</style>
<style>
.avatar-uploader .el-upload {
border: 1px dashed var(--el-border-color);
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
transition: var(--el-transition-duration-fast);
}
.avatar-uploader .el-upload:hover {
border-color: var(--el-color-primary);
}
.el-icon.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 178px;
height: 178px;
text-align: center;
}
</style>

View File

@ -0,0 +1,269 @@
<template>
<div class="system-menu-container layout-padding">
<ProTable ref="tableRef" :request-api="SysMenuApi.page" :columns="columns" :pagination="false"
:tool-button="false">
<template #tools>
<el-button v-auth="'sysmenu:add'" type="primary" icon="ele-Plus" @click="onOpenMenu"> 新增 </el-button>
</template>
<template #index="scope">
{{ scope.$index + 1 }}
</template>
<template #name="scope">
<SvgIcon :name="scope.row.icon" />
<span class="ml10">{{ $t(scope.row.name) }}</span>
</template>
<template #type="scope">
<el-tag :type="scope.row.type === 0 ? '' : scope.row.type === 1 ? 'success' : 'danger'">
{{ scope.row.type === 0 ? '目录' : scope.row.type === 1 ? '菜单' : '按钮' }}</el-tag>
</template>
<template #status="scope">
<el-tag :type="scope.row.status === 0 ? 'success' : 'danger'"> {{ scope.row.status === 0 ? '启用' : '禁用'
}}</el-tag>
</template>
<template #action="scope">
<el-button v-auth="'sysmenu:edit'" icon="ele-Edit" size="small" text type="primary"
@click="onOpenMenu(scope.row.id)"> 编辑 </el-button>
<el-popconfirm title="确认删除吗?" @confirm="onDeleteMenu(scope.row.id)">
<template #reference>
<el-button v-auth="'sysmenu:delete'" icon="ele-Delete" size="small" text type="danger"> 删除
</el-button>
</template>
</el-popconfirm>
</template>
</ProTable>
<!-- 弹窗 -->
<el-dialog title="新增" v-model="isShowDialog">
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="150px"
style="max-width: 600px;margin-bottom: 30px;" v-if="isShowDialog">
<!-- 商户名称 -->
<el-form-item label="优惠券名称" prop="projectName">
<el-input v-model="formData.projectName" placeholder="请输入项目名称"></el-input>
</el-form-item>
<!-- 优惠券类型 -->
<el-form-item label="优惠券类型" prop="projectType">
<el-select v-model="formData.projectType" placeholder="请选择优惠券类型">
<el-option label="折扣" value="discount"></el-option>
<el-option label="定价" value="fixed"></el-option>
</el-select>
</el-form-item>
<!-- 填写折扣 -->
<el-form-item label="填写折扣" prop="participantCount" v-if="formData.projectType === 'discount'">
<el-input v-model="formData.participantCount" placeholder="请输入折扣百分比">
<template #append>%</template>
</el-input>
</el-form-item>
<!-- 填写优惠金额 -->
<el-form-item label="填写优惠金额" prop="description" v-if="formData.projectType === 'fixed'">
<el-input v-model="formData.description" placeholder="请输入优惠金额" class="fee-input">
<template #append>元</template>
</el-input>
</el-form-item>
<!-- 可用赛事 -->
<el-form-item label="可用赛事" prop="phone">
<el-input v-model="formData.phone" placeholder="请输入费用" class="fee-input">
<template slot="append">元</template>
</el-input>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="onCancel">取消</el-button>
<el-button type="primary" @click="onSubmit">确定</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup name="sysMenu">
import { defineAsyncComponent, ref, reactive } 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';
// 定义表单引用类型
const formRef = ref();
const formData = ref({
projectName: '30秒单摇',
projectType: 'discount',
participantCount: '',
description: '',
fee: '100',
durationMinutes: '',
phone: '30'
})
const formRules = ref({
projectName: [
{ required: true, message: '请输入优惠券名称', trigger: 'blur' }
],
projectType: [
{ required: true, message: '请选择优惠券类型', trigger: 'change' }
],
participantCount: [
{ required: true, message: '请输入折扣百分比', trigger: 'blur' }
],
description: [
{ required: true, message: '请输入优惠金额', trigger: 'blur' }
],
});
const columns = ref([
{
prop: 'index',
label: '序号',
align: 'center',
},
{
prop: 'name',
label: '优惠券名称',
align: 'center',
search: {
el: 'input',
},
},
{prop: 'type',label: '优惠券类型',search: { el: 'select',},enum: [ { label: '全部', value: 0 }, { label: '折扣', value: 1 }, { label: '定价', value: 2 },]},
{prop: 'money',label: '详情',},
{ prop: 'code', label: '有效期' },
{ prop: 'img', label: '可用赛事' },
{ prop: 'action', label: '操作', align: 'center', fixed: 'right', isShow: auths(['sysmenu:delete', 'sysmenu:edit']) },
]);
const isShowDialog = ref(false);
const dialogTitle = ref('新增');
const onOpenMenu = (id) => {
dialogTitle.value = id ? '编辑' : '新增';
formData.value = {
projectName: '',
projectType: '',
participantCount: '',
description: '',
fee: '',
durationMinutes: '',
phone: ''
};
isShowDialog.value = true;
};
//删除菜单
const onDeleteMenu = async (id) => {
const { succeeded, errors } = await SysMenuApi.delete({ id });
if (succeeded) {
ElMessage.success('删除成功');
} else {
ElMessage.error(errors);
}
};
const handleAvatarSuccess = ( response,
uploadFile) => {
formData.value.fee = URL.createObjectURL(uploadFile.raw)
}
const beforeAvatarUpload = () => {
// if (rawFile.type !== 'image/jpeg') {
// ElMessage.error('Avatar picture must be JPG format!')
// return false
// } else if (rawFile.size / 1024 / 1024 > 2) {
// ElMessage.error('Avatar picture size can not exceed 2MB!')
// return false
// }
return true
}
// 表单提交
const onSubmit = () => {
if (!formRef.value) return;
formRef.value.validate(async (valid) => {
if (!valid) return;
// try {
// let res;
// if (formData.value.id) {
// res = await SysMenuApi.add(formData.value);
// } else {
// res = await SysMenuApi.add(formData.value);
// }
// if (res.succeeded) {
// ElMessage.success(dialogTitle.value + '成功');
// isShowDialog.value = false;
// } else {
// ElMessage.error(res.errors);
// }
// } catch (error) {
// console.error(error);
// }
});
};
// 取消
const onCancel = () => {
isShowDialog.value = false;
};
</script>
<style scoped lang="scss">
.dialog-footer {
margin-top: 80px;
}
/* 时长组合框样式 */
.duration-row {
display: flex;
align-items: center;
gap: 8px;
}
.duration-row .el-input {
width: 120px;
}
.duration-separator {
color: #606266;
font-size: 14px;
}
.duration-label {
color: #909399;
font-size: 14px;
}
</style>
<style>
.avatar-uploader .el-upload {
border: 1px dashed var(--el-border-color);
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
transition: var(--el-transition-duration-fast);
}
.avatar-uploader .el-upload:hover {
border-color: var(--el-color-primary);
}
.el-icon.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 178px;
height: 178px;
text-align: center;
}
</style>

View File

@ -0,0 +1,284 @@
<template>
<el-form ref="formRef" :model="localFormData" :rules="rules" label-position="right" class="step-form" style="width: 80%;margin: 0 auto;">
<!-- 赛事名称 -->
<el-form-item label="赛事名称" prop="projectName" >
<el-input v-model="localFormData.projectName" placeholder="请输入赛事名称" maxlength="100" show-word-limit>
<template #prefix>
<el-icon>
<Document />
</el-icon>
</template>
</el-input>
</el-form-item>
<!-- 封面 -->
<el-form-item label="封面" prop="projectName">
<el-upload class="avatar-uploader" action=""
:show-file-list="false" :on-success="handleAvatarSuccess" :before-upload="beforeAvatarUpload">
<img v-if="imageUrl" :src="imageUrl" class="avatar" />
<el-icon v-else class="avatar-uploader-icon">
<Plus />
</el-icon>
</el-upload>
</el-form-item>
<!-- 赛事类型 -->
<el-form-item label="赛事类型" prop="declaringUnit">
<el-select v-model="localFormData.declaringUnit" placeholder="请选择赛事类型">
<el-option label="个人" value="0" />
<el-option label="团体" value="1" />
</el-select>
</el-form-item>
<!-- 报名方式 -->
<el-form-item label="报名方式" prop="registrationMethod">
<el-radio-group v-model="localFormData.registrationMethod">
<el-radio value="1" >只允许个人报名</el-radio>
<el-radio value="2" >只允许团体报名</el-radio>
<el-radio value="3" >两者都有</el-radio>
</el-radio-group>
</el-form-item>
<!-- 积分发放规则 -->
<el-form-item label="积分发放规则" prop="pointsRules">
<el-select v-model="localFormData.pointsRules" placeholder="请选择积分发放规则">
<el-option label="一" value="0" />
<el-option label="二" value="1" />
</el-select>
</el-form-item>
<!-- 报名起止时间 -->
<el-form-item label="报名起止时间" prop="registrationTime">
<el-date-picker v-model="localFormData.registrationTime" type="datetimerange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期">
</el-date-picker>
</el-form-item>
<!-- 是否上传素材 -->
<el-form-item label="是否上传素材" prop="uploadMaterial">
<el-select v-model="localFormData.uploadMaterial" placeholder="请选择是否上传素材">
<el-option label="是" value="1" />
<el-option label="否" value="0" />
</el-select>
</el-form-item>
<!-- 上传素材起止时间 -->
<el-form-item label="上传素材起止时间" prop="uploadMaterialTime">
<el-date-picker v-model="localFormData.uploadMaterialTime" type="datetimerange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期">
</el-date-picker>
</el-form-item>
<!-- 比赛起止时间 -->
<el-form-item label="比赛起止时间" prop="matchTime">
<el-date-picker v-model="localFormData.matchTime" type="datetimerange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期">
</el-date-picker>
</el-form-item>
<!-- 赛事地址 -->
<el-form-item label="赛事地址" prop="matchAddress">
<el-cascader
v-model="localFormData.matchAddress"
:options="regionData"
placeholder="请选择赛事地址"
@change="handleChange"
clearable
:props="{ checkStrictly: true }"
/>
</el-form-item>
<!-- 详细地址 -->
<el-form-item label="详细地址" prop="detailAddress">
<el-input v-model="localFormData.detailAddress" placeholder="请输入详细地址" maxlength="50">
<template #prefix>
<el-icon>
<OfficeBuilding />
</el-icon>
</template>
</el-input>
</el-form-item>
<!-- 赛事简介 -->
<el-form-item label="赛事简介" prop="projectDescription">
<el-input v-model="localFormData.projectDescription" placeholder="请输入赛事简介" type="textarea" maxlength="200">
<template #prefix>
<el-icon>
<OfficeBuilding />
</el-icon>
</template>
</el-input>
</el-form-item>
</el-form>
</template>
<script setup>
import { ref, reactive, watch } from 'vue'
import { Document, OfficeBuilding, User, Phone, Message , Plus} from '@element-plus/icons-vue'
import { validatePhone } from './validators'
import { regionData } from 'element-china-area-data';
// 定义 props
const props = defineProps({
modelValue: {
type: Object,
required: true
}
})
// 定义 emits
const emit = defineEmits(['update:modelValue'])
// 表单引用
const formRef = ref(null)
// 本地表单数据副本
const localFormData = ref({
projectName: '', // 项目名称
declaringUnit: '', // 赛事类型
registrationMethod: '', // 报名方式
pointsRules: '', // 积分发放规则
registrationTime: [], // 报名起止时间
uploadMaterial: '', // 是否上传素材
uploadMaterialTime: [], // 上传素材起止时间
matchTime: [], // 比赛起止时间
matchAddress: '', // 赛事地址
detailAddress: '', //详细地址
projectDescription: '' // 赛事简介
})
// 初始化本地数据
Object.keys(localFormData.value).forEach(key => {
console.log(props.modelValue);
if(!props.modelValue) return
localFormData.value[key] = props.modelValue[key] || ''
})
const handleChange = (value) => {
console.log(value); // 处理选择的地址
}
// 监听本地数据变化,同步到父组件
watch(localFormData, (newVal) => {
emit('update:modelValue', { ...props.modelValue, ...newVal })
}, { deep: true })
// 监听父组件数据变化,同步到本地
watch(() => props.modelValue, (newVal) => {
Object.keys(localFormData).forEach(key => {
localFormData[key] = newVal[key] || ''
})
}, { deep: true })
// 表单验证规则
const rules = ref({
projectName: [
{ required: true, message: '请输入项目名称', trigger: 'blur' },
{ min: 2, max: 100, message: '项目名称长度为2-100个字符', trigger: 'blur' }
],
declaringUnit: [
{ required: true, message: '请选择赛事类型', trigger: 'change' },
],
registrationMethod: [
{ required: true, message: '请选择报名方式', trigger: 'change' }
],
pointsRules: [
{ required: true, message: '请选择积分发放规则', trigger: 'change' }
],
registrationTime: [
{ type: 'array', required: true, message: '请选择报名起止时间', trigger: 'change' }
],
uploadMaterial: [
{ required: true, message: '请选择是否上传素材', trigger: 'change' }
],
uploadMaterialTime: [
{ type: 'array', required: true, message: '请选择上传素材起止时间', trigger: 'change' }
],
matchTime: [
{ type: 'array', required: true, message: '请选择比赛起止时间', trigger: 'change' }
],
matchAddress: [
{ type: 'array', required: true, message: '请选择赛事地址', trigger: 'change' }
],
projectDescription: [
{ required: true, message: '请输入赛事简介', trigger: 'blur' },
{ min: 10, max: 200, message: '赛事简介长度为10-200个字符', trigger: 'blur' }
]
})
const imageUrl = ref('')
const handleAvatarSuccess = (
response,
uploadFile
) => {
imageUrl.value = URL.createObjectURL(uploadFile.raw)
}
const beforeAvatarUpload = (rawFile) => {
// if (rawFile.type !== 'image/jpeg') {
// ElMessage.error('Avatar picture must be JPG format!')
// return false
// } else if (rawFile.size / 1024 / 1024 > 2) {
// ElMessage.error('Avatar picture size can not exceed 2MB!')
// return false
// }
return true
}
// 暴露验证方法给父组件
const validate = async () => {
if (!formRef.value) return false
return await formRef.value.validate()
}
defineExpose({
validate
})
</script>
<style scoped>
.step-form {
padding: 0 20px;
}
.step-form .el-form-item {
margin-bottom: 24px;
}
.step-form .el-input,
.step-form .el-select {
width: 100%;
}
.form-tips {
margin-top: 20px;
}
.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;
cursor: pointer;
position: relative;
overflow: hidden;
transition: var(--el-transition-duration-fast);
}
.avatar-uploader .el-upload:hover {
border-color: var(--el-color-primary);
}
.el-icon.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 178px;
height: 178px;
text-align: center;
}
</style>

View File

@ -0,0 +1,369 @@
<template>
<div class="step-units">
<el-form
ref="formRef"
:model="localFormData"
:rules="rules"
label-position="right"
>
<!-- 参与单位列表 -->
<div class="section-header">
<h3>参与单位列表</h3>
<p class="description">添加本项目涉及的参与单位信息</p>
</div>
<el-table
:data="localFormData.participatingUnits"
style="width: 100%"
border
stripe
class="units-table"
>
<el-table-column prop="index" label="序号" width="80" align="center">
<template #default="{ $index }">
{{ $index + 1 }}
</template>
</el-table-column>
<el-table-column label="单位Logo" width="120" align="center">
<template #default="{ row, $index }">
<el-upload
class="logo-uploader"
:show-file-list="false"
:before-upload="(file) => handleLogoUpload(file, $index)"
accept="image/*"
>
<div v-if="row.logoUrl" class="logo-preview">
<img :src="row.logoUrl" class="logo-image" />
<div class="logo-overlay">
<el-icon><Camera /></el-icon>
<span>更换</span>
</div>
</div>
<div v-else class="logo-placeholder">
<el-icon><Plus /></el-icon>
<span>上传Logo</span>
</div>
</el-upload>
</template>
</el-table-column>
<el-table-column label="单位名称" min-width="200" align="center">
<template #default="{ row, $index }">
<el-form-item
:prop="`participatingUnits.${$index}.unitName`"
:rules="unitNameRules"
style="margin-bottom: 0"
>
<el-input
v-model="row.unitName"
placeholder="请输入单位名称"
maxlength="100"
/>
</el-form-item>
</template>
</el-table-column>
<el-table-column label="单位角色" min-width="150" align="center">
<template #default="{ row, $index }">
<el-form-item
:prop="`participatingUnits.${$index}.role`"
:rules="roleRules"
style="margin-bottom: 0"
>
<el-select v-model="row.role" placeholder="请选择角色">
<el-option label="主办单位" value="host" />
<el-option label="协办单位" value="coorganizer" />
<el-option label="支持单位" value="supporter" />
<el-option label="合作单位" value="partner" />
<el-option label="赞助单位" value="sponsor" />
</el-select>
</el-form-item>
</template>
</el-table-column>
<el-table-column label="操作" width="120" align="center">
<template #default="{ $index }">
<div class="action-buttons">
<el-button
type="danger"
size="small"
@click="handleDeleteUnit($index)"
:disabled="localFormData.participatingUnits.length <= 1"
>
<el-icon><Delete /></el-icon>
</el-button>
<el-tooltip content="删除该单位" placement="top">
<span :class="{ 'disabled-tip': localFormData.participatingUnits.length > 1 }"></span>
</el-tooltip>
</div>
</template>
</el-table-column>
</el-table>
<!-- 添加单位按钮 -->
<div class="add-unit-action">
<el-button type="primary" @click="handleAddUnit" plain>
<el-icon><Plus /></el-icon>
添加单位
</el-button>
</div>
<!-- 验证提示 -->
<el-alert
v-if="localFormData.participatingUnits.length === 0"
title="提示"
type="warning"
:closable="false"
class="validation-alert"
>
请至少添加一个参与单位
</el-alert>
</el-form>
</div>
</template>
<script setup>
import { ref, reactive, watch } from 'vue'
import { Delete, Plus, Camera } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
// 定义 props
const props = defineProps({
modelValue: {
type: Object,
required: true
}
})
// 定义 emits
const emit = defineEmits(['update:modelValue'])
// 表单引用
const formRef = ref(null)
// 本地表单数据副本
const localFormData = reactive({
participatingUnits: []
})
// 初始化本地数据
// localFormData.participatingUnits = JSON.parse(JSON.stringify(props.modelValue.participatingUnits || []))
// 监听本地数据变化,同步到父组件
watch(localFormData, (newVal) => {
emit('update:modelValue', newVal)
}, { deep: true })
// 监听父组件数据变化,同步到本地
watch(() => props.modelValue, (newVal) => {
if (newVal && newVal.participatingUnits) {
localFormData.participatingUnits = JSON.parse(JSON.stringify(newVal.participatingUnits))
}
}, { deep: true })
// 单位名称验证规则
const unitNameRules = [
{ required: true, message: '请输入单位名称', trigger: 'blur' },
{ min: 2, max: 100, message: '单位名称长度为2-100个字符', trigger: 'blur' }
]
// 角色验证规则
const roleRules = [
{ required: true, message: '请选择单位角色', trigger: 'change' }
]
// 表单验证规则
const rules = reactive({
participatingUnits: [
{
type: 'array',
required: true,
message: '请至少添加一个参与单位',
trigger: 'change',
validator: (rule, value, callback) => {
if (!value || value.length === 0) {
callback(new Error('请至少添加一个参与单位'))
} else {
// 检查是否所有行都填写完整
const incomplete = value.some(unit => !unit.unitName || !unit.role)
if (incomplete) {
callback(new Error('请完善所有单位信息'))
} else {
callback()
}
}
}
}
]
})
// 处理 Logo 上传
const handleLogoUpload = (file, index) => {
const isImage = file.type.startsWith('image/')
const isLt2M = file.size / 1024 / 1024 < 2
if (!isImage) {
ElMessage.error('只能上传图片文件!')
return false
}
if (!isLt2M) {
ElMessage.error('图片大小不能超过 2MB!')
return false
}
// 使用 FileReader 读取文件并转换为 base64
const reader = new FileReader()
reader.onload = (e) => {
const base64Url = e.target.result
localFormData.participatingUnits[index].logoUrl = base64Url
ElMessage.success('Logo 上传成功!')
}
reader.readAsDataURL(file)
return false // 阻止默认上传行为
}
// 添加单位
const handleAddUnit = () => {
localFormData.participatingUnits.push({
unitName: '',
role: '',
logoUrl: ''
})
}
// 删除单位
const handleDeleteUnit = (index) => {
localFormData.participatingUnits.splice(index, 1)
}
// 暴露验证方法给父组件
const validate = async () => {
if (!formRef.value) return false
try {
await formRef.value.validate()
return true
} catch (error) {
return false
}
}
defineExpose({
validate
})
</script>
<style scoped>
.step-units {
padding: 0 20px;
}
.section-header {
margin-bottom: 20px;
}
.section-header h3 {
font-size: 18px;
color: #303133;
margin-bottom: 8px;
}
.section-header .description {
font-size: 14px;
color: #909399;
}
.units-table {
margin-bottom: 20px;
}
/* Logo 上传样式 */
.logo-uploader {
display: inline-block;
}
.logo-uploader .el-upload {
border: 1px dashed #d9d9d9;
border-radius: 8px;
cursor: pointer;
position: relative;
overflow: hidden;
transition: all 0.3s;
}
.logo-uploader .el-upload:hover {
border-color: #409eff;
}
.logo-placeholder {
width: 80px;
height: 80px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: #8c939d;
font-size: 12px;
}
.logo-placeholder .el-icon {
font-size: 24px;
margin-bottom: 4px;
}
.logo-preview {
width: 80px;
height: 80px;
position: relative;
}
.logo-image {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 8px;
}
.logo-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: none;
flex-direction: column;
align-items: center;
justify-content: center;
color: #fff;
font-size: 12px;
border-radius: 8px;
}
.logo-preview:hover .logo-overlay {
display: flex;
}
.logo-overlay .el-icon {
font-size: 20px;
margin-bottom: 4px;
}
.action-buttons {
display: flex;
gap: 8px;
justify-content: center;
}
.add-unit-action {
display: flex;
justify-content: center;
margin-top: 20px;
}
.validation-alert {
margin-top: 20px;
}
</style>

View File

@ -0,0 +1,250 @@
<template>
<el-form
ref="formRef"
:model="localFormData"
:rules="rules"
label-position="top"
class="step-config"
>
<el-row :gutter="20">
<!-- 项目类型 -->
<el-col :span="24" :md="12">
<el-form-item label="项目类型" prop="projectType">
<el-select
v-model="localFormData.projectType"
placeholder="请选择项目类型"
style="width: 100%"
>
<el-option label="科研类项目" value="research">
<el-icon><Search /></el-icon>
科研类项目
</el-option>
<el-option label="开发类项目" value="development">
<el-icon><Edit /></el-icon>
开发类项目
</el-option>
<el-option label="产业化类项目" value="production">
<el-icon><Goods /></el-icon>
产业化类项目
</el-option>
<el-option label="创新创业类项目" value="innovation">
<el-icon><Aim /></el-icon>
创新创业类项目
</el-option>
</el-select>
</el-form-item>
</el-col>
<!-- 限报数量 -->
<el-col :span="24" :md="12">
<el-form-item label="限报数量" prop="maxDeclarations">
<el-input-number
v-model="localFormData.maxDeclarations"
:min="1"
:max="100"
:step="1"
style="width: 100%"
/>
<div class="input-hint">单个项目最多可申报 {{ localFormData.maxDeclarations }} 项</div>
</el-form-item>
</el-col>
<!-- 是否公开 -->
<el-col :span="24" :md="12">
<el-form-item label="是否公开" prop="isPublic">
<el-switch
v-model="localFormData.isPublic"
active-text="公开"
inactive-text="不公开"
active-value="true"
inactive-value="false"
/>
<div class="switch-hint">
{{ localFormData.isPublic ? '项目信息将对所有人可见' : '项目信息仅对参与单位可见' }}
</div>
</el-form-item>
</el-col>
<!-- 申报截止日期 -->
<el-col :span="24" :md="12">
<el-form-item label="申报截止日期" prop="deadline">
<el-date-picker
v-model="localFormData.deadline"
type="date"
placeholder="请选择申报截止日期"
style="width: 100%"
:disabled-date="disabledDate"
format="YYYY年MM月DD日"
value-format="YYYY-MM-DD"
/>
</el-form-item>
</el-col>
<!-- 项目类型说明 -->
<el-col :span="24">
<el-card class="type-info-card" shadow="hover">
<template #header>
<div class="card-header">
<el-icon><InfoFilled /></el-icon>
<span>项目类型说明</span>
</div>
</template>
<div class="type-list">
<div class="type-item">
<span class="type-name">科研类项目</span>
<span class="type-desc">以科学研究为主的创新性研究项目</span>
</div>
<div class="type-item">
<span class="type-name">开发类项目</span>
<span class="type-desc">以技术研发和产品开发为主的项目</span>
</div>
<div class="type-item">
<span class="type-name">产业化类项目</span>
<span class="type-desc">以成果转化和产业推广为主的项目</span>
</div>
<div class="type-item">
<span class="type-name">创新创业类项目</span>
<span class="type-desc">鼓励创新创业的孵化型项目</span>
</div>
</div>
</el-card>
</el-col>
</el-row>
</el-form>
</template>
<script setup>
import { ref, reactive, watch } from 'vue'
import { Search, Edit, Goods, Aim, InfoFilled } from '@element-plus/icons-vue'
// 定义 props
const props = defineProps({
modelValue: {
type: Object,
required: true
}
})
// 定义 emits
const emit = defineEmits(['update:modelValue'])
// 表单引用
const formRef = ref(null)
// 本地表单数据副本
const localFormData = reactive({
projectType: '',
maxDeclarations: 1,
isPublic: true,
deadline: ''
})
// 初始化本地数据
Object.keys(localFormData).forEach(key => {
localFormData[key] = props.modelValue[key] !== undefined ? props.modelValue[key] : localFormData[key]
})
// 监听本地数据变化,同步到父组件
watch(localFormData, (newVal) => {
emit('update:modelValue', { ...props.modelValue, ...newVal })
}, { deep: true })
// 监听父组件数据变化,同步到本地
watch(() => props.modelValue, (newVal) => {
Object.keys(localFormData).forEach(key => {
if (newVal[key] !== undefined) {
localFormData[key] = newVal[key]
}
})
}, { deep: true })
// 禁用过去的日期
const disabledDate = (time) => {
return time.getTime() < Date.now() - 8.64e7
}
// 表单验证规则
const rules = reactive({
projectType: [
{ required: true, message: '请选择项目类型', trigger: 'change' }
],
maxDeclarations: [
{ required: true, message: '请设置限报数量', trigger: 'blur' },
{ type: 'number', min: 1, max: 100, message: '限报数量必须在1-100之间', trigger: 'blur' }
],
deadline: [
{ required: true, message: '请选择申报截止日期', trigger: 'change' }
]
})
// 暴露验证方法给父组件
const validate = async () => {
if (!formRef.value) return false
return await formRef.value.validate()
}
defineExpose({
validate
})
</script>
<style scoped>
.step-config {
padding: 0 20px;
}
.step-config .el-form-item {
margin-bottom: 24px;
}
.input-hint {
font-size: 12px;
color: #909399;
margin-top: 5px;
}
.switch-hint {
font-size: 12px;
color: #909399;
margin-top: 5px;
}
.type-info-card {
margin-top: 20px;
background: #f5f7fa;
}
.card-header {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
}
.type-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.type-item {
display: flex;
align-items: center;
gap: 15px;
padding: 10px 15px;
background: #fff;
border-radius: 6px;
border: 1px solid #e4e7ed;
}
.type-name {
font-weight: 600;
color: #409eff;
min-width: 120px;
}
.type-desc {
color: #606266;
font-size: 14px;
}
</style>

View File

@ -0,0 +1,282 @@
<template>
<el-form ref="formRef" :model="localFormData" :rules="rules" label-position="right" class="step-fee" >
<!-- 费用类型选择 -->
<el-form-item label="费用类型" prop="feeType">
<el-radio-group v-model="localFormData.feeType">
<el-radio border label="0">免费</el-radio border>
<el-radio border label="1">定价(赛事)</el-radio border>
<el-radio border label="2">单价(赛事)</el-radio border>
<el-radio border label="3">单价(项目)</el-radio border>
</el-radio-group>
</el-form-item>
<el-form-item label="价格" prop="price" v-if="localFormData.feeType == 3">
<el-input v-model="localFormData.price" placeholder="请输入项目一价格" maxlength="50" style="margin-bottom: 10px;">
<template #prefix>
<el-icon>
<Money />
</el-icon>
</template>
</el-input>
<el-input v-model="localFormData.price" placeholder="请输入项目二价格" maxlength="50">
<template #prefix>
<el-icon>
<Money />
</el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item label="价格" prop="price" v-if="localFormData.feeType == 2 || localFormData.feeType == 1">
<el-input v-model="localFormData.price" placeholder="请输入价格" maxlength="50">
<template #prefix>
<el-icon>
<Money />
</el-icon>
</template>
</el-input>
</el-form-item>
<el-divider content-position="center">
<el-icon>
<CreditCard />
</el-icon>
账户信息
</el-divider>
<!-- 账户信息表单 -->
<el-row :gutter="20">
<!-- 开户银行 -->
<el-col :span="24" :md="12">
<el-form-item label="开户银行" prop="bankName">
<el-select v-model="localFormData.bankName" placeholder="请选择开户银行" style="width: 100%" filterable>
<el-option label="中国工商银行" value="ICBC" />
<el-option label="中国建设银行" value="CCB" />
<el-option label="中国农业银行" value="ABC" />
<el-option label="中国银行" value="BOC" />
<el-option label="交通银行" value="BCOM" />
<el-option label="招商银行" value="CMB" />
<el-option label="中国邮政储蓄银行" value="PSBC" />
<el-option label="中信银行" value="CCIT" />
<el-option label="华夏银行" value="HXBank" />
<el-option label="民生银行" value="CMBC" />
</el-select>
</el-form-item>
</el-col>
<!-- 账户名称 -->
<el-col :span="24" :md="12">
<el-form-item label="账户名称" prop="accountName">
<el-input v-model="localFormData.accountName" placeholder="请输入账户名称(与营业执照一致)" maxlength="100">
<template #prefix>
<el-icon>
<OfficeBuilding />
</el-icon>
</template>
</el-input>
</el-form-item>
</el-col>
<!-- 银行账号 -->
<el-col :span="24" :md="12">
<el-form-item label="银行账号" prop="accountNumber">
<el-input v-model="localFormData.accountNumber" placeholder="请输入银行账号" maxlength="30"
@input="handleAccountInput">
<template #prefix>
<el-icon>
<CreditCard />
</el-icon>
</template>
</el-input>
</el-form-item>
</el-col>
<!-- 账号确认 -->
<el-col :span="24" :md="12">
<el-form-item label="确认银行账号" prop="accountNumberConfirm">
<el-input v-model="accountNumberConfirm" placeholder="请再次输入银行账号" maxlength="30" @input="handleConfirmInput">
<template #prefix>
<el-icon>
<CircleCheck />
</el-icon>
</template>
</el-input>
</el-form-item>
</el-col>
</el-row>
</el-form>
</template>
<script setup>
import { ref, reactive, watch, computed } from 'vue'
import { Wallet, Money, CreditCard, OfficeBuilding, CircleCheck, Warning } from '@element-plus/icons-vue'
// 定义 props
const props = defineProps({
modelValue: {
type: Object,
required: true
}
})
// 定义 emits
const emit = defineEmits(['update:modelValue'])
// 表单引用
const formRef = ref(null)
// 确认账号
const accountNumberConfirm = ref('')
// 本地表单数据副本
const localFormData = reactive({
feeType: 'self',
bankName: '',
accountName: '',
accountNumber: ''
})
// 初始化本地数据
Object.keys(localFormData).forEach(key => {
if (props.modelValue) {
localFormData[key] = props.modelValue[key]
}
})
// accountNumberConfirm.value = props.modelValue.accountNumberConfirm || ''
// 监听本地数据变化,同步到父组件
watch(localFormData, (newVal) => {
emit('update:modelValue', { ...props.modelValue, ...newVal })
}, { deep: true })
// 监听父组件数据变化,同步到本地
watch(() => props.modelValue, (newVal) => {
Object.keys(localFormData).forEach(key => {
if (newVal[key] !== undefined) {
localFormData[key] = newVal[key]
}
})
}, { deep: true })
// 监听确认账号变化
watch(accountNumberConfirm, (newVal) => {
emit('update:modelValue', { ...props.modelValue, accountNumberConfirm: newVal })
})
// // 费用类型说明
// const feeTypeDescription = computed(() => {
// return localFormData.feeType === 'self'
// ? '自筹经费:项目费用由申报单位自行承担,无需政府资助。'
// : '政府资助:项目费用由政府财政资助,需按照相关规定使用经费。'
// })
// 处理银行账号输入(只允许数字)
const handleAccountInput = (value) => {
localFormData.accountNumber = value.replace(/\D/g, '')
}
// 处理确认账号输入(只允许数字)
const handleConfirmInput = (value) => {
accountNumberConfirm.value = value.replace(/\D/g, '')
}
// 自定义验证器:账号确认
const validateAccountConfirm = (rule, value, callback) => {
if (value !== localFormData.accountNumber) {
callback(new Error('两次输入的银行账号不一致'))
} else {
callback()
}
}
// 表单验证规则
const rules = reactive({
feeType: [
{ required: true, message: '请选择费用类型', trigger: 'change' }
],
price: [
{ required: true, message: '请输入价格', trigger: 'blur' }
],
bankName: [
{ required: true, message: '请选择开户银行', trigger: 'change' }
],
accountName: [
{ required: true, message: '请输入账户名称', trigger: 'blur' },
{ min: 2, max: 100, message: '账户名称长度为2-100个字符', trigger: 'blur' }
],
accountNumber: [
{ required: true, message: '请输入银行账号', trigger: 'blur' },
{ min: 10, max: 30, message: '银行账号长度不正确', trigger: 'blur' }
],
accountNumberConfirm: [
{ required: true, message: '请确认银行账号', trigger: 'blur' },
{ validator: validateAccountConfirm, trigger: 'blur' }
]
})
// 暴露验证方法给父组件
const validate = async () => {
if (!formRef.value) return false
return await formRef.value.validate()
}
defineExpose({
validate
})
</script>
<style scoped>
.step-fee {
padding: 0 20px;
}
.step-fee .el-form-item {
margin-bottom: 24px;
}
.fee-type-group {
display: flex;
width: 100%;
}
.fee-type-group .el-radio-button {
flex: 1;
}
.fee-type-group .el-radio-button__inner {
width: 100%;
}
.fee-description {
margin-bottom: 25px;
}
.notice-card {
margin-top: 20px;
background: #fff9f0;
border-color: #e6a23c;
}
.notice-card .el-card__header {
background: #fdf6ec;
}
.card-header {
display: flex;
align-items: center;
gap: 8px;
font-weight: 600;
color: #e6a23c;
}
.notice-list {
margin: 0;
padding-left: 20px;
color: #606266;
}
.notice-list li {
margin: 8px 0;
line-height: 1.6;
}
</style>

View File

@ -0,0 +1,139 @@
/**
* 表单验证器工具函数
*/
/**
* 验证中国大陆手机号码
* @param {Object} rule - 验证规则对象
* @param {string} value - 要验证的值
* @param {Function} callback - 回调函数
*/
export const validatePhone = (rule, value, callback) => {
if (!value) {
callback(new Error('请输入联系电话'))
return
}
// 中国大陆手机号码正则表达式
const phoneRegex = /^1[3-9]\d{9}$/
if (!phoneRegex.test(value)) {
callback(new Error('请输入正确的手机号码'))
return
}
callback()
}
/**
* 验证座机号码(带区号)
* @param {Object} rule - 验证规则对象
* @param {string} value - 要验证的值
* @param {Function} callback - 回调函数
*/
export const validateTelephone = (rule, value, callback) => {
if (!value) {
callback(new Error('请输入座机号码'))
return
}
// 座机号码正则表达式(支持带区号和不带区号)
const telephoneRegex = /^(0[0-9]{2,3}-)?[0-9]{7,8}$/
if (!telephoneRegex.test(value)) {
callback(new Error('请输入正确的座机号码(如:010-12345678)'))
return
}
callback()
}
/**
* 验证身份证号码
* @param {Object} rule - 验证规则对象
* @param {string} value - 要验证的值
* @param {Function} callback - 回调函数
*/
export const validateIdCard = (rule, value, callback) => {
if (!value) {
callback(new Error('请输入身份证号码'))
return
}
// 身份证号码正则表达式(18位)
const idCardRegex = /^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[1-2]\d|3[0-1])\d{3}(\d|X|x)$/
if (!idCardRegex.test(value)) {
callback(new Error('请输入正确的身份证号码'))
return
}
callback()
}
/**
* 验证统一社会信用代码
* @param {Object} rule - 验证规则对象
* @param {string} value - 要验证的值
* @param {Function} callback - 回调函数
*/
export const validateCreditCode = (rule, value, callback) => {
if (!value) {
callback(new Error('请输入统一社会信用代码'))
return
}
// 统一社会信用代码正则表达式(18位)
const creditCodeRegex = /^[1-9A-GY][1-9A-N]\d{6}[1-9A-Z]\d{6}[1-9A-Z]\d{6}[1-9A-Z]\d|[1-9A-Z]\d{14}$/
if (!creditCodeRegex.test(value)) {
callback(new Error('请输入正确的统一社会信用代码'))
return
}
callback()
}
/**
* 验证正整数
* @param {Object} rule - 验证规则对象
* @param {string} value - 要验证的值
* @param {Function} callback - 回调函数
*/
export const validatePositiveInteger = (rule, value, callback) => {
if (value === '' || value === null || value === undefined) {
callback(new Error('请输入数值'))
return
}
const num = Number(value)
if (!Number.isInteger(num) || num <= 0) {
callback(new Error('请输入正整数'))
return
}
callback()
}
/**
* 验证URL地址
* @param {Object} rule - 验证规则对象
* @param {string} value - 要验证的值
* @param {Function} callback - 回调函数
*/
export const validateUrl = (rule, value, callback) => {
if (!value) {
callback(new Error('请输入URL地址'))
return
}
// URL正则表达式
const urlRegex = /^https?:\/\/([\w-]+\.)+[\w-]+([\w-./?%&=]*)?$/
if (!urlRegex.test(value)) {
callback(new Error('请输入正确的URL地址'))
return
}
callback()
}

View File

@ -0,0 +1,366 @@
<template>
<el-dialog title="新增" v-model="dialog.isShowDialog">
<div class="step-form-container">
<el-card class="step-form-card">
<!-- 步骤条导航 -->
<el-steps :active="activeStep" finish-status="success" align-center >
<el-step title="基本信息" />
<el-step title="参与单位" />
<el-step title="项目与限报配置" />
<el-step title="费用与账户配置" />
</el-steps>
<!-- 步骤内容区域 -->
<div class="step-content">
<keep-alive>
<component :is="currentStepComponent" ref="stepRef" :model-value="currentStepData" @update:model-value="handleStepDataUpdate" />
</keep-alive>
</div>
<!-- 底部操作按钮 -->
<div class="step-actions">
<el-button v-if="activeStep > 0" @click="handlePrevious" size="large">
<el-icon>
<ArrowLeft />
</el-icon>
上一步
</el-button>
<el-button v-if="activeStep < totalSteps - 1" type="primary" @click="handleNext" size="large">
下一步
<el-icon>
<ArrowRight />
</el-icon>
</el-button>
<el-button v-if="activeStep === totalSteps - 1" type="success" @click="handleSubmit" size="large"
:loading="submitting">
<el-icon>
<Check />
</el-icon>
提交申报
</el-button>
</div>
</el-card>
<!-- 提交成功对话框 -->
<el-dialog v-model="successDialogVisible" title="申报提交成功" width="400px" center :close-on-click-modal="false">
<div class="success-content">
<el-icon class="success-icon">
<CircleCheckFilled />
</el-icon>
<p>您的项目申报已成功提交!</p>
<p class="tips">我们将在3-5个工作日内审核您的申报材料。</p>
</div>
<template #footer>
<el-button type="primary" @click="handleReset">确定</el-button>
</template>
</el-dialog>
</div>
</el-dialog>
</template>
<script setup>
import { ref, reactive, computed, markRaw } from 'vue'
import { ElMessage } from 'element-plus'
import { ArrowLeft, ArrowRight, Check, CircleCheckFilled } from '@element-plus/icons-vue'
import Step1BasicInfo from './Steps/Step1BasicInfo.vue'
import Step2Units from './Steps/Step2Units.vue'
import Step3ProjectConfig from './Steps/Step3ProjectConfig.vue'
import Step4FeeConfig from './Steps/Step4FeeConfig.vue'
// 当前激活的步骤(0-3)
const activeStep = ref(3)
// 提交状态
const submitting = ref(false)
// 成功对话框显示状态
const successDialogVisible = ref(false)
// 子组件引用
const stepRef = ref(null)
// 总步骤数
const totalSteps = 4
// 步骤组件映射
const stepComponents = markRaw([
Step1BasicInfo,
Step2Units,
Step3ProjectConfig,
Step4FeeConfig
])
// 计算当前显示的步骤组件
const currentStepComponent = computed(() => {
return stepComponents[activeStep.value]
})
const currentStepData = computed(() => {
switch (activeStep.value) {
case 0: return formData.basicInfo
case 1: return formData.participatingUnits
case 2: return formData.projectConfig
case 3: return formData.feeConfig
}
})
const handleStepDataUpdate = (newData) => {
console.log('Received data from step', activeStep.value, newData)
switch (activeStep.value) {
case 0: formData.value.basicInfo = newData; break
case 1: formData.value.participatingUnits = newData; break
case 2: formData.value.projectConfig = newData; break
case 3: formData.value.feeConfig = newData; break
}
}
// 表单数据(响应式对象)
const formData = ref({
// 步骤1:基本信息
basicInfo: {
projectName: '', // 项目名称
declaringUnit: '', // 赛事类型
registrationMethod: '', // 报名方式
pointsRules: '', // 积分发放规则
registrationTime: [], // 报名起止时间
uploadMaterial: '', // 是否上传素材
uploadMaterialTime: [], // 上传素材起止时间
matchTime: [], // 比赛起止时间
matchAddress: '', // 赛事地址
detailAddress: '', //详细地址
projectDescription: '' // 赛事简介
},
// 步骤2:参与单位
participatingUnits: [],
// 步骤3:项目与限报配置
projectConfig: {
projectType: '',
maxDeclarations: 1,
isPublic: true,
deadline: ''
},
// 步骤4:费用与账户配置
feeConfig: {
feeType: 'self',
bankName: '',
accountName: '',
accountNumber: ''
}
})
// 处理上一步
const handlePrevious = () => {
if (activeStep.value > 0) {
activeStep.value--
}
}
const dialog = reactive ({
isShowDialog: false,
type: '',
title: '',
submitTxt: '',
loading: true,
})
// 打开弹窗
const openDialog = async (id) => {
dialog.isShowDialog = true;
dialog.loading = true;
if (id > 0) {
dialog.title = '修改菜单';
dialog.submitTxt = '修 改';
} else {
// ruleForm.id = 0;
dialog.title = '新增菜单';
dialog.submitTxt = '新 增';
// 清空表单,此项需加表单验证才能使用
// menuDialogFormRef.value?.resetFields();
}
dialog.loading = false;
};
// 处理下一步
const handleNext = async () => {
if (!stepRef.value) return
try {
// 调用子组件的验证方法
const valid = await stepRef.value.validate()
if (valid) {
if (activeStep.value < totalSteps - 1) {
activeStep.value++
}
} else {
ElMessage.warning('请完善当前步骤的信息')
}
} catch (error) {
console.error('验证失败:', error)
ElMessage.error('验证失败,请检查输入')
}
}
// 处理提交
const handleSubmit = async () => {
if (!stepRef.value) return
try {
// 调用子组件的验证方法
const valid = await stepRef.value.validate()
if (valid) {
submitting.value = true
// 模拟API提交
setTimeout(() => {
console.log('提交的表单数据:', JSON.stringify(formData.value, null, 2))
submitting.value = false
successDialogVisible.value = true
}, 1500)
} else {
ElMessage.warning('请完善当前步骤的信息')
}
} catch (error) {
console.error('提交失败:', error)
ElMessage.error('提交失败,请重试')
submitting.value = false
}
}
// 重置表单
const handleReset = () => {
successDialogVisible.value = false
// 重置所有表单数据
formData.value.basicInfo = {
projectName: '', // 项目名称
declaringUnit: '', // 赛事类型
registrationMethod: '', // 报名方式
pointsRules: '', // 积分发放规则
registrationTime: [], // 报名起止时间
uploadMaterial: '', // 是否上传素材
uploadMaterialTime: [], // 上传素材起止时间
matchTime: [], // 比赛起止时间
matchAddress: '', // 赛事地址
detailAddress: '', //详细地址
projectDescription: '' // 赛事简介
}
formData.value.participatingUnits = []
formData.value.projectConfig = {
projectType: '',
maxDeclarations: 1,
isPublic: true,
deadline: ''
}
formData.value.feeConfig = {
feeType: 'self',
bankName: '',
accountName: '',
accountNumber: ''
}
// 重置步骤
activeStep.value = 0
ElMessage.success('表单已重置,可以开始新的申报')
}
// 暴露变量
defineExpose({
openDialog,
});
</script>
<style scoped>
.step-form-container {
/* max-width: 1000px; */
margin: 0 auto;
}
.step-form-card {
/* border-radius: 12px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15); */
}
.card-header {
text-align: center;
}
.card-header h2 {
color: #303133;
font-size: 24px;
font-weight: 600;
margin-bottom: 8px;
}
.card-header .subtitle {
color: #909399;
font-size: 14px;
}
.steps-navigation {
margin: 40px 0;
}
.step-content {
min-height: 400px;
padding: 20px 0;
}
.step-actions {
display: flex;
justify-content: center;
gap: 20px;
padding-top: 30px;
border-top: 1px solid #ebeef5;
margin-top: 20px;
}
.step-actions .el-button {
padding: 12px 30px;
font-size: 16px;
}
.success-content {
text-align: center;
padding: 20px 0;
}
.success-icon {
font-size: 64px;
color: #67c23a;
margin-bottom: 20px;
}
.success-content p {
font-size: 16px;
color: #303133;
margin: 10px 0;
}
.success-content .tips {
font-size: 14px;
color: #909399;
}
/* 响应式设计 */
@media (max-width: 768px) {
.step-form-container {
padding: 10px;
}
.card-header h2 {
font-size: 20px;
}
.step-actions {
flex-direction: column;
gap: 10px;
}
.step-actions .el-button {
width: 100%;
}
}
</style>

View File

@ -0,0 +1,149 @@
<template>
<div class="system-menu-container layout-padding">
<ProTable ref="tableRef" :request-api="SysMenuApi.page" :columns="columns" :pagination="false"
:tool-button="false">
<template #tools>
<el-button v-auth="'sysmenu:add'" type="primary" icon="ele-Plus" @click="onOpenMenu"> 新增 </el-button>
<el-button v-auth="'sysmenu:add'" type="primary" icon="ele-Plus" @click="onOpenMenu"> 导出 </el-button>
</template>
<template #index="scope">
{{ scope.$index + 1 }}
</template>
<template #name="scope">
<SvgIcon :name="scope.row.icon" />
<span class="ml10">{{ $t(scope.row.name) }}</span>
</template>
<template #type="scope">
<el-tag :type="scope.row.type === 0 ? '' : scope.row.type === 1 ? 'success' : 'danger'">
{{ scope.row.type === 0 ? '目录' : scope.row.type === 1 ? '菜单' : '按钮' }}</el-tag>
</template>
<template #status="scope">
<el-tag :type="scope.row.status === 0 ? 'success' : 'danger'"> {{ scope.row.status === 0 ? '启用' : '禁用'
}}</el-tag>
</template>
<template #action="scope">
<el-button v-auth="'sysmenu:edit'" icon="ele-Edit" size="small" text type="primary"
@click="onOpenMenu(scope.row.id)"> 编辑 </el-button>
<el-popconfirm title="确认删除吗?" @confirm="onDeleteMenu(scope.row.id)">
<template #reference>
<el-button v-auth="'sysmenu:delete'" icon="ele-Delete" size="small" text type="danger"> 删除
</el-button>
</template>
</el-popconfirm>
</template>
</ProTable>
<MenuDialog ref="menuDialogRef" @refresh="tableRef?.reset" />
</div>
</template>
<script setup lang="ts" name="sysMenu">
import { defineAsyncComponent, ref, reactive } from 'vue';
import { ElMessage } from 'element-plus';
import SysMenuApi from '/@/api/SysMenuApi';
import { auths } from '/@/utils/authFunction';
import ProTable from '/@/components/ProTable/index.vue';
import { ColumnProps } from '/@/components/ProTable/interface';
// 引入组件
const MenuDialog = defineAsyncComponent(() => import('/@/views/match/manage/dialog.vue'));
//table组件实例
const tableRef = ref<InstanceType<typeof ProTable>>();
const columns = reactive<ColumnProps[]>([
{
prop: 'index',
label: '序号',
align: 'center',
},
{
prop: 'name',
label: '赛事名称',
align: 'center',
search: {
el: 'input',
},
},
{
prop: 'type',
label: '赛事类型',
search: {
el: 'select',
},
enum: [
{ label: '全部', value: 0 },
{ label: '线上', value: 1 },
{ label: '线下', value: 2 },
],
},
{
prop: 'path',
label: '报名方式',
},
{
prop: 'component',
label: '收费模式',
search: {
el: 'select',
},
enum: [
{ label: '全部', value: 0 },
{ label: '免费', value: 1 },
{ label: '定价', value: 2 },
{ label: '单价', value: 3 },
],
},
{
prop: 'statu',
label: '赛事状态',
search: {
el: 'select',
},
enum: [
{ label: '报名中', value: 0 },
{ label: '比赛中', value: 1 },
{ label: '已结束', value: 2 },
],
},
{ prop: 'code', label: '收款账户' },
{
prop: 'status',
label: '报名起止时间',
search: {
el: 'date-picker',
props: {
type: 'daterange',
},
},
},
{
prop: 'sort',
label: '比赛起止时间',
search: {
el: 'date-picker',
props: {
type: 'daterange',
},
},
},
{ prop: 'action', label: '操作', align: 'center', width: 150, fixed: 'right', isShow: auths(['sysmenu:edit', 'sysmenu:delete']) },
]);
// 定义变量内容
const menuDialogRef = ref<InstanceType<typeof MenuDialog>>();
// 打开添加编辑菜单弹窗
const onOpenMenu = async (id: number = 0) => {
await menuDialogRef.value!.openDialog(id);
};
//删除菜单
const onDeleteMenu = async (id: number) => {
const { succeeded, errors } = await SysMenuApi.delete({ id });
if (succeeded) {
ElMessage.success('删除成功');
tableRef.value?.reset();
} else {
ElMessage.error(errors);
}
};
</script>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,245 @@
<template>
<div class="system-menu-container layout-padding">
<ProTable ref="tableRef" :request-api="SysMenuApi.page" :columns="columns" :pagination="false"
:tool-button="false">
<template #tools>
<el-button v-auth="'sysmenu:add'" type="primary" icon="ele-Plus" @click="onOpenMenu"> 导出 </el-button>
</template>
<template #index="scope">
{{ scope.$index + 1 }}
</template>
<template #name="scope">
<SvgIcon :name="scope.row.icon" />
<span class="ml10">{{ $t(scope.row.name) }}</span>
</template>
<template #type="scope">
<el-tag :type="scope.row.type === 0 ? '' : scope.row.type === 1 ? 'success' : 'danger'">
{{ scope.row.type === 0 ? '目录' : scope.row.type === 1 ? '菜单' : '按钮' }}</el-tag>
</template>
<template #status="scope">
<el-tag :type="scope.row.status === 0 ? 'success' : 'danger'"> {{ scope.row.status === 0 ? '启用' : '禁用'
}}</el-tag>
</template>
<template #action="scope">
<!-- <el-button v-auth="'sysmenu:edit'" icon="ele-Edit" size="small" text type="primary"
@click="onOpenMenu(scope.row.id)"> 编辑 </el-button>
<el-popconfirm title="确认删除吗?" @confirm="onDeleteMenu(scope.row.id)">
<template #reference>
<el-button v-auth="'sysmenu:delete'" icon="ele-Delete" size="small" text type="danger"> 删除
</el-button>
</template>
</el-popconfirm> -->
<el-button v-auth="'sysmenu:add'" size="small" text type="primary" @click="onOpenMenu(scope.row.id)">
查看详情 </el-button>
</template>
</ProTable>
<!-- 弹窗 -->
<el-dialog title="新增" v-model="isShowDialog">
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="150px"
style="max-width: 600px;margin-bottom: 30px;" v-if="isShowDialog">
<!-- 项目名称 -->
<el-form-item label="项目名称:" prop="projectName">
<el-input v-model="formData.projectName" placeholder="请输入项目名称"></el-input>
</el-form-item>
<!-- 项目类型 -->
<el-form-item label="项目类型:" prop="projectType">
<el-select v-model="formData.projectType" placeholder="请选择项目类型" style="width: 100%;">
<el-option label="个人速度" value="personal_speed"></el-option>
<el-option label="个人花样" value="personal_endurance"></el-option>
<el-option label="团队速度" value="team_speed"></el-option>
<el-option label="团队花样" value="team_cooperation"></el-option>
</el-select>
</el-form-item>
<!-- 完成人数 -->
<el-form-item label="完成人数:" prop="participantCount">
<el-input v-model="formData.participantCount" placeholder="请输入完成人数"></el-input>
</el-form-item>
<!-- 项目描述(根据图片中的模糊区域推断) -->
<el-form-item label="项目描述:" prop="description">
<el-input type="textarea" v-model="formData.description" :rows="4" placeholder="请输入项目描述"></el-input>
</el-form-item>
<!-- 费用 -->
<el-form-item label="费用:" prop="fee">
<el-input v-model="formData.fee" placeholder="请输入费用" class="fee-input">
<template slot="append">元</template>
</el-input>
</el-form-item>
<!-- 时长 -->
<el-form-item label="时长:" prop="duration">
<div class="duration-row">
<el-input v-model="formData.durationMinutes" placeholder="分"></el-input>
<span class="duration-label">分</span>
<span class="duration-separator">--</span>
<el-input v-model="formData.durationSeconds" placeholder="秒" value="30"></el-input>
<span class="duration-label">秒</span>
</div>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="onCancel">取消</el-button>
<el-button type="primary" @click="onSubmit">确定</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts" name="sysMenu">
import { defineAsyncComponent, ref, reactive } 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 { ColumnProps } from '/@/components/ProTable/interface';
// 定义表单引用类型
const formRef = ref<InstanceType<typeof ElForm>>();
const formData = ref({
projectName: '30秒单摇',
projectType: 'personal_speed',
participantCount: '',
description: '',
fee: '100',
durationMinutes: '',
durationSeconds: '30'
})
const formRules = ref({
projectName: [
{ required: true, message: '请输入项目名称', trigger: 'blur' }
],
projectType: [
{ required: true, message: '请选择项目类型', trigger: 'change' }
],
participantCount: [
{ required: true, message: '请输入完成人数', trigger: 'blur' }
],
description: [
{ required: true, message: '请输入项目描述', trigger: 'blur' }
],
fee: [
{ required: true, message: '请输入费用', trigger: 'blur' }
],
durationMinutes: [
{ required: true, message: '请输入时长(分)', trigger: 'blur' }
],
durationSeconds: [
{ required: true, message: '请输入时长(秒)', trigger: 'blur' }
]
});
const columns = ref<ColumnProps[]>([
{ prop: 'index',label: '订单编号',align: 'center',search: { el: 'input',},},
{prop: 'name',label: '单位名称',align: 'center',},
{prop: 'type',label: '所属赛事',search: { el: 'input',},},
{prop: 'path',label: '订单类型',search: { el: 'select',},enum: [ { label: '全部', value: 0 }, { label: '收款', value: 1 }, { label: '退款', value: 2 },],},
{ prop: 'code', label: '关联订单' },
{prop: 'pay',label: '支付方式',search: { el: 'select',},enum: [ { label: '全部', value: 0 }, { label: '线上', value: 1 }, { label: '线下', value: 2 },],},
{ prop: 'code', label: '付款凭证' },
{prop: 'typeRegistration',label: '报名方式',search: { el: 'select',},enum: [ { label: '全部', value: 0 }, { label: '个人', value: 1 }, { label: '团队', value: 2 },],},
{ prop: 'code1', label: '报名用户' },
{ prop: 'code2', label: '报名账号', search: { el: 'input' } },
{ prop: 'num', label: '原金额' },
{ prop: 'num1', label: '优惠金额' },
{ prop: 'num2', label: '实付金额' },
{ prop: 'num3', label: '退款金额' },
{ prop: 'time', label: '生成时间', search: { el: 'date-picker', props: { type: 'daterange' } } },
{ prop: 'action', label: '操作', align: 'center', fixed: 'right', width: 120, isShow: auths(['sysmenu:add']) },
]);
const isShowDialog = ref(false);
const dialogTitle = ref('新增');
const onOpenMenu = (id: number = 0) => {
dialogTitle.value = id ? '编辑' : '新增';
formData.value = {
projectName: '',
projectType: '',
participantCount: '',
description: '',
fee: '',
durationMinutes: '',
durationSeconds: ''
};
isShowDialog.value = true;
};
//删除菜单
const onDeleteMenu = async (id: number) => {
const { succeeded, errors } = await SysMenuApi.delete({ id });
if (succeeded) {
ElMessage.success('删除成功');
} else {
ElMessage.error(errors);
}
};
// 表单提交
const onSubmit = () => {
if (!formRef.value) return;
formRef.value.validate(async (valid) => {
if (!valid) return;
// try {
// let res;
// if (formData.value.id) {
// res = await SysMenuApi.add(formData.value);
// } else {
// res = await SysMenuApi.add(formData.value);
// }
// if (res.succeeded) {
// ElMessage.success(dialogTitle.value + '成功');
// isShowDialog.value = false;
// } else {
// ElMessage.error(res.errors);
// }
// } catch (error) {
// console.error(error);
// }
});
};
// 取消
const onCancel = () => {
isShowDialog.value = false;
};
</script>
<style scoped lang="scss">
.dialog-footer {
margin-top: 80px;
}
/* 时长组合框样式 */
.duration-row {
display: flex;
align-items: center;
gap: 8px;
}
.duration-row .el-input {
width: 120px;
}
.duration-separator {
color: #606266;
font-size: 14px;
}
.duration-label {
color: #909399;
font-size: 14px;
}
</style>

View File

@ -0,0 +1,262 @@
<template>
<div class="system-menu-container layout-padding">
<ProTable ref="tableRef" :request-api="SysMenuApi.page" :columns="columns" :pagination="false"
:tool-button="false">
<template #tools>
<el-button v-auth="'sysmenu:add'" type="primary" icon="ele-Plus" @click="onOpenMenu"> 新增 </el-button>
</template>
<template #index="scope">
{{ scope.$index + 1 }}
</template>
<template #name="scope">
<SvgIcon :name="scope.row.icon" />
<span class="ml10">{{ $t(scope.row.name) }}</span>
</template>
<template #type="scope">
<el-tag :type="scope.row.type === 0 ? '' : scope.row.type === 1 ? 'success' : 'danger'">
{{ scope.row.type === 0 ? '目录' : scope.row.type === 1 ? '菜单' : '按钮' }}</el-tag>
</template>
<template #status="scope">
<el-tag :type="scope.row.status === 0 ? 'success' : 'danger'"> {{ scope.row.status === 0 ? '启用' : '禁用'
}}</el-tag>
</template>
<template #action="scope">
<el-button v-auth="'sysmenu:edit'" icon="ele-Edit" size="small" text type="primary"
@click="onOpenMenu(scope.row.id)"> 编辑 </el-button>
<el-popconfirm title="确认删除吗?" @confirm="onDeleteMenu(scope.row.id)">
<template #reference>
<el-button v-auth="'sysmenu:delete'" icon="ele-Delete" size="small" text type="danger"> 删除
</el-button>
</template>
</el-popconfirm>
<el-button v-auth="'sysmenu:add'" size="small" text type="primary" @click="onOpenMenu(scope.row.id)">
查看详情 </el-button>
</template>
</ProTable>
<!-- 弹窗 -->
<el-dialog title="新增" v-model="isShowDialog" width="60%">
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="150px"
style="width: 70%;margin-bottom: 30px;" v-if="isShowDialog">
<!-- 项目名称 -->
<el-form-item label="项目名称:" prop="projectName">
<el-input v-model="formData.projectName" placeholder="请输入项目名称"></el-input>
</el-form-item>
<!-- 项目类型 -->
<el-form-item label="项目类型:" prop="projectType">
<el-select v-model="formData.projectType" placeholder="请选择项目类型" style="width: 100%;">
<el-option label="个人速度" value="personal_speed"></el-option>
<el-option label="个人花样" value="personal_endurance"></el-option>
<el-option label="团队速度" value="team_speed"></el-option>
<el-option label="团队花样" value="team_cooperation"></el-option>
</el-select>
</el-form-item>
<!-- 完成人数 -->
<el-form-item label="完成人数:" prop="participantCount">
<el-input v-model="formData.participantCount" placeholder="请输入完成人数"></el-input>
</el-form-item>
<!-- 项目描述(根据图片中的模糊区域推断) -->
<el-form-item label="项目描述:" prop="description">
<el-input type="textarea" v-model="formData.description" :rows="4" placeholder="请输入项目描述"></el-input>
</el-form-item>
<!-- 费用 -->
<el-form-item label="费用:" prop="fee">
<el-input v-model="formData.fee" placeholder="请输入费用" class="fee-input">
<template slot="append">元</template>
</el-input>
</el-form-item>
<!-- 时长 -->
<el-form-item label="时长:" prop="duration">
<div class="duration-row">
<el-input v-model="formData.durationMinutes" placeholder="分"></el-input>
<span class="duration-label">分</span>
<span class="duration-separator">--</span>
<el-input v-model="formData.durationSeconds" placeholder="秒" value="30"></el-input>
<span class="duration-label">秒</span>
</div>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="onCancel">取消</el-button>
<el-button type="primary" @click="onSubmit">确定</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts" name="sysMenu">
import { defineAsyncComponent, ref, reactive } 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 { ColumnProps } from '/@/components/ProTable/interface';
// 定义表单引用类型
const formRef = ref<InstanceType<typeof ElForm>>();
const formData = ref({
projectName: '30秒单摇',
projectType: 'personal_speed',
participantCount: '',
description: '',
fee: '100',
durationMinutes: '',
durationSeconds: '30'
})
const formRules = ref({
projectName: [
{ required: true, message: '请输入项目名称', trigger: 'blur' }
],
projectType: [
{ required: true, message: '请选择项目类型', trigger: 'change' }
],
participantCount: [
{ required: true, message: '请输入完成人数', trigger: 'blur' }
],
description: [
{ required: true, message: '请输入项目描述', trigger: 'blur' }
],
fee: [
{ required: true, message: '请输入费用', trigger: 'blur' }
],
durationMinutes: [
{ required: true, message: '请输入时长(分)', trigger: 'blur' }
],
durationSeconds: [
{ required: true, message: '请输入时长(秒)', trigger: 'blur' }
]
});
const columns = ref<ColumnProps[]>([
{
prop: 'index',
label: '序号',
align: 'center',
},
{
prop: 'name',
label: '项目名称',
align: 'center',
search: {
el: 'input',
},
},
{
prop: 'type',
label: '项目类型',
search: {
el: 'select',
},
enum: [
{ label: '全部', value: 0 },
{ label: '线上', value: 1 },
{ label: '线下', value: 2 },
],
},
{
prop: 'path',
label: '完成人数',
},
{ prop: 'code', label: '费用' },
{ prop: 'action', label: '操作', align: 'center', fixed: 'right', isShow: auths(['sysmenu:delete', 'sysmenu:edit', 'sysmenu:add']) },
]);
const isShowDialog = ref(false);
const dialogTitle = ref('新增');
const onOpenMenu = (id: number = 0) => {
dialogTitle.value = id ? '编辑' : '新增';
formData.value = {
projectName: '',
projectType: '',
participantCount: '',
description: '',
fee: '',
durationMinutes: '',
durationSeconds: ''
};
isShowDialog.value = true;
};
//删除菜单
const onDeleteMenu = async (id: number) => {
const { succeeded, errors } = await SysMenuApi.delete({ id });
if (succeeded) {
ElMessage.success('删除成功');
} else {
ElMessage.error(errors);
}
};
// 表单提交
const onSubmit = () => {
if (!formRef.value) return;
formRef.value.validate(async (valid) => {
if (!valid) return;
// try {
// let res;
// if (formData.value.id) {
// res = await SysMenuApi.add(formData.value);
// } else {
// res = await SysMenuApi.add(formData.value);
// }
// if (res.succeeded) {
// ElMessage.success(dialogTitle.value + '成功');
// isShowDialog.value = false;
// } else {
// ElMessage.error(res.errors);
// }
// } catch (error) {
// console.error(error);
// }
});
};
// 取消
const onCancel = () => {
isShowDialog.value = false;
};
</script>
<style scoped lang="scss">
.dialog-footer {
margin-top: 80px;
}
/* 时长组合框样式 */
.duration-row {
display: flex;
align-items: center;
gap: 8px;
}
.duration-row .el-input {
width: 120px;
}
.duration-separator {
color: #606266;
font-size: 14px;
}
.duration-label {
color: #909399;
font-size: 14px;
}
</style>

View File

@ -0,0 +1,277 @@
<template>
<div class="system-menu-container layout-padding">
<ProTable ref="tableRef" :request-api="SysMenuApi.page" :columns="columns" :pagination="false"
:tool-button="false">
<template #tools>
<el-button v-auth="'sysmenu:add'" type="primary" icon="ele-Plus" @click="onOpenMenu"> 新增 </el-button>
</template>
<template #index="scope">
{{ scope.$index + 1 }}
</template>
<template #name="scope">
<SvgIcon :name="scope.row.icon" />
<span class="ml10">{{ $t(scope.row.name) }}</span>
</template>
<template #type="scope">
<el-tag :type="scope.row.type === 0 ? '' : scope.row.type === 1 ? 'success' : 'danger'">
{{ scope.row.type === 0 ? '目录' : scope.row.type === 1 ? '菜单' : '按钮' }}</el-tag>
</template>
<template #status="scope">
<el-tag :type="scope.row.status === 0 ? 'success' : 'danger'"> {{ scope.row.status === 0 ? '启用' : '禁用'
}}</el-tag>
</template>
<template #action="scope">
<el-button v-auth="'sysmenu:edit'" icon="ele-Edit" size="small" text type="primary"
@click="onOpenMenu(scope.row.id)"> 编辑 </el-button>
<el-popconfirm title="确认删除吗?" @confirm="onDeleteMenu(scope.row.id)">
<template #reference>
<el-button v-auth="'sysmenu:delete'" icon="ele-Delete" size="small" text type="danger"> 删除
</el-button>
</template>
</el-popconfirm>
</template>
</ProTable>
<!-- 弹窗 -->
<el-dialog title="新增" v-model="isShowDialog">
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="200px"
style="max-width: 600px;margin-bottom: 30px;" v-if="isShowDialog">
<!-- 商户名称 -->
<el-form-item label="商户名称" prop="projectName">
<el-input v-model="formData.projectName" placeholder="请输入项目名称"></el-input>
</el-form-item>
<!-- 统一社会信用代码 -->
<el-form-item label="统一社会信用代码" prop="projectType">
<el-input v-model="formData.projectType" placeholder="请输入项目名称"></el-input>
</el-form-item>
<!-- 开户行 -->
<el-form-item label="开户行" prop="participantCount">
<el-input v-model="formData.participantCount" placeholder="请输入完成人数"></el-input>
</el-form-item>
<!-- 账号 -->
<el-form-item label="账号" prop="description">
<el-input v-model="formData.description" placeholder="请输入项目描述"></el-input>
</el-form-item>
<!-- 营业执照 -->
<el-form-item label="营业执照" prop="fee">
<el-upload class="avatar-uploader" action=""
:show-file-list="false" :on-success="handleAvatarSuccess" :before-upload="beforeAvatarUpload">
<img v-if="formData.fee" :src="formData.fee" class="avatar" />
<el-icon v-else class="avatar-uploader-icon">
<Plus />
</el-icon>
</el-upload>
</el-form-item>
<!-- 费用 -->
<el-form-item label="联系电话" prop="phone">
<el-input v-model="formData.phone" placeholder="请输入费用" class="fee-input">
<template slot="append">元</template>
</el-input>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="onCancel">取消</el-button>
<el-button type="primary" @click="onSubmit">确定</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup name="sysMenu">
import { defineAsyncComponent, ref, reactive } 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';
// 定义表单引用类型
const formRef = ref();
const formData = ref({
projectName: '30秒单摇',
projectType: 'personal_speed',
participantCount: '',
description: '',
fee: '100',
durationMinutes: '',
phone: '30'
})
const formRules = ref({
projectName: [
{ required: true, message: '请输入商户名称', trigger: 'blur' }
],
projectType: [
{ required: true, message: '请选择统一社会信用代码', trigger: 'change' }
],
participantCount: [
{ required: true, message: '请输入开户行', trigger: 'blur' }
],
description: [
{ required: true, message: '请输入账号', trigger: 'blur' }
],
fee: [
{ required: true, message: '请上传营业执照', trigger: 'blur' }
],
});
const columns = ref([
{
prop: 'index',
label: '序号',
align: 'center',
},
{
prop: 'name',
label: '商户名称',
align: 'center',
search: {
el: 'input',
},
},
{prop: 'type',label: '统一社会信用代码',},
{prop: 'path',label: '开户行',},
{ prop: 'code', label: '账号' },
{ prop: 'img', label: '营业执照' },
{ prop: 'phone', label: '联系电话' },
{ prop: 'action', label: '操作', align: 'center', fixed: 'right', isShow: auths(['sysmenu:delete', 'sysmenu:edit']) },
]);
const isShowDialog = ref(false);
const dialogTitle = ref('新增');
const onOpenMenu = (id) => {
dialogTitle.value = id ? '编辑' : '新增';
formData.value = {
projectName: '',
projectType: '',
participantCount: '',
description: '',
fee: '',
durationMinutes: '',
phone: ''
};
isShowDialog.value = true;
};
//删除菜单
const onDeleteMenu = async (id) => {
const { succeeded, errors } = await SysMenuApi.delete({ id });
if (succeeded) {
ElMessage.success('删除成功');
} else {
ElMessage.error(errors);
}
};
const handleAvatarSuccess = ( response,
uploadFile) => {
formData.value.fee = URL.createObjectURL(uploadFile.raw)
}
const beforeAvatarUpload = () => {
// if (rawFile.type !== 'image/jpeg') {
// ElMessage.error('Avatar picture must be JPG format!')
// return false
// } else if (rawFile.size / 1024 / 1024 > 2) {
// ElMessage.error('Avatar picture size can not exceed 2MB!')
// return false
// }
return true
}
// 表单提交
const onSubmit = () => {
if (!formRef.value) return;
formRef.value.validate(async (valid) => {
if (!valid) return;
// try {
// let res;
// if (formData.value.id) {
// res = await SysMenuApi.add(formData.value);
// } else {
// res = await SysMenuApi.add(formData.value);
// }
// if (res.succeeded) {
// ElMessage.success(dialogTitle.value + '成功');
// isShowDialog.value = false;
// } else {
// ElMessage.error(res.errors);
// }
// } catch (error) {
// console.error(error);
// }
});
};
// 取消
const onCancel = () => {
isShowDialog.value = false;
};
</script>
<style scoped lang="scss">
.dialog-footer {
margin-top: 80px;
}
/* 时长组合框样式 */
.duration-row {
display: flex;
align-items: center;
gap: 8px;
}
.duration-row .el-input {
width: 120px;
}
.duration-separator {
color: #606266;
font-size: 14px;
}
.duration-label {
color: #909399;
font-size: 14px;
}
</style>
<style>
.avatar-uploader .el-upload {
border: 1px dashed var(--el-border-color);
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
transition: var(--el-transition-duration-fast);
}
.avatar-uploader .el-upload:hover {
border-color: var(--el-color-primary);
}
.el-icon.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 178px;
height: 178px;
text-align: center;
}
</style>

View File

@ -0,0 +1,290 @@
<template>
<div class="system-menu-container layout-padding">
<ProTable ref="tableRef" :request-api="SysMenuApi.page" :columns="columns" :pagination="false"
:tool-button="false">
<template #tools>
<el-button v-auth="'sysmenu:add'" type="primary" icon="ele-Plus" @click="onOpenMenu"> 新增 </el-button>
</template>
<template #index="scope">
{{ scope.$index + 1 }}
</template>
<template #name="scope">
<SvgIcon :name="scope.row.icon" />
<span class="ml10">{{ $t(scope.row.name) }}</span>
</template>
<template #type="scope">
<el-tag :type="scope.row.type === 0 ? '' : scope.row.type === 1 ? 'success' : 'danger'">
{{ scope.row.type === 0 ? '目录' : scope.row.type === 1 ? '菜单' : '按钮' }}</el-tag>
</template>
<template #status="scope">
<el-tag :type="scope.row.status === 0 ? 'success' : 'danger'"> {{ scope.row.status === 0 ? '启用' : '禁用'
}}</el-tag>
</template>
<template #action="scope">
<el-button v-auth="'sysmenu:edit'" icon="ele-Edit" size="small" text type="primary"
@click="onOpenMenu(scope.row.id)"> 编辑 </el-button>
<el-popconfirm title="确认删除吗?" @confirm="onDeleteMenu(scope.row.id)">
<template #reference>
<el-button v-auth="'sysmenu:delete'" icon="ele-Delete" size="small" text type="danger"> 删除
</el-button>
</template>
</el-popconfirm>
</template>
</ProTable>
<!-- 弹窗 -->
<el-dialog title="新增" v-model="isShowDialog">
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="200px"
style="max-width: 600px;margin-bottom: 30px;" v-if="isShowDialog">
<!-- 商户名称 -->
<el-form-item label="商户名称" prop="projectName">
<el-input v-model="formData.projectName" placeholder="请输入项目名称"></el-input>
</el-form-item>
<!-- 统一社会信用代码 -->
<el-form-item label="统一社会信用代码" prop="projectType">
<el-input v-model="formData.projectType" placeholder="请输入项目名称"></el-input>
</el-form-item>
<!-- 开户行 -->
<el-form-item label="开户行" prop="participantCount">
<el-input v-model="formData.participantCount" placeholder="请输入完成人数"></el-input>
</el-form-item>
<!-- 账号 -->
<el-form-item label="账号" prop="description">
<el-input v-model="formData.description" placeholder="请输入项目描述"></el-input>
</el-form-item>
<!-- 营业执照 -->
<el-form-item label="营业执照" prop="fee">
<el-upload class="avatar-uploader" action="" :show-file-list="false"
:on-success="handleAvatarSuccess" :before-upload="beforeAvatarUpload">
<img v-if="formData.fee" :src="formData.fee" class="avatar" />
<el-icon v-else class="avatar-uploader-icon">
<Plus />
</el-icon>
</el-upload>
</el-form-item>
<!-- 费用 -->
<el-form-item label="联系电话" prop="phone">
<el-input v-model="formData.phone" placeholder="请输入费用" class="fee-input">
<template slot="append">元</template>
</el-input>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="onCancel">取消</el-button>
<el-button type="primary" @click="onSubmit">确定</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup name="sysMenu">
import { defineAsyncComponent, ref, reactive } 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';
// 定义表单引用类型
const formRef = ref();
const formData = ref({
projectName: '30秒单摇',
projectType: 'personal_speed',
participantCount: '',
description: '',
fee: '100',
durationMinutes: '',
phone: '30'
})
const formRules = ref({
projectName: [
{ required: true, message: '请输入商户名称', trigger: 'blur' }
],
projectType: [
{ required: true, message: '请选择统一社会信用代码', trigger: 'change' }
],
participantCount: [
{ required: true, message: '请输入开户行', trigger: 'blur' }
],
description: [
{ required: true, message: '请输入账号', trigger: 'blur' }
],
fee: [
{ required: true, message: '请上传营业执照', trigger: 'blur' }
],
});
const columns = ref([
{ prop: 'ss', type: 'selection' },
{ prop: 'index', label: '序号', align: 'center', },
{ prop: 'type', label: '姓名', search: { el: 'input' } },
{
prop: 'path', label: '性别', search: {
el: 'select',
},
enum: [
{ label: '全部', value: 0 },
{ label: '男', value: 1 },
{ label: '女', value: 2 },
],
},
{ prop: 'code', label: '联系电话' , search: { el: 'input' }},
{ prop: 'card', label: '身份证' },
{ prop: 'img', label: '审核状态' ,
search: {
el: 'select',
},
enum: [
{ label: '全部', value: 0 },
{ label: '已通过', value: 1 },
{ label: '未审核', value: 2 },
{ label: '未提交', value: 3 },
{ label: '未通过', value: 4 },
],
},
{
prop: 'phone', label: '生成时间', search: {
el: 'date-picker',
props: { type: 'daterange' },
}
},
{ prop: 'action', label: '操作', align: 'center', fixed: 'right', isShow: auths(['sysmenu:delete', 'sysmenu:edit']) },
]);
const isShowDialog = ref(false);
const dialogTitle = ref('新增');
const onOpenMenu = (id) => {
dialogTitle.value = id ? '编辑' : '新增';
formData.value = {
projectName: '',
projectType: '',
participantCount: '',
description: '',
fee: '',
durationMinutes: '',
phone: ''
};
isShowDialog.value = true;
};
//删除菜单
const onDeleteMenu = async (id) => {
const { succeeded, errors } = await SysMenuApi.delete({ id });
if (succeeded) {
ElMessage.success('删除成功');
} else {
ElMessage.error(errors);
}
};
const handleAvatarSuccess = (response,
uploadFile) => {
formData.value.fee = URL.createObjectURL(uploadFile.raw)
}
const beforeAvatarUpload = () => {
// if (rawFile.type !== 'image/jpeg') {
// ElMessage.error('Avatar picture must be JPG format!')
// return false
// } else if (rawFile.size / 1024 / 1024 > 2) {
// ElMessage.error('Avatar picture size can not exceed 2MB!')
// return false
// }
return true
}
// 表单提交
const onSubmit = () => {
if (!formRef.value) return;
formRef.value.validate(async (valid) => {
if (!valid) return;
// try {
// let res;
// if (formData.value.id) {
// res = await SysMenuApi.add(formData.value);
// } else {
// res = await SysMenuApi.add(formData.value);
// }
// if (res.succeeded) {
// ElMessage.success(dialogTitle.value + '成功');
// isShowDialog.value = false;
// } else {
// ElMessage.error(res.errors);
// }
// } catch (error) {
// console.error(error);
// }
});
};
// 取消
const onCancel = () => {
isShowDialog.value = false;
};
</script>
<style scoped lang="scss">
.dialog-footer {
margin-top: 80px;
}
/* 时长组合框样式 */
.duration-row {
display: flex;
align-items: center;
gap: 8px;
}
.duration-row .el-input {
width: 120px;
}
.duration-separator {
color: #606266;
font-size: 14px;
}
.duration-label {
color: #909399;
font-size: 14px;
}
</style>
<style>
.avatar-uploader .el-upload {
border: 1px dashed var(--el-border-color);
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
transition: var(--el-transition-duration-fast);
}
.avatar-uploader .el-upload:hover {
border-color: var(--el-color-primary);
}
.el-icon.avatar-uploader-icon {
font-size: 28px;
color: #8c939d;
width: 178px;
height: 178px;
text-align: center;
}
</style>

View File

@ -30,7 +30,8 @@ const viteConfig = defineConfig((mode: ConfigEnv) => {
hmr: true,
proxy: {
'/api': {
target: 'http://localhost:53785',
// target: 'http://localhost:53785',
target: 'http://8.153.108.90:9996',
ws: true,
changeOrigin: true,
secure: false, //解决target使用https出错问题

3086
yarn.lock Normal file

File diff suppressed because it is too large Load Diff