chore: 初始化平台管理端

This commit is contained in:
msumshk
2026-01-29 04:21:09 +00:00
commit 914dcc4166
533 changed files with 104838 additions and 0 deletions

View File

@@ -0,0 +1,689 @@
<script setup lang="ts">
import { computed, reactive, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { fetchUploadFile } from '@/api/files'
import { bindInitialTenantSubscription, submitTenantVerification } from '@/api/tenant-onboarding'
import { ElMessage } from 'element-plus'
import { useI18n } from 'vue-i18n'
// 1. 从 URL 读取 tenantId / tenantPackageId后续提交时携带
const route = useRoute()
const router = useRouter()
const tenantId = computed(() => route.query.tenantId as string | undefined)
const tenantPackageId = computed(() => route.query.tenantPackageId as string | undefined)
const { t } = useI18n()
// 定义表单数据接口
interface ProfileForm {
BusinessLicenseNumber: string
BusinessLicenseUrl: string
LegalPersonName: string
LegalPersonIdNumber: string
LegalPersonIdFrontUrl: string
LegalPersonIdBackUrl: string
BankAccountName: string
BankName: string
BankAccountNumber: string
}
// 使用 reactive 定义响应式数据
const form = reactive<ProfileForm>({
BusinessLicenseNumber: '',
BusinessLicenseUrl: '',
LegalPersonName: '',
LegalPersonIdNumber: '',
LegalPersonIdFrontUrl: '',
LegalPersonIdBackUrl: '',
BankAccountName: '',
BankName: '',
BankAccountNumber: ''
})
type UploadField = keyof Pick<
ProfileForm,
'BusinessLicenseUrl' | 'LegalPersonIdFrontUrl' | 'LegalPersonIdBackUrl'
>
// 2. 上传状态与文件名
const isUploading = reactive<Record<UploadField, boolean>>({
BusinessLicenseUrl: false,
LegalPersonIdFrontUrl: false,
LegalPersonIdBackUrl: false
})
const uploadedFileName = reactive<Record<UploadField, string>>({
BusinessLicenseUrl: '',
LegalPersonIdFrontUrl: '',
LegalPersonIdBackUrl: ''
})
// 3. 隐藏 input 引用
const businessLicenseInputRef = ref<HTMLInputElement | null>(null)
const legalPersonFrontInputRef = ref<HTMLInputElement | null>(null)
const legalPersonBackInputRef = ref<HTMLInputElement | null>(null)
const fileInputMap: Record<UploadField, typeof businessLicenseInputRef> = {
BusinessLicenseUrl: businessLicenseInputRef,
LegalPersonIdFrontUrl: legalPersonFrontInputRef,
LegalPersonIdBackUrl: legalPersonBackInputRef
}
const openFileDialog = (field: UploadField) => {
// 3.1 触发隐藏 input 选择文件
fileInputMap[field].value?.click()
}
const handleFileChange = async (field: UploadField, event: Event) => {
const target = event.target as HTMLInputElement
const file = target.files?.[0]
if (!file) return
// 3.2 清空 input支持重复选择同一文件
target.value = ''
await uploadFile(field, file)
}
const uploadFile = async (field: UploadField, file: File) => {
// 1. 设置上传中状态
isUploading[field] = true
try {
// 2. 调用上传接口并获取地址
// 2.1 入驻资质上传目前统一使用 business_license 类型
const res = await fetchUploadFile(file, 'business_license')
const url = res.url || ''
if (!url) {
alert('上传失败,请重试')
return
}
// 3. 写入表单地址与文件名
form[field] = url
uploadedFileName[field] = res.fileName || file.name
} catch (error) {
alert((error as Error).message || '上传失败,请重试')
} finally {
isUploading[field] = false
}
}
// 提交处理
const isSubmitting = ref<boolean>(false)
const handleSubmit = async () => {
// 1. 表单基本校验
if (!form.BusinessLicenseUrl || !form.LegalPersonIdFrontUrl || !form.LegalPersonIdBackUrl) {
alert('请确保所有证件照片已上传')
return
}
// 2. 校验租户与套餐参数
if (!tenantId.value || !tenantPackageId.value) {
ElMessage.warning(t('onboarding.messages.missingTenantInfo'))
return
}
if (isSubmitting.value) return
// 3. 组装实名提交命令(字段转为后端 camelCase
const verificationCommand: Api.Tenant.SubmitTenantVerificationCommand = {
tenantId: tenantId.value,
businessLicenseNumber: form.BusinessLicenseNumber,
businessLicenseUrl: form.BusinessLicenseUrl,
legalPersonName: form.LegalPersonName,
legalPersonIdNumber: form.LegalPersonIdNumber,
legalPersonIdFrontUrl: form.LegalPersonIdFrontUrl,
legalPersonIdBackUrl: form.LegalPersonIdBackUrl,
bankAccountName: form.BankAccountName,
bankAccountNumber: form.BankAccountNumber,
bankName: form.BankName
}
// 4. 串行执行:提交实名资料 -> 绑定套餐订阅
isSubmitting.value = true
try {
await submitTenantVerification(tenantId.value, verificationCommand)
await bindInitialTenantSubscription(tenantId.value, {
tenantId: tenantId.value,
tenantPackageId: tenantPackageId.value,
autoRenew: false
})
ElMessage.success(t('onboarding.messages.submitSuccess'))
await router.push({
name: 'TenantOnboardingWaiting',
query: {
tenantId: tenantId.value,
tenantPackageId: tenantPackageId.value
}
})
} catch (error) {
console.error('[OnboardingStatus] 提交审核失败:', error)
} finally {
isSubmitting.value = false
}
}
</script>
<template>
<div class="h-screen flex overflow-hidden bg-slate-50 text-slate-800 font-sans antialiased">
<!-- 左侧信息与指引 (Left Side - Context) -->
<div
class="hidden lg:flex lg:w-4/12 relative bg-slate-900 flex-col justify-between p-12 text-white"
>
<!-- 背景图案 -->
<div class="absolute inset-0 overflow-hidden pointer-events-none">
<div
class="absolute top-0 right-0 w-96 h-96 bg-indigo-600/20 rounded-full blur-3xl -translate-y-1/2 translate-x-1/2"
></div>
<div
class="absolute bottom-0 left-0 w-80 h-80 bg-blue-600/20 rounded-full blur-3xl translate-y-1/2 -translate-x-1/2"
></div>
<!-- 网格纹理 -->
<div
class="absolute inset-0"
style="
background-image: radial-gradient(rgb(255 255 255 / 10%) 1px, transparent 1px);
background-size: 32px 32px;
opacity: 0.2;
"
></div>
</div>
<!-- 顶部 Logo -->
<div class="relative z-10 flex items-center gap-2 text-indigo-300">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect width="18" height="18" x="3" y="3" rx="2" ry="2" />
<line x1="3" x2="21" y1="9" y2="9" />
<line x1="9" x2="9" y1="21" y2="9" />
</svg>
<span class="font-bold tracking-wide text-white">CloudSaaS</span>
</div>
<!-- 中间说明 -->
<div class="relative z-10">
<h2 class="text-3xl font-bold mb-6">最后一步<br />实名认证与资质审核</h2>
<p class="text-slate-300 leading-relaxed mb-8">
依据相关法律法规我们需要验证您的企业资质以开通正式服务您提交的所有信息将通过银行级加密传输仅用于资质审核
</p>
<div class="space-y-4">
<div class="flex items-start gap-4">
<div
class="w-8 h-8 rounded-full bg-indigo-500/20 flex items-center justify-center shrink-0 text-indigo-400 mt-1"
>
1
</div>
<div>
<h4 class="font-medium text-white">注册账号</h4>
<p class="text-sm text-slate-400">已完成</p>
</div>
</div>
<div class="flex items-start gap-4">
<div
class="w-8 h-8 rounded-full bg-indigo-500/20 flex items-center justify-center shrink-0 text-indigo-400 mt-1"
>
2
</div>
<div>
<h4 class="font-medium text-white">选择套餐</h4>
<p class="text-sm text-slate-400">已完成</p>
</div>
</div>
<div class="flex items-start gap-4">
<div
class="w-8 h-8 rounded-full bg-white text-indigo-900 font-bold flex items-center justify-center shrink-0 mt-1 shadow-lg shadow-indigo-900/50"
>
3
</div>
<div>
<h4 class="font-medium text-white">资质审核</h4>
<p class="text-sm text-indigo-300">进行中...</p>
</div>
</div>
</div>
</div>
<!-- 底部帮助 -->
<div class="relative z-10 text-sm text-slate-400">
遇到问题<a href="#" class="text-white underline decoration-indigo-500 underline-offset-4"
>联系人工客服</a
>
</div>
</div>
<!-- 右侧资料表单 (Right Side - Form) -->
<div class="flex-1 flex flex-col h-full bg-white overflow-hidden relative">
<!-- 顶部导航栏 (Mobile/Context) -->
<div
class="h-16 border-b border-slate-100 flex items-center justify-between px-6 shrink-0 bg-white/80 backdrop-blur z-20"
>
<span class="font-semibold text-slate-700">租户资料补全</span>
<div
class="text-xs text-slate-400 bg-slate-50 px-3 py-1 rounded-full border border-slate-100"
>
资料仅用于审核不会泄露
</div>
</div>
<!-- 可滚动区域 -->
<div class="flex-1 overflow-y-auto p-6 md:p-10 scroll-smooth">
<div class="max-w-3xl mx-auto space-y-10 pb-20">
<form @submit.prevent="handleSubmit">
<!-- 模块 1: 企业资质 (Business License) -->
<section class="space-y-6">
<div class="flex items-center gap-3 pb-2 border-b border-slate-100">
<div class="p-2 bg-indigo-50 text-indigo-600 rounded-lg">
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect width="20" height="14" x="2" y="3" rx="2" />
<line x1="8" x2="16" y1="21" y2="21" />
<line x1="12" x2="12" y1="17" y2="21" />
</svg>
</div>
<h3 class="text-lg font-bold text-slate-800">企业资质信息</h3>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- 营业执照编号 -->
<div class="col-span-1 md:col-span-2">
<label class="block text-sm font-medium text-slate-700 mb-2">
营业执照编号 <span class="text-rose-500">*</span>
</label>
<input
type="text"
v-model="form.BusinessLicenseNumber"
placeholder="请输入统一社会信用代码"
class="w-full px-4 py-2.5 bg-white border border-slate-200 rounded-lg text-sm focus:outline-none focus:border-indigo-500 focus:ring-4 focus:ring-indigo-500/10 transition-all placeholder:text-slate-400"
required
/>
</div>
<!-- 营业执照上传 -->
<div class="col-span-1 md:col-span-2">
<label class="block text-sm font-medium text-slate-700 mb-2">
营业执照照片 <span class="text-rose-500">*</span>
</label>
<!-- 上传组件 -->
<div
class="upload-card relative border-2 border-dashed border-slate-300 rounded-xl p-8 flex flex-col items-center justify-center text-center cursor-pointer transition-all bg-slate-50 min-h-[160px]"
:class="{ 'opacity-60 pointer-events-none': isUploading.BusinessLicenseUrl }"
@click="openFileDialog('BusinessLicenseUrl')"
>
<input
ref="businessLicenseInputRef"
type="file"
accept="image/*,application/pdf"
class="hidden"
@change="(e) => handleFileChange('BusinessLicenseUrl', e)"
/>
<div v-if="!form.BusinessLicenseUrl" class="flex flex-col items-center">
<div
class="w-12 h-12 bg-white rounded-full shadow-sm flex items-center justify-center text-indigo-500 mb-3"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
<polyline points="17 8 12 3 7 8" />
<line x1="12" x2="12" y1="3" y2="15" />
</svg>
</div>
<p class="text-sm font-medium text-slate-700">点击上传营业执照</p>
<p class="text-xs text-slate-400 mt-1">支持 JPG, PNG, PDF (最大 5MB)</p>
</div>
<!-- 上传成功后的预览状态 -->
<div
v-else
class="w-full flex items-center justify-between bg-white p-3 rounded-lg border border-indigo-100 shadow-sm"
>
<div class="flex items-center gap-3 overflow-hidden">
<div
class="w-10 h-10 bg-indigo-100 rounded-lg flex items-center justify-center text-indigo-600 shrink-0"
>IMG</div
>
<div class="flex flex-col text-left truncate">
<span class="text-sm font-medium text-slate-700 truncate">{{
uploadedFileName.BusinessLicenseUrl || 'license_scan_2025.jpg'
}}</span>
<span class="text-xs text-green-600">上传成功</span>
</div>
</div>
<button
type="button"
@click.stop="form.BusinessLicenseUrl = ''"
class="text-slate-400 hover:text-rose-500 p-2"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</svg>
</button>
</div>
</div>
</div>
</div>
</section>
<!-- 模块 2: 法人信息 (Legal Person) -->
<section class="space-y-6 pt-4">
<div class="flex items-center gap-3 pb-2 border-b border-slate-100">
<div class="p-2 bg-indigo-50 text-indigo-600 rounded-lg">
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
<circle cx="9" cy="7" r="4" />
<path d="M22 21v-2a4 4 0 0 0-3-3.87" />
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
</svg>
</div>
<h3 class="text-lg font-bold text-slate-800">法人身份信息</h3>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- 法人姓名 -->
<div>
<label class="block text-sm font-medium text-slate-700 mb-2">
法人姓名 <span class="text-rose-500">*</span>
</label>
<input
type="text"
v-model="form.LegalPersonName"
class="w-full px-4 py-2.5 bg-white border border-slate-200 rounded-lg text-sm focus:outline-none focus:border-indigo-500 focus:ring-4 focus:ring-indigo-500/10 transition-all placeholder:text-slate-400"
required
/>
</div>
<!-- 身份证号 -->
<div>
<label class="block text-sm font-medium text-slate-700 mb-2">
身份证号 <span class="text-rose-500">*</span>
</label>
<input
type="text"
v-model="form.LegalPersonIdNumber"
class="w-full px-4 py-2.5 bg-white border border-slate-200 rounded-lg text-sm focus:outline-none focus:border-indigo-500 focus:ring-4 focus:ring-indigo-500/10 transition-all placeholder:text-slate-400"
required
/>
</div>
<!-- 身份证正面 -->
<div>
<label class="block text-sm font-medium text-slate-700 mb-2">
身份证正面 (人像面) <span class="text-rose-500">*</span>
</label>
<div
class="upload-card border-2 border-dashed border-slate-300 rounded-xl p-4 flex flex-col items-center justify-center text-center cursor-pointer bg-slate-50 h-32"
:class="{ 'opacity-60 pointer-events-none': isUploading.LegalPersonIdFrontUrl }"
@click="openFileDialog('LegalPersonIdFrontUrl')"
>
<input
ref="legalPersonFrontInputRef"
type="file"
accept="image/*"
class="hidden"
@change="(e) => handleFileChange('LegalPersonIdFrontUrl', e)"
/>
<div v-if="!form.LegalPersonIdFrontUrl" class="text-slate-500 text-xs">
<svg
class="w-8 h-8 mx-auto mb-2 text-indigo-400 opacity-80"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
<circle cx="8.5" cy="8.5" r="1.5" />
<polyline points="21 15 16 10 5 21" />
</svg>
点击上传人像面
</div>
<div v-else class="text-green-600 flex items-center gap-2">
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 13l4 4L19 7"
/>
</svg>
<span class="text-sm">已选择文件</span>
</div>
</div>
</div>
<!-- 身份证反面 -->
<div>
<label class="block text-sm font-medium text-slate-700 mb-2">
身份证反面 (国徽面) <span class="text-rose-500">*</span>
</label>
<div
class="upload-card border-2 border-dashed border-slate-300 rounded-xl p-4 flex flex-col items-center justify-center text-center cursor-pointer bg-slate-50 h-32"
:class="{ 'opacity-60 pointer-events-none': isUploading.LegalPersonIdBackUrl }"
@click="openFileDialog('LegalPersonIdBackUrl')"
>
<input
ref="legalPersonBackInputRef"
type="file"
accept="image/*"
class="hidden"
@change="(e) => handleFileChange('LegalPersonIdBackUrl', e)"
/>
<div v-if="!form.LegalPersonIdBackUrl" class="text-slate-500 text-xs">
<svg
class="w-8 h-8 mx-auto mb-2 text-indigo-400 opacity-80"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
<line x1="3" y1="9" x2="21" y2="9" />
<line x1="9" y1="21" x2="9" y2="9" />
</svg>
点击上传国徽面
</div>
<div v-else class="text-green-600 flex items-center gap-2">
<svg class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 13l4 4L19 7"
/>
</svg>
<span class="text-sm">已选择文件</span>
</div>
</div>
</div>
</div>
</section>
<!-- 模块 3: 对公账户 (Bank Account) -->
<section class="space-y-6 pt-4">
<div class="flex items-center gap-3 pb-2 border-b border-slate-100">
<div class="p-2 bg-indigo-50 text-indigo-600 rounded-lg">
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M3 21h18" />
<path d="M5 21V7" />
<path d="M19 21V7" />
<path d="M4 7h16" />
<path d="M15 7 12 2 9 7" />
<line x1="10" x2="10" y1="14" y2="17" />
<line x1="14" x2="14" y1="14" y2="17" />
</svg>
</div>
<h3 class="text-lg font-bold text-slate-800">对公结算账户</h3>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- 开户名 -->
<div class="col-span-1 md:col-span-2">
<label class="block text-sm font-medium text-slate-700 mb-2">
开户名 (公司名称) <span class="text-rose-500">*</span>
</label>
<input
type="text"
v-model="form.BankAccountName"
placeholder="需与营业执照名称一致"
class="w-full px-4 py-2.5 bg-white border border-slate-200 rounded-lg text-sm focus:outline-none focus:border-indigo-500 focus:ring-4 focus:ring-indigo-500/10 transition-all placeholder:text-slate-400"
required
/>
</div>
<!-- 开户行 -->
<div>
<label class="block text-sm font-medium text-slate-700 mb-2">
开户银行 <span class="text-rose-500">*</span>
</label>
<input
type="text"
v-model="form.BankName"
placeholder="如:招商银行北京分行"
class="w-full px-4 py-2.5 bg-white border border-slate-200 rounded-lg text-sm focus:outline-none focus:border-indigo-500 focus:ring-4 focus:ring-indigo-500/10 transition-all placeholder:text-slate-400"
required
/>
</div>
<!-- 银行账号 -->
<div>
<label class="block text-sm font-medium text-slate-700 mb-2">
银行账号 <span class="text-rose-500">*</span>
</label>
<input
type="text"
v-model="form.BankAccountNumber"
class="w-full px-4 py-2.5 bg-white border border-slate-200 rounded-lg text-sm focus:outline-none focus:border-indigo-500 focus:ring-4 focus:ring-indigo-500/10 transition-all placeholder:text-slate-400 font-mono"
required
/>
</div>
</div>
</section>
<!-- 底部操作栏 -->
<div class="pt-8 flex items-center justify-end gap-4 border-t border-slate-100 mt-10">
<button
type="button"
class="px-6 py-2.5 rounded-lg text-slate-500 hover:bg-slate-50 hover:text-slate-700 font-medium transition-colors"
>
上一步
</button>
<button
type="submit"
:disabled="isSubmitting"
:class="{ 'opacity-60 cursor-not-allowed': isSubmitting }"
class="flex items-center gap-2 bg-indigo-600 hover:bg-indigo-700 text-white font-semibold py-2.5 px-8 rounded-lg shadow-lg shadow-indigo-500/30 transition-all transform hover:-translate-y-0.5 active:translate-y-0 disabled:hover:bg-indigo-600"
>
提交审核
<svg
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M5 12h14" />
<path d="m12 5 7 7-7 7" />
</svg>
</button>
</div>
</form>
</div>
<!-- 底部版权 (Mobile) -->
<div class="text-center text-xs text-slate-400 pb-6 md:hidden">
© 2025 CloudSaaS Inc.
</div>
</div>
</div>
</div>
</template>
<style scoped>
/* 自定义滚动条,让右侧滚动更优雅 */
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background-color: #cbd5e1;
border-radius: 20px;
}
.upload-card:hover {
background-color: #f5f3ff; /* indigo-50 */
border-color: #6366f1; /* indigo-500 */
}
</style>