新增 有家相册 uni-app 前端(uniapp/youjiala,H5 / 微信小程序 / App 三端)
从旧 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 及根目录临时截图
This commit is contained in:
1 parent
74757f9eb4
commit
6a8258ac4b
274 files changed
+30291
No files matched your search
@@ -0,0 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
// 登录后的落地页:当前为空白占位,后续由旧 WebVue 项目的
|
||||
// 菜单(/SecurityFunction/FindMenus)+ 权限守卫驱动真实内容
|
||||
import { useAuthGuard } from "@/composables";
|
||||
import { WebSitePolicy } from "@/policy/custom/webSitePolicy";
|
||||
|
||||
// 冷启动深链 / 分享链接直达本页时不经过任何导航 API,拦截器拦不到,故页面级再复核一次
|
||||
useAuthGuard();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<app-page :nav-title="WebSitePolicy.Title">
|
||||
<view class="dashboard">
|
||||
<view class="dashboard__placeholder">
|
||||
{{ WebSitePolicy.Title }}
|
||||
</view>
|
||||
</view>
|
||||
</app-page>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dashboard {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 60vh;
|
||||
}
|
||||
|
||||
.dashboard__placeholder {
|
||||
font-size: 32rpx;
|
||||
color: $u-tips-color;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,57 @@
|
||||
<script setup lang="ts">
|
||||
// 401 未认证页,移植自旧 WebVue 的 views/error/page401.vue
|
||||
// 谁来跳:router/nav-guard(未登录的站内跳转 + 冷启动深链复核)
|
||||
// 请求层的 401 仍按原逻辑直接回登录页(自动续期失败即重登,不必先看一眼错误页)
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import { useLocale } from "uview-pro";
|
||||
import { computed, ref } from "vue";
|
||||
import { goLoginWithRedirect } from "@/router/nav-guard";
|
||||
import { decodeQueryPath, pageUrl } from "@/router/routes";
|
||||
|
||||
const { t } = useLocale();
|
||||
|
||||
/** 出错的地址:守卫跳页时用 ?from= 带过来 */
|
||||
const fromPath = ref("");
|
||||
onLoad((options) => {
|
||||
fromPath.value = decodeQueryPath(options?.from);
|
||||
});
|
||||
|
||||
const reasons = computed(() => [
|
||||
t("ErrorPage.r401_1"),
|
||||
t("ErrorPage.r401_2"),
|
||||
t("ErrorPage.r401_3"),
|
||||
t("ErrorPage.r401_4"),
|
||||
]);
|
||||
|
||||
/** 去登录:带上原地址,登录成功后能直接回到这里 */
|
||||
function handleGoLogin() {
|
||||
goLoginWithRedirect(fromPath.value);
|
||||
}
|
||||
|
||||
/** 返回上一页:深链直达时页面栈只有本页,退回登录页 */
|
||||
function handleGoBack() {
|
||||
if (getCurrentPages().length > 1) {
|
||||
uni.navigateBack();
|
||||
return;
|
||||
}
|
||||
uni.reLaunch({ url: pageUrl("login") });
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<error-panel
|
||||
code="401" tone="blue" :badge="t('ErrorPage.badge401')" :title="t('ErrorPage.title401')"
|
||||
:description="t('ErrorPage.desc401')" :reasons="reasons" :from="fromPath" :hint="t('ErrorPage.hint401')"
|
||||
>
|
||||
<template #icon>
|
||||
<u-icon name="lock-fill" size="52" color="#ffffff" />
|
||||
</template>
|
||||
<template #actions>
|
||||
<u-button
|
||||
type="primary" size="large" shape="circle" :text="t('ErrorPage.goLoginBut')"
|
||||
@click="handleGoLogin"
|
||||
/>
|
||||
<u-button size="large" shape="circle" :text="t('ErrorPage.goBackBut')" @click="handleGoBack" />
|
||||
</template>
|
||||
</error-panel>
|
||||
</template>
|
||||
@@ -0,0 +1,53 @@
|
||||
<script setup lang="ts">
|
||||
// 403 无权限页,移植自旧 WebVue 的 views/error/page403.vue
|
||||
// 谁来跳:请求层拿到 HTTP 403(common/http.interceptor)时带原地址跳本页;
|
||||
// 页面级权限校验(旧 setupPermissionGuard)等 PAGE_PERMISSIONS 接入后接在 nav-guard
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import { useLocale } from "uview-pro";
|
||||
import { computed, ref } from "vue";
|
||||
import { goLoginWithRedirect } from "@/router/nav-guard";
|
||||
import { decodeQueryPath, pageUrl } from "@/router/routes";
|
||||
|
||||
const { t } = useLocale();
|
||||
|
||||
/** 出错的地址:跳页时用 ?from= 带过来 */
|
||||
const fromPath = ref("");
|
||||
onLoad((options) => {
|
||||
fromPath.value = decodeQueryPath(options?.from);
|
||||
});
|
||||
|
||||
const reasons = computed(() => [
|
||||
t("ErrorPage.r403_1"),
|
||||
t("ErrorPage.r403_2"),
|
||||
t("ErrorPage.r403_3"),
|
||||
t("ErrorPage.r403_4"),
|
||||
]);
|
||||
|
||||
/** 重新登录:权限随令牌一起下发,变更后必须重登才会生效 */
|
||||
function handleRelogin() {
|
||||
goLoginWithRedirect(fromPath.value);
|
||||
}
|
||||
|
||||
/** 回首页:首页本身只要求登录,不要求具体功能权限 */
|
||||
function handleGoHome() {
|
||||
uni.reLaunch({ url: pageUrl("dashboard") });
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<error-panel
|
||||
code="403" tone="amber" :badge="t('ErrorPage.badge403')" :title="t('ErrorPage.title403')"
|
||||
:description="t('ErrorPage.desc403')" :reasons="reasons" :from="fromPath" :hint="t('ErrorPage.hint403')"
|
||||
>
|
||||
<template #icon>
|
||||
<u-icon name="warning-fill" size="52" color="#ffffff" />
|
||||
</template>
|
||||
<template #actions>
|
||||
<u-button
|
||||
type="primary" size="large" shape="circle" :text="t('ErrorPage.reloginBut')"
|
||||
@click="handleRelogin"
|
||||
/>
|
||||
<u-button size="large" shape="circle" :text="t('ErrorPage.goHomeBut')" @click="handleGoHome" />
|
||||
</template>
|
||||
</error-panel>
|
||||
</template>
|
||||
@@ -0,0 +1,56 @@
|
||||
<script setup lang="ts">
|
||||
// 404 找不到页面,移植自旧 WebVue 的 views/error/page404.vue
|
||||
// 谁来跳:nav-guard 的 evaluate 认不出目标地址(站内跳错地址),
|
||||
// 以及 H5 地址栏直接访问一个路由表里不存在的地址(setupUnknownPathFallback 兜底)
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import { useLocale } from "uview-pro";
|
||||
import { computed, ref } from "vue";
|
||||
import { decodeQueryPath, pageUrl } from "@/router/routes";
|
||||
|
||||
const { t } = useLocale();
|
||||
|
||||
/** 出错的地址:兜底跳页时用 ?from= 带过来 */
|
||||
const fromPath = ref("");
|
||||
onLoad((options) => {
|
||||
fromPath.value = decodeQueryPath(options?.from);
|
||||
});
|
||||
|
||||
const reasons = computed(() => [
|
||||
t("ErrorPage.r404_1"),
|
||||
t("ErrorPage.r404_2"),
|
||||
t("ErrorPage.r404_3"),
|
||||
t("ErrorPage.r404_4"),
|
||||
]);
|
||||
|
||||
/** 回首页:dashboard 是登录后的落地页,与旧系统的 hasRoute("dashboard") 判断等价 */
|
||||
function handleGoHome() {
|
||||
uni.reLaunch({ url: pageUrl("dashboard") });
|
||||
}
|
||||
|
||||
/** 返回上一页:深链直达时页面栈只有本页,退回首页 */
|
||||
function handleGoBack() {
|
||||
if (getCurrentPages().length > 1) {
|
||||
uni.navigateBack();
|
||||
return;
|
||||
}
|
||||
handleGoHome();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<error-panel
|
||||
code="404" tone="violet" :badge="t('ErrorPage.badge404')" :title="t('ErrorPage.title404')"
|
||||
:description="t('ErrorPage.desc404')" :reasons="reasons" :from="fromPath" :hint="t('ErrorPage.hint404')"
|
||||
>
|
||||
<template #icon>
|
||||
<u-icon name="map" size="52" color="#ffffff" />
|
||||
</template>
|
||||
<template #actions>
|
||||
<u-button
|
||||
type="primary" size="large" shape="circle" :text="t('ErrorPage.goHomeBut')"
|
||||
@click="handleGoHome"
|
||||
/>
|
||||
<u-button size="large" shape="circle" :text="t('ErrorPage.goBackBut')" @click="handleGoBack" />
|
||||
</template>
|
||||
</error-panel>
|
||||
</template>
|
||||
@@ -0,0 +1,183 @@
|
||||
<script setup lang="ts">
|
||||
// 最简登录页:复刻旧系统 WebVue 的登录流程,用于联调 CloudBuilder 后端接口
|
||||
// 流程:GetRsaPublicKey -> RsaUserPassword = RSA(Guid + MD5(密码)) -> Login -> 存 JWT
|
||||
// 加密已抽到 @/common/pwd-crypto(wxmp-rsa 纯 JS 实现,H5/小程序/App 全端可用)
|
||||
import type { LoginData } from "@/data/auto/loginData";
|
||||
import type { LoginProfile } from "@/data/auto/loginProfile";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import { useLocale } from "uview-pro";
|
||||
import { computed, ref } from "vue";
|
||||
import { backendLocale, baseUrl } from "@/common/http.interceptor";
|
||||
import { encryptLoginPassword } from "@/common/pwd-crypto";
|
||||
import { LoginData as LoginDataClass } from "@/data/auto/loginData";
|
||||
import { reloadLocales } from "@/locale";
|
||||
import { decodeQueryPath, pageUrl, resolvePageName } from "@/router/routes";
|
||||
import authService from "@/service/security/authService";
|
||||
import { useLoginProfileStore } from "@/stores";
|
||||
|
||||
const userMobile = ref("");
|
||||
const password = ref("");
|
||||
const loading = ref(false);
|
||||
const loginProfileStore = useLoginProfileStore();
|
||||
|
||||
/**
|
||||
* 登录成功后的落地地址:被守卫送去登录页时由 ?redirect= 带过来(见 nav-guard 的 goLoginWithRedirect)。
|
||||
* 只接受 PAGE_PATHS 里认得出的地址,避免 ?redirect= 把人送到站外或不存在的路径
|
||||
*/
|
||||
const redirectUrl = ref(pageUrl("dashboard"));
|
||||
onLoad((options) => {
|
||||
const target = decodeQueryPath(options?.redirect);
|
||||
if (resolvePageName(target)) {
|
||||
redirectUrl.value = target;
|
||||
}
|
||||
});
|
||||
|
||||
// 登录页中英文切换:走 uView Pro 语言包(t / setLocale),语言包 name 为 zh-CN / en-US
|
||||
const { t, setLocale, currentLocale } = useLocale();
|
||||
const isZh = computed(() => (currentLocale.value?.name ?? "zh-CN") !== "en-US");
|
||||
function toggleLang() {
|
||||
setLocale(isZh.value ? "en-US" : "zh-CN");
|
||||
// 开发构建顺手重拉数据库翻译:改完 DB 词条点一下切换就能看到新值,无需整页刷新
|
||||
// (生产构建 import.meta.env.DEV 为 false,该分支编译期即被剔除)
|
||||
if (import.meta.env.DEV) {
|
||||
reloadLocales();
|
||||
}
|
||||
}
|
||||
|
||||
/** 取消:清空输入 */
|
||||
function handleCancel() {
|
||||
userMobile.value = "";
|
||||
password.value = "";
|
||||
}
|
||||
|
||||
// 开发构建显示「登录测试」标记,发布构建只显示后端地址(import.meta.env.DEV 编译期确定)
|
||||
const isDev = import.meta.env.DEV;
|
||||
|
||||
/** 生成 v4 风格 Guid(无需引入 uuid 包,与旧系统 uuidv4() 等效) */
|
||||
function makeGuid(): string {
|
||||
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
||||
const r = (Math.random() * 16) | 0;
|
||||
const v = c === "x" ? r : (r & 0x3) | 0x8;
|
||||
return v.toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
async function handleLogin() {
|
||||
if (!userMobile.value || !password.value) {
|
||||
uni.showToast({ title: "请输入手机号或者账号名和密码", icon: "none" });
|
||||
return;
|
||||
}
|
||||
loading.value = true;
|
||||
try {
|
||||
const guid = makeGuid();
|
||||
|
||||
// 1. 取后端 RSA 公钥(GET /Auth/GetRsaPublicKey,免鉴权)
|
||||
const key = await authService.GetRsaPublicKey();
|
||||
|
||||
// 2. 密码先 MD5 再与 Guid 拼接后 RSA 加密(算法与旧系统一致,库换为跨端的 wxmp-rsa)
|
||||
const rsaUserPassword = encryptLoginPassword(password.value, guid, String(key));
|
||||
if (!rsaUserPassword) {
|
||||
uni.showToast({ title: "密码加密失败,请重试", icon: "none" });
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 登录(失败时拦截器已统一弹窗提示,这里只处理成功路径)
|
||||
const data: LoginData = new LoginDataClass();
|
||||
data.UserMobile = userMobile.value;
|
||||
data.Guid = guid;
|
||||
data.RsaUserPassword = rsaUserPassword;
|
||||
data.Culture = backendLocale();
|
||||
const profile: LoginProfile = await authService.Login(data);
|
||||
|
||||
// 4. 存登录档案:档案字段进 store,Token 交由请求层保管(此后自动带 Bearer 头 + 401 自动续期)
|
||||
loginProfileStore.updateLoginProfile(profile);
|
||||
|
||||
uni.showToast({ title: `欢迎,${profile.UserLocalName || profile.Username || profile.UserMobile}`, icon: "none" });
|
||||
// 回到被拦前的页面(默认 dashboard):reLaunch 会清空页面栈,避免返回键退回登录页
|
||||
setTimeout(() => uni.reLaunch({ url: redirectUrl.value }), 800);
|
||||
}
|
||||
catch {
|
||||
// 错误提示已由 http.interceptor 统一处理
|
||||
}
|
||||
finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<view class="login-page">
|
||||
<view class="login-title">
|
||||
有家相册
|
||||
</view>
|
||||
<view class="login-subtitle">
|
||||
{{ isDev ? `登录测试 · ${baseUrl}` : baseUrl }}
|
||||
</view>
|
||||
|
||||
<view class="login-form">
|
||||
<u-input v-model="userMobile" :placeholder="t('LoginForm.userLabel')" border clearable />
|
||||
<u-input
|
||||
v-model="password" type="password" :placeholder="t('LoginForm.passwordLabel')" border :password-icon="true"
|
||||
@confirm="handleLogin"
|
||||
/>
|
||||
<u-button type="primary" shape="circle" :loading="loading" :text="t('LoginForm.loginBut')" @click="handleLogin" />
|
||||
<u-button shape="circle" :text="t('LoginForm.cancelBut')" @click="handleCancel" />
|
||||
</view>
|
||||
|
||||
<view class="login-lang" @click="toggleLang">
|
||||
<text class="login-lang__label">
|
||||
{{ t('LoginForm.cultureLabel') }}
|
||||
</text>
|
||||
<text class="login-lang__switch">
|
||||
{{ isZh ? 'English' : '中文' }}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.login-page {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 200rpx 60rpx 0;
|
||||
background: linear-gradient(160deg, rgba(41, 121, 255, 0.08), rgba(25, 190, 107, 0.06));
|
||||
}
|
||||
|
||||
.login-title {
|
||||
font-size: 56rpx;
|
||||
font-weight: 700;
|
||||
color: var(--u-type-primary);
|
||||
}
|
||||
|
||||
.login-subtitle {
|
||||
margin: 16rpx 0 80rpx;
|
||||
font-size: 24rpx;
|
||||
color: $u-tips-color;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 40rpx;
|
||||
}
|
||||
|
||||
.login-lang {
|
||||
margin-top: 48rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.login-lang__label {
|
||||
color: $u-tips-color;
|
||||
}
|
||||
|
||||
.login-lang__switch {
|
||||
color: var(--u-type-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user