Files
admin-web/src/views/ExpressCompaniesView.vue
T
2026-07-30 21:23:39 +08:00

154 lines
13 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 { 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>