feat(F006-W3xC): 7 个新 view 接真 API (W3.x C P0 全部)
- P0.1 数据统计首页 (走 /sys/openapi/order/list 真 API, 4 stat 500/¥47M 真值)
- P0.2 supplier/inquiry/inquiry 重定向 /inquiry (2 个 redirect)
- P0.3 /user/user 用户列表 (2284 真实用户)
- P0.4 /other/operateguide 内容列表 (1 条真数据)
- P0.5 /api/lunbo 轮播占位 (后端 404 优雅提示)
- P0.6 /api/help 帮助中心占位 (后端 404 优雅提示)
- P0.7 /sys/region 行政区域树 (省市区 4 级 3000+ 节点)
同步: src/modules/user/{Enterprise,Finance}ListView.vue (Mac 端 working copy 补传)
P0.7 nginx 修复 (conf 改动非本次提交, 见 nginx-vue.conf.bak-20260704-194200):
- 新增 location = /admin/sys/region { rewrite /admin/index.html last; }
- 原因: location /admin/sys/ 代理抢 /admin/sys/region 走 gateway 返 401
- 已 docker compose restart frontend 生效, devbox 端 7 SPA 路径全 200
This commit is contained in:
parent
552baacee9
commit
78360d1d6e
56
src/modules/api/help/HelpListView.vue
Normal file
56
src/modules/api/help/HelpListView.vue
Normal file
@ -0,0 +1,56 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* F006 帮助中心 - W3.x C P0.6
|
||||
* 后端 /api/help/* 暂未实现,展示检测状态
|
||||
*/
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { httpGet } from '@/utils/request'
|
||||
|
||||
const status = ref<'checking' | '404' | '200' | 'error'>('checking')
|
||||
const errorMsg = ref('')
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const r: any = await httpGet('/api/help/page', { page: 1, limit: 1 })
|
||||
if (r?.code === 0) status.value = '200'
|
||||
else { status.value = 'error'; errorMsg.value = r?.msg || '未知错误' }
|
||||
} catch (e: any) {
|
||||
if (e?.response?.status === 404 || e?.message?.includes('404')) {
|
||||
status.value = '404'; errorMsg.value = '后端 API 未实现(/api/help/page 返回 404)'
|
||||
} else {
|
||||
status.value = 'error'; errorMsg.value = e?.message || '请求失败'
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="rr-page">
|
||||
<div class="rr-page-header">
|
||||
<h2 class="rr-page-title">帮助中心</h2>
|
||||
<p class="rr-page-sub">使用指南 + FAQ 列表</p>
|
||||
</div>
|
||||
<div class="rr-card rr-status-card">
|
||||
<el-result
|
||||
:icon="status === '200' ? 'success' : 'warning'"
|
||||
:title="status === '200' ? 'API 已就绪' : '后端 API 未实现'"
|
||||
:sub-title="status === '200' ? '帮助中心数据接口已通' : (errorMsg || '请联系后端实现 /api/help/page 接口')"
|
||||
>
|
||||
<template #extra>
|
||||
<el-tag v-if="status === 'checking'" type="info">检测中...</el-tag>
|
||||
<el-tag v-else-if="status === '404'" type="warning">404 Not Found</el-tag>
|
||||
<el-tag v-else-if="status === '200'" type="success">200 OK</el-tag>
|
||||
<el-tag v-else type="danger">Error</el-tag>
|
||||
</template>
|
||||
</el-result>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.rr-page { max-width: 1400px; }
|
||||
.rr-page-header { margin-bottom: 20px; }
|
||||
.rr-page-title { font-size: 22px; font-weight: 600; letter-spacing: -0.02em; color: var(--ink); margin-bottom: 4px; }
|
||||
.rr-page-sub { color: var(--ink-3); font-size: 13px; }
|
||||
.rr-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 40px 20px; }
|
||||
</style>
|
||||
56
src/modules/api/lunbo/LunboListView.vue
Normal file
56
src/modules/api/lunbo/LunboListView.vue
Normal file
@ -0,0 +1,56 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* F006 轮播管理 - W3.x C P0.5
|
||||
* 后端 /api/lunbo/* 暂未实现,展示检测状态
|
||||
*/
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { httpGet } from '@/utils/request'
|
||||
|
||||
const status = ref<'checking' | '404' | '200' | 'error'>('checking')
|
||||
const errorMsg = ref('')
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const r: any = await httpGet('/api/lunbo/page', { page: 1, limit: 1 })
|
||||
if (r?.code === 0) status.value = '200'
|
||||
else { status.value = 'error'; errorMsg.value = r?.msg || '未知错误' }
|
||||
} catch (e: any) {
|
||||
if (e?.response?.status === 404 || e?.message?.includes('404')) {
|
||||
status.value = '404'; errorMsg.value = '后端 API 未实现(/api/lunbo/page 返回 404)'
|
||||
} else {
|
||||
status.value = 'error'; errorMsg.value = e?.message || '请求失败'
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="rr-page">
|
||||
<div class="rr-page-header">
|
||||
<h2 class="rr-page-title">轮播管理</h2>
|
||||
<p class="rr-page-sub">Banner 轮播图配置</p>
|
||||
</div>
|
||||
<div class="rr-card rr-status-card">
|
||||
<el-result
|
||||
:icon="status === '200' ? 'success' : 'warning'"
|
||||
:title="status === '200' ? 'API 已就绪' : '后端 API 未实现'"
|
||||
:sub-title="status === '200' ? '轮播图数据接口已通,可正常加载' : (errorMsg || '请联系后端实现 /api/lunbo/page 接口')"
|
||||
>
|
||||
<template #extra>
|
||||
<el-tag v-if="status === 'checking'" type="info">检测中...</el-tag>
|
||||
<el-tag v-else-if="status === '404'" type="warning">404 Not Found</el-tag>
|
||||
<el-tag v-else-if="status === '200'" type="success">200 OK</el-tag>
|
||||
<el-tag v-else type="danger">Error</el-tag>
|
||||
</template>
|
||||
</el-result>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.rr-page { max-width: 1400px; }
|
||||
.rr-page-header { margin-bottom: 20px; }
|
||||
.rr-page-title { font-size: 22px; font-weight: 600; letter-spacing: -0.02em; color: var(--ink); margin-bottom: 4px; }
|
||||
.rr-page-sub { color: var(--ink-3); font-size: 13px; }
|
||||
.rr-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 40px 20px; }
|
||||
</style>
|
||||
62
src/modules/other/operateguide/OperateguideListView.vue
Normal file
62
src/modules/other/operateguide/OperateguideListView.vue
Normal file
@ -0,0 +1,62 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* F006 操作指南 - W3.x C P0.4
|
||||
* 走 /other/operateguide/page
|
||||
*/
|
||||
import { onMounted, ref, reactive } from 'vue'
|
||||
import { httpGet } from '@/utils/request'
|
||||
import DataTable from '@/components/DataTable/DataTable.vue'
|
||||
|
||||
interface Guide {
|
||||
id: string
|
||||
title: string
|
||||
content: string
|
||||
viewSum: number | null
|
||||
createTime: string
|
||||
images: string | null
|
||||
raw: any
|
||||
}
|
||||
|
||||
const list = ref<Guide[]>([])
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
const query = reactive({ page: 1, limit: 20 })
|
||||
|
||||
const columns = [
|
||||
{ prop: 'id', label: 'ID', width: 80 },
|
||||
{ prop: 'title', label: '标题', minWidth: 240 },
|
||||
{ prop: 'viewSum', label: '浏览数', width: 100, formatter: (r: Guide) => String(r.viewSum ?? 0) },
|
||||
{ prop: 'createTime', label: '创建时间', width: 180 }
|
||||
]
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const resp: any = await httpGet('/other/operateguide/page', { page: query.page, limit: query.limit })
|
||||
list.value = resp?.list || []
|
||||
total.value = Number(resp?.total || 0)
|
||||
} catch { list.value = []; total.value = 0 } finally { loading.value = false }
|
||||
}
|
||||
function onPageChange(p: number) { query.page = p; load() }
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="rr-page">
|
||||
<div class="rr-page-header">
|
||||
<h2 class="rr-page-title">内容列表</h2>
|
||||
<p class="rr-page-sub">操作指南内容(共 {{ total }} 条)</p>
|
||||
</div>
|
||||
<div class="rr-card">
|
||||
<DataTable :columns="columns" :data="list" :loading="loading" :total="total" :page="query.page" :limit="query.limit" :show-pagination="true" row-key="id" empty-text="暂无内容" @page-change="onPageChange" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.rr-page { max-width: 1400px; }
|
||||
.rr-page-header { margin-bottom: 20px; }
|
||||
.rr-page-title { font-size: 22px; font-weight: 600; letter-spacing: -0.02em; color: var(--ink); margin-bottom: 4px; }
|
||||
.rr-page-sub { color: var(--ink-3); font-size: 13px; }
|
||||
.rr-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; }
|
||||
</style>
|
||||
62
src/modules/sys/region/RegionListView.vue
Normal file
62
src/modules/sys/region/RegionListView.vue
Normal file
@ -0,0 +1,62 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* F006 行政区域 - W3.x C P0.7
|
||||
* 走 /sys/region/tree 真实数据 (省市区 4 级)
|
||||
*/
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { httpGet } from '@/utils/request'
|
||||
|
||||
interface Region {
|
||||
id: string
|
||||
pid: string
|
||||
name: string
|
||||
children?: Region[]
|
||||
raw: any
|
||||
}
|
||||
|
||||
const tree = ref<Region[]>([])
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
const error = ref('')
|
||||
|
||||
onMounted(async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const resp: any = await httpGet('/sys/region/tree')
|
||||
if (Array.isArray(resp)) {
|
||||
// 数节点
|
||||
const count = (nodes: Region[]): number => nodes.reduce((s, n) => s + 1 + (n.children ? count(n.children) : 0), 0)
|
||||
total.value = count(resp)
|
||||
tree.value = resp
|
||||
}
|
||||
} catch (e: any) {
|
||||
error.value = e?.message || '加载失败'
|
||||
} finally { loading.value = false }
|
||||
})
|
||||
|
||||
const defaultProps = { children: 'children', label: 'name' }
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="rr-page">
|
||||
<div class="rr-page-header">
|
||||
<h2 class="rr-page-title">行政区域</h2>
|
||||
<p class="rr-page-sub">全国省/市/区/县 4 级行政区划(共 {{ total }} 个节点)</p>
|
||||
</div>
|
||||
<div class="rr-card">
|
||||
<div v-loading="loading" class="rr-tree-wrap">
|
||||
<el-tree v-if="!error && tree.length" :data="tree" :props="defaultProps" node-key="id" default-expand-all :expand-on-click-node="false" />
|
||||
<el-empty v-else-if="!loading" :description="error || '暂无数据'" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.rr-page { max-width: 1400px; }
|
||||
.rr-page-header { margin-bottom: 20px; }
|
||||
.rr-page-title { font-size: 22px; font-weight: 600; letter-spacing: -0.02em; color: var(--ink); margin-bottom: 4px; }
|
||||
.rr-page-sub { color: var(--ink-3); font-size: 13px; }
|
||||
.rr-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); padding: 20px; }
|
||||
.rr-tree-wrap { min-height: 400px; }
|
||||
</style>
|
||||
200
src/modules/user/EnterpriseListView.vue
Normal file
200
src/modules/user/EnterpriseListView.vue
Normal file
@ -0,0 +1,200 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* F006 企业管理 — 套 UserListView 模式 + v2.0 设计规范
|
||||
*
|
||||
* 后端端点 (ApiEnterpriseController @ /user/enterprise):
|
||||
* GET /user/enterprise/page 分页列表
|
||||
* GET /user/enterprise/getInfo 详情
|
||||
*/
|
||||
import { onMounted, ref, reactive } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { httpGet } from '@/utils/request'
|
||||
import DataTable from '@/components/DataTable/DataTable.vue'
|
||||
|
||||
interface Enterprise {
|
||||
id: string
|
||||
enterpriseName: string
|
||||
enterpriseType: number
|
||||
enterpriseLogo?: string | null
|
||||
legalMan: string
|
||||
linkMan: string
|
||||
linkMobile: string
|
||||
enterpriseAddress: string
|
||||
mainProducts: string
|
||||
registerTime: string
|
||||
status: number
|
||||
registerNo: string | null
|
||||
email: string
|
||||
raw: any
|
||||
}
|
||||
|
||||
const TYPE_MAP: Record<number, { label: string; type: string }> = {
|
||||
1: { label: '供应商', type: 'success' },
|
||||
2: { label: '采购商', type: 'primary' }
|
||||
}
|
||||
|
||||
const list = ref<Enterprise[]>([])
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
const page = ref(1)
|
||||
const limit = ref(10)
|
||||
const loadError = ref<string | null>(null)
|
||||
|
||||
const filters = reactive({
|
||||
enterpriseName: '',
|
||||
enterpriseType: ''
|
||||
})
|
||||
|
||||
const detailVisible = ref(false)
|
||||
const detailRow = ref<Enterprise | null>(null)
|
||||
|
||||
function fmtDate(ts?: string | number | null) {
|
||||
if (!ts) return '--'
|
||||
const n = typeof ts === 'string' ? Date.parse(ts) : ts
|
||||
if (!n) return String(ts)
|
||||
const d = new Date(n)
|
||||
if (isNaN(d.getTime())) return String(ts)
|
||||
const pad = (x: number) => String(x).padStart(2, '0')
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
loadError.value = null
|
||||
try {
|
||||
const data = await httpGet<{ total: number; list: any[] }>('/user/enterprise/page', {
|
||||
page: page.value,
|
||||
limit: limit.value,
|
||||
...(filters.enterpriseName ? { enterpriseName: filters.enterpriseName } : {}),
|
||||
...(filters.enterpriseType !== '' ? { enterpriseType: Number(filters.enterpriseType) } : {})
|
||||
})
|
||||
list.value = (data.list || []).map((it: any) => ({
|
||||
id: String(it.id),
|
||||
enterpriseName: it.enterpriseName || '--',
|
||||
enterpriseType: it.enterpriseType ?? 0,
|
||||
enterpriseLogo: it.enterpriseLogo,
|
||||
legalMan: it.legalMan || '--',
|
||||
linkMan: it.linkMan || '--',
|
||||
linkMobile: it.linkMobile || '--',
|
||||
enterpriseAddress: it.enterpriseAddress || '--',
|
||||
mainProducts: it.mainProducts || '--',
|
||||
registerTime: it.registerTime || '--',
|
||||
status: it.status ?? 0,
|
||||
registerNo: it.orgNo || null,
|
||||
email: it.email || '--',
|
||||
raw: it
|
||||
}))
|
||||
total.value = data.total || 0
|
||||
} catch (e: any) {
|
||||
loadError.value = e?.msg || e?.message || '加载失败'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function onSearch() { page.value = 1; load() }
|
||||
function onReset() { filters.enterpriseName = ''; filters.enterpriseType = ''; page.value = 1; load() }
|
||||
function onPageChange(p: number) { page.value = p; load() }
|
||||
function onLimitChange(l: number) { limit.value = l; page.value = 1; load() }
|
||||
function showDetail(row: Enterprise) { detailRow.value = row; detailVisible.value = true }
|
||||
|
||||
onMounted(() => { load() })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="rr-sys-user">
|
||||
<div class="rr-page-header">
|
||||
<div class="rr-page-titles">
|
||||
<h2 class="rr-page-title">企业管理</h2>
|
||||
<p class="rr-page-sub">共 {{ total }} 家企业</p>
|
||||
</div>
|
||||
<div class="rr-page-actions">
|
||||
<el-button @click="onReset" class="rr-btn-secondary">重置</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rr-filters-row">
|
||||
<el-input v-model="filters.enterpriseName" placeholder="企业名搜索" clearable class="rr-username-input" size="small" @keyup.enter="onSearch" />
|
||||
<el-select v-model="filters.enterpriseType" placeholder="企业类型" class="rr-type-select" clearable size="small">
|
||||
<el-option value="1" label="供应商" />
|
||||
<el-option value="2" label="采购商" />
|
||||
</el-select>
|
||||
<el-button class="rr-btn-secondary" size="small" @click="onSearch">查询</el-button>
|
||||
</div>
|
||||
|
||||
<div class="rr-list-card">
|
||||
<DataTable
|
||||
:columns="[
|
||||
{ prop: 'enterpriseName', label: '企业名称', width: 220 },
|
||||
{ prop: 'enterpriseType', label: '类型', width: 100, slot: 'enterpriseType' },
|
||||
{ prop: 'legalMan', label: '法人', width: 120 },
|
||||
{ prop: 'linkMan', label: '联系人', width: 120 },
|
||||
{ prop: 'linkMobile', label: '联系电话', width: 130 },
|
||||
{ prop: 'enterpriseAddress', label: '地址', minWidth: 180 },
|
||||
{ prop: 'registerTime', label: '注册时间', width: 170, slot: 'registerTime' },
|
||||
{ label: '操作', width: 100, slot: 'action' }
|
||||
]"
|
||||
:data="list"
|
||||
:loading="loading"
|
||||
:total="total"
|
||||
:page="page"
|
||||
:limit="limit"
|
||||
:show-pagination="true"
|
||||
:error="loadError"
|
||||
empty-text="暂无企业"
|
||||
row-key="id"
|
||||
@page-change="onPageChange"
|
||||
@limit-change="onLimitChange"
|
||||
>
|
||||
<template #enterpriseType="{ row }">
|
||||
<el-tag v-if="TYPE_MAP[row.enterpriseType]" :type="TYPE_MAP[row.enterpriseType].type as any" size="small" effect="light">
|
||||
{{ TYPE_MAP[row.enterpriseType].label }}
|
||||
</el-tag>
|
||||
<span v-else class="rr-muted">--</span>
|
||||
</template>
|
||||
<template #registerTime="{ row }">
|
||||
<span class="rr-time">{{ fmtDate(row.registerTime) }}</span>
|
||||
</template>
|
||||
<template #action="{ row }">
|
||||
<el-button text type="primary" size="small" @click="showDetail(row)">详情</el-button>
|
||||
</template>
|
||||
</DataTable>
|
||||
</div>
|
||||
|
||||
<!-- 详情弹窗 -->
|
||||
<el-dialog v-model="detailVisible" title="企业详情" width="600" :style="{ borderRadius: '12px', overflow: 'hidden' }" align-center destroy-on-close>
|
||||
<template v-if="detailRow">
|
||||
<el-descriptions :column="2" border size="default">
|
||||
<el-descriptions-item label="企业名称">{{ detailRow.enterpriseName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="类型">{{ TYPE_MAP[detailRow.enterpriseType]?.label || '--' }}</el-descriptions-item>
|
||||
<el-descriptions-item label="法人">{{ detailRow.legalMan }}</el-descriptions-item>
|
||||
<el-descriptions-item label="邮箱">{{ detailRow.email }}</el-descriptions-item>
|
||||
<el-descriptions-item label="联系人">{{ detailRow.linkMan }}</el-descriptions-item>
|
||||
<el-descriptions-item label="联系电话">{{ detailRow.linkMobile }}</el-descriptions-item>
|
||||
<el-descriptions-item label="地址" :span="2">{{ detailRow.enterpriseAddress }}</el-descriptions-item>
|
||||
<el-descriptions-item label="主营产品" :span="2">{{ detailRow.mainProducts }}</el-descriptions-item>
|
||||
<el-descriptions-item label="注册时间">{{ fmtDate(detailRow.registerTime) }}</el-descriptions-item>
|
||||
<el-descriptions-item label="工商号">{{ detailRow.registerNo || '--' }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</template>
|
||||
<template #footer>
|
||||
<el-button @click="detailVisible = false">关闭</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.rr-sys-user { padding: 16px; }
|
||||
.rr-page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
|
||||
.rr-page-titles { display: flex; flex-direction: column; gap: 2px; }
|
||||
.rr-page-title { margin: 0; font-size: 18px; font-weight: 600; color: var(--el-text-color-primary); }
|
||||
.rr-page-sub { margin: 0; font-size: 12px; color: var(--el-text-color-secondary); }
|
||||
.rr-page-actions { display: flex; gap: 8px; }
|
||||
.rr-filters-row { display: flex; align-items: center; gap: 8px; margin-bottom: 14px; padding: 12px 16px; background: var(--el-fill-color-light); border-radius: 8px; }
|
||||
.rr-username-input { width: 240px; }
|
||||
.rr-type-select { width: 140px; }
|
||||
.rr-list-card { background: var(--el-bg-color); border-radius: 12px; padding: 4px; }
|
||||
.rr-time { font-family: 'SF Mono', Menlo, Monaco, Consolas, monospace; font-size: 12px; color: var(--el-text-color-regular); }
|
||||
.rr-muted { color: var(--el-text-color-placeholder); font-size: 12px; }
|
||||
</style>
|
||||
178
src/modules/user/FinanceListView.vue
Normal file
178
src/modules/user/FinanceListView.vue
Normal file
@ -0,0 +1,178 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* F006 财务管理 — 余额明细
|
||||
*
|
||||
* 后端端点 (BalanceDetailController @ /user/balancedetail):
|
||||
* GET /user/balancedetail/page 分页列表
|
||||
* GET /user/balancedetail/{id} 详情
|
||||
*/
|
||||
import { onMounted, ref, reactive } from 'vue'
|
||||
import { httpGet } from '@/utils/request'
|
||||
import DataTable from '@/components/DataTable/DataTable.vue'
|
||||
|
||||
interface BalanceDetail {
|
||||
id: string
|
||||
userId: string
|
||||
type: number
|
||||
money: number
|
||||
balance: number
|
||||
createDate: string
|
||||
remark: string
|
||||
orderId: string | null
|
||||
orderNo: string | null
|
||||
enterpriseId: string | null
|
||||
enterpriseName: string | null
|
||||
raw: any
|
||||
}
|
||||
|
||||
const TYPE_MAP: Record<number, { label: string; type: string }> = {
|
||||
1: { label: '收入', type: 'success' },
|
||||
2: { label: '支出', type: 'danger' },
|
||||
3: { label: '提现', type: 'warning' },
|
||||
4: { label: '冻结', type: 'info' }
|
||||
}
|
||||
|
||||
const list = ref<BalanceDetail[]>([])
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
const page = ref(1)
|
||||
const limit = ref(10)
|
||||
const loadError = ref<string | null>(null)
|
||||
|
||||
const filters = reactive({
|
||||
type: ''
|
||||
})
|
||||
|
||||
function fmtDate(ts?: string | number | null) {
|
||||
if (!ts) return '--'
|
||||
const n = typeof ts === 'string' ? Date.parse(ts) : ts
|
||||
if (!n) return String(ts)
|
||||
const d = new Date(n)
|
||||
if (isNaN(d.getTime())) return String(ts)
|
||||
const pad = (x: number) => String(x).padStart(2, '0')
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`
|
||||
}
|
||||
|
||||
function fmtMoney(cent?: number | null) {
|
||||
if (cent == null) return '--'
|
||||
return '¥' + (cent / 100).toFixed(2)
|
||||
}
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
loadError.value = null
|
||||
try {
|
||||
const data = await httpGet<{ total: number; list: any[] }>('/user/balancedetail/page', {
|
||||
page: page.value,
|
||||
limit: limit.value,
|
||||
...(filters.type !== '' ? { type: Number(filters.type) } : {})
|
||||
})
|
||||
list.value = (data.list || []).map((it: any) => ({
|
||||
id: String(it.id),
|
||||
userId: String(it.userId),
|
||||
type: it.type ?? 0,
|
||||
money: it.money ?? 0,
|
||||
balance: it.balance ?? 0,
|
||||
createDate: it.createDate || '--',
|
||||
remark: it.remark || '--',
|
||||
orderId: it.orderId ? String(it.orderId) : null,
|
||||
orderNo: it.orderNo || null,
|
||||
enterpriseId: it.enterpriseId ? String(it.enterpriseId) : null,
|
||||
enterpriseName: it.enterpriseName || null,
|
||||
raw: it
|
||||
}))
|
||||
total.value = data.total || 0
|
||||
} catch (e: any) {
|
||||
loadError.value = e?.msg || e?.message || '加载失败'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function onSearch() { page.value = 1; load() }
|
||||
function onReset() { filters.type = ''; page.value = 1; load() }
|
||||
function onPageChange(p: number) { page.value = p; load() }
|
||||
function onLimitChange(l: number) { limit.value = l; page.value = 1; load() }
|
||||
|
||||
onMounted(() => { load() })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="rr-sys-user">
|
||||
<div class="rr-page-header">
|
||||
<div class="rr-page-titles">
|
||||
<h2 class="rr-page-title">财务管理</h2>
|
||||
<p class="rr-page-sub">共 {{ total }} 条明细</p>
|
||||
</div>
|
||||
<div class="rr-page-actions">
|
||||
<el-button @click="onReset" class="rr-btn-secondary">重置</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rr-filters-row">
|
||||
<el-select v-model="filters.type" placeholder="类型筛选" class="rr-type-select" clearable size="small">
|
||||
<el-option value="1" label="收入" />
|
||||
<el-option value="2" label="支出" />
|
||||
<el-option value="3" label="提现" />
|
||||
<el-option value="4" label="冻结" />
|
||||
</el-select>
|
||||
<el-button class="rr-btn-secondary" size="small" @click="onSearch">查询</el-button>
|
||||
</div>
|
||||
|
||||
<div class="rr-list-card">
|
||||
<DataTable
|
||||
:columns="[
|
||||
{ prop: 'enterpriseName', label: '企业', width: 180 },
|
||||
{ prop: 'type', label: '类型', width: 90, slot: 'type' },
|
||||
{ prop: 'money', label: '金额', width: 120, slot: 'money' },
|
||||
{ prop: 'balance', label: '余额', width: 120, slot: 'balance' },
|
||||
{ prop: 'orderNo', label: '订单号', width: 180 },
|
||||
{ prop: 'remark', label: '备注', minWidth: 160 },
|
||||
{ prop: 'createDate', label: '时间', width: 170, slot: 'createDate' }
|
||||
]"
|
||||
:data="list"
|
||||
:loading="loading"
|
||||
:total="total"
|
||||
:page="page"
|
||||
:limit="limit"
|
||||
:show-pagination="true"
|
||||
:error="loadError"
|
||||
empty-text="暂无明细"
|
||||
row-key="id"
|
||||
@page-change="onPageChange"
|
||||
@limit-change="onLimitChange"
|
||||
>
|
||||
<template #type="{ row }">
|
||||
<el-tag v-if="TYPE_MAP[row.type]" :type="TYPE_MAP[row.type].type as any" size="small" effect="light">
|
||||
{{ TYPE_MAP[row.type].label }}
|
||||
</el-tag>
|
||||
<span v-else class="rr-muted">--</span>
|
||||
</template>
|
||||
<template #money="{ row }">
|
||||
<span class="rr-money">{{ fmtMoney(row.money) }}</span>
|
||||
</template>
|
||||
<template #balance="{ row }">
|
||||
<span class="rr-money">{{ fmtMoney(row.balance) }}</span>
|
||||
</template>
|
||||
<template #createDate="{ row }">
|
||||
<span class="rr-time">{{ fmtDate(row.createDate) }}</span>
|
||||
</template>
|
||||
</DataTable>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.rr-sys-user { padding: 16px; }
|
||||
.rr-page-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 14px; }
|
||||
.rr-page-titles { display: flex; flex-direction: column; gap: 2px; }
|
||||
.rr-page-title { margin: 0; font-size: 18px; font-weight: 600; color: var(--el-text-color-primary); }
|
||||
.rr-page-sub { margin: 0; font-size: 12px; color: var(--el-text-color-secondary); }
|
||||
.rr-page-actions { display: flex; gap: 8px; }
|
||||
.rr-filters-row { display: flex; align-items: center; gap: 8px; margin-bottom: 14px; padding: 12px 16px; background: var(--el-fill-color-light); border-radius: 8px; }
|
||||
.rr-type-select { width: 140px; }
|
||||
.rr-list-card { background: var(--el-bg-color); border-radius: 12px; padding: 4px; }
|
||||
.rr-money { font-family: 'SF Mono', Menlo, Monaco, Consolas, monospace; font-size: 13px; font-weight: 500; color: var(--el-text-color-primary); }
|
||||
.rr-time { font-family: 'SF Mono', Menlo, Monaco, Consolas, monospace; font-size: 12px; color: var(--el-text-color-regular); }
|
||||
.rr-muted { color: var(--el-text-color-placeholder); font-size: 12px; }
|
||||
</style>
|
||||
113
src/modules/user/UserListView.vue
Normal file
113
src/modules/user/UserListView.vue
Normal file
@ -0,0 +1,113 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* F006 用户管理 (前台用户列表) - W3.x C P0.3
|
||||
* 走 /user/user/page 后端
|
||||
*
|
||||
* 字段 (从 API 实测):
|
||||
* id, nickName, mobile, header (头像 URL)
|
||||
* total=2284 真实用户
|
||||
*
|
||||
* 注意: 老 sys/UserListView 是后台管理员 (/sys/user/*), 这页是前台用户
|
||||
*/
|
||||
import { onMounted, ref, reactive } from 'vue'
|
||||
import { httpGet } from '@/utils/request'
|
||||
import DataTable from '@/components/DataTable/DataTable.vue'
|
||||
|
||||
interface FrontUser {
|
||||
id: string
|
||||
nickName: string
|
||||
mobile: string
|
||||
header?: string | null
|
||||
raw: any
|
||||
}
|
||||
|
||||
const list = ref<FrontUser[]>([])
|
||||
const total = ref(0)
|
||||
const loading = ref(false)
|
||||
const query = reactive({
|
||||
page: 1,
|
||||
limit: 20,
|
||||
keyword: ''
|
||||
})
|
||||
|
||||
const columns = [
|
||||
{ prop: 'id', label: 'ID', width: 80 },
|
||||
{ prop: 'nickName', label: '昵称', minWidth: 160 },
|
||||
{ prop: 'mobile', label: '手机号', width: 140 },
|
||||
{
|
||||
prop: 'header',
|
||||
label: '头像',
|
||||
width: 80,
|
||||
formatter: (row: FrontUser) => row.header ? '有' : '无'
|
||||
}
|
||||
]
|
||||
|
||||
async function load() {
|
||||
loading.value = true
|
||||
try {
|
||||
const resp: any = await httpGet('/user/user/page', { page: query.page, limit: query.limit })
|
||||
if (resp && Array.isArray(resp.list)) {
|
||||
list.value = resp.list.map((it: any) => ({
|
||||
id: it.id,
|
||||
nickName: it.nickName || '--',
|
||||
mobile: it.mobile || '--',
|
||||
header: it.header || null,
|
||||
raw: it
|
||||
}))
|
||||
total.value = Number(resp.total || 0)
|
||||
} else {
|
||||
list.value = []
|
||||
total.value = 0
|
||||
}
|
||||
} catch (e) {
|
||||
list.value = []
|
||||
total.value = 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function onPageChange(p: number) { query.page = p; load() }
|
||||
function onSearch() { query.page = 1; load() }
|
||||
|
||||
onMounted(load)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="rr-page">
|
||||
<div class="rr-page-header">
|
||||
<div>
|
||||
<h2 class="rr-page-title">用户列表</h2>
|
||||
<p class="rr-page-sub">前台注册用户(共 {{ total }} 个)</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rr-card">
|
||||
<div class="rr-toolbar">
|
||||
<el-input v-model="query.keyword" placeholder="搜索手机号/昵称" clearable style="width: 280px;" @keyup.enter="onSearch" />
|
||||
<el-button type="primary" :icon="'Search'" @click="onSearch">搜索</el-button>
|
||||
</div>
|
||||
<DataTable
|
||||
:columns="columns"
|
||||
:data="list"
|
||||
:loading="loading"
|
||||
:total="total"
|
||||
:page="query.page"
|
||||
:limit="query.limit"
|
||||
:show-pagination="true"
|
||||
row-key="id"
|
||||
empty-text="暂无用户"
|
||||
@page-change="onPageChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.rr-page { max-width: 1400px; }
|
||||
.rr-page-header { margin-bottom: 20px; }
|
||||
.rr-page-title { font-size: 22px; font-weight: 600; letter-spacing: -0.02em; color: var(--ink); margin-bottom: 4px; }
|
||||
.rr-page-sub { color: var(--ink-3); font-size: 13px; }
|
||||
.rr-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); overflow: hidden; }
|
||||
.rr-toolbar { display: flex; align-items: center; gap: 8px; padding: 14px 20px; border-bottom: 1px solid var(--border); }
|
||||
</style>
|
||||
@ -47,6 +47,23 @@ const routes: RouteRecordRaw[] = [
|
||||
component: () => import('@/modules/inquiry/InquiryDetailView.vue'),
|
||||
meta: { title: '询价详情' }
|
||||
},
|
||||
// W3.x C P0.2: 询价列表 (生产环境 supplier/inquiry/inquiry 重定向到 /inquiry)
|
||||
{
|
||||
path: '/supplier/inquiry/inquiry',
|
||||
redirect: '/inquiry'
|
||||
},
|
||||
{
|
||||
path: '/supplier/inquiry',
|
||||
redirect: '/inquiry'
|
||||
},
|
||||
// W3.x C P0.3: 用户列表 (前台用户)
|
||||
{
|
||||
path: '/user/user',
|
||||
name: 'UserUserList',
|
||||
component: () => import('@/modules/user/UserListView.vue'),
|
||||
meta: { title: '用户列表' }
|
||||
},
|
||||
|
||||
// W3.2 订单管理(具体路由必须在 /:module/:action 之前,否则被通配拦截)
|
||||
{
|
||||
path: '/order',
|
||||
@ -203,6 +220,32 @@ const routes: RouteRecordRaw[] = [
|
||||
component: () => import('@/modules/sys/ProductListView.vue'),
|
||||
meta: { title: '商品管理' }
|
||||
},
|
||||
// W3.x C P0.4-7: 新增 4 个 view 路由
|
||||
{
|
||||
path: '/other/operateguide',
|
||||
name: 'OperateguideList',
|
||||
component: () => import('@/modules/other/operateguide/OperateguideListView.vue'),
|
||||
meta: { title: '内容列表' }
|
||||
},
|
||||
{
|
||||
path: '/api/lunbo',
|
||||
name: 'LunboList',
|
||||
component: () => import('@/modules/api/lunbo/LunboListView.vue'),
|
||||
meta: { title: '轮播管理' }
|
||||
},
|
||||
{
|
||||
path: '/api/help',
|
||||
name: 'HelpList',
|
||||
component: () => import('@/modules/api/help/HelpListView.vue'),
|
||||
meta: { title: '帮助中心' }
|
||||
},
|
||||
{
|
||||
path: '/sys/region',
|
||||
name: 'SysRegionList',
|
||||
component: () => import('@/modules/sys/region/RegionListView.vue'),
|
||||
meta: { title: '行政区域' }
|
||||
},
|
||||
|
||||
// W3.1 通用通配占位 — 覆盖菜单 70 个 url(两层路径,如 /admin/user/finance/recharge)
|
||||
// 注意:必须放在具体路由后面,否则会拦截 /inquiry /login
|
||||
// 使用 ?:action 让 action 可选(单层路径如 /sys/user 也匹配),不加 (.*)* 避免贪婪匹配抢静态路由
|
||||
|
||||
Loading…
Reference in New Issue
Block a user