Files
admin-web/src/modules/suppliers/pages/SupplierFormView.vue
T
2026-08-18 09:09:34 +08:00

125 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { computed, 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 { categoryLabels, settlementLabels, statusLabels, 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', region: '', legalRepresentative: '', contactName: '', phone: '', email: null, address: null, businessLicenseNo: null, licenseFileId: null, status: 'ACTIVE', settlementPeriod: 'MONTHLY_30', remark: null })
const form = reactive(initial())
const rules: FormRules<SaveSupplierRequest> = {
name: [{ required: true, message: '请输入供应商名称', trigger: 'blur' }], category: [{ required: true, message: '请选择供应商类别', 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 {
return { ...form, 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) }
}
let loadSequence = 0
function resetForm() { Object.assign(form, initial()) }
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, 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
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 }
}
watch(supplierId, () => { void load() }, { immediate: true })
</script>
<template>
<AdminPage width="comfortable">
<OperationErrorToast :details="submitError" />
<DetailPageLayout :aria-label="editing ? '编辑供应商' : '新增供应商'">
<template #header><div class="form-page-header"><h2>{{ editing ? '编辑供应商' : '新增供应商' }}</h2><el-button @click="router.push({ name: 'supplier-list' })"><ArrowLeft :size="16" />返回</el-button></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="surface-panel supplier-form-panel">
<el-form ref="formRef" :model="form" :rules="rules" label-position="top" :disabled="saving">
<FormSection 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 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 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 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>
.form-page-header { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 16px 20px; border: 1px solid var(--cc-border); border-radius: 8px; background: #fff; }.form-page-header h2 { margin: 0; font-size: 16px; }
.supplier-form-panel { padding: 22px; }.form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0 20px; }.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: 6px; padding: 16px; border: 1px dashed #d9d9d9; border-radius: 6px; 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: 14px; padding: 18px 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>