chore: initial commit

This commit is contained in:
2026-07-30 17:59:43 +08:00
commit dd0d365b03
73 changed files with 9898 additions and 0 deletions
+190
View File
@@ -0,0 +1,190 @@
<script setup lang="ts">
import { computed, reactive, ref, watch } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Download, Eye, Pencil, Plus, RotateCcw, Search } from '@lucide/vue'
import MetricStrip from '@/components/MetricStrip.vue'
import StatusBadge from '@/components/StatusBadge.vue'
import { useUnsavedWorkRegistration } from '@/composables/useUnsavedWorkRegistration'
import { useAccessPreviewStore } from '@/stores/accessPreview'
import { useAdminDataStore } from '@/stores/adminData'
import type { Customer } from '@/types/admin'
const store = useAdminDataStore()
const access = useAccessPreviewStore()
const page = ref(1)
const pageSize = 10
const editorOpen = ref(false)
const detailOpen = ref(false)
const currentCustomer = ref<Customer | null>(null)
const filters = reactive({ keyword: '', status: '', packageName: '' })
const emptyForm = (): Omit<Customer, 'orders' | 'joinedAt'> => ({
id: '', name: '', creditCode: '', contact: '', phone: '', email: '', sales: '',
packageName: '基础版', period: 30, status: '正常', address: '', remark: '',
})
const form = reactive(emptyForm())
const editorLabel = computed(() => form.id ? '编辑客户' : '新增客户')
const editorRequiredPermission = computed(() => form.id ? null : 'customer:create')
const { captureBaseline } = useUnsavedWorkRegistration({
key: 'customer-editor',
label: editorLabel,
isOpen: editorOpen,
model: form,
requiredPermission: editorRequiredPermission,
})
const filteredRows = computed(() => store.customers.filter((item) => {
const keyword = filters.keyword.trim().toLowerCase()
const matchesKeyword = !keyword || [item.name, item.id, item.contact, item.phone, item.creditCode]
.some((value) => value.toLowerCase().includes(keyword))
return matchesKeyword && (!filters.status || item.status === filters.status) && (!filters.packageName || item.packageName === filters.packageName)
}))
const pagedRows = computed(() => filteredRows.value.slice((page.value - 1) * pageSize, page.value * pageSize))
const metrics = computed(() => [
{ label: '全部客户', value: store.customers.length, note: '已建立企业档案' },
{ label: '正常合作', value: store.customers.filter((item) => item.status === '正常').length, note: '当前可正常下单' },
{ label: '待审核', value: store.pendingCustomers, note: '需要平台处理' },
{ label: '累计订单', value: store.customers.reduce((total, item) => total + item.orders, 0).toLocaleString(), note: '全部客户历史订单' },
])
watch(filters, () => { page.value = 1 })
watch(
() => access.can('customer:create'),
(canCreate) => {
if (canCreate || !editorOpen.value || form.id) return
editorOpen.value = false
Object.assign(form, emptyForm())
},
{ flush: 'sync' },
)
function resetFilters() {
Object.assign(filters, { keyword: '', status: '', packageName: '' })
}
function openCreate() {
Object.assign(form, emptyForm())
captureBaseline()
editorOpen.value = true
}
function openEdit(row: Customer) {
Object.assign(form, row)
captureBaseline()
editorOpen.value = true
}
function openDetail(row: Customer) {
currentCustomer.value = row
detailOpen.value = true
}
function saveCustomer() {
if (!form.name.trim() || !form.contact.trim() || !form.phone.trim()) {
ElMessage.warning('请填写企业名称、联系人和联系电话')
return
}
store.saveCustomer({ ...form, id: form.id || undefined })
editorOpen.value = false
ElMessage.success(form.id ? '客户资料已更新' : '客户已创建')
}
async function toggleStatus(row: Customer) {
const target = row.status === '停用' ? '正常' : '停用'
try {
await ElMessageBox.confirm(`确认将“${row.name}”设置为${target}吗?`, '变更客户状态', {
confirmButtonText: '确认变更', cancelButtonText: '取消',
})
store.setCustomerStatus(row.id, target)
ElMessage.success(`客户状态已更新为${target}`)
} catch {
// 用户取消,不需要反馈。
}
}
function exportCustomers() {
ElMessage.success(`已生成 ${filteredRows.value.length} 条客户数据的导出任务`)
}
</script>
<template>
<div class="business-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-option label="停用" value="停用" />
</el-select>
<el-select v-model="filters.packageName" clearable placeholder="全部套餐" aria-label="客户套餐">
<el-option label="基础版" value="基础版" /><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.usesServerPermissions" @click="exportCustomers"><Download :size="16" />导出</el-button>
<el-button v-if="access.can('customer:create')" type="primary" @click="openCreate"><Plus :size="16" />新增客户</el-button>
</div>
</section>
<section class="surface-panel table-panel">
<header class="panel-heading">
<div><h2>客户档案</h2><p>共 {{ filteredRows.length }} 家企业,客户资料与注册审核统一管理。</p></div>
</header>
<el-table :data="pagedRows" row-key="id" empty-text="没有符合条件的客户">
<el-table-column prop="id" label="客户编号" width="96" />
<el-table-column label="企业信息" min-width="250">
<template #default="{ row }"><div class="primary-cell"><strong>{{ row.name }}</strong><span>{{ row.creditCode }}</span></div></template>
</el-table-column>
<el-table-column label="联系人" min-width="150"><template #default="{ row }"><div class="primary-cell"><strong>{{ row.contact }}</strong><span>{{ row.phone }}</span></div></template></el-table-column>
<el-table-column prop="sales" label="销售负责人" width="108" />
<el-table-column label="套餐/账期" width="130"><template #default="{ row }"><div class="primary-cell"><strong>{{ row.packageName }}</strong><span>{{ row.period }} 天账期</span></div></template></el-table-column>
<el-table-column prop="orders" label="订单数" width="86" align="right" />
<el-table-column label="状态" width="96"><template #default="{ row }"><StatusBadge :status="row.status" /></template></el-table-column>
<el-table-column label="操作" width="246" fixed="right">
<template #default="{ row }">
<div class="table-actions">
<el-button link type="primary" @click="openDetail(row as Customer)"><Eye :size="15" />详情</el-button>
<el-button v-if="!access.usesServerPermissions" link type="primary" @click="openEdit(row as Customer)"><Pencil :size="15" />编辑</el-button>
<el-button v-if="!access.usesServerPermissions && row.status !== '待审核'" link :type="row.status === '停用' ? 'success' : 'danger'" @click="toggleStatus(row as Customer)">{{ row.status === '停用' ? '启用' : '停用' }}</el-button>
</div>
</template>
</el-table-column>
</el-table>
<footer 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" /></footer>
</section>
<el-drawer v-model="editorOpen" :title="form.id ? '编辑客户' : '新增客户'" size="680px" 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="请输入营业执照上的企业名称" /></el-form-item>
<el-form-item label="统一社会信用代码"><el-input v-model="form.creditCode" placeholder="18 位统一社会信用代码" /></el-form-item>
<el-form-item label="套餐"><el-select v-model="form.packageName"><el-option label="基础版" value="基础版" /><el-option label="专业版" value="专业版" /><el-option label="企业版" value="企业版" /></el-select></el-form-item>
<el-form-item label="账期"><el-input-number v-model="form.period" :min="0" :max="180" controls-position="right" /><span class="field-suffix">天</span></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-button label="停用" value="停用" /></el-radio-group></el-form-item>
<el-form-item label="销售负责人"><el-select v-model="form.sales" filterable allow-create placeholder="请选择或输入负责人"><el-option v-for="name in ['王建国','李秀英','张志强','刘德海','陈美玲']" :key="name" :label="name" :value="name" /></el-select></el-form-item>
</div></div>
<div class="form-section"><h3>联系资料</h3><div class="form-grid two-cols">
<el-form-item label="联系人" required><el-input v-model="form.contact" /></el-form-item>
<el-form-item label="联系电话" required><el-input v-model="form.phone" /></el-form-item>
<el-form-item label="联系邮箱"><el-input v-model="form.email" /></el-form-item>
<el-form-item label="注册地址"><el-input v-model="form.address" /></el-form-item>
</div><el-form-item label="内部备注"><el-input v-model="form.remark" type="textarea" :rows="3" placeholder="仅平台内部可见" /></el-form-item></div>
</el-form>
<template #footer><div class="drawer-footer"><el-button @click="editorOpen = false">取消</el-button><el-button v-if="!access.usesServerPermissions && (form.id || access.can('customer:create'))" type="primary" @click="saveCustomer">保存客户</el-button></div></template>
</el-drawer>
<el-drawer v-model="detailOpen" title="客户详情" size="620px" class="business-drawer">
<template v-if="currentCustomer">
<div class="detail-identity"><span>{{ currentCustomer.name.slice(0, 1) }}</span><div><h2>{{ currentCustomer.name }}</h2><p>{{ currentCustomer.id }} · {{ currentCustomer.creditCode }}</p></div><StatusBadge :status="currentCustomer.status" /></div>
<MetricStrip :items="[{ label: '历史订单', value: currentCustomer.orders }, { label: '账期', value: `${currentCustomer.period} 天` }, { label: '合作时长', value: currentCustomer.joinedAt.slice(0,4) + ' 至今' }]" />
<div class="detail-section"><h3>基本资料</h3><el-descriptions :column="1" border><el-descriptions-item label="套餐">{{ currentCustomer.packageName }}</el-descriptions-item><el-descriptions-item label="销售负责人">{{ currentCustomer.sales }}</el-descriptions-item><el-descriptions-item label="联系人">{{ currentCustomer.contact }} · {{ currentCustomer.phone }}</el-descriptions-item><el-descriptions-item label="邮箱">{{ currentCustomer.email }}</el-descriptions-item><el-descriptions-item label="地址">{{ currentCustomer.address }}</el-descriptions-item><el-descriptions-item label="入驻时间">{{ currentCustomer.joinedAt }}</el-descriptions-item></el-descriptions></div>
<div class="detail-section"><h3>内部备注</h3><p class="detail-note">{{ currentCustomer.remark || '暂无内部备注。' }}</p></div>
</template>
<template #footer><div class="drawer-footer"><el-button @click="detailOpen = false">关闭</el-button><el-button v-if="!access.usesServerPermissions" type="primary" @click="currentCustomer && openEdit(currentCustomer)">编辑资料</el-button></div></template>
</el-drawer>
</div>
</template>