优化
This commit is contained in:
Vendored
+1
@@ -44,6 +44,7 @@ declare module 'vue' {
|
||||
ElTag: typeof import('element-plus/es')['ElTag']
|
||||
ElTimeline: typeof import('element-plus/es')['ElTimeline']
|
||||
ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem']
|
||||
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
||||
ElUpload: typeof import('element-plus/es')['ElUpload']
|
||||
FirstLegOrderEditorShell: typeof import('./components/first-leg/FirstLegOrderEditorShell.vue')['default']
|
||||
LoadErrorState: typeof import('./components/common/LoadErrorState.vue')['default']
|
||||
|
||||
@@ -30,6 +30,7 @@ test('implements profile view, edit, save and cancel states', () => {
|
||||
test('implements password validation, strength feedback and double-submit protection', () => {
|
||||
assert.match(viewSource, /async function changePassword\(\)/)
|
||||
assert.match(viewSource, /validatePasswordChange/)
|
||||
assert.match(viewSource, /callback\(new Error\('请输入新密码'\)\)/)
|
||||
assert.match(viewSource, /getPasswordStrength/)
|
||||
assert.match(viewSource, /passwordStrengthState/)
|
||||
assert.match(viewSource, /if \(passwordSaving\.value\) return/)
|
||||
|
||||
@@ -73,6 +73,10 @@ const passwordRules: FormRules<PasswordChangePayload> = {
|
||||
newPassword: [{
|
||||
required: true,
|
||||
validator: (_rule, value: string, callback) => {
|
||||
if (!value) {
|
||||
callback(new Error('请输入新密码'))
|
||||
return
|
||||
}
|
||||
const errors = validatePasswordChange({
|
||||
currentPassword: passwordForm.currentPassword,
|
||||
newPassword: value,
|
||||
@@ -277,28 +281,29 @@ async function changePassword() {
|
||||
.account-form-grid :deep(.el-form-item) { align-content: start; margin-bottom: 21px; }
|
||||
.account-form-actions { display: flex; grid-column: 1 / -1; justify-content: flex-end; gap: 8px; padding-top: 2px; }
|
||||
.field-help { margin-top: 6px; color: var(--muted); font-size: 10px; line-height: 1.5; }
|
||||
.password-layout { display: grid; grid-template-columns: minmax(320px, 520px) minmax(240px, 1fr); align-items: start; gap: clamp(36px, 7vw, 90px); }
|
||||
.password-layout { display: grid; grid-template-columns: minmax(0, 440px) minmax(260px, 1fr); align-items: start; max-width: 980px; gap: clamp(32px, 6vw, 72px); padding-top: 22px; }
|
||||
.password-form { max-width: 440px; }
|
||||
.password-form :deep(.el-form-item) { margin-bottom: 20px; }
|
||||
.password-actions { display: flex; justify-content: flex-end; gap: 8px; padding-top: 2px; }
|
||||
.password-form :deep(.el-form-item) { margin-bottom: 18px; }
|
||||
.password-form :deep(.el-form-item__error) { padding-top: 5px; line-height: 1.45; white-space: normal; }
|
||||
.password-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 28px; padding-top: 18px; border-top: 1px solid var(--border); }
|
||||
.password-result { display: flex; align-items: flex-start; gap: 9px; margin-top: 18px; padding: 14px; background: #edf6f4; color: var(--primary); }
|
||||
.password-result strong { color: var(--navy); font-size: 12px; }
|
||||
.password-result p { margin: 3px 0 0; color: #526b67; font-size: 11px; line-height: 1.6; }
|
||||
.password-strength { display: flex; width: 100%; align-items: center; justify-content: space-between; gap: 12px; margin-top: 8px; color: var(--muted); }
|
||||
.password-strength { display: grid; width: 100%; min-height: 18px; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 10px; margin-top: 9px; color: var(--muted); }
|
||||
.password-strength > span { display: grid; flex: 1; grid-template-columns: repeat(3, 1fr); gap: 4px; }
|
||||
.password-strength i { height: 3px; background: #dce3e6; }
|
||||
.password-strength[data-strength="weak"] i:first-child { background: #c2413b; }
|
||||
.password-strength[data-strength="medium"] i:nth-child(-n+2) { background: #c47913; }
|
||||
.password-strength[data-strength="strong"] i { background: var(--primary); }
|
||||
.password-strength small { white-space: nowrap; font-size: 10px; }
|
||||
.security-note { display: grid; grid-template-columns: auto 1fr; gap: 10px 12px; margin-top: 24px; padding: 22px; background: #f2f7f6; color: var(--primary); }
|
||||
.security-note { display: grid; grid-template-columns: auto 1fr; gap: 10px 12px; margin-top: 0; padding: 22px; background: #f2f7f6; color: var(--primary); }
|
||||
.security-note strong { color: var(--navy); font-size: 13px; }
|
||||
.security-note p { margin: 5px 0 0; color: #526b67; font-size: 11px; line-height: 1.7; }
|
||||
.security-note > p { grid-column: 1 / -1; margin-top: 8px; padding-top: 14px; border-top: 1px solid #d7e5e2; }
|
||||
.security-note code { color: var(--navy); font-weight: 700; }
|
||||
|
||||
@media (max-width: 820px) {
|
||||
.password-layout { grid-template-columns: 1fr; gap: 14px; }
|
||||
.password-layout { grid-template-columns: 1fr; gap: 24px; padding-top: 18px; }
|
||||
.security-note { margin-top: 0; }
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { useTenantAdministrationStore, type TenantCompanyProfile, type TenantQua
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { formatQualificationFileSize, validateQualificationFile } from '@/utils/qualificationFile'
|
||||
import { downloadQualificationImage, uploadBusinessLicense } from '@/api/merchantIam'
|
||||
import { ApiError } from '@/api/http'
|
||||
|
||||
const store = useTenantAdministrationStore()
|
||||
const auth = useAuthStore()
|
||||
@@ -25,6 +26,7 @@ const qualificationOpen = ref(false)
|
||||
const previewOpen = ref(false)
|
||||
const previewRecord = ref<TenantQualification | null>(null)
|
||||
const previewUrl = ref('')
|
||||
const previewError = ref('')
|
||||
const previewLoading = ref(false)
|
||||
const profileFormRef = ref<FormInstance>()
|
||||
const qualificationFormRef = ref<FormInstance>()
|
||||
@@ -41,6 +43,16 @@ const loadError = ref('')
|
||||
const selectedFile = ref<File | null>(null)
|
||||
const draft = reactive<TenantCompanyProfile>({ ...store.company })
|
||||
const qualificationDraft = reactive({ type: '营业执照', fileName: '', certificateNo: '' })
|
||||
const canUploadQualification = computed(() => ['PROFILE_REQUIRED', 'REJECTED'].includes(store.company.auditStatus))
|
||||
const qualificationUploadDisabledReason = computed(() => {
|
||||
if (store.company.auditStatus === 'PENDING') return '企业资料正在审核,暂不能再次提交资质文件。'
|
||||
if (store.company.auditStatus === 'ACTIVE') return '当前企业已审核通过,资质替换请联系平台管理员处理。'
|
||||
if (store.company.auditStatus === 'DISABLED') return '企业账号已停用,暂不能提交资质文件。'
|
||||
return ''
|
||||
})
|
||||
const qualificationPanelHint = computed(() => canUploadQualification.value
|
||||
? '一期仅支持提交营业执照;提交后将进入平台审核。'
|
||||
: qualificationUploadDisabledReason.value)
|
||||
const profileFormRules: FormRules<TenantCompanyProfile> = {
|
||||
companyName: [{ required: true, whitespace: true, message: '请输入公司名称', trigger: 'blur' }],
|
||||
legalRepresentative: [{ required: true, whitespace: true, message: '请输入法人姓名', trigger: 'blur' }],
|
||||
@@ -109,6 +121,10 @@ async function saveProfile() {
|
||||
}
|
||||
|
||||
function openQualificationDialog() {
|
||||
if (!canUploadQualification.value) {
|
||||
ElMessage.warning(qualificationUploadDisabledReason.value)
|
||||
return
|
||||
}
|
||||
resetQualificationDraft()
|
||||
qualificationOpen.value = true
|
||||
}
|
||||
@@ -159,6 +175,12 @@ function resetQualificationDraft() {
|
||||
|
||||
async function submitQualification() {
|
||||
if (qualificationSubmitting.value) return
|
||||
if (!canUploadQualification.value) {
|
||||
qualificationOpen.value = false
|
||||
await loadCompanyContext()
|
||||
ElMessage.warning(qualificationUploadDisabledReason.value)
|
||||
return
|
||||
}
|
||||
const valid = await qualificationFormRef.value?.validate().then(() => true).catch(() => false)
|
||||
if (!valid || !selectedFile.value) {
|
||||
qualificationFileError.value = '请选择营业执照图片'
|
||||
@@ -175,6 +197,12 @@ async function submitQualification() {
|
||||
qualificationOpen.value = false
|
||||
ElMessage.success('营业执照已提交审核')
|
||||
} catch (error) {
|
||||
if (error instanceof ApiError && error.code === 'A0409') {
|
||||
await loadCompanyContext()
|
||||
qualificationOpen.value = false
|
||||
ElMessage.warning('企业资料正在审核,不能再次提交资质文件')
|
||||
return
|
||||
}
|
||||
ElMessage.error(error instanceof Error ? error.message : '营业执照上传失败')
|
||||
} finally {
|
||||
qualificationSubmitting.value = false
|
||||
@@ -182,15 +210,24 @@ async function submitQualification() {
|
||||
}
|
||||
|
||||
async function previewQualification(record: TenantQualification) {
|
||||
if (previewUrl.value) URL.revokeObjectURL(previewUrl.value)
|
||||
previewRecord.value = record
|
||||
previewOpen.value = true
|
||||
if (!record.documentUrl) return
|
||||
previewUrl.value = ''
|
||||
previewError.value = ''
|
||||
if (!record.documentUrl) {
|
||||
previewError.value = '当前资质记录没有可查看的文件地址,请联系平台管理员处理。'
|
||||
return
|
||||
}
|
||||
previewLoading.value = true
|
||||
try {
|
||||
const blob = await downloadQualificationImage(record.documentUrl)
|
||||
previewUrl.value = URL.createObjectURL(blob)
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '资质文件加载失败')
|
||||
previewError.value = error instanceof Error && error.message === '资质文件地址无效'
|
||||
? '当前资质文件尚未迁移到安全预览地址,请联系平台管理员处理。'
|
||||
: '资质文件加载失败,请稍后重试。'
|
||||
ElMessage.error(previewError.value)
|
||||
} finally {
|
||||
previewLoading.value = false
|
||||
}
|
||||
@@ -199,6 +236,7 @@ async function previewQualification(record: TenantQualification) {
|
||||
function closeQualificationPreview() {
|
||||
if (previewUrl.value) URL.revokeObjectURL(previewUrl.value)
|
||||
previewUrl.value = ''
|
||||
previewError.value = ''
|
||||
previewRecord.value = null
|
||||
}
|
||||
|
||||
@@ -247,7 +285,12 @@ function qualificationStatus(status: string) {
|
||||
<el-tab-pane name="qualification">
|
||||
<template #label><span class="tab-label"><FileCheck2 :size="16" />资质文件</span></template>
|
||||
<div class="client-card qualification-panel">
|
||||
<header><div><h3>企业资质</h3><p>文件更新后需等待平台重新审核,历史业务不受影响。</p></div><el-button v-if="auth.hasPermission('company:qualification:upload')" type="primary" @click="openQualificationDialog"><Upload :size="16" />更新资质</el-button></header>
|
||||
<header>
|
||||
<div><h3>企业资质</h3><p>{{ qualificationPanelHint }}</p></div>
|
||||
<el-tooltip v-if="auth.hasPermission('company:qualification:upload')" :disabled="canUploadQualification" :content="qualificationUploadDisabledReason" placement="top">
|
||||
<span class="qualification-update-control"><el-button type="primary" :disabled="!canUploadQualification" @click="openQualificationDialog"><Upload :size="16" />更新资质</el-button></span>
|
||||
</el-tooltip>
|
||||
</header>
|
||||
<DataTable
|
||||
v-model:page="qualificationPage"
|
||||
v-model:page-size="qualificationPageSize"
|
||||
@@ -319,7 +362,8 @@ function qualificationStatus(status: string) {
|
||||
<strong>{{ previewRecord.fileName }}</strong>
|
||||
<el-tag :type="previewRecord.status === 'VERIFIED' ? 'success' : previewRecord.status === 'PENDING' ? 'warning' : 'danger'" effect="light">{{ qualificationStatus(previewRecord.status) }}</el-tag>
|
||||
<dl><div><dt>文件类型</dt><dd>{{ previewRecord.type }}</dd></div><div><dt>有效期</dt><dd>{{ previewRecord.expiresAt }}</dd></div><div><dt>更新时间</dt><dd>{{ previewRecord.updatedAt }}</dd></div></dl>
|
||||
<span>{{ previewUrl ? '文件通过当前商户会话临时加载,关闭弹窗后立即释放。' : '当前文件暂不可预览。' }}</span>
|
||||
<p v-if="previewError" class="document-preview-error" role="alert">{{ previewError }}</p>
|
||||
<span v-else>{{ previewUrl ? '文件通过当前商户会话临时加载,关闭弹窗后立即释放。' : '当前文件暂不可预览。' }}</span>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
@@ -355,6 +399,7 @@ function qualificationStatus(status: string) {
|
||||
.qualification-panel { padding: 0; overflow: hidden; }
|
||||
.qualification-panel > header { padding: 20px 22px; border-bottom: 1px solid var(--border); }
|
||||
.qualification-panel h3 { margin: 0; }
|
||||
.qualification-update-control { display: inline-flex; }
|
||||
.mobile-qualification-list { display: none; }
|
||||
.profile-form h3 { margin: 4px 0 18px; padding-bottom: 10px; border-bottom: 1px solid var(--border); font-size: 15px; }
|
||||
.profile-form h3:not(:first-child) { margin-top: 22px; }
|
||||
@@ -374,6 +419,7 @@ function qualificationStatus(status: string) {
|
||||
.document-preview dt { color: var(--muted); font-size: 10px; }
|
||||
.document-preview dd { margin: 5px 0 0; overflow-wrap: anywhere; color: var(--navy); font-size: 12px; font-weight: 650; }
|
||||
.document-preview > span { max-width: 440px; padding: 0 16px; color: var(--muted); font-size: 11px; line-height: 1.6; text-align: center; }
|
||||
.document-preview-error { max-width: 440px; margin: 0; padding: 0 16px; color: #9a5a09; font-size: 11px; line-height: 1.6; text-align: center; }
|
||||
@media (max-width: 720px) {
|
||||
.profile-grid, .form-grid-two { grid-template-columns: 1fr; }
|
||||
.profile-grid > div:nth-child(odd) { border-right: 0; }
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Check, LockKeyhole, RotateCcw, Save, ShieldCheck } from '@lucide/vue'
|
||||
import { LockKeyhole, RotateCcw, Save, ShieldCheck } from '@lucide/vue'
|
||||
import { TENANT_PERMISSION_GROUPS, useTenantAdministrationStore } from '@/stores/tenantAdministration'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
@@ -110,16 +110,15 @@ async function savePermissions() {
|
||||
<div v-else class="permission-notice"><ShieldCheck :size="18" /><span>保存时会全量替换该角色的权限,请确认所有需要保留的菜单和操作均已勾选。</span></div>
|
||||
|
||||
<div class="permission-groups">
|
||||
<article v-for="group in TENANT_PERMISSION_GROUPS" :key="group.key" class="client-card permission-group">
|
||||
<article v-for="group in TENANT_PERMISSION_GROUPS" :key="group.key" class="client-card permission-group" :class="{ 'permission-group--single': group.items.length === 1 }">
|
||||
<header>
|
||||
<el-checkbox :model-value="groupChecked(group.key)" :indeterminate="groupIndeterminate(group.key)" :disabled="!editable" @change="toggleGroup(group.key, Boolean($event))"><strong>{{ group.label }}</strong></el-checkbox>
|
||||
<span>{{ group.items.filter((item) => selectedCodes.includes(item.code)).length }} / {{ group.items.length }}</span>
|
||||
</header>
|
||||
<el-checkbox-group v-model="selectedCodes" :disabled="!editable">
|
||||
<label v-for="item in group.items" :key="item.code" class="permission-item" :class="{ selected: selectedCodes.includes(item.code) }">
|
||||
<el-checkbox :value="item.code"><span class="permission-check"><Check :size="14" /></span></el-checkbox>
|
||||
<span><strong>{{ item.label }}</strong><small>{{ item.description }}</small><code>{{ item.code }}</code></span>
|
||||
</label>
|
||||
<el-checkbox v-for="item in group.items" :key="item.code" :value="item.code" class="permission-item" :class="{ selected: selectedCodes.includes(item.code) }">
|
||||
<span class="permission-copy"><strong>{{ item.label }}</strong><small>{{ item.description }}</small><code>{{ item.code }}</code></span>
|
||||
</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</article>
|
||||
</div>
|
||||
@@ -151,18 +150,20 @@ async function savePermissions() {
|
||||
.permission-notice.warning { border-color: #f1d4a8; background: #fff7ed; color: #9a5a09; }
|
||||
.permission-groups { display: grid; gap: 12px; }
|
||||
.permission-group { padding: 0; overflow: hidden; }
|
||||
.permission-group > header { padding: 14px 18px; border-bottom: 1px solid var(--border); background: #fafcfc; }
|
||||
.permission-group > header { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 14px 18px; border-bottom: 1px solid var(--border); background: #fafcfc; }
|
||||
.permission-group > header > span { color: var(--muted); font-size: 11px; }
|
||||
.permission-group :deep(.el-checkbox-group) { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.permission-item { display: flex; min-height: 92px; align-items: flex-start; gap: 9px; padding: 16px 18px; border-bottom: 1px solid var(--border); cursor: pointer; }
|
||||
.permission-group--single :deep(.el-checkbox-group) { grid-template-columns: minmax(0, 1fr); }
|
||||
.permission-item { display: flex; width: 100%; min-height: 104px; align-items: flex-start; margin: 0; padding: 16px 18px; border-bottom: 1px solid var(--border); cursor: pointer; }
|
||||
.permission-item:nth-child(odd) { border-right: 1px solid var(--border); }
|
||||
.permission-item:last-child:nth-child(odd) { grid-column: 1 / -1; border-right: 0; }
|
||||
.permission-item.selected { background: #f5faf9; }
|
||||
.permission-item :deep(.el-checkbox__label) { display: none; }
|
||||
.permission-check { display: none; }
|
||||
.permission-item > span { display: flex; min-width: 0; flex-direction: column; gap: 4px; }
|
||||
.permission-item strong { color: var(--navy); font-size: 12px; }
|
||||
.permission-item small { color: var(--muted); font-size: 10px; line-height: 1.5; }
|
||||
.permission-item code { color: #86939a; font-size: 9px; }
|
||||
.permission-item :deep(.el-checkbox__input) { flex: 0 0 auto; margin-top: 3px; }
|
||||
.permission-item :deep(.el-checkbox__label) { display: flex; min-width: 0; flex: 1; padding-left: 10px; white-space: normal; }
|
||||
.permission-copy { display: flex; min-width: 0; flex-direction: column; gap: 5px; }
|
||||
.permission-copy strong { color: var(--navy); font-size: 13px; line-height: 1.35; }
|
||||
.permission-copy small { color: var(--muted); font-size: 11px; line-height: 1.5; }
|
||||
.permission-copy code { overflow-wrap: anywhere; color: #86939a; font-size: 10px; line-height: 1.35; }
|
||||
@media (max-width: 900px) { .permissions-page { grid-template-columns: 1fr; } .role-selector { position: static; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
||||
@media (max-width: 720px) { .permission-actions { width: 100%; } .permission-actions .el-button { flex: 1; } .permission-group :deep(.el-checkbox-group) { grid-template-columns: 1fr; } .permission-item:nth-child(odd) { border-right: 0; } }
|
||||
@media (max-width: 480px) { .role-selector { grid-template-columns: 1fr; } }
|
||||
|
||||
@@ -39,7 +39,12 @@ test('implements company details, contact information and qualification workflow
|
||||
assert.match(profileSource, /prop="fileName" required :error="qualificationFileError"/)
|
||||
assert.match(profileSource, /const valid = await profileFormRef\.value\?\.validate\(\)/)
|
||||
assert.match(profileSource, /qualificationFileError\.value = '请选择营业执照图片'/)
|
||||
assert.match(profileSource, /const canUploadQualification = computed/)
|
||||
assert.match(profileSource, /:disabled="!canUploadQualification"/)
|
||||
assert.match(profileSource, /error\.code === 'A0409'/)
|
||||
assert.match(profileSource, /downloadQualificationImage/)
|
||||
assert.match(profileSource, /当前资质文件尚未迁移到安全预览地址/)
|
||||
assert.match(profileSource, /previewError/)
|
||||
assert.match(profileSource, /URL\.createObjectURL/)
|
||||
assert.match(profileSource, /URL\.revokeObjectURL/)
|
||||
assert.match(profileSource, /企业资料加载失败/)
|
||||
@@ -123,6 +128,12 @@ test('implements grouped menu and action permission editing', () => {
|
||||
assert.match(permissionsSource, /:loading="saving"/)
|
||||
assert.match(permissionsSource, /button:focus-visible/)
|
||||
assert.match(permissionsSource, /class="permission-actions" aria-label="权限操作"/)
|
||||
assert.match(permissionsSource, /<el-checkbox v-for="item in group\.items"[^>]*class="permission-item"/)
|
||||
assert.doesNotMatch(permissionsSource, /<label v-for="item in group\.items"/)
|
||||
assert.match(permissionsSource, /permission-group--single/)
|
||||
assert.match(permissionsSource, /\.permission-item:last-child:nth-child\(odd\) \{ grid-column: 1 \/ -1; border-right: 0; \}/)
|
||||
assert.match(permissionsSource, /\.permission-item :deep\(\.el-checkbox__label\) \{[^}]*white-space: normal/)
|
||||
assert.match(permissionsSource, /\.permission-copy \{[^}]*flex-direction: column[^}]*gap: 5px/)
|
||||
assert.doesNotMatch(permissionsSource, /permission-heading/)
|
||||
assert.doesNotMatch(permissionsSource, /section-label/)
|
||||
assert.doesNotMatch(permissionsSource, /当前只读展示权限资源/)
|
||||
|
||||
Reference in New Issue
Block a user