144 lines
13 KiB
Vue
144 lines
13 KiB
Vue
<script setup lang="ts">
|
||
import '@/styles/adminDensity.css'
|
||
import { computed, onActivated, onDeactivated, reactive, ref, watch } from 'vue'
|
||
import { useRoute, useRouter } from 'vue-router'
|
||
import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
|
||
import { ArrowLeft, FileText, Save } from '@lucide/vue'
|
||
import AdminPage from '@/shared/components/AdminPage.vue'
|
||
import ApiErrorMeta from '@/shared/components/ApiErrorMeta.vue'
|
||
import OperationErrorToast from '@/shared/components/OperationErrorToast.vue'
|
||
import DetailPageLayout from '@/shared/components/DetailPageLayout.vue'
|
||
import FormSection from '@/shared/components/FormSection.vue'
|
||
import LoadErrorState from '@/shared/components/LoadErrorState.vue'
|
||
import { getApiErrorDetails, type ApiErrorDetails } from '@/shared/api/http'
|
||
import { useAccessPreviewStore } from '@/core/access/accessPreview'
|
||
import { useUnsavedWorkRegistration } from '@/core/unsaved-work/useUnsavedWorkRegistration'
|
||
import { createSupplier, getSupplier, updateSupplier } from '@/modules/suppliers/api/platformSuppliers'
|
||
import { truckingScopeLabels, carrierCodeOptions, categoryLabels, settlementLabels, statusLabels, type TruckingScope, type SaveSupplierRequest } from '@/modules/suppliers/types'
|
||
|
||
const route = useRoute()
|
||
const router = useRouter()
|
||
const access = useAccessPreviewStore()
|
||
const supplierId = computed(() => typeof route.params.id === 'string' ? route.params.id : '')
|
||
const editing = computed(() => Boolean(supplierId.value))
|
||
const requiredPermission = computed(() => editing.value ? 'platform:supplier:edit' : 'platform:supplier:create')
|
||
const formRef = ref<FormInstance>()
|
||
const loading = ref(false)
|
||
const saving = ref(false)
|
||
const loadError = ref<ApiErrorDetails | null>(null)
|
||
const submitError = ref<ApiErrorDetails | null>(null)
|
||
const initial = (): SaveSupplierRequest => ({ name: '', category: 'BOOKING', truckingScope: null, carrierCodes: [], region: '', legalRepresentative: '', contactName: '', phone: '', email: null, address: null, businessLicenseNo: null, licenseFileId: null, status: 'ACTIVE', settlementPeriod: 'MONTHLY_30', remark: null })
|
||
const form = reactive(initial())
|
||
const saveAttempted = ref(false)
|
||
const truckingScopeError = computed(() => saveAttempted.value && form.category === 'TRUCKING' && !form.truckingScope ? '请选择国内拖车或海外车队' : '')
|
||
const rules: FormRules<SaveSupplierRequest> = {
|
||
name: [{ required: true, message: '请输入供应商名称', trigger: 'blur' }], category: [{ required: true, message: '请选择供应商类别', trigger: 'change' }],
|
||
truckingScope: [{ required: true, type: 'enum', enum: ['DOMESTIC', 'OVERSEAS'], message: '请选择国内拖车或海外车队', trigger: 'change' }],
|
||
carrierCodes: [{ validator: (_rule, value, callback) => form.category === 'BOOKING' && (!Array.isArray(value) || value.length === 0) ? callback(new Error('请至少选择一家可代理船公司')) : callback(), trigger: 'change' }],
|
||
region: [{ required: true, message: '请输入地区', trigger: 'blur' }], legalRepresentative: [{ required: true, message: '请输入法人代表', trigger: 'blur' }],
|
||
contactName: [{ required: true, message: '请输入联系人', trigger: 'blur' }], phone: [{ required: true, message: '请输入联系电话', trigger: 'blur' }],
|
||
email: [{ type: 'email', message: '请输入正确的邮箱地址', trigger: 'blur' }], status: [{ required: true, message: '请选择合作状态', trigger: 'change' }],
|
||
settlementPeriod: [{ required: true, message: '请选择结算周期', trigger: 'change' }],
|
||
}
|
||
const { captureBaseline } = useUnsavedWorkRegistration({ key: 'supplier-editor', label: computed(() => editing.value ? '编辑供应商' : '新增供应商'), isOpen: computed(() => true), model: form, requiredPermission })
|
||
|
||
function optional(value: string | null) { return value?.trim() || null }
|
||
function payload(): SaveSupplierRequest {
|
||
const { carrierCodes, ...fields } = form
|
||
const normalized = { ...fields, truckingScope: form.category === 'TRUCKING' ? form.truckingScope : null, name: form.name.trim(), region: form.region.trim(), legalRepresentative: form.legalRepresentative.trim(), contactName: form.contactName.trim(), phone: form.phone.trim(), email: optional(form.email), address: optional(form.address), businessLicenseNo: optional(form.businessLicenseNo), licenseFileId: form.licenseFileId, remark: optional(form.remark) }
|
||
return form.category === 'BOOKING' ? { ...normalized, carrierCodes: [...(carrierCodes || [])] } : normalized
|
||
}
|
||
let loadSequence = 0
|
||
function resetForm() { Object.assign(form, initial()); saveAttempted.value = false }
|
||
async function load() {
|
||
const requestedId = supplierId.value
|
||
const sequence = ++loadSequence
|
||
resetForm()
|
||
loadError.value = null
|
||
submitError.value = null
|
||
saving.value = false
|
||
if (!requestedId || !access.can(requiredPermission.value)) { loading.value = false; captureBaseline(); return }
|
||
loading.value = true
|
||
try {
|
||
const data = await getSupplier(requestedId)
|
||
if (sequence !== loadSequence || requestedId !== supplierId.value) return
|
||
Object.assign(form, { name: data.name, category: data.category, truckingScope: data.truckingScope, carrierCodes: data.category === 'BOOKING' ? [...data.carrierCodes] : [], region: data.region, legalRepresentative: data.legalRepresentative, contactName: data.contactName, phone: data.phone, email: data.email, address: data.address, businessLicenseNo: data.businessLicenseNo, licenseFileId: data.licenseFileId, status: data.status, settlementPeriod: data.settlementPeriod, remark: data.remark })
|
||
captureBaseline()
|
||
} catch (error) {
|
||
if (sequence === loadSequence && requestedId === supplierId.value) loadError.value = getApiErrorDetails(error, '供应商详情加载失败')
|
||
} finally { if (sequence === loadSequence) loading.value = false }
|
||
}
|
||
async function save() {
|
||
if (!access.can(requiredPermission.value)) return
|
||
saveAttempted.value = true
|
||
if (truckingScopeError.value) return
|
||
if (!await formRef.value?.validate().catch(() => false)) return
|
||
saving.value = true; submitError.value = null
|
||
try {
|
||
if (editing.value) await updateSupplier(supplierId.value, payload())
|
||
else await createSupplier(payload())
|
||
captureBaseline(); ElMessage.success(editing.value ? '供应商修改成功' : '供应商新增成功')
|
||
await router.push({ name: 'supplier-list' })
|
||
} catch (error) { submitError.value = getApiErrorDetails(error, editing.value ? '供应商修改失败' : '供应商新增失败') } finally { saving.value = false }
|
||
}
|
||
|
||
let active = true
|
||
let activatedOnce = false
|
||
watch(supplierId, () => { if (active) void load() }, { immediate: true })
|
||
onActivated(() => {
|
||
active = true
|
||
if (activatedOnce) void load()
|
||
else activatedOnce = true
|
||
})
|
||
onDeactivated(() => { active = false })
|
||
watch(() => form.category, (category) => { if (category !== 'BOOKING') form.carrierCodes = []; if (category !== 'TRUCKING') form.truckingScope = null })
|
||
</script>
|
||
|
||
<template>
|
||
<AdminPage class="admin-density" >
|
||
<OperationErrorToast :details="submitError" />
|
||
<DetailPageLayout class="density-detail-layout surface-panel" :aria-label="editing ? '编辑供应商' : '新增供应商'">
|
||
<template #header><div class="density-detail-header"><div class="density-detail-heading"><el-button link class="density-back" @click="router.push({ name: 'supplier-list' })"><ArrowLeft :size="16" />返回列表</el-button><h1 class="density-detail-title">{{ editing ? '编辑供应商' : '新增供应商' }}</h1></div></div></template>
|
||
<LoadErrorState v-if="!access.can(requiredPermission)" title="暂无操作权限" :message="`请联系管理员开通${editing ? '供应商编辑' : '供应商新增'}权限。`" :show-retry="false" />
|
||
<LoadErrorState v-else-if="loadError" title="供应商加载失败" :message="loadError.message" :loading="loading" @retry="load"><template #details><ApiErrorMeta :details="loadError" /></template></LoadErrorState>
|
||
<section v-else v-loading="loading" class="supplier-form-panel">
|
||
<el-form class="density-form" ref="formRef" :model="form" :rules="rules" label-position="top" :disabled="saving">
|
||
<FormSection class="density-form-section" title="基础信息">
|
||
<div class="form-grid">
|
||
<el-form-item label="供应商名称" prop="name"><el-input v-model="form.name" maxlength="160" placeholder="请输入供应商名称" /></el-form-item>
|
||
<el-form-item label="供应商类别" prop="category"><el-select v-model="form.category"><el-option v-for="(label, value) in categoryLabels" :key="value" :label="label" :value="value" /></el-select></el-form-item>
|
||
<el-form-item v-if="form.category === 'BOOKING'" label="可代理船公司" prop="carrierCodes"><el-select v-model="form.carrierCodes" multiple collapse-tags collapse-tags-tooltip placeholder="请选择可代理船公司"><el-option v-for="option in carrierCodeOptions" :key="option.value" :label="option.label" :value="option.value" /></el-select></el-form-item>
|
||
<el-form-item v-if="form.category === 'TRUCKING'" label="拖车类型" prop="truckingScope" :error="truckingScopeError" required><el-radio-group :model-value="form.truckingScope ?? undefined" @update:model-value="form.truckingScope = $event as TruckingScope"><el-radio v-for="(label, value) in truckingScopeLabels" :key="value" :value="value">{{ label }}</el-radio></el-radio-group></el-form-item>
|
||
<el-form-item label="地区" prop="region"><el-input v-model="form.region" maxlength="80" placeholder="请输入地区" /></el-form-item>
|
||
<el-form-item label="法人代表" prop="legalRepresentative"><el-input v-model="form.legalRepresentative" maxlength="80" placeholder="请输入法人代表" /></el-form-item>
|
||
<el-form-item label="联系人" prop="contactName"><el-input v-model="form.contactName" maxlength="80" placeholder="请输入联系人" /></el-form-item>
|
||
<el-form-item label="联系电话" prop="phone"><el-input v-model="form.phone" maxlength="40" placeholder="请输入联系电话" /></el-form-item>
|
||
<el-form-item label="电子邮箱" prop="email"><el-input v-model="form.email" maxlength="160" placeholder="请输入邮箱" /></el-form-item>
|
||
<el-form-item label="合作状态" prop="status"><el-select v-model="form.status"><el-option v-for="(label, value) in statusLabels" :key="value" :label="label" :value="value" /></el-select></el-form-item>
|
||
<el-form-item label="结算周期" prop="settlementPeriod"><el-select v-model="form.settlementPeriod"><el-option v-for="(label, value) in settlementLabels" :key="value" :label="label" :value="value" /></el-select></el-form-item>
|
||
<el-form-item class="span-2" label="公司地址" prop="address"><el-input v-model="form.address" maxlength="255" placeholder="请输入公司地址" /></el-form-item>
|
||
</div>
|
||
</FormSection>
|
||
<FormSection class="density-form-section" title="营业执照">
|
||
<div class="form-grid">
|
||
<el-form-item label="营业执照号" prop="businessLicenseNo"><el-input v-model="form.businessLicenseNo" maxlength="64" placeholder="统一社会信用代码" /></el-form-item>
|
||
<el-form-item label="资质文件"><div class="file-placeholder"><FileText :size="24" /><span>{{ form.licenseFileId ? '已保留现有营业执照文件' : '暂无营业执照文件' }}</span><small>平台公共供应商资质上传接口尚未提供,当前不能新增或替换文件。</small></div></el-form-item>
|
||
</div>
|
||
</FormSection>
|
||
<FormSection class="density-form-section" title="其他信息"><el-form-item label="备注" prop="remark"><el-input v-model="form.remark" type="textarea" :rows="3" maxlength="500" show-word-limit placeholder="请输入备注" /></el-form-item></FormSection>
|
||
</el-form>
|
||
<footer class="form-actions"><el-button type="primary" :loading="saving" @click="save"><Save :size="16" />保存</el-button><el-button :disabled="saving" @click="router.push({ name: 'supplier-list' })">取消</el-button></footer>
|
||
</section>
|
||
</DetailPageLayout>
|
||
</AdminPage>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.supplier-form-panel { min-width: 0; }.form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 var(--admin-space-7); }.span-2 { grid-column: 1 / -1; }
|
||
.form-grid :deep(.el-select) { width: 100%; }.file-placeholder { display: flex; width: 100%; min-height: 96px; align-items: center; justify-content: center; flex-direction: column; gap: var(--admin-space-2); padding: var(--admin-space-6); border: 1px dashed #d9d9d9; border-radius: var(--admin-radius); background: #fafafa; color: var(--cc-muted); text-align: center; }.file-placeholder small { max-width: 420px; line-height: 1.5; }
|
||
.form-actions { display: flex; justify-content: center; gap: var(--admin-space-5); padding: var(--admin-space-6) 0 0; border-top: 1px solid var(--cc-border); }.form-actions :deep(.el-button) { min-width: 112px; margin-left: 0; }
|
||
@media (min-width: 1600px) { .form-grid { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
|
||
@media (min-width: 1280px) and (max-width: 1599px) { .form-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
|
||
@media (max-width: 720px) { .supplier-form-panel { padding: 16px; } .form-grid { grid-template-columns: 1fr; }.span-2 { grid-column: auto; }.form-page-header { align-items: stretch; flex-direction: column; }.form-actions { flex-direction: column; }.form-actions :deep(.el-button) { width: 100%; min-height: 44px; } }
|
||
</style>
|