fix: 首页和我的应用排序按 seq 排列
This commit is contained in:
61
.hermes/note-backend-jwt-changes.md
Normal file
61
.hermes/note-backend-jwt-changes.md
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
# 后端 JWT Token 签发 — 需要添加的修改
|
||||||
|
|
||||||
|
以 CRMEB 后端 PHP 为例,找到签发 token 的方法(通常在类似以下位置):
|
||||||
|
|
||||||
|
## 1. 查找签发位置
|
||||||
|
|
||||||
|
CRMEB 后端搜索 `JWT::encode` 或 `makeToken` / `createToken`:
|
||||||
|
- `crmeb/app/services/system/SystemAuthService.php`
|
||||||
|
- `crmeb/app/services/other/CacheTokenService.php`
|
||||||
|
- 或 `crmeb/app/services/wechat/...`
|
||||||
|
|
||||||
|
## 2. 修改 Payload
|
||||||
|
|
||||||
|
原始代码可能类似:
|
||||||
|
|
||||||
|
```php
|
||||||
|
$payload = [
|
||||||
|
'iss' => 'crmeb',
|
||||||
|
'iat' => time(),
|
||||||
|
'exp' => time() + 86400 * 7,
|
||||||
|
'id' => $user->id,
|
||||||
|
];
|
||||||
|
$token = JWT::encode($payload, $key, 'HS256');
|
||||||
|
```
|
||||||
|
|
||||||
|
改为(添加 uid、phone、openid):
|
||||||
|
|
||||||
|
```php
|
||||||
|
$payload = [
|
||||||
|
'iss' => 'crmeb',
|
||||||
|
'iat' => time(),
|
||||||
|
'exp' => time() + 86400 * 7,
|
||||||
|
'uid' => $user->id, // ← 添加:用户 ID
|
||||||
|
'phone' => $user->phone ?? '', // ← 添加:手机号
|
||||||
|
'openid' => $user->openid ?? '', // ← 添加:微信 openid
|
||||||
|
];
|
||||||
|
$token = JWT::encode($payload, $key, 'HS256');
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 验证
|
||||||
|
|
||||||
|
修改后登录任意用户,token 形如:
|
||||||
|
|
||||||
|
```
|
||||||
|
eyJhbGciOiJIUzI1NiJ9.eyJ1aWQiOjEyMywicGhvbmUiOiIxMzgwMDAwMDAwMCIsIm9wZW5pZCI6Im8xMjNfIn0.xxxx
|
||||||
|
```
|
||||||
|
|
||||||
|
在浏览器 DevTools 或 https://jwt.io 解码 payload 应看到:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"uid": 123,
|
||||||
|
"phone": "13800000000",
|
||||||
|
"openid": "o123_xxx",
|
||||||
|
"exp": 1748000000,
|
||||||
|
"iss": "crmeb",
|
||||||
|
"iat": 1747395200
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
前端 `parseJwt()` 就能正确读取这些字段。
|
||||||
77
.trae/documents/mp_to_h5_token_design.md
Normal file
77
.trae/documents/mp_to_h5_token_design.md
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
## 方案:小程序跳转到 H5 页面携带 token
|
||||||
|
|
||||||
|
### Summary
|
||||||
|
- 目标:在微信小程序内通过 web-view 打开指定 H5 地址时,让 H5 自动获得与小程序一致的登录态(token),并在 H5 内完成 Vuex/缓存写入与用户信息同步。
|
||||||
|
- 约束(来自现状与确认):打开方式为 web-view;当前不方便改后端接口;倾向直接携带 Bearer token。
|
||||||
|
- 推荐策略:优先把 token 放到 URL 的 fragment(`#` 之后)以避免出现在 HTTP 请求与 Referer/服务端日志里;H5 启动时从 fragment 解析 token,写入 `LOGIN_STATUS` 缓存并拉取用户信息,随后清理 URL 中的 token。
|
||||||
|
|
||||||
|
### Current State Analysis
|
||||||
|
- 小程序打开 H5 的通用入口:`utils/util.js: JumpPath`,当 url 包含 `http` 时在 MP/APP-PLUS 走 web-view 页面:
|
||||||
|
- [util.js](file:///c:/Users/Jone/Documents/HBuilderProjects/crmeb/utils/util.js#L162-L201)
|
||||||
|
- web-view 页面接收 `option.url` 并 `decodeURIComponent` 后赋给 `<web-view :src="url">`:
|
||||||
|
- [pages/annex/web_view/index.vue](file:///c:/Users/Jone/Documents/HBuilderProjects/crmeb/pages/annex/web_view/index.vue#L1-L28)
|
||||||
|
- H5 端已有“从 URL query 自动登录”的机制,但目前只处理 `remote_token`:
|
||||||
|
- H5 `App.vue`:`if (!isLogin && option.query.hasOwnProperty('remote_token')) this.remoteRegister(...)`
|
||||||
|
- [App.vue](file:///c:/Users/Jone/Documents/HBuilderProjects/crmeb/App.vue#L88-L92)
|
||||||
|
- `remoteRegister` 调用 `GET /api/remote_register`(noAuth)换取 token 或跳转 URL:
|
||||||
|
- [api/public.js](file:///c:/Users/Jone/Documents/HBuilderProjects/crmeb/api/public.js#L231-L239)
|
||||||
|
- token 使用方式:请求层统一在 `utils/request.js` 注入 header `Authori-zation: Bearer <token>`:
|
||||||
|
- [request.js](file:///c:/Users/Jone/Documents/HBuilderProjects/crmeb/utils/request.js#L53-L63)
|
||||||
|
- 登录态落地:`store/modules/app.js: LOGIN` 将 token 写入 `Cache.set(LOGIN_STATUS, token, time)`;并可配合 `getUserInfo()` 写入 uid/userInfo:
|
||||||
|
- [app.js](file:///c:/Users/Jone/Documents/HBuilderProjects/crmeb/store/modules/app.js#L89-L117)
|
||||||
|
|
||||||
|
### Proposed Changes
|
||||||
|
|
||||||
|
#### 1) 小程序侧:构造带 token 的 H5 URL(尽量放在 fragment)
|
||||||
|
- 修改文件:`utils/util.js`
|
||||||
|
- 修改点:在 `JumpPath` 的 `url.indexOf("http") != -1` 分支(MP/APP-PLUS)中,在跳转到 `/pages/annex/web_view/index` 之前对目标 url 做“携带 token”的包装。
|
||||||
|
- 行为设计:
|
||||||
|
- 若用户未登录(`store.getters.isLogin` 为 false 或 `store.state.app.token` 为空),直接走现有登录流程(可复用 `libs/login.js: toLogin`),不打开 H5。
|
||||||
|
- 若已登录:
|
||||||
|
- 优先把 token 附加在 hash 内:`https://h5.xxx.com/#/some/path?token=<encoded>`
|
||||||
|
- 若目标 url 不含 `#`,则退化为 query:`https://h5.xxx.com/some/path?token=<encoded>`(存在服务端日志/Referer 风险,属于“不得不”的兼容路径)。
|
||||||
|
- 在把 url 作为参数传给 web_view 页面时,对整段 url 做 `encodeURIComponent`,避免 token 中的特殊字符以及 `&` 被小程序路由二次解析。
|
||||||
|
- 预计新增/调整函数(放在 util.js 内部):
|
||||||
|
- `appendTokenToUrl(rawUrl, token): string`(只做字符串处理,不引入新依赖)
|
||||||
|
- `encodeForWebView(url): string`(统一 `encodeURIComponent`)
|
||||||
|
- 影响面:
|
||||||
|
- 任何通过 `this.$util.JumpPath('https://...')` 打开的 H5 链接,都将自动带上 token(仅在 MP/APP-PLUS 生效)。
|
||||||
|
|
||||||
|
#### 2) web-view 容器页:强制 decode 前提下保证传参完整
|
||||||
|
- 修改文件:`pages/annex/web_view/index.vue`
|
||||||
|
- 修改点:保持现有 `decodeURIComponent(option.url)`,但要求所有调用方都传入 encode 后的 url。
|
||||||
|
- 如果项目内存在“未 encode 直接拼接 url”的调用点(当前 `utils/util.js` 即如此),统一改为 encode 后再传。
|
||||||
|
|
||||||
|
#### 3) H5 侧:解析 token → 写入登录态 → 拉取用户信息 → 清理 URL
|
||||||
|
- 修改文件:`App.vue`
|
||||||
|
- 修改点:H5 `onLaunch` 内(`// #ifdef H5` 分支)补充 token 自动登录逻辑,优先级建议:
|
||||||
|
1. 若当前已登录(`this.$store.getters.isLogin` 为 true),不处理外部 token(避免覆盖用户主动登录态)。
|
||||||
|
2. 仍保留现有 `remote_token` 分支逻辑(兼容已有能力)。
|
||||||
|
3. 新增 `token` 分支逻辑:
|
||||||
|
- 从 `option.query.token` 读取(query 方案)
|
||||||
|
- 若取不到,再从 `location.hash` 里解析 `token`(fragment 方案)
|
||||||
|
- 拿到 token 后:
|
||||||
|
- `this.$store.commit("LOGIN", { token, time: 0 })`(time=0 表示缓存不主动过期,后续由服务端 410xxx 兜底踢下线)
|
||||||
|
- 调用 `getUserInfo()`,成功后 `SETUID/UPDATE_USERINFO`
|
||||||
|
- 最后用 `history.replaceState` 把 URL 中的 token 移除(避免用户复制链接时携带 token)
|
||||||
|
- 需要补一个轻量解析方法(不引入库):
|
||||||
|
- `parseTokenFromHash(hash): string | ''`:兼容 `#/path?token=...` 和 `#/?token=...` 两种。
|
||||||
|
|
||||||
|
### Assumptions & Decisions
|
||||||
|
- H5 地址可能是 uni-app 默认 hash 路由(`/#/`);因此默认把 token 放在 fragment 内最安全。
|
||||||
|
- 不改后端时,“直接 token”不可避免有泄露风险;通过 fragment + 清理 URL 将风险降到相对可接受:
|
||||||
|
- fragment 不会出现在 HTTP 请求行与一般服务端 access log
|
||||||
|
- Referer 通常不包含 fragment
|
||||||
|
- token 仍可能在以下场景泄露:用户截屏、设备调试、前端错误上报采集 `location.href` 等;因此上线前需确认前端埋点/监控不会上报完整 URL。
|
||||||
|
|
||||||
|
### Verification
|
||||||
|
- 本地/测试环境:
|
||||||
|
- 小程序端登录后,通过任意入口打开一个 H5 链接(走 `JumpPath`):
|
||||||
|
- web-view 打开的 H5 页面无需再次登录
|
||||||
|
- H5 内 `store.getters.isLogin` 为 true,且请求头带 `Authori-zation: Bearer ...`
|
||||||
|
- `getUserInfo()` 可正常返回并写入 uid/userInfo
|
||||||
|
- 打开后检查 H5 当前地址:token 参数已被移除(history.replaceState 生效)
|
||||||
|
- 回归:
|
||||||
|
- 未登录时打开 H5 链接:应先跳登录,不应打开带 token 的 H5
|
||||||
|
- H5 原有 `remote_token` 登录不受影响
|
||||||
|
|
||||||
78
App.vue
78
App.vue
@@ -86,8 +86,14 @@
|
|||||||
// 通过vuex获取并储存活动弹窗的数据
|
// 通过vuex获取并储存活动弹窗的数据
|
||||||
this.$store.dispatch("getActivityModal");
|
this.$store.dispatch("getActivityModal");
|
||||||
// #ifdef H5
|
// #ifdef H5
|
||||||
if (!this.isLogin && option.query.hasOwnProperty('remote_token')) {
|
if (!this.isLogin) {
|
||||||
|
if (option.query && option.query.hasOwnProperty('remote_token')) {
|
||||||
this.remoteRegister(option.query.remote_token);
|
this.remoteRegister(option.query.remote_token);
|
||||||
|
} else {
|
||||||
|
let token = option.query && option.query.token ? option.query.token : '';
|
||||||
|
if (!token) token = this.getTokenFromHash();
|
||||||
|
if (token) this.loginByToken(token);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.setScript();
|
this.setScript();
|
||||||
const queryData = uni.getEnterOptionsSync();
|
const queryData = uni.getEnterOptionsSync();
|
||||||
@@ -120,6 +126,76 @@
|
|||||||
uni.setStorageSync('DEBUG_DIY_RENDER', true)
|
uni.setStorageSync('DEBUG_DIY_RENDER', true)
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
getTokenFromHash() {
|
||||||
|
try {
|
||||||
|
let hash = window.location.hash || '';
|
||||||
|
let qIndex = hash.indexOf('?');
|
||||||
|
if (qIndex === -1) return '';
|
||||||
|
let queryStr = hash.slice(qIndex + 1);
|
||||||
|
let parts = queryStr.split('&');
|
||||||
|
for (let i = 0; i < parts.length; i++) {
|
||||||
|
if (!parts[i]) continue;
|
||||||
|
let eqIndex = parts[i].indexOf('=');
|
||||||
|
let k = eqIndex === -1 ? parts[i] : parts[i].slice(0, eqIndex);
|
||||||
|
if (k === 'token') {
|
||||||
|
let v = eqIndex === -1 ? '' : parts[i].slice(eqIndex + 1);
|
||||||
|
return decodeURIComponent(v);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
} catch (e) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
cleanTokenFromUrl() {
|
||||||
|
try {
|
||||||
|
let href = window.location.href || '';
|
||||||
|
if (href.indexOf('token=') === -1) return;
|
||||||
|
let urlObj = new URL(href);
|
||||||
|
if (urlObj.searchParams && urlObj.searchParams.has('token')) {
|
||||||
|
urlObj.searchParams.delete('token');
|
||||||
|
let search = urlObj.searchParams.toString();
|
||||||
|
history.replaceState(null, '', urlObj.pathname + (search ? '?' + search : '') + urlObj.hash);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
try {
|
||||||
|
let hash = window.location.hash || '';
|
||||||
|
let qIndex = hash.indexOf('?');
|
||||||
|
if (qIndex === -1) return;
|
||||||
|
let hashPath = hash.slice(1, qIndex);
|
||||||
|
let queryStr = hash.slice(qIndex + 1);
|
||||||
|
let parts = queryStr.split('&').filter(Boolean);
|
||||||
|
let newParts = [];
|
||||||
|
for (let i = 0; i < parts.length; i++) {
|
||||||
|
let eqIndex = parts[i].indexOf('=');
|
||||||
|
let k = eqIndex === -1 ? parts[i] : parts[i].slice(0, eqIndex);
|
||||||
|
if (k !== 'token') newParts.push(parts[i]);
|
||||||
|
}
|
||||||
|
let newHash = '#' + hashPath + (newParts.length ? '?' + newParts.join('&') : '');
|
||||||
|
history.replaceState(null, '', window.location.pathname + window.location.search + newHash);
|
||||||
|
} catch (e) {}
|
||||||
|
},
|
||||||
|
loginByToken(token) {
|
||||||
|
this.$store.commit("LOGIN", {
|
||||||
|
'token': token,
|
||||||
|
'time': 0
|
||||||
|
});
|
||||||
|
// 从 JWT payload 中立即提取 uid,无需等 getUserInfo
|
||||||
|
const payload = this.$util.parseJwt(token);
|
||||||
|
if (payload) {
|
||||||
|
if (payload.uid) this.$store.commit("SETUID", payload.uid);
|
||||||
|
// phone 和 openid 也可从 JWT 读取
|
||||||
|
// 后端需在签发 JWT 时加入 uid/phone/openid claims
|
||||||
|
}
|
||||||
|
getUserInfo().then(res => {
|
||||||
|
this.$store.commit("SETUID", res.data.uid);
|
||||||
|
this.$store.commit("UPDATE_USERINFO", res.data);
|
||||||
|
this.cleanTokenFromUrl();
|
||||||
|
}).catch(() => {
|
||||||
|
this.cleanTokenFromUrl();
|
||||||
|
})
|
||||||
|
},
|
||||||
remoteRegister(remote_token) {
|
remoteRegister(remote_token) {
|
||||||
remoteRegister({ remote_token }).then((res) => {
|
remoteRegister({ remote_token }).then((res) => {
|
||||||
let data = res.data;
|
let data = res.data;
|
||||||
|
|||||||
@@ -417,5 +417,5 @@ export function getActivityModalListApi() {
|
|||||||
* 活动弹窗观看上报
|
* 活动弹窗观看上报
|
||||||
*/
|
*/
|
||||||
export function getActivityModalRecordApi(data) {
|
export function getActivityModalRecordApi(data) {
|
||||||
return request.post("marketing/holiday_gift/record",data);
|
return request.post("marketing/holiday_gift/record",data, {noAuth: true});
|
||||||
}
|
}
|
||||||
44
fix_apply.py
Normal file
44
fix_apply.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
with open("C:\\Users\\Jone\\Documents\\HBuilderProjects\\crmeb\\pages\\annex\\web_view\\index.vue", "r", encoding="utf-8") as f:
|
||||||
|
c = f.read()
|
||||||
|
|
||||||
|
# Find and replace the whole _applyIdentity method
|
||||||
|
pattern = r'(\t\t_applyIdentity\(targetUrl, openid, unionid, urlOpenid\) \{[^}]+\}[^}]+?this\._finalizeUrl\(targetUrl\);)'
|
||||||
|
replacement = """\t\t_applyIdentity(targetUrl, openid, unionid, urlOpenid) {
|
||||||
|
\t\t\tif (urlOpenid && urlOpenid !== openid) {
|
||||||
|
\t\t\t\ttargetUrl = stripAuthParams(targetUrl);
|
||||||
|
\t\t\t\tconst joiner = targetUrl.indexOf('?') !== -1 ? '&' : '?';
|
||||||
|
\t\t\t\tconst t = Math.floor(Date.now() / 1000);
|
||||||
|
\t\t\t\ttargetUrl = targetUrl + joiner + 'openid=' + encodeURIComponent(openid);
|
||||||
|
\t\t\t\tif (unionid) targetUrl += '&unionid=' + encodeURIComponent(unionid);
|
||||||
|
\t\t\t\ttargetUrl += '&t=' + t;
|
||||||
|
\t\t\t\tconsole.log('[web_view] 替换为当前用户身份');
|
||||||
|
\t\t\t} else if (!urlOpenid) {
|
||||||
|
\t\t\t\ttargetUrl = stripAuthParams(targetUrl);
|
||||||
|
\t\t\t\tconst joiner = targetUrl.indexOf('?') !== -1 ? '&' : '?';
|
||||||
|
\t\t\t\tconst t = Math.floor(Date.now() / 1000);
|
||||||
|
\t\t\t\ttargetUrl = targetUrl + joiner + 'openid=' + encodeURIComponent(openid);
|
||||||
|
\t\t\t\tif (unionid) targetUrl += '&unionid=' + encodeURIComponent(unionid);
|
||||||
|
\t\t\t\ttargetUrl += '&t=' + t;
|
||||||
|
\t\t\t\tconsole.log('[web_view] 补充身份参数');
|
||||||
|
\t\t\t} else {
|
||||||
|
\t\t\t\tconsole.log('[web_view] 身份一致,保留原 URL');
|
||||||
|
\t\t\t}
|
||||||
|
\t\t\tthis.debugMsg += ' [最终URL] ' + targetUrl.substring(0, 120);
|
||||||
|
\t\t\tthis._finalizeUrl(targetUrl);
|
||||||
|
\t\t},"""
|
||||||
|
|
||||||
|
if re.search(pattern, c, re.DOTALL):
|
||||||
|
c = re.sub(pattern, replacement, c, count=1, flags=re.DOTALL)
|
||||||
|
with open("C:\\Users\\Jone\\Documents\\HBuilderProjects\\crmeb\\pages\\annex\\web_view\\index.vue", "w", encoding="utf-8") as f:
|
||||||
|
f.write(c)
|
||||||
|
script = c[c.find("<script>"):c.find("</script>")]
|
||||||
|
print(f"Balance: {script.count('{') - script.count('}')}")
|
||||||
|
print("SUCCESS")
|
||||||
|
else:
|
||||||
|
print("Pattern not found")
|
||||||
|
# Debug: show what's around _applyIdentity
|
||||||
|
idx = c.find("_applyIdentity")
|
||||||
|
if idx > 0:
|
||||||
|
print(c[idx:idx+500])
|
||||||
@@ -28,6 +28,26 @@ import {
|
|||||||
} from './../config/cache';
|
} from './../config/cache';
|
||||||
import Routine from '@/libs/routine';
|
import Routine from '@/libs/routine';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 不依赖 atob 的 base64 解码(小程序环境不提供 atob)
|
||||||
|
*/
|
||||||
|
function base64Decode(str) {
|
||||||
|
var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
|
||||||
|
var output = '';
|
||||||
|
var i = 0;
|
||||||
|
str = String(str).replace(/[^A-Za-z0-9+/=]/g, '');
|
||||||
|
while (i < str.length) {
|
||||||
|
var b1 = chars.indexOf(str.charAt(i++));
|
||||||
|
var b2 = chars.indexOf(str.charAt(i++));
|
||||||
|
var b3 = chars.indexOf(str.charAt(i++));
|
||||||
|
var b4 = chars.indexOf(str.charAt(i++));
|
||||||
|
output += String.fromCharCode((b1 << 2) | (b2 >> 4));
|
||||||
|
if (b3 !== 64) output += String.fromCharCode(((b2 & 15) << 4) | (b3 >> 2));
|
||||||
|
if (b4 !== 64) output += String.fromCharCode(((b3 & 3) << 6) | b4);
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
function prePage() {
|
function prePage() {
|
||||||
let pages = getCurrentPages();
|
let pages = getCurrentPages();
|
||||||
let prePage = pages[pages.length - 1];
|
let prePage = pages[pages.length - 1];
|
||||||
@@ -42,7 +62,6 @@ function prePage() {
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const toLogin = Debounce(_toLogin, 800)
|
export const toLogin = Debounce(_toLogin, 800)
|
||||||
|
|
||||||
function _toLogin(push, pathLogin) {
|
function _toLogin(push, pathLogin) {
|
||||||
@@ -80,16 +99,6 @@ function _toLogin(push, pathLogin) {
|
|||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url
|
url
|
||||||
})
|
})
|
||||||
// Routine.getCode()
|
|
||||||
// .then(code => {
|
|
||||||
// console.log(code)
|
|
||||||
// Routine.silenceAuth(code).then(res => {
|
|
||||||
// console.log(res)
|
|
||||||
// })
|
|
||||||
// })
|
|
||||||
// .catch(err => {
|
|
||||||
// uni.hideLoading();
|
|
||||||
// });
|
|
||||||
// #endif
|
// #endif
|
||||||
|
|
||||||
// #ifdef APP-PLUS
|
// #ifdef APP-PLUS
|
||||||
@@ -114,9 +123,44 @@ export function checkLogin() {
|
|||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
store.commit('UPDATE_LOGIN', token);
|
store.commit('UPDATE_LOGIN', token);
|
||||||
|
// 从 JWT payload 中恢复 uid / unionid
|
||||||
|
try {
|
||||||
|
let parts = token.split('.');
|
||||||
|
if (parts.length === 3) {
|
||||||
|
let payload = parts[1]
|
||||||
|
.replace(/-/g, '+')
|
||||||
|
.replace(/_/g, '/');
|
||||||
|
while (payload.length % 4 !== 0) payload += '=';
|
||||||
|
let data = JSON.parse(base64Decode(payload));
|
||||||
|
if (data.uid) store.commit('SETUID', data.uid);
|
||||||
|
if (data.unionid) {
|
||||||
|
store.commit('SET_UNIONID', data.unionid);
|
||||||
|
Cache.set('UNIONID', data.unionid);
|
||||||
|
console.log('[checkLogin] unionid 已从 JWT 恢复:', data.unionid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
// 恢复缓存的 openid
|
||||||
|
const cachedOpenid = Cache.get('OPENID');
|
||||||
|
if (cachedOpenid) {
|
||||||
|
store.commit('SET_OPENID', cachedOpenid);
|
||||||
|
console.log('[checkLogin] openid 已从缓存恢复:', cachedOpenid);
|
||||||
|
}
|
||||||
|
// 恢复缓存的 unionid
|
||||||
|
const cachedUnionid = Cache.get('UNIONID');
|
||||||
|
if (cachedUnionid) {
|
||||||
|
store.commit('SET_UNIONID', cachedUnionid);
|
||||||
|
console.log('[checkLogin] unionid 已从缓存恢复:', cachedUnionid);
|
||||||
|
} else {
|
||||||
|
console.log('[checkLogin] unionid 缓存为空');
|
||||||
|
}
|
||||||
|
// 从 userInfo 缓存恢复
|
||||||
let userInfo = Cache.get(USER_INFO, true);
|
let userInfo = Cache.get(USER_INFO, true);
|
||||||
if (userInfo) {
|
if (userInfo) {
|
||||||
store.commit('UPDATE_USERINFO', userInfo);
|
store.commit('UPDATE_USERINFO', userInfo);
|
||||||
|
if (!cachedUnionid && userInfo.unionid) {
|
||||||
|
console.log('[checkLogin] unionid 已从 userInfo 恢复:', userInfo.unionid);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -415,7 +415,7 @@
|
|||||||
},
|
},
|
||||||
watch:{
|
watch:{
|
||||||
reset(val){
|
reset(val){
|
||||||
this.getCartList(1);
|
if (this.isLogin) { this.getCartList(1); }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
@@ -424,7 +424,7 @@
|
|||||||
this.getScrollHeight();
|
this.getScrollHeight();
|
||||||
}, 500);
|
}, 500);
|
||||||
if(this.isLogin){
|
if(this.isLogin){
|
||||||
this.getCartList(1);
|
if (this.isLogin) { this.getCartList(1); }
|
||||||
}
|
}
|
||||||
uni.$on('newAttrNum', (data) => {
|
uni.$on('newAttrNum', (data) => {
|
||||||
if (data.length) {
|
if (data.length) {
|
||||||
@@ -765,7 +765,7 @@
|
|||||||
item.cart_num = item.copy_cart_num;
|
item.cart_num = item.copy_cart_num;
|
||||||
item.showInput =false;
|
item.showInput =false;
|
||||||
this.getCartNum();
|
this.getCartNum();
|
||||||
this.getCartList(1);
|
if (this.isLogin) { this.getCartList(1); }
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
item.copy_cart_num = item.cart_num;
|
item.copy_cart_num = item.cart_num;
|
||||||
|
|||||||
@@ -380,7 +380,7 @@
|
|||||||
},
|
},
|
||||||
watch:{
|
watch:{
|
||||||
reset(val){
|
reset(val){
|
||||||
this.getCartList(1);
|
if (this.isLogin) { this.getCartList(1); }
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
@@ -389,7 +389,7 @@
|
|||||||
this.getScrollHeight();
|
this.getScrollHeight();
|
||||||
}, 500)
|
}, 500)
|
||||||
if(this.isLogin){
|
if(this.isLogin){
|
||||||
this.getCartList(1);
|
if (this.isLogin) { this.getCartList(1); }
|
||||||
}
|
}
|
||||||
uni.$on('newAttrNum', (data) => {
|
uni.$on('newAttrNum', (data) => {
|
||||||
if (data.length) {
|
if (data.length) {
|
||||||
@@ -689,7 +689,7 @@
|
|||||||
item.cart_num = item.copy_cart_num;
|
item.cart_num = item.copy_cart_num;
|
||||||
item.showInput =false;
|
item.showInput =false;
|
||||||
this.getCartNum();
|
this.getCartNum();
|
||||||
this.getCartList(1);
|
if (this.isLogin) { this.getCartList(1); }
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
item.copy_cart_num = item.cart_num;
|
item.copy_cart_num = item.cart_num;
|
||||||
|
|||||||
275
pages/index/components/appCenter.vue
Normal file
275
pages/index/components/appCenter.vue
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
<template>
|
||||||
|
<view v-if="showApps" :style="[boxStyle]">
|
||||||
|
<view :style="[boxContentStyle]">
|
||||||
|
<view class="list-menu" :style="[gridColumns]">
|
||||||
|
<view
|
||||||
|
class="item flex-col flex-center"
|
||||||
|
v-for="(item, index) in displayList"
|
||||||
|
:key="index"
|
||||||
|
@click="onAppTap(item)"
|
||||||
|
>
|
||||||
|
<!-- 更多按钮 -->
|
||||||
|
<template v-if="item._isMore">
|
||||||
|
<view class="img-box skeleton-radius" v-if="menuStyleConfig !== 2">
|
||||||
|
<view class="more-icon-inner flex-col flex-center">
|
||||||
|
<view class="more-dots">
|
||||||
|
<view class="more-dot"></view>
|
||||||
|
<view class="more-dot"></view>
|
||||||
|
<view class="more-dot"></view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<text
|
||||||
|
class="fs-24"
|
||||||
|
:class="{ 'mt-14': menuStyleConfig == 0 }"
|
||||||
|
:style="[nameColorStyle]"
|
||||||
|
v-if="menuStyleConfig !== 1"
|
||||||
|
>更多</text>
|
||||||
|
</template>
|
||||||
|
<!-- 普通应用 -->
|
||||||
|
<template v-else>
|
||||||
|
<view class="img-box skeleton-radius" v-if="menuStyleConfig !== 2">
|
||||||
|
<image
|
||||||
|
:src="item.logo"
|
||||||
|
mode="aspectFill"
|
||||||
|
:style="{ borderRadius: (imgStyle * 2) + 'rpx' }"
|
||||||
|
v-if="item.logo"
|
||||||
|
></image>
|
||||||
|
<view class="empty-box" v-else></view>
|
||||||
|
</view>
|
||||||
|
<text
|
||||||
|
class="fs-24"
|
||||||
|
:class="{ 'mt-14': menuStyleConfig == 0 }"
|
||||||
|
:style="[nameColorStyle]"
|
||||||
|
v-if="menuStyleConfig !== 1"
|
||||||
|
>{{ item.name }}</text>
|
||||||
|
</template>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const STORAGE_KEY = 'MY_HOME_APPS'
|
||||||
|
const MAX_HOME_APPS = 6
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'appCenter',
|
||||||
|
props: {
|
||||||
|
dataConfig: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
menuStyleConfig() {
|
||||||
|
return this.dataConfig.menuStyleConfig?.tabVal ?? 0
|
||||||
|
},
|
||||||
|
number() {
|
||||||
|
return this.dataConfig.number?.tabVal ?? 0
|
||||||
|
},
|
||||||
|
boxStyle() {
|
||||||
|
return {
|
||||||
|
padding: `${(this.dataConfig.topConfig?.val ?? 0) * 2}rpx ${(this.dataConfig.prConfig?.val ?? 0) * 2}rpx ${(this.dataConfig.bottomConfig?.val ?? 0) * 2}rpx`,
|
||||||
|
marginTop: `${(this.dataConfig.mbConfig?.val ?? 0) * 2}rpx`,
|
||||||
|
background: this.dataConfig.bottomBgColor?.color?.[0]?.item ?? 'transparent'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
boxContentStyle() {
|
||||||
|
let borderRadius = `${(this.dataConfig.fillet?.val ?? 0) * 2}rpx`
|
||||||
|
if (this.dataConfig.fillet?.type) {
|
||||||
|
const vl = this.dataConfig.fillet.valList || []
|
||||||
|
borderRadius = `${(vl[0]?.val ?? 0) * 2}rpx ${(vl[1]?.val ?? 0) * 2}rpx ${(vl[3]?.val ?? 0) * 2}rpx ${(vl[2]?.val ?? 0) * 2}rpx`
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
borderRadius,
|
||||||
|
background: `linear-gradient(90deg, ${this.dataConfig.bgColor?.color?.[0]?.item ?? '#fff'} 0%, ${this.dataConfig.bgColor?.color?.[1]?.item ?? '#fff'} 100%)`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
imgStyle() {
|
||||||
|
return this.dataConfig.filletImg?.val ?? 0
|
||||||
|
},
|
||||||
|
gridColumns() {
|
||||||
|
// 应用中心用 4 列固定网格
|
||||||
|
return {
|
||||||
|
gridTemplateColumns: `repeat(4, 1fr)`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
nameColorStyle() {
|
||||||
|
return {
|
||||||
|
color: this.dataConfig.nameColor?.color?.[0]?.item ?? '#333333'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 是否在首页展示
|
||||||
|
showApps() {
|
||||||
|
return this.homeApps.showOnHome && this.displayList.length > 0
|
||||||
|
},
|
||||||
|
// 已选应用的 id 集合(快速查找)
|
||||||
|
selectedIds() {
|
||||||
|
return new Set(this.homeApps.appIds || [])
|
||||||
|
},
|
||||||
|
// 最终展示列表(过滤 + 按 seq 排序 + 截取 + 追加"更多")
|
||||||
|
displayList() {
|
||||||
|
const ids = this.homeApps.appIds || []
|
||||||
|
if (!ids.length) return []
|
||||||
|
|
||||||
|
const idSet = new Set(ids)
|
||||||
|
// 按 allApps API 返回顺序排列(即后台设置的 seq 顺序)
|
||||||
|
const filtered = this.allApps.filter(item => idSet.has(item.id))
|
||||||
|
const indexMap = new Map(this.allApps.map((item, i) => [item.id, i]))
|
||||||
|
filtered.sort((a, b) => (indexMap.get(a.id) ?? 999) - (indexMap.get(b.id) ?? 999))
|
||||||
|
const sliced = filtered.slice(0, MAX_HOME_APPS)
|
||||||
|
|
||||||
|
// 追加"更多"条目
|
||||||
|
sliced.push({
|
||||||
|
_isMore: true,
|
||||||
|
name: '更多'
|
||||||
|
})
|
||||||
|
|
||||||
|
return sliced
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
allApps: [],
|
||||||
|
homeApps: {
|
||||||
|
showOnHome: true,
|
||||||
|
appIds: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.loadHomeApps()
|
||||||
|
this.fetchApps()
|
||||||
|
},
|
||||||
|
// 从个人中心编辑保存后跳回首页时,重新读取 storage
|
||||||
|
onShow() {
|
||||||
|
this.loadHomeApps()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
// 读取本地存储的首页应用配置
|
||||||
|
loadHomeApps() {
|
||||||
|
try {
|
||||||
|
const raw = uni.getStorageSync(STORAGE_KEY)
|
||||||
|
if (raw) {
|
||||||
|
this.homeApps = raw
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[appCenter] loadHomeApps error:', e)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fetchApps() {
|
||||||
|
uni.request({
|
||||||
|
url: 'https://mp.yizhezhe.com/api/mp/apps',
|
||||||
|
data: {
|
||||||
|
isPublished: 1,
|
||||||
|
pageSize: 50
|
||||||
|
},
|
||||||
|
success: (res) => {
|
||||||
|
if (res.data?.code === 200 && res.data?.result?.items) {
|
||||||
|
this.allApps = res.data.result.items
|
||||||
|
} else if (res.data?.status === 200 && res.data?.data) {
|
||||||
|
this.allApps = res.data.data
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.autoFillHomeApps()
|
||||||
|
},
|
||||||
|
fail: () => {
|
||||||
|
this.allApps = []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// 自动填充首页展示应用
|
||||||
|
autoFillHomeApps() {
|
||||||
|
const ids = this.homeApps.appIds || []
|
||||||
|
const currentCount = ids.length
|
||||||
|
if (currentCount >= MAX_HOME_APPS) return
|
||||||
|
|
||||||
|
const idSet = new Set(ids)
|
||||||
|
// 从推荐应用中筛选尚未选中的
|
||||||
|
const recommended = this.allApps
|
||||||
|
.filter(item => item.isRecommended && !idSet.has(item.id))
|
||||||
|
|
||||||
|
if (!recommended.length) return
|
||||||
|
|
||||||
|
const slots = MAX_HOME_APPS - currentCount
|
||||||
|
const toAdd = recommended.slice(0, slots).map(item => item.id)
|
||||||
|
|
||||||
|
this.homeApps.appIds = [...ids, ...toAdd]
|
||||||
|
try {
|
||||||
|
uni.setStorageSync(STORAGE_KEY, this.homeApps)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[appCenter] autoFill save error:', e)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onAppTap(item) {
|
||||||
|
if (item._isMore) {
|
||||||
|
// 点击"更多"跳转到我的页面
|
||||||
|
uni.switchTab({
|
||||||
|
url: '/pages/user/index'
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!item.url) return
|
||||||
|
this.$util.JumpPath(item.url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.list-menu {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto;
|
||||||
|
grid-row-gap: 24rpx;
|
||||||
|
width: 100%;
|
||||||
|
padding: 20rpx 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
|
.item {
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
.img-box {
|
||||||
|
width: 90rpx;
|
||||||
|
height: 90rpx;
|
||||||
|
margin: 0 auto 6rpx;
|
||||||
|
|
||||||
|
image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-box {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: #f3f9ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更多图标
|
||||||
|
.more-icon-inner {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: #f5f5f5;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
|
||||||
|
.more-dots {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6rpx;
|
||||||
|
|
||||||
|
.more-dot {
|
||||||
|
width: 8rpx;
|
||||||
|
height: 8rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #bbb;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -69,6 +69,7 @@
|
|||||||
<signIn v-if="item.name == 'signIn'" :dataConfig="item"></signIn>
|
<signIn v-if="item.name == 'signIn'" :dataConfig="item"></signIn>
|
||||||
<hotspot v-if="item.name == 'hotspot'" :dataConfig="item"></hotspot>
|
<hotspot v-if="item.name == 'hotspot'" :dataConfig="item"></hotspot>
|
||||||
<follow v-if="item.name == 'follow'" :dataConfig="item"></follow>
|
<follow v-if="item.name == 'follow'" :dataConfig="item"></follow>
|
||||||
|
<appCenter v-if="item.name == 'appCenter'" :dataConfig="item"></appCenter>
|
||||||
<community v-if="item.name == 'community'" :dataConfig="item"></community>
|
<community v-if="item.name == 'community'" :dataConfig="item"></community>
|
||||||
</block>
|
</block>
|
||||||
<!-- 分类商品模块 -->
|
<!-- 分类商品模块 -->
|
||||||
@@ -180,6 +181,7 @@ import promotionList from './components/promotionList';
|
|||||||
import signIn from './components/signIn';
|
import signIn from './components/signIn';
|
||||||
import hotspot from './components/hotspot';
|
import hotspot from './components/hotspot';
|
||||||
import follow from './components/follow';
|
import follow from './components/follow';
|
||||||
|
import appCenter from './components/appCenter';
|
||||||
import waterfallsFlow from "@/components/WaterfallsFlow/WaterfallsFlow.vue";
|
import waterfallsFlow from "@/components/WaterfallsFlow/WaterfallsFlow.vue";
|
||||||
import emptyPage from '@/components/emptyPage.vue';
|
import emptyPage from '@/components/emptyPage.vue';
|
||||||
import activityModal from "@/components/activityModal/index.vue"
|
import activityModal from "@/components/activityModal/index.vue"
|
||||||
@@ -256,6 +258,7 @@ import promotionList from './components/promotionList';
|
|||||||
signIn,
|
signIn,
|
||||||
hotspot,
|
hotspot,
|
||||||
follow,
|
follow,
|
||||||
|
appCenter,
|
||||||
waterfallsFlow,
|
waterfallsFlow,
|
||||||
activityModal,
|
activityModal,
|
||||||
emptyPage,
|
emptyPage,
|
||||||
|
|||||||
@@ -73,14 +73,14 @@ export default {
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<!-- 余额、优惠券 -->
|
<!-- 余额、优惠券 -->
|
||||||
<view class="acea-row balance-coupon">
|
<!-- <view class="acea-row balance-coupon">
|
||||||
<view class="item" v-for="(item, index) in property" @tap="goMenuPage(item.url)" :key="index">
|
<view class="item" v-for="(item, index) in property" @tap="goMenuPage(item.url)" :key="index">
|
||||||
<view class="value num-fa-semi">{{ item.value || 0 }}</view>
|
<view class="value num-fa-semi">{{ item.value || 0 }}</view>
|
||||||
<view>{{ item.label }}</view>
|
<view>{{ item.label }}</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view> -->
|
||||||
<!-- 会员中心、积分商城 -->
|
<!-- 会员中心、积分商城 -->
|
||||||
<view class="acea-row member-points">
|
<!-- <view class="acea-row member-points">
|
||||||
<view class="acea-row row-middle row-center item" @tap="intoPage(userInfo.level_status == 1 ? '/pages/users/user_vip/index' : '/pages/annex/vip_grade_active/index')">
|
<view class="acea-row row-middle row-center item" @tap="intoPage(userInfo.level_status == 1 ? '/pages/users/user_vip/index' : '/pages/annex/vip_grade_active/index')">
|
||||||
<view>
|
<view>
|
||||||
<view>会员中心</view>
|
<view>会员中心</view>
|
||||||
@@ -101,7 +101,7 @@ export default {
|
|||||||
</view>
|
</view>
|
||||||
<image src="@/static/images/user-points.png" class="image"></image>
|
<image src="@/static/images/user-points.png" class="image"></image>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view> -->
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
435
pages/user/components/myApps/index.vue
Normal file
435
pages/user/components/myApps/index.vue
Normal file
@@ -0,0 +1,435 @@
|
|||||||
|
<template>
|
||||||
|
<view class="my-apps-wrap">
|
||||||
|
<!-- 展示模式 -->
|
||||||
|
<template v-if="!editing">
|
||||||
|
<!-- 顶部:标题 + 编辑按钮 -->
|
||||||
|
<view class="pt-34 pr-30 pb-20 pl-32 bg--w111-fff rd-t-16rpx mt-20 service ml-20 mr-20">
|
||||||
|
<view class="flex-row-between">
|
||||||
|
<view class="fs-30 fw-500 lh-42rpx text--w111-333">我的应用</view>
|
||||||
|
<view class="edit-btn" @click="enterEdit">
|
||||||
|
<text class="fs-24 text--w111-1890ff">编辑首页应用</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<!-- 应用列表 -->
|
||||||
|
<view class="pt-10 pr-30 pb-32 pl-32 bg--w111-fff rd-b-16rpx service ml-20 mr-20" v-if="allApps.length">
|
||||||
|
<view class="grid-column-4 grid-gap-x-76rpx grid-gap-y-54rpx">
|
||||||
|
<view v-for="(item, index) in allApps" :key="index">
|
||||||
|
<view class="flex-col flex-center" @click="onAppTap(item)">
|
||||||
|
<image :src="item.logo" class="app-icon rd-16rpx" v-if="item.logo"></image>
|
||||||
|
<view class="empty-box" v-else></view>
|
||||||
|
<text class="fs-26 lh-36rpx text--w111-282828 pt-22">{{ item.name }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 编辑模式 -->
|
||||||
|
<template v-else>
|
||||||
|
<!-- 编辑卡片:整合头栏 + 首页展示栏 + 全部应用 -->
|
||||||
|
<view class="bg--w111-fff rd-16rpx mt-20 ml-20 mr-20">
|
||||||
|
<!-- 顶部操作栏 -->
|
||||||
|
<view class="edit-header">
|
||||||
|
<view class="edit-header-inner">
|
||||||
|
<view class="header-btn btn-cancel" @click="cancelEdit">取消</view>
|
||||||
|
<view class="fs-30 fw-500 text--w111-333">编辑首页展示</view>
|
||||||
|
<view class="header-btn btn-save" @click="saveEdit">保存</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 首页展示栏(横向滚动) -->
|
||||||
|
<view class="home-bar-wrap" v-if="editSelectedIds.length">
|
||||||
|
<view class="home-bar-label fs-28 text--w111-333">首页展示栏</view>
|
||||||
|
<scroll-view class="home-bar-scroll" scroll-x enhanced show-scrollbar="false">
|
||||||
|
<view class="home-bar-inner">
|
||||||
|
<view
|
||||||
|
class="home-bar-item"
|
||||||
|
v-for="(item, index) in editSelectedList"
|
||||||
|
:key="item.id"
|
||||||
|
>
|
||||||
|
<view class="home-bar-icon-wrap">
|
||||||
|
<image :src="item.logo" class="home-bar-icon rd-16rpx" v-if="item.logo"></image>
|
||||||
|
<view class="empty-box-sm" v-else></view>
|
||||||
|
<!-- 删除标记 -->
|
||||||
|
<view class="badge badge-remove" @click="removeFromHome(item.id)">✕</view>
|
||||||
|
</view>
|
||||||
|
<text class="home-bar-name">{{ item.name }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 全部应用列表 -->
|
||||||
|
<view class="all-apps-list">
|
||||||
|
<view class="fs-28 text--w111-333 mb-20">全部应用</view>
|
||||||
|
<view class="grid-column-4 grid-gap-x-76rpx grid-gap-y-54rpx">
|
||||||
|
<view v-for="(item, index) in allApps" :key="index">
|
||||||
|
<view
|
||||||
|
class="flex-col flex-center pos-relative"
|
||||||
|
@click="toggleAppSelect(item)"
|
||||||
|
>
|
||||||
|
<view class="app-icon-wrap">
|
||||||
|
<image :src="item.logo" class="app-icon rd-16rpx" v-if="item.logo"></image>
|
||||||
|
<view class="empty-box" v-else></view>
|
||||||
|
<!-- 已选遮罩 -->
|
||||||
|
<view class="selected-overlay" v-if="isSelected(item.id)">
|
||||||
|
<text class="selected-check">✓</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<!-- 添加标记(未选中时) -->
|
||||||
|
<view
|
||||||
|
class="badge badge-add"
|
||||||
|
v-if="!isSelected(item.id) && editSelectedIds.length < 6"
|
||||||
|
@click.stop="addToHome(item)"
|
||||||
|
>+</view>
|
||||||
|
<text
|
||||||
|
class="fs-26 lh-36rpx pt-22"
|
||||||
|
:class="isSelected(item.id) ? 'text--w111-ccc' : 'text--w111-282828'"
|
||||||
|
>{{ item.name }}</text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
</view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const STORAGE_KEY = 'MY_HOME_APPS'
|
||||||
|
const MAX_HOME_APPS = 6
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'userMyApps',
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
allApps: [],
|
||||||
|
// Storage 数据
|
||||||
|
homeApps: {
|
||||||
|
showOnHome: true,
|
||||||
|
appIds: []
|
||||||
|
},
|
||||||
|
editing: false,
|
||||||
|
// 编辑中的临时副本
|
||||||
|
editAppIds: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
editSelectedIds() {
|
||||||
|
return this.editAppIds
|
||||||
|
},
|
||||||
|
// 编辑模式中已选应用的完整对象列表(按 allApps 顺序排列)
|
||||||
|
editSelectedList() {
|
||||||
|
const idSet = new Set(this.editAppIds)
|
||||||
|
const filtered = this.allApps.filter(item => idSet.has(item.id))
|
||||||
|
const indexMap = new Map(this.allApps.map((item, i) => [item.id, i]))
|
||||||
|
filtered.sort((a, b) => (indexMap.get(a.id) ?? 999) - (indexMap.get(b.id) ?? 999))
|
||||||
|
return filtered
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.loadData()
|
||||||
|
this.fetchApps()
|
||||||
|
},
|
||||||
|
// 从首页"更多"跳转过来时刷新数据
|
||||||
|
onShow() {
|
||||||
|
this.loadData()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
// ---------- 数据层 ----------
|
||||||
|
loadData() {
|
||||||
|
try {
|
||||||
|
const raw = uni.getStorageSync(STORAGE_KEY)
|
||||||
|
if (raw) {
|
||||||
|
this.homeApps = raw
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[myApps] loadData error:', e)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
saveData() {
|
||||||
|
try {
|
||||||
|
uni.setStorageSync(STORAGE_KEY, this.homeApps)
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[myApps] saveData error:', e)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fetchApps() {
|
||||||
|
uni.request({
|
||||||
|
url: 'https://mp.yizhezhe.com/api/mp/apps',
|
||||||
|
data: {
|
||||||
|
isPublished: 1,
|
||||||
|
pageSize: 50
|
||||||
|
},
|
||||||
|
success: (res) => {
|
||||||
|
if (res.data?.code === 200 && res.data?.result?.items) {
|
||||||
|
this.allApps = res.data.result.items
|
||||||
|
} else if (res.data?.status === 200 && res.data?.data) {
|
||||||
|
this.allApps = res.data.data
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.autoFillHomeApps()
|
||||||
|
},
|
||||||
|
fail: () => {
|
||||||
|
this.allApps = []
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
// 自动填充首页展示应用(与 appCenter 逻辑一致)
|
||||||
|
autoFillHomeApps() {
|
||||||
|
const ids = this.homeApps.appIds || []
|
||||||
|
const currentCount = ids.length
|
||||||
|
if (currentCount >= 6) return
|
||||||
|
|
||||||
|
const idSet = new Set(ids)
|
||||||
|
const recommended = this.allApps
|
||||||
|
.filter(item => item.isRecommended && !idSet.has(item.id))
|
||||||
|
|
||||||
|
if (!recommended.length) return
|
||||||
|
|
||||||
|
const slots = 6 - currentCount
|
||||||
|
const toAdd = recommended.slice(0, slots).map(item => item.id)
|
||||||
|
|
||||||
|
this.homeApps.appIds = [...ids, ...toAdd]
|
||||||
|
this.saveData()
|
||||||
|
},
|
||||||
|
|
||||||
|
// ---------- 展示模式 ----------
|
||||||
|
onAppTap(item) {
|
||||||
|
if (!item.url) return
|
||||||
|
this.$util.JumpPath(item.url)
|
||||||
|
},
|
||||||
|
|
||||||
|
// ---------- 编辑模式 ----------
|
||||||
|
enterEdit() {
|
||||||
|
// 复制当前 appIds 作为编辑副本
|
||||||
|
this.editAppIds = [...(this.homeApps.appIds || [])]
|
||||||
|
this.editing = true
|
||||||
|
},
|
||||||
|
cancelEdit() {
|
||||||
|
this.editing = false
|
||||||
|
this.editAppIds = []
|
||||||
|
},
|
||||||
|
saveEdit() {
|
||||||
|
this.homeApps.appIds = [...this.editAppIds]
|
||||||
|
this.saveData()
|
||||||
|
this.editing = false
|
||||||
|
},
|
||||||
|
// 判断应用是否已选
|
||||||
|
isSelected(id) {
|
||||||
|
return this.editAppIds.includes(id)
|
||||||
|
},
|
||||||
|
// 添加到首页栏(从全部应用点击+)
|
||||||
|
addToHome(item) {
|
||||||
|
if (this.editAppIds.length >= MAX_HOME_APPS) {
|
||||||
|
uni.showToast({ title: `最多选择${MAX_HOME_APPS}个应用`, icon: 'none' })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!this.editAppIds.includes(item.id)) {
|
||||||
|
this.editAppIds.push(item.id)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 从首页栏移除(点击✕)
|
||||||
|
removeFromHome(id) {
|
||||||
|
this.editAppIds = this.editAppIds.filter(i => i !== id)
|
||||||
|
},
|
||||||
|
// 点击应用图标切换选中状态
|
||||||
|
toggleAppSelect(item) {
|
||||||
|
if (this.isSelected(item.id)) {
|
||||||
|
this.removeFromHome(item.id)
|
||||||
|
} else {
|
||||||
|
this.addToHome(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.my-apps-wrap {
|
||||||
|
// 左右布局
|
||||||
|
.flex-row-between {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 应用图标(展示模式 + 编辑模式通用)
|
||||||
|
.app-icon {
|
||||||
|
width: 96rpx;
|
||||||
|
height: 96rpx;
|
||||||
|
}
|
||||||
|
.empty-box {
|
||||||
|
width: 96rpx;
|
||||||
|
height: 96rpx;
|
||||||
|
background-color: #f3f9ff;
|
||||||
|
border-radius: 8rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-icon-wrap {
|
||||||
|
position: relative;
|
||||||
|
width: 96rpx;
|
||||||
|
height: 96rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 编辑按钮
|
||||||
|
.edit-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 6rpx 20rpx;
|
||||||
|
border: 2rpx solid #1890ff;
|
||||||
|
border-radius: 10rpx;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 编辑模式 ----------
|
||||||
|
.edit-header {
|
||||||
|
padding: 24rpx 30rpx 0;
|
||||||
|
|
||||||
|
.edit-header-inner {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 56rpx;
|
||||||
|
padding: 0 28rpx;
|
||||||
|
border-radius: 28rpx;
|
||||||
|
font-size: 26rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-cancel {
|
||||||
|
color: #666;
|
||||||
|
background: #f5f5f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-save {
|
||||||
|
color: #fff;
|
||||||
|
background: linear-gradient(135deg, #1890ff, #096dd9);
|
||||||
|
box-shadow: 0 4rpx 12rpx rgba(24, 144, 255, 0.35);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 首页展示栏
|
||||||
|
.home-bar-wrap {
|
||||||
|
padding: 24rpx 30rpx 0;
|
||||||
|
margin-top: 20rpx;
|
||||||
|
|
||||||
|
.home-bar-label {
|
||||||
|
margin-bottom: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-bar-scroll {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: visible !important;
|
||||||
|
|
||||||
|
.home-bar-inner {
|
||||||
|
display: flex;
|
||||||
|
gap: 24rpx;
|
||||||
|
|
||||||
|
.home-bar-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 100rpx;
|
||||||
|
padding-top: 12rpx;
|
||||||
|
|
||||||
|
.home-bar-icon-wrap {
|
||||||
|
position: relative;
|
||||||
|
width: 90rpx;
|
||||||
|
height: 90rpx;
|
||||||
|
overflow: visible;
|
||||||
|
|
||||||
|
.home-bar-icon {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
.empty-box-sm {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: #f3f9ff;
|
||||||
|
border-radius: 16rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-bar-name {
|
||||||
|
font-size: 22rpx;
|
||||||
|
color: #333;
|
||||||
|
margin-top: 8rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
max-width: 100rpx;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全部应用列表
|
||||||
|
.all-apps-list {
|
||||||
|
padding: 30rpx 30rpx 34rpx;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- 通用徽标 ----------
|
||||||
|
.badge {
|
||||||
|
position: absolute;
|
||||||
|
width: 36rpx;
|
||||||
|
height: 36rpx;
|
||||||
|
border-radius: 50%;
|
||||||
|
font-size: 20rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: #fff;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-remove {
|
||||||
|
top: -8rpx;
|
||||||
|
right: -8rpx;
|
||||||
|
background: #ff4d4f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-add {
|
||||||
|
top: -8rpx;
|
||||||
|
right: 6rpx;
|
||||||
|
background: #52c41a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.35);
|
||||||
|
border-radius: 8rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
.selected-check {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 28rpx;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pos-relative {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -351,9 +351,16 @@
|
|||||||
this.isShow = false
|
this.isShow = false
|
||||||
},
|
},
|
||||||
back() {
|
back() {
|
||||||
|
const pages = getCurrentPages();
|
||||||
|
if (pages.length > 1) {
|
||||||
uni.navigateBack({
|
uni.navigateBack({
|
||||||
delta: this.configData.wechat_auth_switch ? 2 : 1
|
delta: this.configData.wechat_auth_switch ? 2 : 1
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
uni.switchTab({
|
||||||
|
url: '/pages/index/index'
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
privacy(type) {
|
privacy(type) {
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
|
|||||||
@@ -149,6 +149,16 @@ export default {
|
|||||||
</view>
|
</view>
|
||||||
</navigator>
|
</navigator>
|
||||||
</view>
|
</view>
|
||||||
|
<!-- #ifdef MP -->
|
||||||
|
<view class="list rd-24rpx mx-20">
|
||||||
|
<view class="item acea-row row-between-wrapper" @click="outLogin">
|
||||||
|
<view>退出登录</view>
|
||||||
|
<view class="grab">
|
||||||
|
<text class="iconfont icon-ic_rightarrow"></text>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
<!-- #endif -->
|
||||||
<!-- #ifdef H5 -->
|
<!-- #ifdef H5 -->
|
||||||
<view class="log-out list rd-24rpx mx-20 acea-row row-center-wrapper" @click="outLogin" v-if="!this.$wechat.isWeixin()">退出登录</view>
|
<view class="log-out list rd-24rpx mx-20 acea-row row-center-wrapper" @click="outLogin" v-if="!this.$wechat.isWeixin()">退出登录</view>
|
||||||
<!-- #endif -->
|
<!-- #endif -->
|
||||||
|
|||||||
@@ -24,19 +24,23 @@
|
|||||||
<view class="wechat-login">
|
<view class="wechat-login">
|
||||||
<view class="btn-wrapper">
|
<view class="btn-wrapper">
|
||||||
<!-- #ifdef H5 -->
|
<!-- #ifdef H5 -->
|
||||||
<button hover-class="none" @tap="wechatLogin" class="bg-color btn1">授权登录</button>
|
<button hover-class="none" @tap="handleAuthBtnTap" class="bg-color btn1">授权登录</button>
|
||||||
<!-- #endif -->
|
<!-- #endif -->
|
||||||
<!-- #ifdef MP -->
|
<!-- #ifdef MP -->
|
||||||
<template v-if="configData.wechat_auth_switch">
|
<template v-if="configData.wechat_auth_switch">
|
||||||
<button hover-class="none" v-if="bindPhone" open-type="getPhoneNumber" @getphonenumber="getphonenumber"
|
<button hover-class="none" v-if="bindPhone && protocol" open-type="getPhoneNumber" @getphonenumber="getphonenumber"
|
||||||
class="bg-color btn1">授权登录</button>
|
class="bg-color btn1">授权登录</button>
|
||||||
<button hover-class="none" v-else-if="!bindPhone" @tap="getAuthLogin"
|
<button hover-class="none" v-else-if="bindPhone && !protocol" @tap="handleAuthBtnTap"
|
||||||
class="bg-color btn1">授权登录</button>
|
class="bg-color btn1">授权登录</button>
|
||||||
<button hover-class="none" v-else-if="mp_is_new" @tap="userLogin"
|
<button hover-class="none" v-else-if="!bindPhone" @tap="handleAuthBtnTap"
|
||||||
class="bg-color btn1">授权登录</button>
|
class="bg-color btn1">授权登录</button>
|
||||||
<button v-else-if="canUseGetUserProfile && code" hover-class="none" @tap="getUserProfile"
|
<button hover-class="none" v-else-if="mp_is_new" @tap="onAuthorizeLoginTap('mp_userLogin')"
|
||||||
class="bg-color btn1">授权登录</button>
|
class="bg-color btn1">授权登录</button>
|
||||||
<button v-else hover-class="none" open-type="getUserInfo" @getuserinfo="setUserInfo"
|
<button v-else-if="canUseGetUserProfile && code" hover-class="none" @tap="onAuthorizeLoginTap('mp_userProfile')"
|
||||||
|
class="bg-color btn1">授权登录</button>
|
||||||
|
<button v-else-if="protocol" hover-class="none" open-type="getUserInfo" @getuserinfo="setUserInfo"
|
||||||
|
class="bg-color btn1">授权登录</button>
|
||||||
|
<button v-else hover-class="none" @tap="onAuthorizeLoginTap('mp_userinfo')"
|
||||||
class="bg-color btn1">授权登录</button>
|
class="bg-color btn1">授权登录</button>
|
||||||
</template>
|
</template>
|
||||||
<button v-if="configData.phone_auth_switch" hover-class="none" @tap="phoneLogin" class="btn2">手机号登录</button>
|
<button v-if="configData.phone_auth_switch" hover-class="none" @tap="phoneLogin" class="btn2">手机号登录</button>
|
||||||
@@ -67,6 +71,23 @@
|
|||||||
<privacyAgreementPopup v-if="canGetPrivacySetting" @onReject="onReject" @onAgree="onAgree">
|
<privacyAgreementPopup v-if="canGetPrivacySetting" @onReject="onReject" @onAgree="onAgree">
|
||||||
</privacyAgreementPopup>
|
</privacyAgreementPopup>
|
||||||
<!-- #endif -->
|
<!-- #endif -->
|
||||||
|
|
||||||
|
<view v-if="showProtocolDialog" class="protocol-modal">
|
||||||
|
<view class="protocol-modal__mask" @tap="rejectProtocol"></view>
|
||||||
|
<view class="protocol-modal__box">
|
||||||
|
<view class="protocol-modal__title">用户协议与隐私协议</view>
|
||||||
|
<view class="protocol-modal__content">
|
||||||
|
<text>已阅读并同意</text>
|
||||||
|
<text class="font-num" @tap.stop="privacy('user')">《用户协议》</text>
|
||||||
|
<text>与</text>
|
||||||
|
<text class="font-num" @tap.stop="privacy('privacy')">《隐私协议》</text>
|
||||||
|
</view>
|
||||||
|
<view class="protocol-modal__actions">
|
||||||
|
<view class="protocol-modal__btn" @tap="rejectProtocol">不同意</view>
|
||||||
|
<view class="protocol-modal__btn protocol-modal__btn--primary bg-color" @tap="agreeProtocol">同意并继续</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -123,6 +144,8 @@
|
|||||||
protocol: false,
|
protocol: false,
|
||||||
bindPhone: false,
|
bindPhone: false,
|
||||||
isShow: false,
|
isShow: false,
|
||||||
|
showProtocolDialog: false,
|
||||||
|
pendingAuthorizeAction: '',
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
@@ -169,8 +192,62 @@
|
|||||||
uni.removeStorageSync('form_type_cart');
|
uni.removeStorageSync('form_type_cart');
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
handleAuthBtnTap() {
|
||||||
|
let action = '';
|
||||||
|
// #ifdef H5
|
||||||
|
action = 'h5_wechat';
|
||||||
|
// #endif
|
||||||
|
// #ifdef MP
|
||||||
|
action = this.bindPhone ? 'mp_phone' : 'mp_auth';
|
||||||
|
// #endif
|
||||||
|
this.openProtocolDialog(action);
|
||||||
|
},
|
||||||
|
onAuthorizeLoginTap(action) {
|
||||||
|
this.openProtocolDialog(action);
|
||||||
|
},
|
||||||
|
openProtocolDialog(action) {
|
||||||
|
this.pendingAuthorizeAction = action || '';
|
||||||
|
this.showProtocolDialog = true;
|
||||||
|
},
|
||||||
|
agreeProtocol() {
|
||||||
|
const action = this.pendingAuthorizeAction;
|
||||||
|
this.pendingAuthorizeAction = '';
|
||||||
|
this.showProtocolDialog = false;
|
||||||
|
this.protocol = true;
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.runAuthorizeAction(action, {
|
||||||
|
afterAgree: true
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
rejectProtocol() {
|
||||||
|
this.pendingAuthorizeAction = '';
|
||||||
|
this.showProtocolDialog = false;
|
||||||
|
},
|
||||||
|
runAuthorizeAction(action, options = {}) {
|
||||||
|
if (!action) return;
|
||||||
|
if (action === 'h5_wechat') return this.wechatLogin();
|
||||||
|
if (action === 'mp_auth') return this.getAuthLogin();
|
||||||
|
if (action === 'mp_userLogin') return this.userLogin();
|
||||||
|
if (action === 'mp_userProfile') return this.getUserProfile();
|
||||||
|
if (action === 'mp_phone' || action === 'mp_userinfo') {
|
||||||
|
if (options.afterAgree) {
|
||||||
|
return this.$util.Tips({
|
||||||
|
title: '请点击授权登录继续'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
},
|
||||||
cancelLogin(){
|
cancelLogin(){
|
||||||
uni.navigateBack()
|
const pages = getCurrentPages();
|
||||||
|
if (pages.length > 1) {
|
||||||
|
uni.navigateBack();
|
||||||
|
} else {
|
||||||
|
uni.switchTab({
|
||||||
|
url: '/pages/index/index'
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
wechatAuthLogin(d, back_url) {
|
wechatAuthLogin(d, back_url) {
|
||||||
let that = this;
|
let that = this;
|
||||||
@@ -286,10 +363,24 @@
|
|||||||
this.protocol = true
|
this.protocol = true
|
||||||
},
|
},
|
||||||
onReject() {
|
onReject() {
|
||||||
|
const pages = getCurrentPages();
|
||||||
|
if (pages.length > 1) {
|
||||||
uni.navigateBack();
|
uni.navigateBack();
|
||||||
|
} else {
|
||||||
|
uni.switchTab({
|
||||||
|
url: '/pages/index/index'
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
back() {
|
back() {
|
||||||
|
const pages = getCurrentPages();
|
||||||
|
if (pages.length > 1) {
|
||||||
uni.navigateBack();
|
uni.navigateBack();
|
||||||
|
} else {
|
||||||
|
uni.switchTab({
|
||||||
|
url: '/pages/index/index'
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
home() {
|
home() {
|
||||||
uni.switchTab({
|
uni.switchTab({
|
||||||
@@ -531,7 +622,7 @@
|
|||||||
},
|
},
|
||||||
// 公众号登录
|
// 公众号登录
|
||||||
wechatLogin() {
|
wechatLogin() {
|
||||||
if (!this.protocol && this.configData.store_user_agreement) {
|
if (!this.protocol) {
|
||||||
return this.$util.Tips({
|
return this.$util.Tips({
|
||||||
title: '请先阅读并同意协议'
|
title: '请先阅读并同意协议'
|
||||||
});
|
});
|
||||||
@@ -690,4 +781,69 @@
|
|||||||
height: 40rpx;
|
height: 40rpx;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.protocol-modal {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 20000;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.protocol-modal__mask {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
.protocol-modal__box {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
width: 620rpx;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 24rpx;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.protocol-modal__title {
|
||||||
|
padding: 40rpx 40rpx 0;
|
||||||
|
font-size: 32rpx;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #333333;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.protocol-modal__content {
|
||||||
|
padding: 32rpx 40rpx 40rpx;
|
||||||
|
font-size: 28rpx;
|
||||||
|
color: #333333;
|
||||||
|
line-height: 42rpx;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.protocol-modal__actions {
|
||||||
|
display: flex;
|
||||||
|
border-top: 1px solid #EEEEEE;
|
||||||
|
}
|
||||||
|
|
||||||
|
.protocol-modal__btn {
|
||||||
|
flex: 1;
|
||||||
|
height: 88rpx;
|
||||||
|
line-height: 88rpx;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 30rpx;
|
||||||
|
color: #666666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.protocol-modal__btn--primary {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -21,11 +21,33 @@ import {
|
|||||||
} from '@/config/cache';
|
} from '@/config/cache';
|
||||||
import Cache from '@/utils/cache';
|
import Cache from '@/utils/cache';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 不依赖 atob 的 base64 解码(小程序环境不提供 atob)
|
||||||
|
*/
|
||||||
|
function base64Decode(str) {
|
||||||
|
var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
|
||||||
|
var output = '';
|
||||||
|
var i = 0;
|
||||||
|
str = String(str).replace(/[^A-Za-z0-9+/=]/g, '');
|
||||||
|
while (i < str.length) {
|
||||||
|
var b1 = chars.indexOf(str.charAt(i++));
|
||||||
|
var b2 = chars.indexOf(str.charAt(i++));
|
||||||
|
var b3 = chars.indexOf(str.charAt(i++));
|
||||||
|
var b4 = chars.indexOf(str.charAt(i++));
|
||||||
|
output += String.fromCharCode((b1 << 2) | (b2 >> 4));
|
||||||
|
if (b3 !== 64) output += String.fromCharCode(((b2 & 15) << 4) | (b3 >> 2));
|
||||||
|
if (b4 !== 64) output += String.fromCharCode(((b3 & 3) << 6) | b4);
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
token: Cache.get(LOGIN_STATUS) || false,
|
token: Cache.get(LOGIN_STATUS) || false,
|
||||||
backgroundColor: "#fff",
|
backgroundColor: "#fff",
|
||||||
userInfo: Cache.get(USER_INFO) || {},
|
userInfo: Cache.get(USER_INFO) || {},
|
||||||
uid: Cache.get(UID) || 0,
|
uid: Cache.get(UID) || 0,
|
||||||
|
openid: Cache.get('OPENID') || '',
|
||||||
|
unionid: Cache.get('UNIONID') || '',
|
||||||
homeActive: false,
|
homeActive: false,
|
||||||
phoneStatus: true,
|
phoneStatus: true,
|
||||||
autoplay: Cache.get(NON_WIFI_AUTOPLAY) || false,
|
autoplay: Cache.get(NON_WIFI_AUTOPLAY) || false,
|
||||||
@@ -89,11 +111,46 @@ const mutations = {
|
|||||||
LOGIN(state, opt) {
|
LOGIN(state, opt) {
|
||||||
state.token = opt.token;
|
state.token = opt.token;
|
||||||
Cache.set(LOGIN_STATUS, opt.token, opt.time);
|
Cache.set(LOGIN_STATUS, opt.token, opt.time);
|
||||||
|
// 登录时自动提取 openid 缓存起来
|
||||||
|
if (opt.token) {
|
||||||
|
try {
|
||||||
|
let parts = opt.token.split('.');
|
||||||
|
if (parts.length !== 3) {
|
||||||
|
console.warn('[LOGIN] token 不是 JWT 格式,parts=' + parts.length);
|
||||||
|
} else {
|
||||||
|
let payload = parts[1]
|
||||||
|
.replace(/-/g, '+')
|
||||||
|
.replace(/_/g, '/');
|
||||||
|
while (payload.length % 4 !== 0) payload += '=';
|
||||||
|
const data = JSON.parse(base64Decode(payload));
|
||||||
|
if (data.openid) {
|
||||||
|
state.openid = data.openid;
|
||||||
|
Cache.set('OPENID', data.openid);
|
||||||
|
console.log('[LOGIN] openid 已缓存:', data.openid);
|
||||||
|
} else {
|
||||||
|
console.warn('[LOGIN] JWT payload 无 openid 字段:', JSON.stringify(data));
|
||||||
|
}
|
||||||
|
if (data.unionid) {
|
||||||
|
state.unionid = data.unionid;
|
||||||
|
Cache.set('UNIONID', data.unionid);
|
||||||
|
console.log('[LOGIN] unionid 已缓存:', data.unionid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('[LOGIN] JWT 解码失败:', e.message, 'token前20位:', opt.token.substring(0, 20));
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
SETUID(state, val) {
|
SETUID(state, val) {
|
||||||
state.uid = val;
|
state.uid = val;
|
||||||
Cache.set(UID, val);
|
Cache.set(UID, val);
|
||||||
},
|
},
|
||||||
|
SET_OPENID(state, val) {
|
||||||
|
state.openid = val || '';
|
||||||
|
},
|
||||||
|
SET_UNIONID(state, val) {
|
||||||
|
state.unionid = val || '';
|
||||||
|
},
|
||||||
UPDATE_LOGIN(state, token) {
|
UPDATE_LOGIN(state, token) {
|
||||||
state.token = token;
|
state.token = token;
|
||||||
},
|
},
|
||||||
@@ -102,8 +159,12 @@ const mutations = {
|
|||||||
Cache.clear(UID);
|
Cache.clear(UID);
|
||||||
Cache.clear(USER_INFO);
|
Cache.clear(USER_INFO);
|
||||||
Cache.clear('newcomerGift');
|
Cache.clear('newcomerGift');
|
||||||
|
Cache.clear('OPENID');
|
||||||
|
Cache.clear('UNIONID');
|
||||||
state.token = undefined;
|
state.token = undefined;
|
||||||
state.uid = undefined
|
state.uid = undefined;
|
||||||
|
state.openid = '';
|
||||||
|
state.unionid = '';
|
||||||
state.userInfo = {}
|
state.userInfo = {}
|
||||||
},
|
},
|
||||||
BACKGROUND_COLOR(state, color) {
|
BACKGROUND_COLOR(state, color) {
|
||||||
|
|||||||
103
utils/util.js
103
utils/util.js
@@ -10,15 +10,39 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
TOKENNAME,
|
TOKENNAME,
|
||||||
HTTP_REQUEST_URL
|
HTTP_REQUEST_URL,
|
||||||
|
TRUSTED_DOMAINS
|
||||||
} from '../config/app.js';
|
} from '../config/app.js';
|
||||||
|
import { LOGIN_STATUS } from '../config/cache.js';
|
||||||
import store from '../store';
|
import store from '../store';
|
||||||
|
import { toLogin } from '../libs/login';
|
||||||
import {
|
import {
|
||||||
customerType
|
customerType
|
||||||
} from '@/api/api.js'
|
} from '@/api/api.js'
|
||||||
// #ifdef APP-PLUS
|
// #ifdef APP-PLUS
|
||||||
import permision from "./permission.js"
|
import permision from "./permission.js"
|
||||||
// #endif
|
// #endif
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 不依赖 atob 的 base64 解码(小程序环境不提供 atob)
|
||||||
|
*/
|
||||||
|
function base64Decode(str) {
|
||||||
|
var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
|
||||||
|
var output = '';
|
||||||
|
var i = 0;
|
||||||
|
str = String(str).replace(/[^A-Za-z0-9+/=]/g, '');
|
||||||
|
while (i < str.length) {
|
||||||
|
var b1 = chars.indexOf(str.charAt(i++));
|
||||||
|
var b2 = chars.indexOf(str.charAt(i++));
|
||||||
|
var b3 = chars.indexOf(str.charAt(i++));
|
||||||
|
var b4 = chars.indexOf(str.charAt(i++));
|
||||||
|
output += String.fromCharCode((b1 << 2) | (b2 >> 4));
|
||||||
|
if (b3 !== 64) output += String.fromCharCode(((b2 & 15) << 4) | (b3 >> 2));
|
||||||
|
if (b4 !== 64) output += String.fromCharCode(((b3 & 3) << 6) | b4);
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
/**
|
/**
|
||||||
* opt object | string
|
* opt object | string
|
||||||
@@ -195,8 +219,65 @@ export default {
|
|||||||
window.open(url)
|
window.open(url)
|
||||||
// #endif
|
// #endif
|
||||||
// #ifdef MP || APP-PLUS
|
// #ifdef MP || APP-PLUS
|
||||||
|
let openUrl = url;
|
||||||
|
let token = store.state.app.token;
|
||||||
|
if (token && url.indexOf('token=') === -1 && (url.indexOf(HTTP_REQUEST_URL) === 0 || (TRUSTED_DOMAINS || []).some(d => url.indexOf(d) === 0))) {
|
||||||
|
// 从 JWT payload 中提取 openid 和 unionid
|
||||||
|
let openid = store.state.app.openid;
|
||||||
|
let unionid = store.state.app.unionid || '';
|
||||||
|
console.log('[GoUrl] store中 unionid=' + (store.state.app.unionid || '(空)'));
|
||||||
|
if (!openid) {
|
||||||
|
try { openid = uni.getStorageSync('OPENID'); } catch (e) {}
|
||||||
|
}
|
||||||
|
if (!unionid) {
|
||||||
|
try {
|
||||||
|
unionid = uni.getStorageSync('UNIONID');
|
||||||
|
console.log('[GoUrl] 缓存中 unionid=' + (unionid || '(空)'));
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
if ((!openid || !unionid) && token) {
|
||||||
|
try {
|
||||||
|
let parts = token.split('.');
|
||||||
|
if (parts.length === 3) {
|
||||||
|
let payload = parts[1]
|
||||||
|
.replace(/-/g, '+')
|
||||||
|
.replace(/_/g, '/');
|
||||||
|
while (payload.length % 4 !== 0) payload += '=';
|
||||||
|
const data = JSON.parse(base64Decode(payload));
|
||||||
|
if (!openid) openid = data.openid || '';
|
||||||
|
if (!unionid) unionid = data.unionid || '';
|
||||||
|
console.log('[GoUrl] JWT中 unionid=' + (data.unionid || '(空)'));
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
// 最后兜底:从用户信息缓存中读 unionid
|
||||||
|
if (!unionid) {
|
||||||
|
try {
|
||||||
|
const userInfo = uni.getStorageSync('user_info');
|
||||||
|
if (userInfo) {
|
||||||
|
const info = typeof userInfo === 'string' ? JSON.parse(userInfo) : userInfo;
|
||||||
|
unionid = info.unionid || '';
|
||||||
|
console.log('[GoUrl] userInfo中 unionid=' + (info.unionid || '(空)'));
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
console.log('[GoUrl] 最终参数: openid=' + (openid || '(空)') + ', unionid=' + (unionid || '(空)') + ', t=' + Math.floor(Date.now() / 1000));
|
||||||
|
if (!openid) {
|
||||||
|
// 先存目标路径,登录后自动跳回 web_view
|
||||||
|
Cache.set('login_back_url', '/pages/annex/web_view/index?url=' + encodeURIComponent(openUrl));
|
||||||
|
toLogin();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const joiner = url.indexOf('?') !== -1 ? '&' : '?';
|
||||||
|
const t = Math.floor(Date.now() / 1000);
|
||||||
|
openUrl = url + joiner + 'openid=' + encodeURIComponent(openid);
|
||||||
|
if (unionid) {
|
||||||
|
openUrl += '&unionid=' + encodeURIComponent(unionid);
|
||||||
|
}
|
||||||
|
openUrl += '&t=' + t;
|
||||||
|
}
|
||||||
uni.navigateTo({
|
uni.navigateTo({
|
||||||
url: `/pages/annex/web_view/index?url=${url}`
|
url: `/pages/annex/web_view/index?url=${encodeURIComponent(openUrl)}`
|
||||||
});
|
});
|
||||||
// #endif
|
// #endif
|
||||||
} else {
|
} else {
|
||||||
@@ -1262,6 +1343,24 @@ export default {
|
|||||||
}
|
}
|
||||||
return status;
|
return status;
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* 解析 JWT token 的 payload(不验证签名)
|
||||||
|
* 返回 { uid, phone, openid, exp, ... } 或 null
|
||||||
|
* 依赖后端在签发 JWT 时加入 uid、phone、openid 等自定义 claims
|
||||||
|
*/
|
||||||
|
parseJwt: function(token) {
|
||||||
|
try {
|
||||||
|
let parts = token.split('.');
|
||||||
|
if (parts.length !== 3) return null;
|
||||||
|
let payload = parts[1]
|
||||||
|
.replace(/-/g, '+')
|
||||||
|
.replace(/_/g, '/');
|
||||||
|
while (payload.length % 4 !== 0) payload += '=';
|
||||||
|
return JSON.parse(atob(payload));
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user