从旧 WebVue 移植的地基部分: - 登录页(RSA+MD5 密码加密、redirect 回跳、中英文切换)与登录后落地页 - 401 / 403 / 404 错误页 + error-panel 通用外壳(文案走 ErrorPage 语言包) - 登录守卫双链路:uni.addInterceptor 拦站内跳转,页面级 onShow 复核冷启动深链 - 单一请求链路 common/http.interceptor:JWT 唯一来源、401 并发刷新单例、 业务异常识别、HTTP 403 跳无权限页 - H5 干净短地址:initRouter 前向 __uniRoutes 追加克隆记录(router/h5-clean-url) - 路由登记表 router/routes.ts(PAGE_PATHS / pageUrl / PAGE_PERMISSIONS) 与新增页面四步登记约定(src/md/page-routing.md) - i18n:uView Pro 语言包 + 后端词条合并,双取值(后端 zh-cn/en,包名 zh-CN/en-US) - 后端接口层 service/ 与数据模型 data/auto/ 为生成器产物,不手改 不入库的内容: - 摘除脚手架残留的 Harmony 签名密码(manifest.json signingConfigs 与 harmony-configs/build-profile.json5 的 keyPassword / storePassword 已置空) - 忽略 node_modules、dist、unpackage、.hbuilderx、.agents、pnpm-lock.yaml 及根目录临时截图
317 lines
7.0 KiB
Vue
317 lines
7.0 KiB
Vue
<script setup lang="ts">
|
||
import type { PropType } from "vue";
|
||
import { useLocale } from "uview-pro";
|
||
import { WebSitePolicy } from "@/policy/custom/webSitePolicy";
|
||
|
||
/**
|
||
* 错误页通用外壳(401 / 403 / 404 共用),移植自旧 WebVue 的 components/basic/ErrorPanel.vue。
|
||
* 只负责排版与配色,具体文案、图标、按钮由各错误页提供。
|
||
* 旧版用 ant-design 图标 + <a> 链接,这里换成 uView Pro 的 u-icon 与纯文本,跨端可用。
|
||
*/
|
||
defineProps({
|
||
/** 状态码,如 401 */
|
||
code: {
|
||
type: String,
|
||
required: true,
|
||
},
|
||
/** 状态码下方的短标签,如「UNAUTHORIZED · 未认证」 */
|
||
badge: {
|
||
type: String,
|
||
default: "",
|
||
},
|
||
/** 主题色:blue(默认)/ amber(无权限)/ violet(找不到页面) */
|
||
tone: {
|
||
type: String as PropType<"blue" | "amber" | "violet">,
|
||
default: "blue",
|
||
},
|
||
/** 主标题:一句话说清出了什么问题 */
|
||
title: {
|
||
type: String,
|
||
required: true,
|
||
},
|
||
/** 副标题:补充说明 */
|
||
description: {
|
||
type: String,
|
||
default: "",
|
||
},
|
||
/** 可能的原因清单 */
|
||
reasons: {
|
||
type: Array as PropType<string[]>,
|
||
default: () => [],
|
||
},
|
||
/** 出错的地址(守卫跳页时用 ?from= 带过来) */
|
||
from: {
|
||
type: String,
|
||
default: "",
|
||
},
|
||
/** 兜底提示 */
|
||
hint: {
|
||
type: String,
|
||
default: "",
|
||
},
|
||
});
|
||
|
||
// 「可能的原因」「出错的地址:」是组件自己的固定标签,走语言包;业务文案仍由各错误页传入
|
||
const { t } = useLocale();
|
||
</script>
|
||
|
||
<template>
|
||
<view class="ep" :class="`ep--${tone}`">
|
||
<!-- 顶部渐变头图:品牌 + 大号状态码 -->
|
||
<view class="ep-hero">
|
||
<view class="ep-brand">
|
||
<image class="ep-brand__logo" src="/static/logo.png" mode="aspectFill" />
|
||
<text class="ep-brand__name">
|
||
{{ WebSitePolicy.Title }}
|
||
</text>
|
||
</view>
|
||
<view class="ep-code">
|
||
{{ code }}
|
||
</view>
|
||
<view class="ep-tag">
|
||
{{ badge }}
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 悬浮卡片:图标 + 说明 + 可能原因 + 操作 -->
|
||
<view class="ep-card">
|
||
<view class="ep-icon">
|
||
<slot name="icon" />
|
||
</view>
|
||
|
||
<view class="ep-title">
|
||
{{ title }}
|
||
</view>
|
||
<view v-if="description" class="ep-desc">
|
||
{{ description }}
|
||
</view>
|
||
|
||
<view v-if="reasons && reasons.length" class="ep-reasons">
|
||
<view class="ep-reasons__title">
|
||
<text>{{ t('ErrorPage.reasonsTitle') }}</text>
|
||
</view>
|
||
<view v-for="(item, i) in reasons" :key="i" class="ep-reasons__item">
|
||
<text class="ep-reasons__dot">
|
||
•
|
||
</text>
|
||
<text class="ep-reasons__text">
|
||
{{ item }}
|
||
</text>
|
||
</view>
|
||
</view>
|
||
|
||
<view v-if="from" class="ep-from">
|
||
<text>{{ t('ErrorPage.fromLabel') }}</text>
|
||
<text class="ep-from__code">
|
||
{{ from }}
|
||
</text>
|
||
</view>
|
||
|
||
<view class="ep-actions">
|
||
<slot name="actions" />
|
||
</view>
|
||
|
||
<view v-if="hint" class="ep-hint">
|
||
{{ hint }}
|
||
</view>
|
||
</view>
|
||
|
||
<view class="ep-footer">
|
||
<text class="ep-footer__line">
|
||
{{ WebSitePolicy.Copy }}
|
||
</text>
|
||
<text class="ep-footer__line">
|
||
{{ WebSitePolicy.Beian }}
|
||
</text>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<style lang="scss" scoped>
|
||
.ep {
|
||
/* 主题色:默认品牌蓝→青渐变,amber / violet 用于区分不同错误类型 */
|
||
--ep-from: #1677ff;
|
||
--ep-to: #36cfc9;
|
||
--ep-shadow: rgba(22, 119, 255, 0.18);
|
||
|
||
display: flex;
|
||
flex-direction: column;
|
||
min-height: 100vh;
|
||
background-color: $u-bg-white;
|
||
}
|
||
|
||
.ep--amber {
|
||
--ep-from: #fa8c16;
|
||
--ep-to: #ffc53d;
|
||
--ep-shadow: rgba(250, 140, 22, 0.18);
|
||
}
|
||
|
||
.ep--violet {
|
||
--ep-from: #722ed1;
|
||
--ep-to: #b37feb;
|
||
--ep-shadow: rgba(114, 46, 209, 0.18);
|
||
}
|
||
|
||
/* ---------- 头图 ---------- */
|
||
.ep-hero {
|
||
/* 状态栏高度:navigationStyle 为 custom,页面顶部会被状态栏压住 */
|
||
padding: calc(var(--status-bar-height) + 32rpx) 32rpx 88rpx;
|
||
border-radius: 0 0 44rpx 44rpx;
|
||
background-image: linear-gradient(135deg, var(--ep-from) 0%, var(--ep-to) 100%);
|
||
box-shadow: 0 12rpx 36rpx var(--ep-shadow);
|
||
color: $u-bg-white;
|
||
}
|
||
|
||
.ep-brand {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.ep-brand__logo {
|
||
width: 52rpx;
|
||
height: 52rpx;
|
||
border-radius: 10rpx;
|
||
background-color: rgba(255, 255, 255, 0.24);
|
||
}
|
||
|
||
.ep-brand__name {
|
||
margin-left: 16rpx;
|
||
font-size: 30rpx;
|
||
font-weight: 600;
|
||
opacity: 0.95;
|
||
}
|
||
|
||
.ep-code {
|
||
margin-top: 16rpx;
|
||
font-size: 104rpx;
|
||
font-weight: 800;
|
||
line-height: 1;
|
||
letter-spacing: 4rpx;
|
||
}
|
||
|
||
.ep-tag {
|
||
margin-top: 12rpx;
|
||
font-size: 24rpx;
|
||
letter-spacing: 2rpx;
|
||
opacity: 0.9;
|
||
}
|
||
|
||
/* ---------- 悬浮卡片 ---------- */
|
||
.ep-card {
|
||
margin: -56rpx 32rpx 36rpx;
|
||
padding: 36rpx 32rpx 40rpx;
|
||
border-radius: 40rpx;
|
||
background-color: #fff;
|
||
box-shadow: 0 16rpx 48rpx rgba(15, 35, 60, 0.08);
|
||
}
|
||
|
||
.ep-icon {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 96rpx;
|
||
height: 96rpx;
|
||
margin-bottom: 24rpx;
|
||
border-radius: 28rpx;
|
||
background-image: linear-gradient(135deg, var(--ep-from) 0%, var(--ep-to) 100%);
|
||
box-shadow: 0 4rpx 16rpx var(--ep-shadow);
|
||
}
|
||
|
||
.ep-title {
|
||
font-size: 36rpx;
|
||
font-weight: 700;
|
||
line-height: 1.4;
|
||
color: $u-main-color;
|
||
}
|
||
|
||
.ep-desc {
|
||
margin-top: 12rpx;
|
||
font-size: 27rpx;
|
||
line-height: 1.65;
|
||
color: $u-content-color;
|
||
}
|
||
|
||
.ep-reasons {
|
||
margin-top: 28rpx;
|
||
padding: 24rpx;
|
||
border: 2rpx solid #eef0f4;
|
||
border-radius: 28rpx;
|
||
background-color: #f7f9fc;
|
||
}
|
||
|
||
.ep-reasons__title {
|
||
margin-bottom: 12rpx;
|
||
font-size: 26rpx;
|
||
font-weight: 600;
|
||
color: $u-main-color;
|
||
}
|
||
|
||
.ep-reasons__item {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
margin-top: 8rpx;
|
||
}
|
||
|
||
.ep-reasons__dot {
|
||
margin-right: 12rpx;
|
||
font-size: 26rpx;
|
||
line-height: 1.6;
|
||
color: var(--ep-from);
|
||
}
|
||
|
||
.ep-reasons__text {
|
||
flex: 1;
|
||
font-size: 26rpx;
|
||
line-height: 1.6;
|
||
color: $u-tips-color;
|
||
}
|
||
|
||
.ep-from {
|
||
margin-top: 24rpx;
|
||
font-size: 24rpx;
|
||
line-height: 1.6;
|
||
color: $u-light-color;
|
||
word-break: break-all;
|
||
}
|
||
|
||
.ep-from__code {
|
||
margin-left: 8rpx;
|
||
padding: 2rpx 12rpx;
|
||
border-radius: 12rpx;
|
||
background-color: #f2f4f8;
|
||
color: $u-main-color;
|
||
}
|
||
|
||
.ep-actions {
|
||
display: flex;
|
||
flex-direction: column;
|
||
/* 按钮来自各页面的插槽,拿不到本组件的 scoped 样式,故间距写在容器上 */
|
||
gap: 20rpx;
|
||
margin-top: 36rpx;
|
||
}
|
||
|
||
.ep-hint {
|
||
margin-top: 28rpx;
|
||
font-size: 24rpx;
|
||
line-height: 1.6;
|
||
color: $u-light-color;
|
||
}
|
||
|
||
/* ---------- 底部信息 ---------- */
|
||
.ep-footer {
|
||
display: flex;
|
||
flex: 1;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: flex-end;
|
||
padding: 0 32rpx 28rpx;
|
||
text-align: center;
|
||
}
|
||
|
||
.ep-footer__line {
|
||
font-size: 22rpx;
|
||
line-height: 32rpx;
|
||
color: $u-light-color;
|
||
}
|
||
</style>
|