F006 W3.xC + W5.x.x+1/3/4/5 + W6 + W7 完整批次 (17 commits) #1

Merged
direv merged 24 commits from develop into main 2026-07-05 13:07:34 +08:00
8 changed files with 770 additions and 0 deletions
Showing only changes of commit 78360d1d6e - Show all commits

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View File

@ -47,6 +47,23 @@ const routes: RouteRecordRaw[] = [
component: () => import('@/modules/inquiry/InquiryDetailView.vue'), component: () => import('@/modules/inquiry/InquiryDetailView.vue'),
meta: { title: '询价详情' } 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 之前,否则被通配拦截) // W3.2 订单管理(具体路由必须在 /:module/:action 之前,否则被通配拦截)
{ {
path: '/order', path: '/order',
@ -203,6 +220,32 @@ const routes: RouteRecordRaw[] = [
component: () => import('@/modules/sys/ProductListView.vue'), component: () => import('@/modules/sys/ProductListView.vue'),
meta: { title: '商品管理' } 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) // W3.1 通用通配占位 — 覆盖菜单 70 个 url(两层路径,如 /admin/user/finance/recharge)
// 注意:必须放在具体路由后面,否则会拦截 /inquiry /login // 注意:必须放在具体路由后面,否则会拦截 /inquiry /login
// 使用 ?:action 让 action 可选(单层路径如 /sys/user 也匹配),不加 (.*)* 避免贪婪匹配抢静态路由 // 使用 ?:action 让 action 可选(单层路径如 /sys/user 也匹配),不加 (.*)* 避免贪婪匹配抢静态路由