优化小程序部分

This commit is contained in:
hero920103 committed 2026-10-31 19:17:37 +08:00
1 parent 727319a6eb
commit d4e89788c5
46 files changed
+4906 -63

No files matched your search

+33
View File
@@ -1,5 +1,37 @@
<script>
import { getBaseURL } from '@/api/request.js'
import { isLoggedIn } from '@/utils/auth.js'
const LOGIN_PAGE = '/pages/login/login'
// 不需要登录即可停留的页面:登录 / 注册 / 忘记密码 / 人机验证 / 小程序码确认页 / 启动跳板页
const PUBLIC_ROUTES = [
'pages/login/login',
'pages/login/register',
'pages/login/forget',
'pages/login/geetest-webview',
'pages/register/confirm',
'pages/index/index'
]
/** 当前页面路由(取不到时返回空串,交给页面级检测兜底) */
function currentRoute() {
try {
const pages = getCurrentPages()
if (!pages.length) return ''
const cur = pages[pages.length - 1]
return (cur && (cur.route || (cur.$page && cur.$page.path))) || ''
} catch (e) {
return ''
}
}
/** 未登录一律回登录页:覆盖「切后台回来」「从其他入口直接进内页」等场景 */
function ensureLogin() {
const route = currentRoute()
if (!route || PUBLIC_ROUTES.indexOf(route) !== -1) return
if (isLoggedIn()) return
uni.reLaunch({ url: LOGIN_PAGE })
}
export default {
onLaunch() {
@@ -7,6 +39,7 @@
},
onShow() {
console.log('App Show')
ensureLogin()
},
onHide() {
console.log('App Hide')
+19
View File
@@ -11,6 +11,20 @@ const DEFAULT_PROD_API = 'https://api.yunzer.cn'
/**
* @returns {string} 去掉末尾斜杠的 baseURL
*/
/**
* 小程序端 baseURL 兜底:微信要求 request 必须走「已配置的 https 合法域名」,
* localhost / 127.0.0.1 / 相对路径 / http 明文在体验版与正式版都会直接报
* "url not in domain list"。出现这类取值一律回退生产 API(本地调试请用 H5 端,
* 或在开发者工具勾选「不校验合法域名」时用真机调试)。
*/
function sanitizeMiniProgramBase(base) {
const b = String(base || '')
if (!b || b.startsWith('/') || !/^https:\/\//i.test(b) || /(localhost|127\.0\.0\.1)/i.test(b)) {
return DEFAULT_PROD_API
}
return b
}
export function resolveBaseURL() {
let fromEnv = ''
try {
@@ -24,7 +38,12 @@ export function resolveBaseURL() {
}
if (fromEnv && fromEnv !== 'undefined' && fromEnv !== 'null') {
// #ifdef MP
return sanitizeMiniProgramBase(String(fromEnv))
// #endif
// #ifndef MP
return String(fromEnv).replace(/\/$/, '')
// #endif
}
// #ifdef H5
+41
View File
@@ -0,0 +1,41 @@
/**
* 小程序订阅消息(对应后端 go/controllers/api_mini_subscribe.go)
*
* 订阅消息是「用户授权 → 服务器主动推给用户」的通道:
* 1. getSubscribeTemplates() 取当前可订阅的模板
* 2. 用户在微信弹窗里同意后,authorizeSubscribe() 上报,服务器记一次发送机会
* 3. 之后业务侧发站内信/审批待办,服务器自动推送到用户微信
*/
import { request } from '@/api/request.js'
/** 取可订阅的模板列表(含模板 ID、跳转页面、剩余可发送次数) */
export function getSubscribeTemplates() {
return request({
url: '/api/mini/subscribe/templates',
method: 'get'
})
}
/**
* 上报授权结果:每授权一次,服务器记一次可发送机会
* @param {string[]} templateIds 用户同意的模板 ID 列表
*/
export function authorizeSubscribe(templateIds) {
return request({
url: '/api/mini/subscribe/authorize',
method: 'post',
data: { template_ids: templateIds }
})
}
/**
* 发一条测试订阅消息(验证模板配置是否正确)
* @param {{title?: string, content?: string}} payload
*/
export function sendTestSubscribeNotice(payload = {}) {
return request({
url: '/api/mini/subscribe/test',
method: 'post',
data: payload
})
}
+3 -4
View File
@@ -412,20 +412,19 @@
}
},
{
"path {
"path": "pages/login/forget",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "忘记密码"
}
},
{
},
{
"path": "pages/register/confirm",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "确认注册"
}
},
},
{
"path": "pages/login/geetest-webview",
"style": {
+20 -34
View File
@@ -1,52 +1,38 @@
<template>
<view class="content">
<image class="logo" src="/static/logo.png"></image>
<view class="text-area">
<text class="title">{{title}}</text>
</view>
<view class="boot">
<image class="boot-logo" src="/static/logo.png" mode="aspectFit"></image>
<text class="boot-text">正在进入…</text>
</view>
</template>
<script>
export default {
data() {
return {
title: 'Hello'
}
},
onLoad() {
import { onShow } from '@dcloudio/uni-app'
import { isLoggedIn } from '@/utils/auth.js'
},
methods: {
}
}
/**
* 启动跳板页:若启动入口指向本页(脚手架默认首页),先做登录检测再分流,
* 未登录直接进登录页,已登录进首页(pages/nav/nav)。
*/
onShow(() => {
uni.reLaunch({ url: isLoggedIn() ? '/pages/nav/nav' : '/pages/login/login' })
})
</script>
<style>
.content {
.boot {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100vh;
}
.logo {
height: 200rpx;
width: 200rpx;
margin-top: 200rpx;
margin-left: auto;
margin-right: auto;
margin-bottom: 50rpx;
.boot-logo {
width: 180rpx;
height: 180rpx;
margin-bottom: 32rpx;
}
.text-area {
display: flex;
justify-content: center;
}
.title {
font-size: 36rpx;
.boot-text {
font-size: 28rpx;
color: #8f8f94;
}
</style>
-5
View File
@@ -172,11 +172,6 @@
<text class="agree-link" @tap.stop="showAgreement">《隐私政策》</text>
</text>
</view>
<view class="foot-tip">
<FaIcon name="lock" color="#a3acbf" :size="11" />
<text class="foot-tip-text">统一身份认证 · 一次登录,全平台通行</text>
</view>
</view>
</template>
+6 -1
View File
@@ -170,7 +170,12 @@ onMounted(() => {
})
onShow(() => {
if (isLoggedIn() && !loading.value) load()
// 未登录(含后台切回来时令牌被清)直接回登录页,不再停留在首页
if (!isLoggedIn()) {
uni.reLaunch({ url: '/pages/login/login' })
return
}
if (!loading.value) load()
})
</script>
+6
View File
@@ -68,6 +68,7 @@ import { mpHeadPad } from '@/utils/mpHead.js'
const headPad = mpHeadPad()
import { getUser, setUser, logout, isLoggedIn, isSSOEnabled, getToken } from '@/utils/auth.js'
import { getCurrentUser, logoutApi, logoutUAC } from '@/api/auth.js'
import { requestSubscribeNotice } from '@/utils/subscribe.js'
import AppTabbar from '@/components/MainTabbar.vue'
const user = ref(null)
@@ -147,6 +148,11 @@ function onMenuTap(item) {
uni.navigateTo({ url: '/pages/login/forget' })
return
}
if (item.title === '消息通知') {
// 订阅消息授权:用户同意后,服务器才有一次主动推送的机会
requestSubscribeNotice()
return
}
uni.showToast({ title: item.title, icon: 'none' })
}
+84
View File
@@ -0,0 +1,84 @@
/**
* 订阅消息授权助手
*
* 用法:在需要提醒用户的入口调用一次即可
* import { requestSubscribeNotice } from '@/utils/subscribe.js'
* await requestSubscribeNotice()
*
* 说明:
* - 微信一次性订阅:用户每同意一次,服务器才有一次推送机会,所以要挑「用户确实
* 可能需要收到提醒」的时机调用(比如进入工作台、提交完审批后)。
* - 微信限制一次最多弹 3 个模板,这里自动分批。
*/
import { getSubscribeTemplates, authorizeSubscribe } from '@/api/subscribe.js'
/** 单次弹窗最多 3 个模板 ID */
const MAX_TMPL_PER_CALL = 3
/**
* 拉起微信订阅授权弹窗,并把授权结果上报给服务器
* @param {{silent?: boolean, tip?: string}} options
* @returns {Promise<{ok: boolean, reason?: string, accepted?: string[]}>}
*/
export async function requestSubscribeNotice(options = {}) {
const silent = !!options.silent
// #ifdef MP-WEIXIN
let templates = []
try {
const data = await getSubscribeTemplates()
templates = (data && data.list) || []
} catch (e) {
if (!silent) uni.showToast({ title: '获取订阅模板失败', icon: 'none' })
return { ok: false, reason: 'template_fetch_failed' }
}
const tmplIds = templates.map((t) => t.template_id).filter(Boolean)
if (!tmplIds.length) {
if (!silent) uni.showToast({ title: '暂未配置订阅消息模板', icon: 'none' })
return { ok: false, reason: 'no_template' }
}
const accepted = []
for (let i = 0; i < tmplIds.length; i += MAX_TMPL_PER_CALL) {
const batch = tmplIds.slice(i, i + MAX_TMPL_PER_CALL)
let result = {}
try {
result = await new Promise((resolve, reject) => {
uni.requestSubscribeMessage({
tmplIds: batch,
success: resolve,
fail: reject
})
})
} catch (e) {
if (!silent) uni.showToast({ title: '订阅授权失败', icon: 'none' })
return { ok: false, reason: 'request_failed' }
}
batch.forEach((id) => {
if (result[id] === 'accept') accepted.push(id)
})
}
if (!accepted.length) {
if (!silent) uni.showToast({ title: '你取消了订阅授权', icon: 'none' })
return { ok: false, reason: 'refuse' }
}
try {
await authorizeSubscribe(accepted)
} catch (e) {
if (!silent) uni.showToast({ title: '授权结果上报失败', icon: 'none' })
return { ok: false, reason: 'report_failed' }
}
if (!silent) {
uni.showToast({ title: options.tip || '已开启消息提醒', icon: 'success' })
}
return { ok: true, accepted }
// #endif
// #ifndef MP-WEIXIN
if (!silent) uni.showToast({ title: '请在微信小程序中开启消息提醒', icon: 'none' })
return { ok: false, reason: 'not_mp' }
// #endif
}
+19 -1
View File
@@ -3,7 +3,25 @@ import uni from '@dcloudio/vite-plugin-uni'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
const rootDir = path.dirname(fileURLToPath(import.meta.url))
// 新版 Vite / uni 编译器会把配置文件预打包成 CJS 加载,此时 import.meta.url 为 undefined,
// fileURLToPath(undefined) 会抛 "The path argument must be of type string. Received undefined"。
// 这里做兜底:拿不到模块路径时退回 __dirname / 当前工作目录(构建命令均在 uniapp 目录执行)。
function resolveRootDir() {
try {
const url = typeof import.meta !== 'undefined' ? import.meta.url : ''
if (url) return path.dirname(fileURLToPath(url))
} catch (e) {
/* import.meta.url 不可用时继续兜底 */
}
try {
if (typeof __dirname !== 'undefined') return __dirname
} catch (e) {
/* 忽略 */
}
return process.cwd()
}
const rootDir = resolveRootDir()
export default defineConfig({
base: '/h5/',