chore: 初始化平台管理端

This commit is contained in:
msumshk
2026-01-29 04:21:09 +00:00
commit 914dcc4166
533 changed files with 104838 additions and 0 deletions

View File

@@ -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>