优化
This commit is contained in:
Vendored
+1
@@ -44,6 +44,7 @@ declare module 'vue' {
|
|||||||
ElTag: typeof import('element-plus/es')['ElTag']
|
ElTag: typeof import('element-plus/es')['ElTag']
|
||||||
ElTimeline: typeof import('element-plus/es')['ElTimeline']
|
ElTimeline: typeof import('element-plus/es')['ElTimeline']
|
||||||
ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem']
|
ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem']
|
||||||
|
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
||||||
ElUpload: typeof import('element-plus/es')['ElUpload']
|
ElUpload: typeof import('element-plus/es')['ElUpload']
|
||||||
FirstLegOrderEditorShell: typeof import('./components/first-leg/FirstLegOrderEditorShell.vue')['default']
|
FirstLegOrderEditorShell: typeof import('./components/first-leg/FirstLegOrderEditorShell.vue')['default']
|
||||||
LoadErrorState: typeof import('./components/common/LoadErrorState.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', () => {
|
test('implements password validation, strength feedback and double-submit protection', () => {
|
||||||
assert.match(viewSource, /async function changePassword\(\)/)
|
assert.match(viewSource, /async function changePassword\(\)/)
|
||||||
assert.match(viewSource, /validatePasswordChange/)
|
assert.match(viewSource, /validatePasswordChange/)
|
||||||
|
assert.match(viewSource, /callback\(new Error\('请输入新密码'\)\)/)
|
||||||
assert.match(viewSource, /getPasswordStrength/)
|
assert.match(viewSource, /getPasswordStrength/)
|
||||||
assert.match(viewSource, /passwordStrengthState/)
|
assert.match(viewSource, /passwordStrengthState/)
|
||||||
assert.match(viewSource, /if \(passwordSaving\.value\) return/)
|
assert.match(viewSource, /if \(passwordSaving\.value\) return/)
|
||||||
|
|||||||
@@ -73,6 +73,10 @@ const passwordRules: FormRules<PasswordChangePayload> = {
|
|||||||
newPassword: [{
|
newPassword: [{
|
||||||
required: true,
|
required: true,
|
||||||
validator: (_rule, value: string, callback) => {
|
validator: (_rule, value: string, callback) => {
|
||||||
|
if (!value) {
|
||||||
|
callback(new Error('请输入新密码'))
|
||||||
|
return
|
||||||
|
}
|
||||||
const errors = validatePasswordChange({
|
const errors = validatePasswordChange({
|
||||||
currentPassword: passwordForm.currentPassword,
|
currentPassword: passwordForm.currentPassword,
|
||||||
newPassword: value,
|
newPassword: value,
|
||||||
@@ -277,28 +281,29 @@ async function changePassword() {
|
|||||||
.account-form-grid :deep(.el-form-item) { align-content: start; margin-bottom: 21px; }
|
.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; }
|
.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; }
|
.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 { max-width: 440px; }
|
||||||
.password-form :deep(.el-form-item) { margin-bottom: 20px; }
|
.password-form :deep(.el-form-item) { margin-bottom: 18px; }
|
||||||
.password-actions { display: flex; justify-content: flex-end; gap: 8px; padding-top: 2px; }
|
.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 { 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 strong { color: var(--navy); font-size: 12px; }
|
||||||
.password-result p { margin: 3px 0 0; color: #526b67; font-size: 11px; line-height: 1.6; }
|
.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 > span { display: grid; flex: 1; grid-template-columns: repeat(3, 1fr); gap: 4px; }
|
||||||
.password-strength i { height: 3px; background: #dce3e6; }
|
.password-strength i { height: 3px; background: #dce3e6; }
|
||||||
.password-strength[data-strength="weak"] i:first-child { background: #c2413b; }
|
.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="medium"] i:nth-child(-n+2) { background: #c47913; }
|
||||||
.password-strength[data-strength="strong"] i { background: var(--primary); }
|
.password-strength[data-strength="strong"] i { background: var(--primary); }
|
||||||
.password-strength small { white-space: nowrap; font-size: 10px; }
|
.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 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 { 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 > 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; }
|
.security-note code { color: var(--navy); font-weight: 700; }
|
||||||
|
|
||||||
@media (max-width: 820px) {
|
@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; }
|
.security-note { margin-top: 0; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { useTenantAdministrationStore, type TenantCompanyProfile, type TenantQua
|
|||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import { formatQualificationFileSize, validateQualificationFile } from '@/utils/qualificationFile'
|
import { formatQualificationFileSize, validateQualificationFile } from '@/utils/qualificationFile'
|
||||||
import { downloadQualificationImage, uploadBusinessLicense } from '@/api/merchantIam'
|
import { downloadQualificationImage, uploadBusinessLicense } from '@/api/merchantIam'
|
||||||
|
import { ApiError } from '@/api/http'
|
||||||
|
|
||||||
const store = useTenantAdministrationStore()
|
const store = useTenantAdministrationStore()
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
@@ -25,6 +26,7 @@ const qualificationOpen = ref(false)
|
|||||||
const previewOpen = ref(false)
|
const previewOpen = ref(false)
|
||||||
const previewRecord = ref<TenantQualification | null>(null)
|
const previewRecord = ref<TenantQualification | null>(null)
|
||||||
const previewUrl = ref('')
|
const previewUrl = ref('')
|
||||||
|
const previewError = ref('')
|
||||||
const previewLoading = ref(false)
|
const previewLoading = ref(false)
|
||||||
const profileFormRef = ref<FormInstance>()
|
const profileFormRef = ref<FormInstance>()
|
||||||
const qualificationFormRef = ref<FormInstance>()
|
const qualificationFormRef = ref<FormInstance>()
|
||||||
@@ -41,6 +43,16 @@ const loadError = ref('')
|
|||||||
const selectedFile = ref<File | null>(null)
|
const selectedFile = ref<File | null>(null)
|
||||||
const draft = reactive<TenantCompanyProfile>({ ...store.company })
|
const draft = reactive<TenantCompanyProfile>({ ...store.company })
|
||||||
const qualificationDraft = reactive({ type: '营业执照', fileName: '', certificateNo: '' })
|
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> = {
|
const profileFormRules: FormRules<TenantCompanyProfile> = {
|
||||||
companyName: [{ required: true, whitespace: true, message: '请输入公司名称', trigger: 'blur' }],
|
companyName: [{ required: true, whitespace: true, message: '请输入公司名称', trigger: 'blur' }],
|
||||||
legalRepresentative: [{ required: true, whitespace: true, message: '请输入法人姓名', trigger: 'blur' }],
|
legalRepresentative: [{ required: true, whitespace: true, message: '请输入法人姓名', trigger: 'blur' }],
|
||||||
@@ -109,6 +121,10 @@ async function saveProfile() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openQualificationDialog() {
|
function openQualificationDialog() {
|
||||||
|
if (!canUploadQualification.value) {
|
||||||
|
ElMessage.warning(qualificationUploadDisabledReason.value)
|
||||||
|
return
|
||||||
|
}
|
||||||
resetQualificationDraft()
|
resetQualificationDraft()
|
||||||
qualificationOpen.value = true
|
qualificationOpen.value = true
|
||||||
}
|
}
|
||||||
@@ -159,6 +175,12 @@ function resetQualificationDraft() {
|
|||||||
|
|
||||||
async function submitQualification() {
|
async function submitQualification() {
|
||||||
if (qualificationSubmitting.value) return
|
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)
|
const valid = await qualificationFormRef.value?.validate().then(() => true).catch(() => false)
|
||||||
if (!valid || !selectedFile.value) {
|
if (!valid || !selectedFile.value) {
|
||||||
qualificationFileError.value = '请选择营业执照图片'
|
qualificationFileError.value = '请选择营业执照图片'
|
||||||
@@ -175,6 +197,12 @@ async function submitQualification() {
|
|||||||
qualificationOpen.value = false
|
qualificationOpen.value = false
|
||||||
ElMessage.success('营业执照已提交审核')
|
ElMessage.success('营业执照已提交审核')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (error instanceof ApiError && error.code === 'A0409') {
|
||||||
|
await loadCompanyContext()
|
||||||
|
qualificationOpen.value = false
|
||||||
|
ElMessage.warning('企业资料正在审核,不能再次提交资质文件')
|
||||||
|
return
|
||||||
|
}
|
||||||
ElMessage.error(error instanceof Error ? error.message : '营业执照上传失败')
|
ElMessage.error(error instanceof Error ? error.message : '营业执照上传失败')
|
||||||
} finally {
|
} finally {
|
||||||
qualificationSubmitting.value = false
|
qualificationSubmitting.value = false
|
||||||
@@ -182,15 +210,24 @@ async function submitQualification() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function previewQualification(record: TenantQualification) {
|
async function previewQualification(record: TenantQualification) {
|
||||||
|
if (previewUrl.value) URL.revokeObjectURL(previewUrl.value)
|
||||||
previewRecord.value = record
|
previewRecord.value = record
|
||||||
previewOpen.value = true
|
previewOpen.value = true
|
||||||
if (!record.documentUrl) return
|
previewUrl.value = ''
|
||||||
|
previewError.value = ''
|
||||||
|
if (!record.documentUrl) {
|
||||||
|
previewError.value = '当前资质记录没有可查看的文件地址,请联系平台管理员处理。'
|
||||||
|
return
|
||||||
|
}
|
||||||
previewLoading.value = true
|
previewLoading.value = true
|
||||||
try {
|
try {
|
||||||
const blob = await downloadQualificationImage(record.documentUrl)
|
const blob = await downloadQualificationImage(record.documentUrl)
|
||||||
previewUrl.value = URL.createObjectURL(blob)
|
previewUrl.value = URL.createObjectURL(blob)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ElMessage.error(error instanceof Error ? error.message : '资质文件加载失败')
|
previewError.value = error instanceof Error && error.message === '资质文件地址无效'
|
||||||
|
? '当前资质文件尚未迁移到安全预览地址,请联系平台管理员处理。'
|
||||||
|
: '资质文件加载失败,请稍后重试。'
|
||||||
|
ElMessage.error(previewError.value)
|
||||||
} finally {
|
} finally {
|
||||||
previewLoading.value = false
|
previewLoading.value = false
|
||||||
}
|
}
|
||||||
@@ -199,6 +236,7 @@ async function previewQualification(record: TenantQualification) {
|
|||||||
function closeQualificationPreview() {
|
function closeQualificationPreview() {
|
||||||
if (previewUrl.value) URL.revokeObjectURL(previewUrl.value)
|
if (previewUrl.value) URL.revokeObjectURL(previewUrl.value)
|
||||||
previewUrl.value = ''
|
previewUrl.value = ''
|
||||||
|
previewError.value = ''
|
||||||
previewRecord.value = null
|
previewRecord.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,7 +285,12 @@ function qualificationStatus(status: string) {
|
|||||||
<el-tab-pane name="qualification">
|
<el-tab-pane name="qualification">
|
||||||
<template #label><span class="tab-label"><FileCheck2 :size="16" />资质文件</span></template>
|
<template #label><span class="tab-label"><FileCheck2 :size="16" />资质文件</span></template>
|
||||||
<div class="client-card qualification-panel">
|
<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
|
<DataTable
|
||||||
v-model:page="qualificationPage"
|
v-model:page="qualificationPage"
|
||||||
v-model:page-size="qualificationPageSize"
|
v-model:page-size="qualificationPageSize"
|
||||||
@@ -319,7 +362,8 @@ function qualificationStatus(status: string) {
|
|||||||
<strong>{{ previewRecord.fileName }}</strong>
|
<strong>{{ previewRecord.fileName }}</strong>
|
||||||
<el-tag :type="previewRecord.status === 'VERIFIED' ? 'success' : previewRecord.status === 'PENDING' ? 'warning' : 'danger'" effect="light">{{ qualificationStatus(previewRecord.status) }}</el-tag>
|
<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>
|
<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>
|
</div>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
@@ -355,6 +399,7 @@ function qualificationStatus(status: string) {
|
|||||||
.qualification-panel { padding: 0; overflow: hidden; }
|
.qualification-panel { padding: 0; overflow: hidden; }
|
||||||
.qualification-panel > header { padding: 20px 22px; border-bottom: 1px solid var(--border); }
|
.qualification-panel > header { padding: 20px 22px; border-bottom: 1px solid var(--border); }
|
||||||
.qualification-panel h3 { margin: 0; }
|
.qualification-panel h3 { margin: 0; }
|
||||||
|
.qualification-update-control { display: inline-flex; }
|
||||||
.mobile-qualification-list { display: none; }
|
.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 { 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; }
|
.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 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 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 > 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) {
|
@media (max-width: 720px) {
|
||||||
.profile-grid, .form-grid-two { grid-template-columns: 1fr; }
|
.profile-grid, .form-grid-two { grid-template-columns: 1fr; }
|
||||||
.profile-grid > div:nth-child(odd) { border-right: 0; }
|
.profile-grid > div:nth-child(odd) { border-right: 0; }
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { computed, onMounted, ref, watch } from 'vue'
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { ElMessage } from 'element-plus'
|
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 { TENANT_PERMISSION_GROUPS, useTenantAdministrationStore } from '@/stores/tenantAdministration'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
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 v-else class="permission-notice"><ShieldCheck :size="18" /><span>保存时会全量替换该角色的权限,请确认所有需要保留的菜单和操作均已勾选。</span></div>
|
||||||
|
|
||||||
<div class="permission-groups">
|
<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>
|
<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>
|
<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>
|
<span>{{ group.items.filter((item) => selectedCodes.includes(item.code)).length }} / {{ group.items.length }}</span>
|
||||||
</header>
|
</header>
|
||||||
<el-checkbox-group v-model="selectedCodes" :disabled="!editable">
|
<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 v-for="item in group.items" :key="item.code" :value="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 class="permission-copy"><strong>{{ item.label }}</strong><small>{{ item.description }}</small><code>{{ item.code }}</code></span>
|
||||||
<span><strong>{{ item.label }}</strong><small>{{ item.description }}</small><code>{{ item.code }}</code></span>
|
</el-checkbox>
|
||||||
</label>
|
|
||||||
</el-checkbox-group>
|
</el-checkbox-group>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
@@ -151,18 +150,20 @@ async function savePermissions() {
|
|||||||
.permission-notice.warning { border-color: #f1d4a8; background: #fff7ed; color: #9a5a09; }
|
.permission-notice.warning { border-color: #f1d4a8; background: #fff7ed; color: #9a5a09; }
|
||||||
.permission-groups { display: grid; gap: 12px; }
|
.permission-groups { display: grid; gap: 12px; }
|
||||||
.permission-group { padding: 0; overflow: hidden; }
|
.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 > 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-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: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.selected { background: #f5faf9; }
|
||||||
.permission-item :deep(.el-checkbox__label) { display: none; }
|
.permission-item :deep(.el-checkbox__input) { flex: 0 0 auto; margin-top: 3px; }
|
||||||
.permission-check { display: none; }
|
.permission-item :deep(.el-checkbox__label) { display: flex; min-width: 0; flex: 1; padding-left: 10px; white-space: normal; }
|
||||||
.permission-item > span { display: flex; min-width: 0; flex-direction: column; gap: 4px; }
|
.permission-copy { display: flex; min-width: 0; flex-direction: column; gap: 5px; }
|
||||||
.permission-item strong { color: var(--navy); font-size: 12px; }
|
.permission-copy strong { color: var(--navy); font-size: 13px; line-height: 1.35; }
|
||||||
.permission-item small { color: var(--muted); font-size: 10px; line-height: 1.5; }
|
.permission-copy small { color: var(--muted); font-size: 11px; line-height: 1.5; }
|
||||||
.permission-item code { color: #86939a; font-size: 9px; }
|
.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: 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: 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; } }
|
@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, /prop="fileName" required :error="qualificationFileError"/)
|
||||||
assert.match(profileSource, /const valid = await profileFormRef\.value\?\.validate\(\)/)
|
assert.match(profileSource, /const valid = await profileFormRef\.value\?\.validate\(\)/)
|
||||||
assert.match(profileSource, /qualificationFileError\.value = '请选择营业执照图片'/)
|
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, /downloadQualificationImage/)
|
||||||
|
assert.match(profileSource, /当前资质文件尚未迁移到安全预览地址/)
|
||||||
|
assert.match(profileSource, /previewError/)
|
||||||
assert.match(profileSource, /URL\.createObjectURL/)
|
assert.match(profileSource, /URL\.createObjectURL/)
|
||||||
assert.match(profileSource, /URL\.revokeObjectURL/)
|
assert.match(profileSource, /URL\.revokeObjectURL/)
|
||||||
assert.match(profileSource, /企业资料加载失败/)
|
assert.match(profileSource, /企业资料加载失败/)
|
||||||
@@ -123,6 +128,12 @@ test('implements grouped menu and action permission editing', () => {
|
|||||||
assert.match(permissionsSource, /:loading="saving"/)
|
assert.match(permissionsSource, /:loading="saving"/)
|
||||||
assert.match(permissionsSource, /button:focus-visible/)
|
assert.match(permissionsSource, /button:focus-visible/)
|
||||||
assert.match(permissionsSource, /class="permission-actions" aria-label="权限操作"/)
|
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, /permission-heading/)
|
||||||
assert.doesNotMatch(permissionsSource, /section-label/)
|
assert.doesNotMatch(permissionsSource, /section-label/)
|
||||||
assert.doesNotMatch(permissionsSource, /当前只读展示权限资源/)
|
assert.doesNotMatch(permissionsSource, /当前只读展示权限资源/)
|
||||||
|
|||||||
Reference in New Issue
Block a user