优化
This commit is contained in:
@@ -43,7 +43,7 @@ pnpm test
|
|||||||
|
|
||||||
“我的信息”使用真实个人资料和修改密码接口,姓名、电话、邮箱按服务端约束维护;修改密码成功后清理旧会话并返回登录页。成员列表返回姓名、联系方式和 `roleIds`,新增、编辑、角色全量保存、启停和管理员重置密码均使用真实接口,不回退静态成功。角色页同时加载成员关系,直接打开或刷新后成员数仍按真实 `roleIds` 统计。角色列表返回 `permissionIds`,页面可回显并全量保存角色权限;角色编辑、启停、删除分别调用 `PUT /merchant/v1/iam/roles/{roleId}`、`PUT /merchant/v1/iam/roles/{roleId}/status` 和 `DELETE /merchant/v1/iam/roles/{roleId}`,内置角色及仍有关联成员的角色继续受保护。
|
“我的信息”使用真实个人资料和修改密码接口,姓名、电话、邮箱按服务端约束维护;修改密码成功后清理旧会话并返回登录页。成员列表返回姓名、联系方式和 `roleIds`,新增、编辑、角色全量保存、启停和管理员重置密码均使用真实接口,不回退静态成功。角色页同时加载成员关系,直接打开或刷新后成员数仍按真实 `roleIds` 统计。角色列表返回 `permissionIds`,页面可回显并全量保存角色权限;角色编辑、启停、删除分别调用 `PUT /merchant/v1/iam/roles/{roleId}`、`PUT /merchant/v1/iam/roles/{roleId}/status` 和 `DELETE /merchant/v1/iam/roles/{roleId}`,内置角色及仍有关联成员的角色继续受保护。
|
||||||
|
|
||||||
企业资料和资质列表使用真实租户接口,覆盖 `PROFILE_REQUIRED`、`PENDING`、`ACTIVE`、`REJECTED`、`DISABLED` 五种企业状态。企业名称、法定名称和联系人信息支持真实保存;营业执照、法人身份证、货代资质证书和银行开户许可证使用文件服务分别上传,接受 PDF、JPG、PNG 且不超过 5MB,四类齐全后显式提交审核,文件预览通过当前商户 Token 临时下载并在关闭弹窗后释放。
|
企业资料和资质列表使用真实租户接口,覆盖 `PROFILE_REQUIRED`、`PENDING`、`ACTIVE`、`REJECTED`、`DISABLED` 五种企业状态。企业名称、法定名称和联系人信息支持真实保存;营业执照、法人身份证为必填资质,货代资质证书和银行开户许可证为选填资质,均使用文件服务分别上传,接受 PDF、JPG、PNG 且不超过 5MB;两项必填资质齐全后可显式提交审核,文件预览通过当前商户 Token 临时下载并在关闭弹窗后释放。
|
||||||
|
|
||||||
头程订单列表、详情、仓库查询、创建/编辑草稿和提交已经接入真实 API,并显式映射后端 `transportMode`、`containerMode` 和订单状态。创建与提交请求携带幂等键,`merchant:order:create`、`merchant:order:edit`、`merchant:order:submit` 分别映射到独立前端操作权限。页面补齐渠道、提货方式、报关模式、目的仓类型、包装类型等后端必填字段;API 模式先清空本地记录,请求失败不会回退模拟成功。FBA 和海外仓按公共仓类型筛选,选择后由仓库主数据生成目的仓字段,不再要求客户重复选择收货单位;私人仓才填写并按当前用户缓存收件资料。只有“我司提货”需要填写提货联系人和地址,其他送货方式不发送发货方快照,订单也不再依赖往来单位 `customerId`。快递信息仍由平台维护、客户只读。订单取消不属于本轮本地一期验收,客户端不暴露取消写操作。订单产品状态仍按已确认的七状态执行,不设置“已驳回”;若兼容旧接口数据收到 `REJECTED`,只按待审核展示且不开放编辑。
|
头程订单列表、详情、仓库查询、创建/编辑草稿和提交已经接入真实 API,并显式映射后端 `transportMode`、`containerMode` 和订单状态。创建与提交请求携带幂等键,`merchant:order:create`、`merchant:order:edit`、`merchant:order:submit` 分别映射到独立前端操作权限。页面补齐渠道、提货方式、报关模式、目的仓类型、包装类型等后端必填字段;API 模式先清空本地记录,请求失败不会回退模拟成功。FBA 和海外仓按公共仓类型筛选,选择后由仓库主数据生成目的仓字段,不再要求客户重复选择收货单位;私人仓才填写并按当前用户缓存收件资料。只有“我司提货”需要填写提货联系人和地址,其他送货方式不发送发货方快照,订单也不再依赖往来单位 `customerId`。快递信息仍由平台维护、客户只读。订单取消不属于本轮本地一期验收,客户端不暴露取消写操作。订单产品状态仍按已确认的七状态执行,不设置“已驳回”;若兼容旧接口数据收到 `REJECTED`,只按待审核展示且不开放编辑。
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ server {
|
|||||||
|
|
||||||
# 前端统一请求 /api,保持原始路径转发给 Gateway。
|
# 前端统一请求 /api,保持原始路径转发给 Gateway。
|
||||||
location /api/ {
|
location /api/ {
|
||||||
|
client_max_body_size 6m;
|
||||||
proxy_pass ${API_UPSTREAM};
|
proxy_pass ${API_UPSTREAM};
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import { readFileSync } from 'node:fs'
|
||||||
|
import test from 'node:test'
|
||||||
|
|
||||||
|
const config = readFileSync(new URL('../../nginx/default.conf.template', import.meta.url), 'utf8')
|
||||||
|
|
||||||
|
test('allows qualification uploads with multipart overhead through the API proxy', () => {
|
||||||
|
assert.match(config, /location \/api\/ \{[\s\S]*?client_max_body_size 6m;/)
|
||||||
|
})
|
||||||
@@ -42,8 +42,9 @@ test('shows immediate login after registration and separates company review from
|
|||||||
assert.match(registerSource, /企业账号注册成功/)
|
assert.match(registerSource, /企业账号注册成功/)
|
||||||
assert.match(registerSource, /无需等待注册审核/)
|
assert.match(registerSource, /无需等待注册审核/)
|
||||||
assert.match(registerSource, /提交订单前还需完成企业审核/)
|
assert.match(registerSource, /提交订单前还需完成企业审核/)
|
||||||
assert.match(registerSource, /营业执照、法人身份证、货代资质证书和银行开户许可证/)
|
assert.match(registerSource, /营业执照和法人身份证/)
|
||||||
assert.match(registerSource, /一次补齐四类必填资质并提交审核/)
|
assert.match(registerSource, /货代资质证书、银行开户许可证可选填/)
|
||||||
|
assert.match(registerSource, /其他资质可选填/)
|
||||||
assert.match(registerSource, /立即登录/)
|
assert.match(registerSource, /立即登录/)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ async function submit() {
|
|||||||
<span><RouterLink to="/legal/terms" target="_blank" rel="noopener">《用户服务协议》</RouterLink> 和 <RouterLink to="/legal/privacy" target="_blank" rel="noopener">《隐私政策》</RouterLink></span>
|
<span><RouterLink to="/legal/terms" target="_blank" rel="noopener">《用户服务协议》</RouterLink> 和 <RouterLink to="/legal/privacy" target="_blank" rel="noopener">《隐私政策》</RouterLink></span>
|
||||||
</div>
|
</div>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<p class="register-security-note">注册成功后账号可立即登录;首次登录需补齐营业执照、法人身份证、货代资质证书和银行开户许可证,提交平台审核通过后才能提交订单。</p>
|
<p class="register-security-note">注册成功后账号可立即登录;首次登录需上传营业执照和法人身份证,货代资质证书、银行开户许可证可选填。提交平台审核通过后才能提交订单。</p>
|
||||||
<div class="register-actions"><el-button @click="$router.push('/login')">取消</el-button><el-button native-type="submit" type="primary" :loading="submitting">注册企业账号</el-button></div>
|
<div class="register-actions"><el-button @click="$router.push('/login')">取消</el-button><el-button native-type="submit" type="primary" :loading="submitting">注册企业账号</el-button></div>
|
||||||
</el-form>
|
</el-form>
|
||||||
</template>
|
</template>
|
||||||
@@ -126,7 +126,7 @@ async function submit() {
|
|||||||
<h1>企业账号注册成功</h1>
|
<h1>企业账号注册成功</h1>
|
||||||
<p><strong>{{ form.username }}</strong> 现在可以直接登录客户工作台,无需等待注册审核。</p>
|
<p><strong>{{ form.username }}</strong> 现在可以直接登录客户工作台,无需等待注册审核。</p>
|
||||||
<dl><div><dt>企业名称</dt><dd>{{ form.companyName }}</dd></div><div><dt>登录账号</dt><dd>{{ form.username }}</dd></div><div><dt>注册时间</dt><dd>{{ submittedAt }}</dd></div><div><dt>账号状态</dt><dd><el-tag type="success" effect="light">可登录</el-tag></dd></div></dl>
|
<dl><div><dt>企业名称</dt><dd>{{ form.companyName }}</dd></div><div><dt>登录账号</dt><dd>{{ form.username }}</dd></div><div><dt>注册时间</dt><dd>{{ submittedAt }}</dd></div><div><dt>账号状态</dt><dd><el-tag type="success" effect="light">可登录</el-tag></dd></div></dl>
|
||||||
<div class="registration-next-step" role="note"><strong>提交订单前还需完成企业审核</strong><span>登录后请在“企业资料”一次补齐四类必填资质并提交审核。审核期间仍可维护资料、成员和订单草稿。</span></div>
|
<div class="registration-next-step" role="note"><strong>提交订单前还需完成企业审核</strong><span>登录后请在“企业资料”上传营业执照和法人身份证并提交审核,其他资质可选填。审核期间仍可维护资料、成员和订单草稿。</span></div>
|
||||||
<div class="registration-result-actions"><el-button type="primary" @click="router.push({ path: '/login', query: { account: form.username } })">立即登录</el-button></div>
|
<div class="registration-result-actions"><el-button type="primary" @click="router.push({ path: '/login', query: { account: form.username } })">立即登录</el-button></div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import assert from 'node:assert/strict'
|
import assert from 'node:assert/strict'
|
||||||
import test from 'node:test'
|
import test from 'node:test'
|
||||||
import { formatQualificationFileSize, QUALIFICATION_MAX_FILE_SIZE, validateQualificationFile } from './qualificationFile.ts'
|
import { formatQualificationFileSize, qualificationUploadErrorMessage, QUALIFICATION_MAX_FILE_SIZE, validateQualificationFile } from './qualificationFile.ts'
|
||||||
|
|
||||||
test('accepts the backend-confirmed PDF, JPG and PNG formats up to 5MB', () => {
|
test('accepts the backend-confirmed PDF, JPG and PNG formats up to 5MB', () => {
|
||||||
assert.equal(validateQualificationFile({ name: '营业执照.jpeg', size: QUALIFICATION_MAX_FILE_SIZE, type: 'image/jpeg' }), '')
|
assert.equal(validateQualificationFile({ name: '营业执照.jpeg', size: QUALIFICATION_MAX_FILE_SIZE, type: 'image/jpeg' }), '')
|
||||||
@@ -20,3 +20,9 @@ test('formats selected file sizes for review', () => {
|
|||||||
assert.equal(formatQualificationFileSize(1536), '1.5 KB')
|
assert.equal(formatQualificationFileSize(1536), '1.5 KB')
|
||||||
assert.equal(formatQualificationFileSize(2 * 1024 * 1024), '2.0 MB')
|
assert.equal(formatQualificationFileSize(2 * 1024 * 1024), '2.0 MB')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('turns gateway 413 responses into an actionable upload message', () => {
|
||||||
|
assert.match(qualificationUploadErrorMessage({ status: 413 }), /上传网关拒绝(413)/)
|
||||||
|
assert.match(qualificationUploadErrorMessage({ status: 413 }), /已小于 5MB.*检查网关上传限制/)
|
||||||
|
assert.equal(qualificationUploadErrorMessage(new Error('服务暂不可用')), '服务暂不可用')
|
||||||
|
})
|
||||||
|
|||||||
@@ -23,3 +23,13 @@ export function formatQualificationFileSize(bytes: number) {
|
|||||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
||||||
return `${(bytes / 1024 / 1024).toFixed(1)} MB`
|
return `${(bytes / 1024 / 1024).toFixed(1)} MB`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function qualificationUploadErrorMessage(error: unknown) {
|
||||||
|
const status = typeof error === 'object' && error !== null && 'status' in error
|
||||||
|
? (error as { status?: unknown }).status
|
||||||
|
: undefined
|
||||||
|
if (status === 413) {
|
||||||
|
return '文件被上传网关拒绝(413)。请压缩后重试;若文件已小于 5MB,请联系平台管理员检查网关上传限制'
|
||||||
|
}
|
||||||
|
return error instanceof Error ? error.message : '资质文件上传失败'
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { Building2, CheckCircle2, Clock3, FileCheck2, FileText, Pencil, Trash2,
|
|||||||
import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue'
|
import DataTable, { type DataTableColumn } from '@/components/common/DataTable.vue'
|
||||||
import { useTenantAdministrationStore, type TenantCompanyProfile, type TenantQualification } from '@/modules/tenant-management/store/tenantAdministration'
|
import { useTenantAdministrationStore, type TenantCompanyProfile, type TenantQualification } from '@/modules/tenant-management/store/tenantAdministration'
|
||||||
import { useAuthStore } from '@/modules/auth/public'
|
import { useAuthStore } from '@/modules/auth/public'
|
||||||
import { formatQualificationFileSize, validateQualificationFile } from '@/modules/tenant-management/utils/qualificationFile'
|
import { formatQualificationFileSize, qualificationUploadErrorMessage, validateQualificationFile } from '@/modules/tenant-management/utils/qualificationFile'
|
||||||
import { qualificationStatusMeta } from '@/modules/tenant-management/utils/qualificationStatus'
|
import { qualificationStatusMeta } from '@/modules/tenant-management/utils/qualificationStatus'
|
||||||
import {
|
import {
|
||||||
downloadQualificationImage,
|
downloadQualificationImage,
|
||||||
@@ -19,10 +19,10 @@ const store = useTenantAdministrationStore()
|
|||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
const companyApproved = computed(() => store.company.auditStatus === 'ACTIVE')
|
const companyApproved = computed(() => store.company.auditStatus === 'ACTIVE')
|
||||||
const auditStatusCopy = computed(() => ({
|
const auditStatusCopy = computed(() => ({
|
||||||
PROFILE_REQUIRED: { title: '请完善企业资质', detail: '四类必填资质提交审核前不能提交头程订单。' },
|
PROFILE_REQUIRED: { title: '请完善企业资质', detail: '上传营业执照和法人身份证并提交审核后,才能提交头程订单。' },
|
||||||
PENDING: { title: '企业资料正在等待平台审核', detail: '审核期间仍可维护资料和保存订单草稿;审核通过前不能提交订单。' },
|
PENDING: { title: '企业资料正在等待平台审核', detail: '审核期间仍可维护资料和保存订单草稿;审核通过前不能提交订单。' },
|
||||||
ACTIVE: { title: '企业资料已审核,下单资格已开通', detail: '您可以创建、保存并提交头程订单。' },
|
ACTIVE: { title: '企业资料已审核,下单资格已开通', detail: '您可以创建、保存并提交头程订单。' },
|
||||||
REJECTED: { title: '企业资料审核未通过', detail: '请根据平台反馈替换对应资质,四类文件齐全后可重新提交审核。' },
|
REJECTED: { title: '企业资料审核未通过', detail: '请根据平台反馈替换对应资质,必填文件齐全后可重新提交审核。' },
|
||||||
DISABLED: { title: '企业账号已被平台停用', detail: '当前不能提交订单,请联系平台客服核实。' },
|
DISABLED: { title: '企业账号已被平台停用', detail: '当前不能提交订单,请联系平台客服核实。' },
|
||||||
})[store.company.auditStatus])
|
})[store.company.auditStatus])
|
||||||
const activeTab = ref('basic')
|
const activeTab = ref('basic')
|
||||||
@@ -44,7 +44,7 @@ const loading = ref(false)
|
|||||||
const loadError = ref('')
|
const loadError = ref('')
|
||||||
const draft = reactive<TenantCompanyProfile>({ ...store.company })
|
const draft = reactive<TenantCompanyProfile>({ ...store.company })
|
||||||
type QualificationUploadState = 'idle' | 'uploading' | 'uploaded' | 'error'
|
type QualificationUploadState = 'idle' | 'uploading' | 'uploaded' | 'error'
|
||||||
type QualificationTypeDefinition = { value: MerchantQualificationType; label: string; description: string }
|
type QualificationTypeDefinition = { value: MerchantQualificationType; label: string; description: string; required: boolean }
|
||||||
type QualificationDraftItem = QualificationTypeDefinition & {
|
type QualificationDraftItem = QualificationTypeDefinition & {
|
||||||
file: File | null
|
file: File | null
|
||||||
fileName: string
|
fileName: string
|
||||||
@@ -57,11 +57,12 @@ type QualificationDraftItem = QualificationTypeDefinition & {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const qualificationTypes: QualificationTypeDefinition[] = [
|
const qualificationTypes: QualificationTypeDefinition[] = [
|
||||||
{ value: 'BUSINESS_LICENSE', label: '营业执照', description: '上传清晰完整的营业执照文件。' },
|
{ value: 'BUSINESS_LICENSE', label: '营业执照', description: '上传清晰完整的营业执照文件。', required: true },
|
||||||
{ value: 'LEGAL_PERSON_ID', label: '法人身份证', description: '正反面请合并为一个 PDF、JPG 或 PNG 文件。' },
|
{ value: 'LEGAL_PERSON_ID', label: '法人身份证', description: '正反面请合并为一个 PDF、JPG 或 PNG 文件。', required: true },
|
||||||
{ value: 'FREIGHT_FORWARDER_CERTIFICATE', label: '货代资质证书', description: '上传当前有效的货代资质证明。' },
|
{ value: 'FREIGHT_FORWARDER_CERTIFICATE', label: '货代资质证书', description: '如有,可上传当前有效的货代资质证明。', required: false },
|
||||||
{ value: 'BANK_ACCOUNT_LICENSE', label: '银行开户许可证', description: '上传企业对公账户开户证明。' },
|
{ value: 'BANK_ACCOUNT_LICENSE', label: '银行开户许可证', description: '如有,可上传企业对公账户开户证明。', required: false },
|
||||||
]
|
]
|
||||||
|
const requiredQualificationTypes = qualificationTypes.filter((type) => type.required)
|
||||||
const qualificationDrafts = reactive<QualificationDraftItem[]>(qualificationTypes.map((type) => ({
|
const qualificationDrafts = reactive<QualificationDraftItem[]>(qualificationTypes.map((type) => ({
|
||||||
...type,
|
...type,
|
||||||
file: null,
|
file: null,
|
||||||
@@ -80,11 +81,11 @@ const normalizedQualificationCertificateNo = computed(() => qualificationCertifi
|
|||||||
const businessLicenseFileSelected = computed(() => Boolean(
|
const businessLicenseFileSelected = computed(() => Boolean(
|
||||||
qualificationDrafts.find((item) => item.value === 'BUSINESS_LICENSE')?.file,
|
qualificationDrafts.find((item) => item.value === 'BUSINESS_LICENSE')?.file,
|
||||||
))
|
))
|
||||||
const qualificationCreditCodeReady = computed(() => companyCreditCodeValid.value
|
const qualificationCreditCodeReady = computed(() => !businessLicenseFileSelected.value
|
||||||
|| (businessLicenseFileSelected.value && unifiedSocialCreditCodePattern.test(normalizedQualificationCertificateNo.value)))
|
|| companyCreditCodeValid.value
|
||||||
|
|| unifiedSocialCreditCodePattern.test(normalizedQualificationCertificateNo.value))
|
||||||
const qualificationCreditCodeError = computed(() => {
|
const qualificationCreditCodeError = computed(() => {
|
||||||
if (qualificationCreditCodeReady.value) return ''
|
if (qualificationCreditCodeReady.value) return ''
|
||||||
if (!businessLicenseFileSelected.value) return '请重新选择营业执照,并填写18位统一社会信用代码'
|
|
||||||
return '请输入18位字母或数字组成的统一社会信用代码'
|
return '请输入18位字母或数字组成的统一社会信用代码'
|
||||||
})
|
})
|
||||||
const canUploadQualification = computed(() => ['PROFILE_REQUIRED', 'REJECTED'].includes(store.company.auditStatus))
|
const canUploadQualification = computed(() => ['PROFILE_REQUIRED', 'REJECTED'].includes(store.company.auditStatus))
|
||||||
@@ -98,22 +99,12 @@ const qualificationUploadDisabledReason = computed(() => {
|
|||||||
if (store.company.auditStatus === 'DISABLED') return '企业账号已停用,暂不能提交资质文件。'
|
if (store.company.auditStatus === 'DISABLED') return '企业账号已停用,暂不能提交资质文件。'
|
||||||
return ''
|
return ''
|
||||||
})
|
})
|
||||||
const missingQualificationTypes = computed(() => qualificationTypes
|
|
||||||
.filter((type) => {
|
|
||||||
const draftItem = qualificationDrafts.find((item) => item.value === type.value)
|
|
||||||
return !uploadedQualificationTypes.value.has(type.value)
|
|
||||||
&& !draftItem?.file
|
|
||||||
&& draftItem?.uploadState !== 'uploaded'
|
|
||||||
})
|
|
||||||
.map((type) => type.label))
|
|
||||||
const selectedQualificationCount = computed(() => qualificationDrafts.filter((item) => Boolean(item.file)).length)
|
const selectedQualificationCount = computed(() => qualificationDrafts.filter((item) => Boolean(item.file)).length)
|
||||||
const canSaveQualificationBatch = computed(() => canUploadQualification.value
|
const canSaveQualificationBatch = computed(() => canUploadQualification.value
|
||||||
&& selectedQualificationCount.value > 0
|
&& selectedQualificationCount.value > 0
|
||||||
&& missingQualificationTypes.value.length === 0
|
|
||||||
&& qualificationCreditCodeReady.value)
|
&& qualificationCreditCodeReady.value)
|
||||||
const qualificationSaveDisabledReason = computed(() => {
|
const qualificationSaveDisabledReason = computed(() => {
|
||||||
if (!canUploadQualification.value) return qualificationUploadDisabledReason.value
|
if (!canUploadQualification.value) return qualificationUploadDisabledReason.value
|
||||||
if (missingQualificationTypes.value.length) return `还需选择:${missingQualificationTypes.value.join('、')}`
|
|
||||||
if (!qualificationCreditCodeReady.value) return qualificationCreditCodeError.value
|
if (!qualificationCreditCodeReady.value) return qualificationCreditCodeError.value
|
||||||
if (!selectedQualificationCount.value) return '请选择需要新增或替换的资质文件'
|
if (!selectedQualificationCount.value) return '请选择需要新增或替换的资质文件'
|
||||||
return ''
|
return ''
|
||||||
@@ -123,15 +114,15 @@ const qualificationDialogHint = computed(() => qualificationSaveDisabledReason.v
|
|||||||
: qualificationSaveDisabledReason.value)
|
: qualificationSaveDisabledReason.value)
|
||||||
const canSubmitQualificationReview = computed(() => canUploadQualification.value
|
const canSubmitQualificationReview = computed(() => canUploadQualification.value
|
||||||
&& companyCreditCodeValid.value
|
&& companyCreditCodeValid.value
|
||||||
&& qualificationTypes.every((type) => uploadedQualificationTypes.value.has(type.value)))
|
&& requiredQualificationTypes.every((type) => uploadedQualificationTypes.value.has(type.value)))
|
||||||
const qualificationReviewDisabledReason = computed(() => {
|
const qualificationReviewDisabledReason = computed(() => {
|
||||||
if (!canUploadQualification.value) return qualificationUploadDisabledReason.value
|
if (!canUploadQualification.value) return qualificationUploadDisabledReason.value
|
||||||
if (!companyCreditCodeValid.value) return '请先通过营业执照填写18位统一社会信用代码'
|
if (!companyCreditCodeValid.value) return '请先通过营业执照填写18位统一社会信用代码'
|
||||||
const missing = qualificationTypes.filter((type) => !uploadedQualificationTypes.value.has(type.value)).map((type) => type.label)
|
const missing = requiredQualificationTypes.filter((type) => !uploadedQualificationTypes.value.has(type.value)).map((type) => type.label)
|
||||||
return missing.length ? `请先上传:${missing.join('、')}` : ''
|
return missing.length ? `请先上传:${missing.join('、')}` : ''
|
||||||
})
|
})
|
||||||
const qualificationPanelHint = computed(() => canUploadQualification.value
|
const qualificationPanelHint = computed(() => canUploadQualification.value
|
||||||
? '一次选择四类必填资质;文件保存不会自动送审,全部齐全后再提交企业审核。'
|
? '营业执照、法人身份证为必填;货代资质证书、银行开户许可证可选填。文件保存后需另行提交审核。'
|
||||||
: qualificationUploadDisabledReason.value)
|
: qualificationUploadDisabledReason.value)
|
||||||
const profileFormRules: FormRules<TenantCompanyProfile> = {
|
const profileFormRules: FormRules<TenantCompanyProfile> = {
|
||||||
companyName: [{ required: true, whitespace: true, message: '请输入公司名称', trigger: 'blur' }],
|
companyName: [{ required: true, whitespace: true, message: '请输入公司名称', trigger: 'blur' }],
|
||||||
@@ -243,14 +234,14 @@ function qualificationSlotLabel(item: QualificationDraftItem) {
|
|||||||
const record = qualificationRecord(item.value)
|
const record = qualificationRecord(item.value)
|
||||||
if (record?.status === 'PENDING') return '已上传'
|
if (record?.status === 'PENDING') return '已上传'
|
||||||
if (record?.status === 'REJECTED') return '需重新上传'
|
if (record?.status === 'REJECTED') return '需重新上传'
|
||||||
return '待上传'
|
return item.required ? '待上传' : '选填'
|
||||||
}
|
}
|
||||||
|
|
||||||
function qualificationSlotTagType(item: QualificationDraftItem) {
|
function qualificationSlotTagType(item: QualificationDraftItem) {
|
||||||
if (item.file) return 'warning'
|
if (item.file) return 'warning'
|
||||||
if (item.uploadState === 'uploaded' || qualificationRecord(item.value)?.status === 'PENDING') return 'success'
|
if (item.uploadState === 'uploaded' || qualificationRecord(item.value)?.status === 'PENDING') return 'success'
|
||||||
if (item.uploadState === 'error' || qualificationRecord(item.value)?.status === 'REJECTED') return 'danger'
|
if (item.uploadState === 'error' || qualificationRecord(item.value)?.status === 'REJECTED') return 'danger'
|
||||||
return 'warning'
|
return item.required ? 'warning' : 'info'
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleQualificationFileChange(type: MerchantQualificationType, file: UploadFile) {
|
function handleQualificationFileChange(type: MerchantQualificationType, file: UploadFile) {
|
||||||
@@ -323,13 +314,6 @@ async function submitQualification() {
|
|||||||
ElMessage.warning(qualificationUploadDisabledReason.value)
|
ElMessage.warning(qualificationUploadDisabledReason.value)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (missingQualificationTypes.value.length) {
|
|
||||||
qualificationDrafts.forEach((item) => {
|
|
||||||
if (!uploadedQualificationTypes.value.has(item.value) && !item.file) item.error = '请选择该项资质文件'
|
|
||||||
})
|
|
||||||
ElMessage.warning(`请补齐:${missingQualificationTypes.value.join('、')}`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (!qualificationCreditCodeReady.value) {
|
if (!qualificationCreditCodeReady.value) {
|
||||||
const businessLicense = qualificationDraftItem('BUSINESS_LICENSE')
|
const businessLicense = qualificationDraftItem('BUSINESS_LICENSE')
|
||||||
if (businessLicense) businessLicense.error = qualificationCreditCodeError.value
|
if (businessLicense) businessLicense.error = qualificationCreditCodeError.value
|
||||||
@@ -366,7 +350,7 @@ async function submitQualification() {
|
|||||||
item.uploadState = 'uploaded'
|
item.uploadState = 'uploaded'
|
||||||
uploadedCount += 1
|
uploadedCount += 1
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : '资质文件上传失败'
|
const message = qualificationUploadErrorMessage(error)
|
||||||
item.error = message
|
item.error = message
|
||||||
item.uploadState = 'error'
|
item.uploadState = 'error'
|
||||||
failures.push(`${item.label}:${message}`)
|
failures.push(`${item.label}:${message}`)
|
||||||
@@ -542,14 +526,14 @@ function closeQualificationPreview() {
|
|||||||
@closed="resetQualificationDraft"
|
@closed="resetQualificationDraft"
|
||||||
>
|
>
|
||||||
<div class="qualification-dialog-intro">
|
<div class="qualification-dialog-intro">
|
||||||
<strong>一次补齐四类必填资质</strong>
|
<strong>必填资质与选填资料</strong>
|
||||||
<span>每类选择一个文件后统一保存;文件保存不会自动提交企业审核。</span>
|
<span>营业执照、法人身份证必填;其余两项可按需上传。文件保存不会自动提交企业审核。</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="qualification-upload-grid">
|
<div class="qualification-upload-grid">
|
||||||
<article v-for="item in qualificationDrafts" :key="item.value" class="qualification-upload-item">
|
<article v-for="item in qualificationDrafts" :key="item.value" class="qualification-upload-item">
|
||||||
<header>
|
<header>
|
||||||
<div>
|
<div>
|
||||||
<strong>{{ item.label }}</strong>
|
<strong>{{ item.label }}({{ item.required ? '必填' : '选填' }})</strong>
|
||||||
<p>{{ item.description }}</p>
|
<p>{{ item.description }}</p>
|
||||||
</div>
|
</div>
|
||||||
<el-tag :type="qualificationSlotTagType(item)" effect="light">{{ qualificationSlotLabel(item) }}</el-tag>
|
<el-tag :type="qualificationSlotTagType(item)" effect="light">{{ qualificationSlotLabel(item) }}</el-tag>
|
||||||
@@ -579,7 +563,7 @@ function closeQualificationPreview() {
|
|||||||
<p v-if="item.error" class="qualification-upload-error" role="alert">{{ item.error }}</p>
|
<p v-if="item.error" class="qualification-upload-error" role="alert">{{ item.error }}</p>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
<el-form-item label="统一社会信用代码" class="qualification-credit-code" :required="!companyCreditCodeValid" :error="qualificationCreditCodeReady ? '' : qualificationCreditCodeError">
|
<el-form-item label="统一社会信用代码" class="qualification-credit-code" :required="businessLicenseFileSelected && !companyCreditCodeValid" :error="qualificationCreditCodeReady ? '' : qualificationCreditCodeError">
|
||||||
<el-input v-model="qualificationCertificateNo" maxlength="18" placeholder="请输入18位统一社会信用代码" @input="qualificationCertificateNo = String($event).toUpperCase()" />
|
<el-input v-model="qualificationCertificateNo" maxlength="18" placeholder="请输入18位统一社会信用代码" @input="qualificationCertificateNo = String($event).toUpperCase()" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<p v-if="qualificationDialogHint" class="qualification-dialog-hint">{{ qualificationDialogHint }}</p>
|
<p v-if="qualificationDialogHint" class="qualification-dialog-hint">{{ qualificationDialogHint }}</p>
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ test('implements company details, contact information and qualification workflow
|
|||||||
assert.match(profileSource, /:close-on-click-modal="!qualificationSubmitting"/)
|
assert.match(profileSource, /:close-on-click-modal="!qualificationSubmitting"/)
|
||||||
assert.match(profileSource, /:close-on-press-escape="!qualificationSubmitting"/)
|
assert.match(profileSource, /:close-on-press-escape="!qualificationSubmitting"/)
|
||||||
assert.match(profileSource, /:show-close="!qualificationSubmitting"/)
|
assert.match(profileSource, /:show-close="!qualificationSubmitting"/)
|
||||||
assert.match(profileSource, /请重新选择营业执照,并填写18位统一社会信用代码/)
|
assert.match(profileSource, /const qualificationCreditCodeReady = computed\(\(\) => !businessLicenseFileSelected\.value/)
|
||||||
assert.match(profileSource, /companyCreditCodeValid\.value/)
|
assert.match(profileSource, /companyCreditCodeValid\.value/)
|
||||||
assert.doesNotMatch(profileSource, /<el-select v-model="qualificationDraft\.type"/)
|
assert.doesNotMatch(profileSource, /<el-select v-model="qualificationDraft\.type"/)
|
||||||
assert.match(profileSource, /class="mobile-qualification-list"/)
|
assert.match(profileSource, /class="mobile-qualification-list"/)
|
||||||
@@ -61,7 +61,12 @@ test('implements company details, contact information and qualification workflow
|
|||||||
assert.match(profileSource, /ref="profileFormRef" :model="draft" :rules="profileFormRules"/)
|
assert.match(profileSource, /ref="profileFormRef" :model="draft" :rules="profileFormRules"/)
|
||||||
assert.match(profileSource, /prop="companyName" required><el-input v-model="draft\.companyName" placeholder="请输入公司名称"/)
|
assert.match(profileSource, /prop="companyName" required><el-input v-model="draft\.companyName" placeholder="请输入公司名称"/)
|
||||||
assert.match(profileSource, /const valid = await profileFormRef\.value\?\.validate\(\)/)
|
assert.match(profileSource, /const valid = await profileFormRef\.value\?\.validate\(\)/)
|
||||||
assert.match(profileSource, /请选择该项资质文件/)
|
assert.match(profileSource, /const requiredQualificationTypes = qualificationTypes\.filter\(\(type\) => type\.required\)/)
|
||||||
|
assert.match(profileSource, /货代资质证书'.*required: false/)
|
||||||
|
assert.match(profileSource, /银行开户许可证'.*required: false/)
|
||||||
|
assert.match(profileSource, /requiredQualificationTypes\.every/)
|
||||||
|
assert.doesNotMatch(profileSource, /四类必填资质/)
|
||||||
|
assert.match(profileSource, /qualificationUploadErrorMessage\(error\)/)
|
||||||
assert.match(profileSource, /submitTenantQualificationReview/)
|
assert.match(profileSource, /submitTenantQualificationReview/)
|
||||||
assert.match(profileSource, /提交企业审核/)
|
assert.match(profileSource, /提交企业审核/)
|
||||||
assert.match(profileSource, /已保存.*项资质文件,尚未提交审核/)
|
assert.match(profileSource, /已保存.*项资质文件,尚未提交审核/)
|
||||||
|
|||||||
Reference in New Issue
Block a user