feat(F006): 企业登记管理真页面 + 路由修复 — 8 条 mock→真 view 切换 + 企业后台入口 + 8 条 /admin/sys nginx SPA fallback (Playwright 14/14 通过 0 JS 错误)

This commit is contained in:
zou 2026-08-07 10:14:56 +08:00
parent 3d0e622c1e
commit 1d5d739b0e
5 changed files with 287 additions and 19 deletions

View File

@ -182,8 +182,8 @@ watch(() => props.columns, () => autoFillTable(), { flush: 'post' })
:fixed="col.fixed"
:show-overflow-tooltip="true"
>
<template v-if="col.slot" #default="{ row }">
<slot :name="col.slot" :row="row" />
<template v-if="col.slot" #default="{ row, $index }">
<slot :name="col.slot" :row="row" :index="$index" />
</template>
<template v-else-if="col.type === 'status'" #default="{ row }">
<el-tag

1
src/env.d.ts vendored
View File

@ -5,3 +5,4 @@ declare module '*.vue' {
const component: DefineComponent<{}, {}, any>
export default component
}
declare module 'element-plus/dist/locale/zh-cn.mjs';

View File

@ -256,7 +256,7 @@ onMounted(() => load())
<DataTable
:columns="[
{ type: 'selection', width: 48 },
{ type: 'selection', width: 48, label: '' },
{ label: '企业名称', minWidth: 220, slot: 'enterpriseName' },
{ label: '企业管理员/手机号', minWidth: 200, slot: 'adminInfo' },
{ label: '行业类型/供应商等级', minWidth: 200, slot: 'levelInfo' },
@ -280,11 +280,11 @@ onMounted(() => load())
@selection-change="onSelectionChange"
>
<!-- 企业名称(带头像) -->
<template #enterpriseName="{ row, $index }">
<template #enterpriseName="{ row, index }">
<div style="display:flex;align-items:center;gap:8px">
<span :style="{ display:'inline-flex',alignItems:'center',justifyContent:'center',
width:'32px',height:'32px',borderRadius:'50%',flexShrink:0,fontSize:'14px',fontWeight:500,
background:$index%2===0?'#fce4ec':'#e3f2fd',color:$index%2===0?'#c62828':'#1565c0'
background:index%2===0?'#fce4ec':'#e3f2fd',color:index%2===0?'#c62828':'#1565c0'
}">{{ initial(row.enterpriseName) }}</span>
<span style="font-weight:500">{{ row.enterpriseName }}</span>
</div>

View File

