feat(shop): 添加商户入驻申请功能

- 在API模型中新增证件类型字段(idType)
- 修改API请求路径,移除SERVER_API_URL前缀
- 新增根据入驻申请创建商户的接口方法
- 在菜单配置中添加商城管理和商户入驻申请菜单项
- 新增商家入驻申请和申请成功页面路由
- 创建商家入驻申请表单页面,包含三步流程:基本信息、资质信息、确认提交
- 实现图片上传和预览功能,支持营业执照、身份证等资质文件上传
- 添加表单验证规则,确保必填信息完整
- 创建申请提交成功页面,提供返回首页和查看申请按钮
- 优化CMS网站搜索组件代码结构和格式
This commit is contained in:
2025-12-05 23:48:16 +08:00
parent f59b581836
commit a485faa0e4
17 changed files with 2904 additions and 236 deletions

View File

@@ -0,0 +1,581 @@
<template>
<div class="merchant-apply-container">
<div class="merchant-apply-header">
<h1>商家入驻申请</h1>
<p>欢迎申请成为平台商家请填写以下信息</p>
</div>
<a-steps :current="currentStep" style="margin-bottom: 30px">
<a-step title="基本信息" />
<a-step title="资质信息" />
<a-step title="确认提交" />
</a-steps>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
class="merchant-apply-form"
>
<!-- 第一步基本信息 -->
<div v-show="currentStep === 0">
<a-card title="基本信息" style="margin-bottom: 20px">
<a-row :gutter="24">
<a-col :span="12">
<a-form-item label="商户名称" name="merchantName">
<a-input
v-model:value="form.merchantName"
placeholder="请输入商户名称"
:maxlength="100"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="证件类型" name="idType">
<a-select
v-model:value="form.idType"
placeholder="请选择证件类型"
>
<a-select-option value="1">营业执照</a-select-option>
<a-select-option value="2">统一社会信用代码</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="证件号码" name="merchantCode">
<a-input
v-model:value="form.merchantCode"
placeholder="请输入证件号码"
:maxlength="50"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="商户手机号" name="phone">
<a-input
v-model:value="form.phone"
placeholder="请输入联系人手机号"
:maxlength="11"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="商户姓名" name="realName">
<a-input
v-model:value="form.realName"
placeholder="请输入联系人姓名"
:maxlength="20"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="身份证号码" name="idCard">
<a-input
v-model:value="form.idCard"
placeholder="请输入法人身份证号码"
:maxlength="18"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="店铺类型" name="shopType">
<a-input
v-model:value="form.shopType"
placeholder="请输入店铺类型"
:maxlength="50"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="商户分类" name="category">
<a-input
v-model:value="form.category"
placeholder="请输入商户所属分类"
:maxlength="100"
/>
</a-form-item>
</a-col>
</a-row>
</a-card>
</div>
<!-- 第二步资质信息 -->
<div v-show="currentStep === 1">
<a-card title="资质信息" style="margin-bottom: 20px">
<a-row :gutter="24">
<a-col :span="12">
<a-form-item label="营业执照" name="yyzz">
<div class="upload-container">
<a-image
v-if="form.yyzz"
:src="form.yyzz"
:width="120"
:height="120"
style="margin-bottom: 10px"
/>
<SelectFile
:placeholder="`请上传营业执照`"
:limit="1"
:data="yyzzImages"
@done="chooseYyzzImage"
@del="onDeleteYyzzImage"
/>
<p class="upload-tip">请上传清晰的营业执照照片</p>
</div>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="身份证正面" name="sfz1">
<div class="upload-container">
<a-image
v-if="form.sfz1"
:src="form.sfz1"
:width="120"
:height="120"
style="margin-bottom: 10px"
/>
<SelectFile
:placeholder="`请上传身份证正面`"
:limit="1"
:data="sfz1Images"
@done="chooseSfz1Image"
@del="onDeleteSfz1Image"
/>
<p class="upload-tip">请上传清晰的身份证正面照片</p>
</div>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="身份证反面" name="sfz2">
<div class="upload-container">
<a-image
v-if="form.sfz2"
:src="form.sfz2"
:width="120"
:height="120"
style="margin-bottom: 10px"
/>
<SelectFile
:placeholder="`请上传身份证反面`"
:limit="1"
:data="sfz2Images"
@done="chooseSfz2Image"
@del="onDeleteSfz2Image"
/>
<p class="upload-tip">请上传清晰的身份证反面照片</p>
</div>
</a-form-item>
</a-col>
<a-col :span="24">
<a-form-item label="资质图片" name="files">
<SelectFile
:placeholder="`请上传其他资质证明文件`"
:limit="9"
:data="files"
@done="chooseFiles"
@del="onDeleteFiles"
/>
<p class="upload-tip"
>可上传产品合格证授权书等相关资质文件最多9张</p
>
</a-form-item>
</a-col>
</a-row>
</a-card>
</div>
<!-- 第三步确认提交 -->
<div v-show="currentStep === 2">
<a-card title="申请信息确认" style="margin-bottom: 20px">
<a-descriptions
bordered
:column="{ xxl: 1, xl: 1, lg: 1, md: 1, sm: 1, xs: 1 }"
>
<a-descriptions-item label="商户名称">{{
form.merchantName
}}</a-descriptions-item>
<a-descriptions-item label="证件类型">{{
getIdTypeName(form.idType)
}}</a-descriptions-item>
<a-descriptions-item label="证件号码">{{
form.merchantCode
}}</a-descriptions-item>
<a-descriptions-item label="联系人手机号">{{
form.phone
}}</a-descriptions-item>
<a-descriptions-item label="联系人姓名">{{
form.realName
}}</a-descriptions-item>
<a-descriptions-item label="身份证号码">{{
form.idCard
}}</a-descriptions-item>
<a-descriptions-item label="店铺类型">{{
form.shopType
}}</a-descriptions-item>
<a-descriptions-item label="商户分类">{{
form.category
}}</a-descriptions-item>
<a-descriptions-item label="营业执照">
<a-image
v-if="form.yyzz"
:src="form.yyzz"
:width="120"
:height="120"
/>
</a-descriptions-item>
<a-descriptions-item label="身份证正面">
<a-image
v-if="form.sfz1"
:src="form.sfz1"
:width="120"
:height="120"
/>
</a-descriptions-item>
<a-descriptions-item label="身份证反面">
<a-image
v-if="form.sfz2"
:src="form.sfz2"
:width="120"
:height="120"
/>
</a-descriptions-item>
<a-descriptions-item label="其他资质文件">
<div v-if="files.length > 0" class="files-preview">
<a-image
v-for="(file, index) in files"
:key="index"
:src="file.url"
:width="80"
:height="80"
style="margin-right: 10px; margin-bottom: 10px"
/>
</div>
<span v-else></span>
</a-descriptions-item>
</a-descriptions>
<a-alert
message="请仔细核对以上信息提交后将无法修改。审核结果将在3个工作日内通过短信通知您。"
type="info"
show-icon
style="margin-top: 20px"
/>
</a-card>
</div>
<!-- 操作按钮 -->
<div class="form-actions">
<a-button
v-if="currentStep > 0"
@click="prevStep"
style="margin-right: 10px"
>
上一步
</a-button>
<a-button v-if="currentStep < 2" type="primary" @click="nextStep">
下一步
</a-button>
<a-button
v-if="currentStep === 2"
type="primary"
@click="submitApply"
:loading="submitLoading"
>
提交申请
</a-button>
</div>
</a-form>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive } from 'vue';
import { Form, message } from 'ant-design-vue';
import { useRouter } from 'vue-router';
import { addShopMerchantApply } from '@/api/shop/shopMerchantApply';
import { ShopMerchantApply } from '@/api/shop/shopMerchantApply/model';
import { ItemType } from 'ele-admin-pro/es/ele-image-upload/types';
import { FileRecord } from '@/api/system/file/model';
const useForm = Form.useForm;
const router = useRouter();
// 当前步骤
const currentStep = ref(0);
// 提交状态
const submitLoading = ref(false);
// 表单引用
const formRef = ref<any>(null);
// 图片数据
const yyzzImages = ref<ItemType[]>([]);
const sfz1Images = ref<ItemType[]>([]);
const sfz2Images = ref<ItemType[]>([]);
const files = ref<ItemType[]>([]);
// 表单数据
const form = reactive<ShopMerchantApply>({
type: 1,
idType: '1', // 默认营业执照
merchantName: undefined,
merchantCode: undefined,
image: undefined,
phone: undefined,
realName: undefined,
idCard: undefined,
shopType: undefined,
category: undefined,
commission: undefined,
keywords: undefined,
yyzz: undefined,
sfz1: undefined,
sfz2: undefined,
files: undefined,
userId: undefined,
ownStore: 0,
recommend: 0,
goodsReview: 1,
name2: undefined,
reason: undefined,
comments: '商家入驻申请',
status: 0,
sortNumber: 100,
tenantId: undefined
});
// 表单验证规则
const rules = reactive({
merchantName: [
{ required: true, message: '请输入商户名称', trigger: 'blur' }
],
idType: [{ required: true, message: '请选择证件类型', trigger: 'change' }],
merchantCode: [
{ required: true, message: '请输入证件号码', trigger: 'blur' }
],
phone: [{ required: true, message: '请输入联系人手机号', trigger: 'blur' }],
realName: [
{ required: true, message: '请输入联系人姓名', trigger: 'blur' }
],
idCard: [{ required: true, message: '请输入身份证号码', trigger: 'blur' }],
shopType: [{ required: true, message: '请输入店铺类型', trigger: 'blur' }],
category: [{ required: true, message: '请输入商户分类', trigger: 'blur' }],
yyzz: [{ required: true, message: '请上传营业执照', trigger: 'change' }],
sfz1: [{ required: true, message: '请上传身份证正面', trigger: 'change' }],
sfz2: [{ required: true, message: '请上传身份证反面', trigger: 'change' }]
});
// 获取证件类型名称
const getIdTypeName = (type: string | undefined) => {
switch (type) {
case '1':
return '营业执照';
case '2':
return '统一社会信用代码';
default:
return '';
}
};
// 图片选择处理
const chooseYyzzImage = (data: FileRecord) => {
yyzzImages.value = [
{
uid: data.id,
url: data.path,
status: 'done'
}
];
form.yyzz = data.path;
};
const onDeleteYyzzImage = () => {
yyzzImages.value = [];
form.yyzz = undefined;
};
const chooseSfz1Image = (data: FileRecord) => {
sfz1Images.value = [
{
uid: data.id,
url: data.path,
status: 'done'
}
];
form.sfz1 = data.path;
};
const onDeleteSfz1Image = () => {
sfz1Images.value = [];
form.sfz1 = undefined;
};
const chooseSfz2Image = (data: FileRecord) => {
sfz2Images.value = [
{
uid: data.id,
url: data.path,
status: 'done'
}
];
form.sfz2 = data.path;
};
const onDeleteSfz2Image = () => {
sfz2Images.value = [];
form.sfz2 = undefined;
};
const chooseFiles = (data: FileRecord) => {
files.value.push({
uid: data.id,
url: data.path,
status: 'done'
});
};
const onDeleteFiles = (index: number) => {
files.value.splice(index, 1);
};
// 步骤控制
const nextStep = () => {
if (!formRef.value) return;
// 验证当前步骤的表单
const validateFields = getValidateFields();
if (validateFields.length > 0) {
formRef.value
.validateFields(validateFields)
.then(() => {
currentStep.value++;
})
.catch(() => {
message.error('请完善必填信息');
});
} else {
currentStep.value++;
}
};
const prevStep = () => {
currentStep.value--;
};
// 获取当前步骤需要验证的字段
const getValidateFields = () => {
switch (currentStep.value) {
case 0:
return [
'merchantName',
'idType',
'merchantCode',
'phone',
'realName',
'idCard',
'shopType',
'category'
];
case 1:
return ['yyzz', 'sfz1', 'sfz2'];
default:
return [];
}
};
// 提交申请
const submitApply = () => {
if (!formRef.value) return;
submitLoading.value = true;
// 处理表单数据
const formData = {
...form,
files:
files.value.length > 0
? JSON.stringify(files.value.map((item) => item.url))
: undefined
};
addShopMerchantApply(formData)
.then((msg) => {
submitLoading.value = false;
message.success('申请提交成功我们将在3个工作日内完成审核');
// 跳转到申请成功页面
router.push('/merchant/success');
})
.catch((e) => {
submitLoading.value = false;
message.error(e.message || '申请提交失败,请稍后重试');
});
};
const { resetFields } = useForm(form, rules);
</script>
<style lang="less" scoped>
.merchant-apply-container {
max-width: 1000px;
margin: 20px auto;
padding: 20px;
background: #fff;
border-radius: 4px;
}
.merchant-apply-header {
text-align: center;
margin-bottom: 30px;
h1 {
font-size: 24px;
color: #333;
margin-bottom: 10px;
}
p {
color: #666;
}
}
.merchant-apply-form {
margin-top: 20px;
}
.upload-container {
display: flex;
flex-direction: column;
align-items: flex-start;
}
.upload-tip {
color: #999;
font-size: 12px;
margin-top: 5px;
}
.files-preview {
display: flex;
flex-wrap: wrap;
}
.form-actions {
text-align: center;
margin-top: 30px;
}
</style>

View File

@@ -0,0 +1,41 @@
<template>
<div class="success-container">
<a-result
status="success"
title="商家入驻申请提交成功"
sub-title="我们将在3个工作日内完成审核审核结果将通过短信通知您"
>
<template #extra>
<a-button key="console" type="primary" @click="goHome"
>返回首页</a-button
>
<a-button key="buy" @click="viewApplication">查看申请</a-button>
</template>
</a-result>
</div>
</template>
<script lang="ts" setup>
import { useRouter } from 'vue-router';
const router = useRouter();
const goHome = () => {
router.push('/');
};
const viewApplication = () => {
router.push('/merchant/apply');
};
</script>
<style lang="less" scoped>
.success-container {
padding: 40px;
background: #fff;
border-radius: 4px;
max-width: 800px;
margin: 20px auto;
text-align: center;
}
</style>