新增 有家相册 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:
owenchen committed 2026-10-13 08:27:34 +08:00
1 parent 74757f9eb4
commit 6a8258ac4b
274 files changed
+30291

No files matched your search

+183
View File
@@ -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>