432 lines
14 KiB
Vue
432 lines
14 KiB
Vue
<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>
|