用户原话: 不是让你沿用老vue页面内容了吗, 只是更换成新版本的UI 用户原话: 要能够交付使用, 像原有的vue平台一样使用 整改 (5 个 view 从严重简化变完整版): - OrderListView 240 行: 订单号/合同号/供应商/状态/时间范围 完整搜索, 8 种状态 tab, 操作 (详情/付款/发货/收货/退款/取消/打印), 批量导出, type 自适应 good/service/group - OrderDetailView 582 行: 完整 4-tab (订单信息/商品明细/物流/付款发票), 状态卡 + 操作按钮 (按状态动态显示), 9 种订单状态, 推广信息, 多状态按需渲染 - GoodListView 507 行: 12 列表格 (封面/商品/供应商/3 级分类/品牌/单位/铺货/审核/状态), 4 种状态 tab, 5 种操作 (详情/审核/上下架/改分类/删除), 5 种批量操作, 详情弹窗含 SKU 列表 (编码/原价/现价/秒杀价/拼团价/库存) - ServiceGoodListView 302 行: 同上针对服务商品 - InquiryDetailView 229 行: 询价基本信息 (产品/品牌/规格/数量/收货地址/交货日期/价格有效), 附件下载, 报价列表 (供应商/状态/金额/次数/来源/查看报价) 字段 100% 沿用老 vue (supplierGoodId/contractCode/groupNumber/acceptNum 等), 新增 mock 数据 (3-8 条覆盖各种业务场景), 弹窗/操作/批量/导出 全部走通 (mock 实现) + W3.x C P0-P2 累计 30+ 个 view 文件补 commit (inquiry/openapi/order/sys 等 11+ 模块) router 调整: 9 个 order 路由统一到 OrderListView (type 自动推断) + /supplier/servicegood 路由独立 (避免被 tree-shake)
357 lines
11 KiB
Vue
357 lines
11 KiB
Vue
<script setup lang="ts">
|
|
/**
|
|
* F006 W4.17 智能占位页
|
|
*
|
|
* 自动检测后端 API 状态:
|
|
* - 200: 显示数据
|
|
* - 500: 显示"后端服务异常"
|
|
* - 401/403: 显示"权限不足,请联系管理员"
|
|
* - 404: 显示"后端 API 未实现"
|
|
* - 其他: 显示"加载失败"
|
|
*
|
|
* 接收 route params:
|
|
* - moduleName: 模块名(显示用)
|
|
* - id: 路径参数 id(可选)
|
|
* - name: query 模块名
|
|
*/
|
|
import { ref, onMounted, computed } from 'vue'
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
import { ElMessage } from 'element-plus'
|
|
import { useUserStore } from '@/stores/user'
|
|
import { httpGet } from '@/utils/request'
|
|
|
|
const route = useRoute()
|
|
const router = useRouter()
|
|
const userStore = useUserStore()
|
|
|
|
const moduleName = computed(() => {
|
|
return (route.query.name as string) || (route.params.id as string) || '该模块'
|
|
})
|
|
|
|
const status = ref<'checking' | '200' | '500' | '401' | '404' | 'error'>('checking')
|
|
const statusText = ref('检测后端 API 状态...')
|
|
const errorDetail = ref('')
|
|
const apiUrl = ref('')
|
|
const userRole = computed(() => userStore.superAdmin ? '超级管理员' : (userStore.username || '当前用户'))
|
|
|
|
// 自动检测:从当前路径推断 API URL
|
|
const inferApiUrl = computed(() => {
|
|
const path = route.path
|
|
// /admin 前缀去掉
|
|
let apiPath = path
|
|
if (apiPath.startsWith('/admin')) {
|
|
apiPath = apiPath.replace('/admin', '')
|
|
}
|
|
// 已知需要 /page 后缀的路径(列表类)
|
|
const needsPageSuffix = (
|
|
apiPath.startsWith('/sys/') ||
|
|
apiPath === '/user/user' ||
|
|
apiPath.startsWith('/user/enterprise') ||
|
|
apiPath.startsWith('/user/finance') ||
|
|
apiPath === '/order/ordergood' ||
|
|
apiPath === '/order/serviceorder' ||
|
|
apiPath === '/supplier/good' ||
|
|
apiPath === '/supplier/servicegood' ||
|
|
apiPath === '/supplier/activityzone' ||
|
|
apiPath === '/course/coupon' ||
|
|
apiPath === '/other/textconfig' ||
|
|
apiPath === '/devtools/param'
|
|
)
|
|
if (needsPageSuffix && !apiPath.endsWith('/page') && !apiPath.includes('/search')) {
|
|
return `${apiPath}/page?page=1&limit=1`
|
|
}
|
|
// 列表 API 已经有(不需要 /page)
|
|
if (apiPath.startsWith('/openapi/') && apiPath.endsWith('/list')) {
|
|
return `${apiPath}?page=1&limit=1`
|
|
}
|
|
// 默认
|
|
return `${apiPath}?page=1&limit=1`
|
|
})
|
|
|
|
async function checkApi() {
|
|
apiUrl.value = inferApiUrl.value
|
|
if (!apiUrl.value) {
|
|
status.value = '404'
|
|
statusText.value = '该模块未配置 API 端点'
|
|
return
|
|
}
|
|
try {
|
|
const token = localStorage.getItem('access_token')
|
|
if (!token) {
|
|
status.value = '401'
|
|
statusText.value = '请先登录'
|
|
return
|
|
}
|
|
const res = await fetch(apiUrl.value, {
|
|
headers: { Authorization: `Bearer ${token}` }
|
|
})
|
|
// W4.19 修复:nginx 没代理的业务模块会返 404 HTML 页, 不是 JSON
|
|
// 检测 content-type 防止误判
|
|
const contentType = res.headers.get('content-type') || ''
|
|
const isJson = contentType.includes('application/json')
|
|
|
|
if (res.status === 200) {
|
|
if (isJson) {
|
|
const data = await res.json()
|
|
if (data.code === 0) {
|
|
status.value = '200'
|
|
statusText.value = '后端 API 正常'
|
|
} else {
|
|
status.value = 'error'
|
|
statusText.value = data.msg || '后端业务异常'
|
|
errorDetail.value = JSON.stringify(data).substring(0, 200)
|
|
}
|
|
} else {
|
|
// 200 但不是 JSON(罕见, 通常是 HTML 错误页)
|
|
status.value = 'error'
|
|
statusText.value = '后端返了非 JSON 响应(可能被 nginx 拦截)'
|
|
errorDetail.value = `Content-Type: ${contentType}`
|
|
}
|
|
} else if (res.status === 401) {
|
|
status.value = '401'
|
|
statusText.value = '未登录或 token 过期'
|
|
} else if (res.status === 403) {
|
|
status.value = '500'
|
|
statusText.value = '权限不足,后端拒绝了请求'
|
|
errorDetail.value = 'HTTP 403 Forbidden - 常见于 admin 用户缺少该 controller 权限'
|
|
} else if (res.status === 404) {
|
|
// nginx 返 404 可能是 HTML (代理缺失) 或 JSON (后端真没)
|
|
status.value = '404'
|
|
statusText.value = isJson
|
|
? '后端 API 未实现'
|
|
: 'nginx 无此路径代理(后端可能需要 controller 或 nginx 需加代理)'
|
|
errorDetail.value = isJson ? 'JSON 404' : 'HTML 404,检查 nginx location 配置'
|
|
} else if (res.status === 500) {
|
|
const data = isJson ? await res.json().catch(() => ({})) : {}
|
|
status.value = '500'
|
|
statusText.value = data.msg || '后端服务异常(可能权限不足)'
|
|
errorDetail.value = `HTTP 500 - ${apiUrl.value}`
|
|
} else {
|
|
status.value = 'error'
|
|
statusText.value = `HTTP ${res.status}`
|
|
}
|
|
} catch (e: any) {
|
|
status.value = 'error'
|
|
statusText.value = e?.message || '网络错误'
|
|
}
|
|
}
|
|
|
|
function goBack() {
|
|
if (window.history.length > 1) {
|
|
router.back()
|
|
} else {
|
|
router.push('/')
|
|
}
|
|
}
|
|
|
|
function copyApiUrl() {
|
|
navigator.clipboard?.writeText(apiUrl.value).then(() => {
|
|
ElMessage.success('API URL 已复制')
|
|
}).catch(() => {
|
|
ElMessage.warning('复制失败')
|
|
})
|
|
}
|
|
|
|
onMounted(() => {
|
|
checkApi()
|
|
})
|
|
|
|
const statusColor = computed(() => {
|
|
switch (status.value) {
|
|
case '200': return '#10B981'
|
|
case '500': return '#EF4444'
|
|
case '401': return '#F59E0B'
|
|
case '404': return '#6B7280'
|
|
case 'error': return '#EF4444'
|
|
default: return '#6B7280'
|
|
}
|
|
})
|
|
|
|
const statusTextZh = computed(() => {
|
|
switch (status.value) {
|
|
case '200': return '后端 API 正常'
|
|
case '500': return '后端服务异常'
|
|
case '401': return '未登录'
|
|
case '404': return 'API 未实现'
|
|
case 'error': return '请求失败'
|
|
case 'checking': return '检测中...'
|
|
default: return '未知状态'
|
|
}
|
|
})
|
|
|
|
const statusHint = computed(() => {
|
|
switch (status.value) {
|
|
case '200':
|
|
return '后端 API 返回 200,但页面尚未实装。可能是占位路由。'
|
|
case '500':
|
|
return '后端 Spring Security 拒绝了请求,通常是 admin 用户缺少对应权限。' +
|
|
'请通过老后台 /vue/sys/role 给管理员角色添加对应权限,或修改后端 controller 允许 super-admin 通过。'
|
|
case '401':
|
|
return 'Token 过期或未登录。请重新登录。'
|
|
case '404':
|
|
return '后端没有这个 controller 端点。需要后端开发新 controller 或使用其他服务路径(nginx 代理)。'
|
|
case 'error':
|
|
return '网络错误或后端异常。请检查后端服务状态。'
|
|
case 'checking':
|
|
return '正在自动检测后端 API 状态...'
|
|
default:
|
|
return ''
|
|
}
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div class="rr-placeholder">
|
|
<div class="rr-placeholder-card">
|
|
<div class="rr-status-badge" :style="{ background: statusColor + '20', color: statusColor }">
|
|
<span class="rr-status-dot" :style="{ background: statusColor }"></span>
|
|
{{ statusTextZh }}
|
|
</div>
|
|
|
|
<h2 class="rr-placeholder-title">{{ moduleName }}</h2>
|
|
<p class="rr-placeholder-sub">{{ statusText }}</p>
|
|
|
|
<div v-if="apiUrl" class="rr-api-info">
|
|
<div class="rr-api-label">检测 API</div>
|
|
<code class="rr-api-url" @click="copyApiUrl" :title="'点击复制'">{{ apiUrl }}</code>
|
|
</div>
|
|
|
|
<el-alert
|
|
v-if="status === '500'"
|
|
type="error"
|
|
:closable="false"
|
|
style="margin-bottom: 14px; text-align: left;"
|
|
>
|
|
<template #title><strong>权限不足</strong></template>
|
|
<p>当前账号 <code>{{ userRole }}</code> 缺少对应权限。</p>
|
|
<p>解决方式:去老后台 <code>/vue/sys/role</code> 给"管理员"角色加权限菜单,然后让 admin 重新登录。</p>
|
|
<p>详情: <code>{{ errorDetail }}</code></p>
|
|
</el-alert>
|
|
|
|
<el-alert
|
|
v-else-if="status === '404'"
|
|
type="info"
|
|
:closable="false"
|
|
style="margin-bottom: 14px; text-align: left;"
|
|
>
|
|
<template #title><strong>后端 API 未实现</strong></template>
|
|
<p>后端 admin-server 没有这个 controller 端点,或在 user-server/supplier-server 等其他服务需要 nginx 代理。</p>
|
|
<p>解决方式:让后端开发补 controller,或加 nginx 代理路径。</p>
|
|
</el-alert>
|
|
|
|
<el-alert
|
|
v-else-if="status === '200'"
|
|
type="success"
|
|
:closable="false"
|
|
style="margin-bottom: 14px; text-align: left;"
|
|
>
|
|
<template #title><strong>后端 API 工作正常</strong></template>
|
|
<p>后端 API 200 + 业务 code=0,但前端 Vue 组件尚未实装,显示此占位页。</p>
|
|
<p>可以:1) 让我把这个模块做成正式页面 / 2) 等后续开发</p>
|
|
</el-alert>
|
|
|
|
<el-alert
|
|
v-else-if="status === 'error'"
|
|
type="error"
|
|
:closable="false"
|
|
style="margin-bottom: 14px; text-align: left;"
|
|
>
|
|
<template #title><strong>请求失败</strong></template>
|
|
<p>{{ errorDetail }}</p>
|
|
</el-alert>
|
|
|
|
<p class="rr-hint">{{ statusHint }}</p>
|
|
|
|
<div class="rr-actions">
|
|
<el-button @click="checkApi" :loading="status === 'checking'">重新检测</el-button>
|
|
<el-button @click="goBack" plain>返回</el-button>
|
|
<el-button type="primary" @click="router.push('/')">返回数据看板</el-button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.rr-placeholder {
|
|
min-height: calc(100vh - 96px);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 24px;
|
|
}
|
|
.rr-placeholder-card {
|
|
background: var(--el-bg-color);
|
|
border: 1px solid var(--el-border-color-lighter);
|
|
border-radius: 12px;
|
|
padding: 40px 48px;
|
|
text-align: center;
|
|
max-width: 640px;
|
|
width: 100%;
|
|
box-shadow: 0 4px 16px -4px rgba(15, 23, 42, 0.06);
|
|
}
|
|
.rr-status-badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 6px 16px;
|
|
border-radius: 9999px;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
margin-bottom: 16px;
|
|
}
|
|
.rr-status-dot {
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 50%;
|
|
display: inline-block;
|
|
}
|
|
.rr-placeholder-title {
|
|
margin: 0 0 8px 0;
|
|
font-size: 20px;
|
|
font-weight: 600;
|
|
color: var(--el-text-color-primary);
|
|
}
|
|
.rr-placeholder-sub {
|
|
margin: 0 0 20px 0;
|
|
font-size: 13px;
|
|
color: var(--el-text-color-secondary);
|
|
}
|
|
.rr-api-info {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 10px 14px;
|
|
background: var(--el-fill-color-light);
|
|
border-radius: 8px;
|
|
margin-bottom: 14px;
|
|
font-size: 12px;
|
|
}
|
|
.rr-api-label {
|
|
color: var(--el-text-color-secondary);
|
|
flex-shrink: 0;
|
|
}
|
|
.rr-api-url {
|
|
flex: 1;
|
|
font-family: 'SF Mono', Menlo, Monaco, Consolas, monospace;
|
|
font-size: 11px;
|
|
color: var(--el-color-primary);
|
|
cursor: pointer;
|
|
word-break: break-all;
|
|
text-align: left;
|
|
background: transparent;
|
|
}
|
|
.rr-api-url:hover {
|
|
text-decoration: underline;
|
|
}
|
|
.rr-hint {
|
|
margin: 0 0 20px 0;
|
|
font-size: 12px;
|
|
color: var(--el-text-color-placeholder);
|
|
line-height: 1.6;
|
|
}
|
|
.rr-actions {
|
|
display: flex;
|
|
gap: 8px;
|
|
justify-content: center;
|
|
}
|
|
</style> |