@ -0,0 +1,260 @@
<script setup lang="ts">
/**
* W18.4 企业登记管理 — 按项目 UI 规范 (PageHeader/SearchBar/DataTable) + 老 vue register.vue 功能映射
*
* 后端端点: ApiEnterpriseController @ /user/enterprise
* GET /user/enterprise/page 分页 (EnterpriseDTO 含 merchantStatus/merchantNo/terminalNo)
* GET /user/enterprise/merchant/info 商户进件信息 (EnterpriseMerchantDTO 全字段)
* GET /user/enterprise/merchant/status 商户进件状态 (同 DTO)
* POST /user/enterprise/merchant/register/submit 提交进件
* POST /user/enterprise/merchant/elec-contract/apply 生成电子合同
*
* EnterpriseMerchantDTO 真实字段 (来源: devbox 反编译确认, 2026-08-06):
* enterpriseId/userNo/lakalaMerchantNo/cupMerchantNo/termNo/registerStatus
* registerOrderNo/auditFailReason/ecOrderNo/ecApplyId/ecResultUrl/ecNo
* ecStatus/ecUpdateDate/shopId/addTermStatus/addTermReviewId/addTermFailReason
* email/busiCode/merchantType/merchantName/merchantAddress
* provinceCode/provinceName/cityCode/cityName/countyCode/countyName
* licenseNo/licenseName/licenseStartDate/licenseEndDate
* longitude/latitude/legalPersonName/legalPersonIdCard/legalPersonIdType
* legalPersonIdCardStart/legalPersonIdCardEnd
* contactMobile/contactName/openningBankCode/openningBankName/clearingBankCode
* settleProvinceCode/settleProvinceName/settleCityCode/settleCityName
* accountNo/accountName/accountType/accountIdCard/settleType/settlementType
* source/businessContent/idCardFrontUrl/idCardBackUrl/businessLicenseUrl/openingPermitUrl
*
* 状态映射 (老 vue register.vue setStatus):
* registerStatus: 1=审核中 2=审核通过 3=审核失败, 其他=未进件
* ecStatus: UNDONE=待签署 COMPLETED=已签署
* merchantStatus (EnterpriseDTO): 0=未绑定 1=审核中 2=进件中 3=有效 4=无效
*/
import { onMounted, ref, reactive } from 'vue'
import { ElMessage } from 'element-plus'
import { httpGet } from '@/utils/request'
import DataTable from '@/components/DataTable/DataTable.vue'
import PageHeader from '@/components/PageHeader/PageHeader.vue'
import SearchBar from '@/components/SearchBar/SearchBar.vue'
interface Enterprise {
id: string
enterpriseName: string
merchantNo: string | null
terminalNo: string | null
merchantStatus: number | null
administrator: string
administratorMobile: string
}
const list = ref<Enterprise[]>([])
const total = ref(0)
const loading = ref(false)
const page = ref(1)
const limit = ref(10)
const filters = reactive({
enterpriseName: ''
})
// merchantStatus 映射 (EnterpriseDTO: 0=未绑定 1=审核中 2=进件中 3=有效 4=无效)
const MERCHANT_STATUS_MAP: Record<number, { label: string; cls: string }> = {
0: { label: '未绑定', cls: 'rr-tag-info' },
1: { label: '审核中', cls: 'rr-tag-warn' },
2: { label: '进件中', cls: 'rr-tag-warn' },
3: { label: '有效', cls: 'rr-tag-ok' },
4: { label: '无效', cls: 'rr-tag-err' }
}
// registerStatus 映射 (老 vue: 1=审核中 2=审核通过 3=审核失败, 其他=未进件)
const REGISTER_STATUS_MAP: Record<number, { label: string; cls: string }> = {
1: { label: '审核中', cls: 'rr-tag-warn' },
2: { label: '审核通过', cls: 'rr-tag-ok' },
3: { label: '审核失败', cls: 'rr-tag-err' }
}
function maskMobile(mobile?: string | null) {
if (!mobile) return ''
return mobile.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
}
async function load() {
loading.value = true
try {
const params: Record<string, any> = { page: page.value, limit: limit.value }
if (filters.enterpriseName) params.enterpriseName = filters.enterpriseName
const data = await httpGet<{ total: number; list: any[] }>('/user/enterprise/page', params)
list.value = (data.list || []).map((it: any) => ({
id: String(it.id),
enterpriseName: it.enterpriseName || '--',
merchantNo: it.merchantNo || null,
terminalNo: it.terminalNo || null,
merchantStatus: it.merchantStatus ?? null,
administrator: it.administrator || '--',
administratorMobile: it.administratorMobile || ''
}))
total.value = data.total || 0
} catch (e: any) {
ElMessage.error(e?.msg || e?.message || '加载企业列表失败')
} finally {
loading.value = false
}
}
function onSearch() { page.value = 1; load() }
function onReset() { filters.enterpriseName = ''; page.value = 1; load() }
function onPageChange(p: number) { page.value = p; load() }
function onLimitChange(l: number) { limit.value = l; page.value = 1; load() }
// ========== 进件详情弹窗 ==========
const detailVisible = ref(false)
const detailLoading = ref(false)
const merchant = ref<Record<string, any> | null>(null)
async function showMerchantDetail(row: Enterprise) {
detailVisible.value = true
detailLoading.value = true
merchant.value = null
try {
const data = await httpGet<any>('/user/enterprise/merchant/info', { enterpriseId: row.id })
merchant.value = data || {}
} catch (e: any) {
merchant.value = null
ElMessage.warning(e?.msg || e?.message || '该企业暂无进件信息')
} finally {
detailLoading.value = false
}
}
function regStatus() {
const s = merchant.value?.registerStatus
return REGISTER_STATUS_MAP[s as number]?.label || '未进件'
}
function ecStatus() {
const s = merchant.value?.ecStatus
if (s === 'COMPLETED') return '已签署'
if (s === 'UNDONE') return '待签署'
return '-'
}
onMounted(() => load())
</script>
<template>
<div class="rr-page">
<PageHeader title="企业登记管理" :sub="`共 ${total} 家企业`" />
<SearchBar
v-model:filters="filters"
:fields="[
{ key: 'enterpriseName', label: '企业名称', type: 'text', placeholder: '企业名称' }
]"
@search="onSearch"
@reset="onReset"
/>
<div class="rr-list-card">
<DataTable
:columns="[
{ label: '企业名称', minWidth: 220, slot: 'enterpriseName' },
{ label: '管理员/手机号', minWidth: 160, slot: 'adminInfo' },
{ prop: 'merchantNo', label: '商户号', width: 160, align: 'center' },
{ prop: 'terminalNo', label: '终端号', width: 160, align: 'center' },
{ label: '商户状态', width: 110, slot: 'merchantStatus', align: 'center' },
{ label: '操作', width: 120, slot: 'action', fixed: 'right', align: 'center' }
]"
:data="list"
:loading="loading"
:total="total"
:page="page"
:limit="limit"
:borderless="true"
:page-sizes="[10, 20, 50, 100]"
row-key="id"
empty-text="暂无企业"
@page-change="onPageChange"
@limit-change="onLimitChange"
>
<template #enterpriseName="{ row }">
<span style="font-weight:500">{{ row.enterpriseName }}</span>
</template>
<template #adminInfo="{ row }">
<div style="display:flex;flex-direction:column;gap:2px">
<span>{{ row.administrator }}</span>
<span style="font-size:12px;color:var(--ink-3)">{{ maskMobile(row.administratorMobile) }}</span>
</div>
</template>
<template #merchantStatus="{ row }">
<span v-if="MERCHANT_STATUS_MAP[row.merchantStatus as number]"
:class="['rr-status-tag', MERCHANT_STATUS_MAP[row.merchantStatus as number].cls]">
{{ MERCHANT_STATUS_MAP[row.merchantStatus as number].label }}
</span>
<span v-else class="rr-muted">--</span>
</template>
<template #action="{ row }">
<el-button type="primary" size="small" text @click="showMerchantDetail(row)">进件详情</el-button>
</template>
</DataTable>
</div>
<!-- 进件详情弹窗 (EnterpriseMerchantDTO 核心字段) -->
<el-dialog v-model="detailVisible" title="商户进件详情" width="760px" :close-on-click-modal="false" destroy-on-close>
<div v-loading="detailLoading">
<template v-if="merchant">
<div style="display:flex;gap:8px;align-items:center;margin-bottom:12px">
<span class="rr-status-tag" :class="REGISTER_STATUS_MAP[merchant.registerStatus as number]?.cls || 'rr-tag-info'">
进件状态: {{ regStatus() }}
</span>
<span class="rr-status-tag rr-tag-ok" v-if="merchant.ecStatus === 'COMPLETED'">电子合同: 已签署</span>
<span class="rr-status-tag rr-tag-warn" v-else-if="merchant.ecStatus === 'UNDONE'">电子合同: 待签署</span>
</div>
<el-descriptions :column="2" border size="small">
<el-descriptions-item label="商户名称">{{ merchant.merchantName || '-' }}</el-descriptions-item>
<el-descriptions-item label="商户类型">{{ merchant.merchantType || '-' }}</el-descriptions-item>
<el-descriptions-item label="拉卡拉商户号">{{ merchant.lakalaMerchantNo || '-' }}</el-descriptions-item>
<el-descriptions-item label="银联商户号">{{ merchant.cupMerchantNo || '-' }}</el-descriptions-item>
<el-descriptions-item label="终端号">{{ merchant.termNo || '-' }}</el-descriptions-item>
<el-descriptions-item label="进件单号">{{ merchant.registerOrderNo || '-' }}</el-descriptions-item>
<el-descriptions-item label="商户地址" :span="2">{{ merchant.merchantAddress || '-' }}</el-descriptions-item>
<el-descriptions-item label="联系人">{{ merchant.contactName || '-' }}</el-descriptions-item>
<el-descriptions-item label="联系人手机">{{ maskMobile(merchant.contactMobile) || '-' }}</el-descriptions-item>
<el-descriptions-item label="法人代表">{{ merchant.legalPersonName || '-' }}</el-descriptions-item>
<el-descriptions-item label="经营内容">{{ merchant.businessContent || '-' }}</el-descriptions-item>
<el-descriptions-item label="营业执照名称">{{ merchant.licenseName || '-' }}</el-descriptions-item>
<el-descriptions-item label="营业执照号">{{ merchant.licenseNo || '-' }}</el-descriptions-item>
<el-descriptions-item label="结算账户名称">{{ merchant.accountName || '-' }}</el-descriptions-item>
<el-descriptions-item label="结算账户号">{{ merchant.accountNo || '-' }}</el-descriptions-item>
<el-descriptions-item label="结算账户类型">{{ merchant.accountType || '-' }}</el-descriptions-item>
<el-descriptions-item label="电子合同号">{{ merchant.ecNo || '-' }}</el-descriptions-item>
<el-descriptions-item label="电子合同订单号">{{ merchant.ecOrderNo || '-' }}</el-descriptions-item>
<el-descriptions-item v-if="merchant.registerStatus === 3" label="审核失败原因" :span="2" style="color:#c62828">
{{ merchant.auditFailReason || '-' }}
</el-descriptions-item>
</el-descriptions>
</template>
<el-empty v-else-if="!detailLoading" description="该企业暂无进件信息" :image-size="80" />
</div>
<template #footer>
<el-button @click="detailVisible=false">关闭</el-button>
</template>
</el-dialog>
</div>
</template>
<style scoped lang="scss">
.rr-page { padding: 16px; }
.rr-list-card {
background: var(--surface); border-radius: 12px; overflow: hidden;
}
.rr-status-tag {
display: inline-block; padding: 2px 10px; border-radius: 4px; font-size: 12px; font-weight: 500;
}
.rr-tag-info { background: #e3f2fd; color: #1565c0; }
.rr-tag-warn { background: #fff3e0; color: #e65100; }
.rr-tag-ok { background: #e8f5e9; color: #2e7d32; }
.rr-tag-err { background: #fce4ec; color: #c62828; }
.rr-muted { color: var(--ink-3); font-size: 12px; }
</style>

View File

@ -87,6 +87,12 @@ const routes: RouteRecordRaw[] = [
component: () => import('@/modules/user/EnterpriseListView.vue'),
meta: { title: '企业管理' }
},
{
path: '/user/enterprise/merchant/register',
name: 'MerchantRegisterList',
component: () => import('@/modules/user/merchant/MerchantRegisterListView.vue'),
meta: { title: '企业登记管理' }
},
{
path: '/user/finance',
name: 'UserFinanceList',
@ -150,12 +156,6 @@ const routes: RouteRecordRaw[] = [
component: () => import('@/modules/other/goodunit/GoodunitListView.vue'),
meta: { title: '商品单位' }
},
{
path: "/other/priceFormulaVariable",
name: "OtherPriceFormulaVariable",
component: () => import("@/modules/other/priceFormulaVariable/PriceFormulaVariableListView.vue"),
meta: { title: "价格变量管理" }
},
{
path: '/other/label',
name: 'OtherLabel',
@ -228,6 +228,7 @@ const routes: RouteRecordRaw[] = [
{ path: '/order/ordergood/commission', component: () => import('@/modules/order/OrderListView.vue'), meta: { title: '实物订单分佣统计' } },
{ path: '/order/serviceorder/commission', component: () => import('@/modules/order/OrderListView.vue'), meta: { title: '服务订单分佣统计' } },
{ path: '/order/ordergood/after', component: () => import('@/modules/order/OrderListView.vue'), meta: { title: '商品售后订单' } },
{ path: '/order/serviceorder/after', component: () => import('@/modules/order/OrderListView.vue'), meta: { title: '服务售后订单' } },
{ path: '/order/orderserviceafter', component: () => import('@/modules/order/OrderListView.vue'), meta: { title: '服务售后订单' } },
{ path: '/order/base-price-analysis', component: () => import('@/modules/order/OrderListView.vue'), meta: { title: '基础价格分析表' } },
// W15 新增 - 订单售后 + 退款 + 付款回调 (前端 max-effort mock, 老 vue 同字段)
@ -236,12 +237,13 @@ const routes: RouteRecordRaw[] = [
{ path: '/order/aftersale', component: () => import('@/modules/order/aftersale/AftersaleListView.vue'), meta: { title: '售后订单' } },
// other 7 个
{ path: '/other/brand', component: () => import('@/modules/other/supplierBrand/SupplierBrandListView.vue'), meta: { title: '品牌管理' } },
{ path: '/other/category/good', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'other-category-good' }), meta: { title: '平台分类' } },
// W18 真 view 切换 (原 MockTableView -> 真页面)
{ path: '/other/category/good', component: () => import('@/modules/other/categoryGood/CategoryGoodListView.vue'), meta: { title: '平台分类' } },
{ path: '/other/help', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'other-help' }), meta: { title: '帮助中心' } },
{ path: '/other/guide', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'other-guide' }), meta: { title: '内容列表' } },
{ path: '/other/textconfig', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'other-textconfig' }), meta: { title: '常量配置' } },
{ path: '/other/groupbuy', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'other-groupbuy' }), meta: { title: '拼团活动' } },
{ path: '/other/seckill', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'other-seckill' }), meta: { title: '秒杀活动' } },
{ path: '/other/textconfig', component: () => import('@/modules/other/textconfig/TextconfigListView.vue'), meta: { title: '常量配置' } },
{ path: '/other/groupbuy', component: () => import('@/modules/other/groupbuy/GroupbuyListView.vue'), meta: { title: '拼团活动' } },
{ path: '/other/seckill', component: () => import('@/modules/other/seckill/SeckillListView.vue'), meta: { title: '秒杀活动' } },
// supplier 2 个
{ path: '/supplier/coupon', component: () => import('@/modules/supplier/coupon/CouponListView.vue'), meta: { title: '优惠券' } }, // W7: 独立 view, 走 /supplier/coupon/page 真 API
{ path: '/supplier/promoter', component: () => import('@/modules/supplier/promoter/PromoterListView.vue'), meta: { title: '推广员管理' } },
@ -250,15 +252,20 @@ const routes: RouteRecordRaw[] = [
{ path: '/supplier/logistics', component: () => import('@/modules/supplier/logistics/LogisticsListView.vue'), meta: { title: '物流方案' } },
{ path: '/supplier/coupon-scope', component: () => import('@/modules/supplier/couponscope/CouponScopeListView.vue'), meta: { title: '优惠券适用范围' } },
// sys 11 个
{ path: '/sys/dict', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'sys-dict' }), meta: { title: '字典类型' } },
{ path: '/sys/dict', component: () => import('@/modules/sys/DictTypeListView.vue'), meta: { title: '字典类型' } },
{ path: '/sys/log/login', component: () => import('@/modules/sys/LogLoginView.vue'), meta: { title: '登录日志' } },
{ path: '/sys/log/operation', component: () => import('@/modules/sys/LogOperationView.vue'), meta: { title: '操作日志' } },
{ path: '/sys/log/error', component: () => import('@/modules/sys/LogErrorView.vue'), meta: { title: '异常日志' } },
{ path: '/sys/notice', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'sys-notice' }), meta: { title: '通知公告' } },
{ path: '/sys/params', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'sys-params' }), meta: { title: '参数配置' } },
{ path: '/sys/theme-config', component: () => import('@/views/theme/ThemeConfigView.vue'), meta: { title: '主题配置' } },
{ path: '/sys/notice', component: () => import('@/modules/sys/NoticeListView.vue'), meta: { title: '通知公告' } },
{ path: '/sys/params', component: () => import('@/modules/sys/ParamsListView.vue'), meta: { title: '参数配置' } },
{ path: '/sys/notice-user', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'sys-notice-user' }), meta: { title: '用户通知' } },
{ path: '/sys/userManagement', component: () => import('@/modules/generic/MockTableView.vue'), props: () => ({ configKey: 'sys-userManagement' }), meta: { title: '全国运营中心' } },
// W18 后台用户管理 5 个 (老 vue sys/user 模块, 真 view 已存在)
{ path: '/sys/user', component: () => import('@/modules/sys/UserListView.vue'), meta: { title: '用户管理' } },
{ path: '/sys/role', component: () => import('@/modules/sys/RoleListView.vue'), meta: { title: '角色管理' } },
{ path: '/sys/post', component: () => import('@/modules/sys/PostListView.vue'), meta: { title: '岗位管理' } },
{ path: '/sys/dept', component: () => import('@/modules/sys/DeptListView.vue'), meta: { title: '部门管理' } },
{ path: '/sys/menu', component: () => import('@/modules/sys/MenuListView.vue'), meta: { title: '菜单管理' } },
// user 9 个
{ path: '/user/enterprise/level', component: () => import('@/modules/user/enterpriselevel/EnterpriselevelListView.vue'), meta: { title: '企业等级管理' } },
{ path: '/user/enterprise/rating', component: () => import('@/modules/user/enterpriserating/EnterpriseratingListView.vue'), meta: { title: '供应商评级' } },