chore: 初始化平台管理端
This commit is contained in:
@@ -0,0 +1,431 @@
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<ElTag v-if="checkResult" :type="checkResult.isComplete ? 'success' : 'warning'">
|
||||
{{
|
||||
checkResult.isComplete
|
||||
? t('store.qualification.complete')
|
||||
: t('store.qualification.incomplete')
|
||||
}}
|
||||
</ElTag>
|
||||
<ElTag v-if="checkResult" type="info">
|
||||
{{ t('store.qualification.expiringSoon', { count: checkResult.expiringSoonCount }) }}
|
||||
</ElTag>
|
||||
<ElTag v-if="checkResult" type="danger">
|
||||
{{ t('store.qualification.expired', { count: checkResult.expiredCount }) }}
|
||||
</ElTag>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<ElButton @click="loadData">{{ t('store.action.refresh') }}</ElButton>
|
||||
<ElButton type="primary" @click="handleCreate">
|
||||
{{ t('store.qualification.action.add') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ElAlert
|
||||
v-if="checkResult && checkResult.warnings.length > 0"
|
||||
type="warning"
|
||||
:title="t('store.qualification.warningTitle')"
|
||||
show-icon
|
||||
class="mb-2"
|
||||
>
|
||||
<template #default>
|
||||
<ul class="list-disc pl-4">
|
||||
<li v-for="item in checkResult.warnings" :key="item">{{ item }}</li>
|
||||
</ul>
|
||||
</template>
|
||||
</ElAlert>
|
||||
|
||||
<ElTable :data="qualifications" border stripe v-loading="loading">
|
||||
<ElTableColumn :label="t('store.qualification.table.type')" min-width="140">
|
||||
<template #default="{ row }">
|
||||
{{ getQualificationTypeText(row.qualificationType) }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn :label="t('store.qualification.table.documentNumber')" min-width="160">
|
||||
<template #default="{ row }">
|
||||
{{ row.documentNumber || '-' }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn :label="t('store.qualification.table.expiresAt')" min-width="140">
|
||||
<template #default="{ row }">
|
||||
{{ formatDate(row.expiresAt) }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn :label="t('store.qualification.table.status')" width="120">
|
||||
<template #default="{ row }">
|
||||
<ElTag :type="getStatusType(row)">
|
||||
{{ getStatusText(row) }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn :label="t('common.action')" width="160">
|
||||
<template #default="{ row }">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<ElButton link type="primary" @click="handleEdit(row)">
|
||||
{{ t('store.action.edit') }}
|
||||
</ElButton>
|
||||
<ElButton link type="danger" @click="handleDelete(row)">
|
||||
{{ t('store.action.delete') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
|
||||
<ElDialog
|
||||
v-model="dialogVisible"
|
||||
:title="
|
||||
editingItem ? t('store.qualification.action.edit') : t('store.qualification.action.add')
|
||||
"
|
||||
width="560px"
|
||||
destroy-on-close
|
||||
>
|
||||
<ElForm ref="formRef" :model="formModel" :rules="formRules" label-width="110px">
|
||||
<ElFormItem :label="t('store.qualification.form.type')" prop="qualificationType">
|
||||
<ElSelect v-model="formModel.qualificationType" class="w-full">
|
||||
<ElOption
|
||||
v-for="option in qualificationTypeOptions"
|
||||
:key="option.value"
|
||||
:label="option.label"
|
||||
:value="option.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem :label="t('store.qualification.form.fileUrl')" prop="fileUrl">
|
||||
<ImageUpload
|
||||
v-model="formModel.fileUrl"
|
||||
:placeholder="t('store.qualification.form.fileUrlPlaceholder')"
|
||||
:upload-type="resolveUploadType(formModel.qualificationType)"
|
||||
box-width="100%"
|
||||
box-height="160px"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem :label="t('store.qualification.form.documentNumber')" prop="documentNumber">
|
||||
<ElInput
|
||||
v-model="formModel.documentNumber"
|
||||
:placeholder="t('store.qualification.form.documentNumberPlaceholder')"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem :label="t('store.qualification.form.issuedAt')" prop="issuedAt">
|
||||
<ElDatePicker v-model="formModel.issuedAt" type="date" value-format="YYYY-MM-DD" />
|
||||
</ElFormItem>
|
||||
<ElFormItem :label="t('store.qualification.form.expiresAt')" prop="expiresAt">
|
||||
<ElDatePicker v-model="formModel.expiresAt" type="date" value-format="YYYY-MM-DD" />
|
||||
</ElFormItem>
|
||||
<ElFormItem :label="t('store.qualification.form.sortOrder')" prop="sortOrder">
|
||||
<ElInputNumber v-model="formModel.sortOrder" class="w-full" :min="0" />
|
||||
</ElFormItem>
|
||||
</ElForm>
|
||||
<template #footer>
|
||||
<ElButton @click="dialogVisible = false">{{ t('common.cancel') }}</ElButton>
|
||||
<ElButton type="primary" :loading="saving" @click="handleSubmit">
|
||||
{{ t('common.confirm') }}
|
||||
</ElButton>
|
||||
</template>
|
||||
</ElDialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import {
|
||||
ElAlert,
|
||||
ElButton,
|
||||
ElDatePicker,
|
||||
ElDialog,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInputNumber,
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
ElOption,
|
||||
ElSelect,
|
||||
ElTable,
|
||||
ElTableColumn,
|
||||
ElTag
|
||||
} from 'element-plus'
|
||||
import {
|
||||
fetchCheckStoreQualifications,
|
||||
fetchCreateStoreQualification,
|
||||
fetchDeleteStoreQualification,
|
||||
fetchStoreQualifications,
|
||||
fetchUpdateStoreQualification
|
||||
} from '@/api/store'
|
||||
import { StoreQualificationType } from '@/enums/StoreQualificationType'
|
||||
import { formatDate } from '@/utils/billing'
|
||||
import ImageUpload from '@/components/common/ImageUpload.vue'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
|
||||
defineOptions({ name: 'StoreQualificationPanel' })
|
||||
|
||||
interface QualificationFormState {
|
||||
qualificationType: StoreQualificationType
|
||||
fileUrl: string
|
||||
documentNumber?: string
|
||||
issuedAt?: string
|
||||
expiresAt?: string
|
||||
sortOrder: number
|
||||
}
|
||||
|
||||
// 1. 基础输入
|
||||
const props = defineProps<{ storeId: string; tenantId?: string }>()
|
||||
const { t } = useI18n()
|
||||
|
||||
const createDefaultForm = (): QualificationFormState => ({
|
||||
qualificationType: StoreQualificationType.BusinessLicense,
|
||||
fileUrl: '',
|
||||
documentNumber: '',
|
||||
issuedAt: '',
|
||||
expiresAt: '',
|
||||
sortOrder: 100
|
||||
})
|
||||
|
||||
const isLicenseType = (type: StoreQualificationType) =>
|
||||
type === StoreQualificationType.BusinessLicense ||
|
||||
type === StoreQualificationType.FoodServiceLicense
|
||||
|
||||
// 2. 资质数据
|
||||
const loading = ref(false)
|
||||
const qualifications = ref<Api.Store.StoreQualificationDto[]>([])
|
||||
const checkResult = ref<Api.Store.StoreQualificationCheckResultDto | null>(null)
|
||||
|
||||
// 3. 表单状态
|
||||
const dialogVisible = ref(false)
|
||||
const saving = ref(false)
|
||||
const editingItem = ref<Api.Store.StoreQualificationDto | null>(null)
|
||||
const formRef = ref<FormInstance>()
|
||||
const formModel = ref<QualificationFormState>(createDefaultForm())
|
||||
|
||||
// 4. 选项配置
|
||||
const qualificationTypeOptions = [
|
||||
{
|
||||
value: StoreQualificationType.BusinessLicense,
|
||||
label: t('store.qualification.type.businessLicense')
|
||||
},
|
||||
{
|
||||
value: StoreQualificationType.FoodServiceLicense,
|
||||
label: t('store.qualification.type.foodService')
|
||||
},
|
||||
{
|
||||
value: StoreQualificationType.StorefrontPhoto,
|
||||
label: t('store.qualification.type.storefront')
|
||||
},
|
||||
{ value: StoreQualificationType.InteriorPhoto, label: t('store.qualification.type.interior') }
|
||||
]
|
||||
|
||||
const resolveUploadType = (type: StoreQualificationType): Api.Files.UploadFileType => {
|
||||
// 1. 证照类统一按营业执照类型上传
|
||||
if (isLicenseType(type)) {
|
||||
return 'business_license'
|
||||
}
|
||||
|
||||
// 2. 其他图片类型统一归类为 other
|
||||
return 'other'
|
||||
}
|
||||
|
||||
const resolveRequestOptions = () => {
|
||||
// 1. 未提供租户ID时使用默认请求配置
|
||||
if (!props.tenantId) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
// 2. 携带租户上下文执行请求
|
||||
return { tenantId: props.tenantId }
|
||||
}
|
||||
|
||||
// 5. 校验规则
|
||||
const formRules: FormRules<QualificationFormState> = {
|
||||
qualificationType: [
|
||||
{ required: true, message: t('store.qualification.rules.type'), trigger: 'change' }
|
||||
],
|
||||
fileUrl: [
|
||||
{ required: true, message: t('store.qualification.rules.fileUrl'), trigger: 'change' }
|
||||
],
|
||||
documentNumber: [
|
||||
{
|
||||
validator: (_, value, callback) => {
|
||||
if (!isLicenseType(formModel.value.qualificationType)) {
|
||||
callback()
|
||||
return
|
||||
}
|
||||
if (!value || !value.trim()) {
|
||||
callback(new Error(t('store.qualification.rules.documentNumber')))
|
||||
return
|
||||
}
|
||||
callback()
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
expiresAt: [
|
||||
{
|
||||
validator: (_, value, callback) => {
|
||||
if (!isLicenseType(formModel.value.qualificationType) && !value) {
|
||||
callback()
|
||||
return
|
||||
}
|
||||
if (!value) {
|
||||
callback(new Error(t('store.qualification.rules.expiresAt')))
|
||||
return
|
||||
}
|
||||
callback()
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// 6. 初始化加载
|
||||
const loadData = async () => {
|
||||
if (!props.storeId) {
|
||||
return
|
||||
}
|
||||
loading.value = true
|
||||
try {
|
||||
// 1. 组装租户上下文
|
||||
const requestOptions = resolveRequestOptions()
|
||||
|
||||
// 2. 并行加载资质与完整性检查
|
||||
const [list, check] = await Promise.all([
|
||||
fetchStoreQualifications(props.storeId, requestOptions),
|
||||
fetchCheckStoreQualifications(props.storeId, requestOptions)
|
||||
])
|
||||
qualifications.value = list
|
||||
checkResult.value = check
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [props.storeId, props.tenantId],
|
||||
() => {
|
||||
loadData()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
// 7. 交互处理
|
||||
const handleCreate = () => {
|
||||
editingItem.value = null
|
||||
formModel.value = createDefaultForm()
|
||||
dialogVisible.value = true
|
||||
}
|
||||
const handleEdit = (row: Api.Store.StoreQualificationDto) => {
|
||||
editingItem.value = row
|
||||
formModel.value = {
|
||||
qualificationType: row.qualificationType,
|
||||
fileUrl: row.fileUrl,
|
||||
documentNumber: row.documentNumber ?? '',
|
||||
issuedAt: row.issuedAt ?? '',
|
||||
expiresAt: row.expiresAt ?? '',
|
||||
sortOrder: row.sortOrder
|
||||
}
|
||||
dialogVisible.value = true
|
||||
}
|
||||
const handleDelete = async (row: Api.Store.StoreQualificationDto) => {
|
||||
// 1. 二次确认
|
||||
await ElMessageBox.confirm(
|
||||
t('store.qualification.deleteConfirm'),
|
||||
t('store.qualification.deleteTitle'),
|
||||
{
|
||||
type: 'warning'
|
||||
}
|
||||
)
|
||||
|
||||
// 2. 执行删除
|
||||
const requestOptions = resolveRequestOptions()
|
||||
await fetchDeleteStoreQualification(props.storeId, row.id, requestOptions)
|
||||
ElMessage.success(t('store.message.deleteSuccess'))
|
||||
loadData()
|
||||
}
|
||||
const handleSubmit = async () => {
|
||||
if (!formRef.value) {
|
||||
return
|
||||
}
|
||||
|
||||
// 1. 校验表单
|
||||
await formRef.value.validate(async (valid) => {
|
||||
if (!valid) {
|
||||
return
|
||||
}
|
||||
|
||||
// 2. 执行保存
|
||||
saving.value = true
|
||||
try {
|
||||
const requestOptions = resolveRequestOptions()
|
||||
|
||||
if (editingItem.value) {
|
||||
await fetchUpdateStoreQualification(
|
||||
props.storeId,
|
||||
editingItem.value.id,
|
||||
{
|
||||
fileUrl: formModel.value.fileUrl.trim(),
|
||||
documentNumber: formModel.value.documentNumber?.trim() || undefined,
|
||||
issuedAt: formModel.value.issuedAt || undefined,
|
||||
expiresAt: formModel.value.expiresAt || undefined,
|
||||
sortOrder: formModel.value.sortOrder
|
||||
},
|
||||
requestOptions
|
||||
)
|
||||
ElMessage.success(t('store.message.updateSuccess'))
|
||||
} else {
|
||||
await fetchCreateStoreQualification(
|
||||
props.storeId,
|
||||
{
|
||||
qualificationType: formModel.value.qualificationType,
|
||||
fileUrl: formModel.value.fileUrl.trim(),
|
||||
documentNumber: formModel.value.documentNumber?.trim() || undefined,
|
||||
issuedAt: formModel.value.issuedAt || undefined,
|
||||
expiresAt: formModel.value.expiresAt || undefined,
|
||||
sortOrder: formModel.value.sortOrder
|
||||
},
|
||||
requestOptions
|
||||
)
|
||||
ElMessage.success(t('store.message.createSuccess'))
|
||||
}
|
||||
dialogVisible.value = false
|
||||
loadData()
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
// 8. 显示格式
|
||||
const getQualificationTypeText = (type: StoreQualificationType) => {
|
||||
switch (type) {
|
||||
case StoreQualificationType.BusinessLicense:
|
||||
return t('store.qualification.type.businessLicense')
|
||||
case StoreQualificationType.FoodServiceLicense:
|
||||
return t('store.qualification.type.foodService')
|
||||
case StoreQualificationType.StorefrontPhoto:
|
||||
return t('store.qualification.type.storefront')
|
||||
case StoreQualificationType.InteriorPhoto:
|
||||
return t('store.qualification.type.interior')
|
||||
default:
|
||||
return '-'
|
||||
}
|
||||
}
|
||||
const getStatusType = (row: Api.Store.StoreQualificationDto) => {
|
||||
if (row.isExpired) {
|
||||
return 'danger'
|
||||
}
|
||||
if (row.isExpiringSoon) {
|
||||
return 'warning'
|
||||
}
|
||||
return 'success'
|
||||
}
|
||||
const getStatusText = (row: Api.Store.StoreQualificationDto) => {
|
||||
if (row.isExpired) {
|
||||
return t('store.qualification.status.expired')
|
||||
}
|
||||
if (row.isExpiringSoon) {
|
||||
return t('store.qualification.status.expiringSoon')
|
||||
}
|
||||
return t('store.qualification.status.valid')
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user