154 lines
13 KiB
Vue
154 lines
13 KiB
Vue
<script setup lang="ts">
|
||
import { computed, reactive, ref, watch } from 'vue'
|
||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||
import { ExternalLink, FileUp, Pencil, Phone, Plus, RotateCcw, Search, Truck } from '@lucide/vue'
|
||
import MetricStrip from '@/components/MetricStrip.vue'
|
||
import StaticBatchImportDialog from '@/components/StaticBatchImportDialog.vue'
|
||
import StatusBadge from '@/components/StatusBadge.vue'
|
||
import { useUnsavedWorkRegistration } from '@/composables/useUnsavedWorkRegistration'
|
||
import { useAccessPreviewStore } from '@/stores/accessPreview'
|
||
import { useAdminDataStore } from '@/stores/adminData'
|
||
import type { ExpressCompany, ExpressCompanyStatus } from '@/types/admin'
|
||
|
||
const store = useAdminDataStore()
|
||
const access = useAccessPreviewStore()
|
||
const detailOpen = ref(false)
|
||
const editorOpen = ref(false)
|
||
const importOpen = ref(false)
|
||
const selectedCompany = ref<ExpressCompany | null>(null)
|
||
const page = ref(1)
|
||
const pageSize = 10
|
||
const filters = reactive({ keyword: '', status: '' as ExpressCompanyStatus | '' })
|
||
const emptyForm = (): Omit<ExpressCompany, 'updatedAt'> => ({ id: '', name: '', code: '', website: '', phone: '', status: '合作中', remark: '' })
|
||
const form = reactive(emptyForm())
|
||
const editorLabel = computed(() => form.id ? '编辑快递公司' : '新增快递公司')
|
||
const { captureBaseline } = useUnsavedWorkRegistration({
|
||
key: 'express-company-editor',
|
||
label: editorLabel,
|
||
isOpen: editorOpen,
|
||
model: form,
|
||
requiredPermission: computed(() => form.id ? null : 'express-company:write'),
|
||
})
|
||
|
||
const filteredRows = computed(() => store.expressCompanies.filter((company) => {
|
||
const keyword = filters.keyword.trim().toLowerCase()
|
||
const matchesKeyword = !keyword || [company.name, company.code, company.website, company.phone]
|
||
.some((value) => value.toLowerCase().includes(keyword))
|
||
return matchesKeyword && (!filters.status || company.status === filters.status)
|
||
}))
|
||
const pagedRows = computed(() => filteredRows.value.slice((page.value - 1) * pageSize, page.value * pageSize))
|
||
const metrics = computed(() => [
|
||
{ label: '全部快递公司', value: store.expressCompanies.length, note: '平台快递档案' },
|
||
{ label: '合作中', value: store.activeExpressCompanies, note: '可新增到履约信息' },
|
||
{ label: '已终止', value: store.expressCompanies.length - store.activeExpressCompanies, note: '历史订单继续展示' },
|
||
{ label: '官网已登记', value: store.expressCompanies.filter((item) => item.website).length, note: '公开查询入口' },
|
||
])
|
||
|
||
watch(filters, () => { page.value = 1 })
|
||
|
||
function resetFilters() { filters.keyword = ''; filters.status = '' }
|
||
function openDetail(company: ExpressCompany) { selectedCompany.value = company; detailOpen.value = true }
|
||
function openCreate() { Object.assign(form, emptyForm()); captureBaseline(); editorOpen.value = true }
|
||
function openEdit(company: ExpressCompany) { Object.assign(form, company); captureBaseline(); detailOpen.value = false; editorOpen.value = true }
|
||
function websiteUrl(company: ExpressCompany) { return /^https?:\/\//i.test(company.website) ? company.website : `https://${company.website}` }
|
||
|
||
function saveCompany() {
|
||
if (!form.name.trim() || !form.code.trim()) { ElMessage.warning('请填写快递公司名称和稳定编码'); return }
|
||
try {
|
||
store.saveExpressCompany({ ...form, id: form.id || undefined })
|
||
editorOpen.value = false
|
||
ElMessage.success(form.id ? '快递公司已更新' : '快递公司已创建')
|
||
} catch (error) {
|
||
ElMessage.error(error instanceof Error ? error.message : '快递公司保存失败')
|
||
}
|
||
}
|
||
|
||
async function toggleStatus(company: ExpressCompany) {
|
||
const target: ExpressCompanyStatus = company.status === '合作中' ? '已终止' : '合作中'
|
||
try {
|
||
await ElMessageBox.confirm(
|
||
target === '已终止' ? '停用后不能新增到订单履约信息,历史订单仍保留公司名称和编码。' : '启用后可重新用于订单履约信息。',
|
||
`${target === '已终止' ? '停用' : '启用'}快递公司“${company.name}”`,
|
||
{ confirmButtonText: target === '已终止' ? '确认停用' : '确认启用', cancelButtonText: '取消', type: 'warning' },
|
||
)
|
||
store.setExpressCompanyStatus(company.id, target)
|
||
ElMessage.success(target === '已终止' ? '快递公司已停用' : '快递公司已启用')
|
||
} catch {
|
||
// 用户取消,不需要反馈。
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<!-- THESIS: 把快递档案升级为可维护、可批量校验的履约主数据,而不是停留在只读名录。
|
||
OWN-WORLD: 继承 Admin 的白色数据面板、海军蓝导航、紧凑表格、克制状态色和右侧抽屉。
|
||
STORY: 平台人员筛选档案,维护唯一编码与名称,控制启停,并按标准模板执行整批导入。
|
||
FIRST VIEWPORT: 摘要、双条件筛选、导入与新增动作、完整名录连续排列。
|
||
FORM: 现有 Operate 页面结构的深化,详情与编辑分层,导入使用受保护对话框。
|
||
FINISH: unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, and DESIGN.md -->
|
||
<div class="business-page express-company-page">
|
||
<MetricStrip :items="metrics" />
|
||
|
||
<section class="surface-panel filter-panel" aria-label="快递公司筛选">
|
||
<div class="filter-fields">
|
||
<el-input v-model="filters.keyword" clearable placeholder="公司名称、编码、官网或电话" aria-label="快递公司关键词"><template #prefix><Search :size="16" /></template></el-input>
|
||
<el-select v-model="filters.status" clearable placeholder="全部状态" aria-label="合作状态"><el-option label="合作中" value="合作中" /><el-option label="已终止" value="已终止" /></el-select>
|
||
</div>
|
||
<div class="filter-actions">
|
||
<el-button @click="resetFilters"><RotateCcw :size="16" />重置</el-button>
|
||
<el-button v-if="access.can('express-company:import')" @click="importOpen = true"><FileUp :size="16" />批量导入</el-button>
|
||
<el-button v-if="access.can('express-company:write')" type="primary" @click="openCreate"><Plus :size="16" />新增快递公司</el-button>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="surface-panel table-panel">
|
||
<div class="panel-heading"><div><h2>快递公司名录</h2><p>编码和名称必须唯一;停用后不允许新增引用,历史订单快照不受影响。</p></div></div>
|
||
<el-table :data="pagedRows" row-key="id" empty-text="没有符合条件的快递公司">
|
||
<el-table-column label="操作" width="220" fixed="left"><template #default="{ row }"><div class="table-actions"><el-button link type="primary" @click="openDetail(row as ExpressCompany)">详情</el-button><el-button v-if="access.can('express-company:write')" link type="primary" @click="openEdit(row as ExpressCompany)"><Pencil :size="15" />编辑</el-button><el-button v-if="access.can('express-company:write')" link :type="(row as ExpressCompany).status === '合作中' ? 'danger' : 'success'" @click="toggleStatus(row as ExpressCompany)">{{ (row as ExpressCompany).status === '合作中' ? '停用' : '启用' }}</el-button></div></template></el-table-column>
|
||
<el-table-column label="公司名称" min-width="190"><template #default="{ row }"><div class="express-company-cell"><span><Truck :size="18" /></span><div><strong>{{ (row as ExpressCompany).name }}</strong><small>{{ (row as ExpressCompany).id }} · {{ (row as ExpressCompany).updatedAt }}</small></div></div></template></el-table-column>
|
||
<el-table-column label="编码" min-width="110"><template #default="{ row }"><code class="code-value">{{ (row as ExpressCompany).code }}</code></template></el-table-column>
|
||
<el-table-column label="官网" min-width="220"><template #default="{ row }"><a v-if="(row as ExpressCompany).website" class="company-link" :href="websiteUrl(row as ExpressCompany)" target="_blank" rel="noopener noreferrer">{{ (row as ExpressCompany).website }}<ExternalLink :size="14" /></a><span v-else>—</span></template></el-table-column>
|
||
<el-table-column label="客服电话" min-width="150"><template #default="{ row }"><span class="phone-value"><Phone :size="14" />{{ (row as ExpressCompany).phone || '—' }}</span></template></el-table-column>
|
||
<el-table-column label="状态" width="100"><template #default="{ row }"><StatusBadge :status="(row as ExpressCompany).status" /></template></el-table-column>
|
||
</el-table>
|
||
<div class="table-footer"><span>显示 {{ pagedRows.length }} 条,共 {{ filteredRows.length }} 条</span><el-pagination v-model:current-page="page" :page-size="pageSize" :total="filteredRows.length" layout="prev, pager, next" /></div>
|
||
</section>
|
||
|
||
<el-drawer v-model="editorOpen" :title="form.id ? '编辑快递公司' : '新增快递公司'" size="620px" class="business-drawer editor-drawer" :modal="false" :close-on-click-modal="false">
|
||
<el-form label-position="top" class="drawer-form"><div class="form-section"><h3>档案信息</h3><div class="form-grid two-cols">
|
||
<el-form-item label="公司名称" required><el-input v-model="form.name" placeholder="请输入快递公司名称" maxlength="80" /></el-form-item>
|
||
<el-form-item label="稳定编码" required><el-input v-model="form.code" placeholder="如 SF" maxlength="30" /></el-form-item>
|
||
<el-form-item label="官方查询网址"><el-input v-model="form.website" placeholder="如 www.sf-express.com" maxlength="160" /></el-form-item>
|
||
<el-form-item label="客服电话"><el-input v-model="form.phone" maxlength="40" /></el-form-item>
|
||
<el-form-item label="状态"><el-radio-group v-model="form.status"><el-radio-button label="合作中" value="合作中" /><el-radio-button label="已终止" value="已终止" /></el-radio-group></el-form-item>
|
||
</div><el-form-item label="内部备注"><el-input v-model="form.remark" type="textarea" :rows="3" maxlength="300" show-word-limit /></el-form-item></div></el-form>
|
||
<template #footer><div class="drawer-footer"><el-button @click="editorOpen = false">取消</el-button><el-button v-if="access.can('express-company:write')" type="primary" @click="saveCompany">保存快递公司</el-button></div></template>
|
||
</el-drawer>
|
||
|
||
<el-drawer v-model="detailOpen" title="快递公司详情" size="520px" class="business-drawer" :close-on-click-modal="false">
|
||
<template v-if="selectedCompany"><div class="detail-identity"><span><Truck :size="20" /></span><div><h2>{{ selectedCompany.name }}</h2><p>{{ selectedCompany.code }} · {{ selectedCompany.id }}</p></div><StatusBadge :status="selectedCompany.status" /></div><section class="detail-section"><h3>公开联系信息</h3><dl class="company-detail-list"><div><dt>公司官网</dt><dd><a v-if="selectedCompany.website" class="company-link" :href="websiteUrl(selectedCompany)" target="_blank" rel="noopener noreferrer">{{ selectedCompany.website }}<ExternalLink :size="14" /></a><span v-else>—</span></dd></div><div><dt>客服电话</dt><dd>{{ selectedCompany.phone || '—' }}</dd></div><div><dt>更新时间</dt><dd>{{ selectedCompany.updatedAt }}</dd></div></dl><p class="detail-note">{{ selectedCompany.remark || '暂无内部备注。' }}</p></section></template>
|
||
<template #footer><div class="drawer-footer"><el-button @click="detailOpen = false">关闭</el-button><el-button v-if="access.can('express-company:write') && selectedCompany" type="primary" @click="openEdit(selectedCompany)">编辑资料</el-button></div></template>
|
||
</el-drawer>
|
||
|
||
<StaticBatchImportDialog v-model="importOpen" entity-label="快递公司" template-url="/templates/express-companies-import-template.xlsx" template-file-name="快递公司导入标准模板-v1.xlsx" />
|
||
</div>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.express-company-cell { display: flex; min-width: 0; align-items: center; gap: 10px; }
|
||
.express-company-cell > span { display: grid; width: 34px; height: 34px; flex: 0 0 34px; place-items: center; background: #eef3fa; color: var(--cc-primary); }
|
||
.express-company-cell > div { display: flex; min-width: 0; flex-direction: column; gap: 3px; }
|
||
.express-company-cell strong, .express-company-cell small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||
.express-company-cell small { color: var(--cc-muted); font-size: 11px; }
|
||
.company-link { display: inline-flex; max-width: 100%; align-items: center; gap: 5px; color: var(--cc-primary); text-decoration: none; overflow-wrap: anywhere; }
|
||
.company-link:hover { text-decoration: underline; }
|
||
.company-link:focus-visible { outline: 3px solid rgba(21, 94, 239, .2); outline-offset: 2px; }
|
||
.phone-value { display: inline-flex; align-items: center; gap: 6px; color: #344054; }
|
||
.phone-value svg { color: var(--cc-muted); }
|
||
.company-detail-list { margin: 0; }
|
||
.company-detail-list > div { display: grid; grid-template-columns: 92px minmax(0, 1fr); gap: 16px; padding: 14px 0; border-bottom: 1px solid var(--cc-border); }
|
||
.company-detail-list dt { color: var(--cc-muted); font-size: 12px; }
|
||
.company-detail-list dd { min-width: 0; margin: 0; color: #172033; font-size: 13px; font-weight: 650; }
|
||
@media (max-width: 420px) { .company-detail-list > div { grid-template-columns: 1fr; gap: 6px; } }
|
||
</style>
|