diff --git a/yelaymaths/pages/kecheng/kecheng.js b/yelaymaths/pages/kecheng/kecheng.js index 70ecdc4..29e62f2 100644 --- a/yelaymaths/pages/kecheng/kecheng.js +++ b/yelaymaths/pages/kecheng/kecheng.js @@ -11,6 +11,10 @@ Page({ hasCourse: false, ownedCourses: [], tipsHtml: '', + /** 账号被封禁:不显示已购权益,改为显示提示。文字全部来自后台配置 */ + blocked: false, + blockedTitle: '', + blockedDesc: '', }, onLoad() { @@ -31,9 +35,23 @@ Page({ .getEntitlements() .then((list) => { store.setEntitlements(list); - this.setData({ ownedCourses: list || [], hasCourse: (list || []).length > 0 }); + this.setData({ ownedCourses: list || [], hasCourse: (list || []).length > 0, blocked: false }); }) - .catch((err) => console.warn('[kecheng] 已购课程加载失败', err)); + .catch((err) => { + if (err && err.code === api.ACCOUNT_DISABLED) { + // 服务端不返回权益了,本地缓存也一并清掉,免得露出上一次的课程 + store.setEntitlements([]); + this.setData({ + ownedCourses: [], + hasCourse: false, + blocked: true, + blockedTitle: store.cfg('account.blocked_title'), + blockedDesc: store.cfg('account.blocked_desc'), + }); + return; + } + console.warn('[kecheng] 已购课程加载失败', err); + }); }, loadTips() { diff --git a/yelaymaths/pages/kecheng/kecheng.wxml b/yelaymaths/pages/kecheng/kecheng.wxml index 66f9430..afa9e92 100644 --- a/yelaymaths/pages/kecheng/kecheng.wxml +++ b/yelaymaths/pages/kecheng/kecheng.wxml @@ -1,5 +1,12 @@ - + + + {{blockedTitle}} + {{blockedDesc}} + + + + @@ -7,7 +14,7 @@ - + diff --git a/yelaymaths/pages/kecheng/kecheng.wxss b/yelaymaths/pages/kecheng/kecheng.wxss index 3df464b..c4794d8 100644 --- a/yelaymaths/pages/kecheng/kecheng.wxss +++ b/yelaymaths/pages/kecheng/kecheng.wxss @@ -46,4 +46,29 @@ font-size: 25rpx; margin-top: 15rpx; margin-left: 15rpx; -} \ No newline at end of file +} + +/* 账号异常提示的容器:整页替换成这一块,文字由后台配置下发 */ +.blocked-tip { + box-sizing: border-box; + width: 710rpx; + margin-top: 120rpx; + padding: 60rpx 40rpx; + border: 1rpx solid #ffd9d9; + border-radius: 20rpx; + background-color: #fff7f7; +} +.blocked-title { + display: block; + font-size: 36rpx; + font-weight: bold; + text-align: center; + color: #e60012; +} +.blocked-desc { + display: block; + margin-top: 20rpx; + font-size: 26rpx; + text-align: center; + color: #888; +} diff --git a/yelaymaths/utils/api.js b/yelaymaths/utils/api.js index 7acc722..39c4066 100644 --- a/yelaymaths/utils/api.js +++ b/yelaymaths/utils/api.js @@ -14,6 +14,13 @@ const store = require('./store'); /** 进行中的会话恢复,避免并发 401 触发多次恢复流程 */ let recovering = null; +/** + * 服务端错误码:账号被封禁。 + * 要和「登录过期」分开:页面据此走自己的提示分支,而不是把人赶去登录。 + * 具体提示什么文字由后台配置决定,这里只管把错误码原样带出去。 + */ +const ACCOUNT_DISABLED = 2003; + function staticUrl(path) { if (!path) return ''; if (/^https?:\/\//i.test(path)) return path; @@ -74,7 +81,10 @@ function request(options) { if (needLogin && options.auth !== false && options.retried !== true) { return recoverSession() .then(() => request(Object.assign({}, options, { retried: true }))) - .catch(() => { + .catch((err) => { + // 账号异常:把原因原样抛给页面,并且不清本地用户—— + // 清掉的话知识页会把它当成未登录,只剩一片空白,看不出发生了什么 + if (err && err.code === ACCOUNT_DISABLED) throw err; store.reset(); throw { code: 2000, message: '登录已过期,请重新进入小程序' }; }); @@ -236,4 +246,4 @@ function requestVirtualPayment(payData) { }); } -module.exports = Object.assign(api, { requestVirtualPayment, devicePlatform: platform }); +module.exports = Object.assign(api, { requestVirtualPayment, devicePlatform: platform, ACCOUNT_DISABLED }); diff --git a/yelaymaths_admin/src/app/(admin)/users/page.tsx b/yelaymaths_admin/src/app/(admin)/users/page.tsx index 36a14d0..d97a6b3 100644 --- a/yelaymaths_admin/src/app/(admin)/users/page.tsx +++ b/yelaymaths_admin/src/app/(admin)/users/page.tsx @@ -3,7 +3,7 @@ import { useCallback, useEffect, useState } from 'react'; import { api } from '@/lib/api'; import { Button, Card, Empty, Input, Modal, Notice, Select } from '@/components/ui'; -import { formatDate } from '@/lib/datetime'; +import { formatDateTime } from '@/lib/datetime'; interface UserRow { id: number; @@ -44,6 +44,11 @@ export default function UsersPage() { const [total, setTotal] = useState(0); const [page, setPage] = useState(1); const [keyword, setKeyword] = useState(''); + /** 筛选与排序:'-1' / '' 表示全部 */ + const [statusFilter, setStatusFilter] = useState('-1'); + const [entitlementFilter, setEntitlementFilter] = useState(''); + const [sortKey, setSortKey] = useState('id'); + const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc'); const [error, setError] = useState(''); const [editing, setEditing] = useState(null); const [form, setForm] = useState({ nickname: '', headicon_id: '0', status: '1' }); @@ -54,22 +59,36 @@ export default function UsersPage() { const load = useCallback((nextPage: number, nextKeyword: string) => { setError(''); api - .listUsers(nextPage, PAGE_SIZE, nextKeyword) + .listUsers({ + page: nextPage, + pageSize: PAGE_SIZE, + keyword: nextKeyword, + status: statusFilter === '-1' ? undefined : Number(statusFilter), + entitlement: entitlementFilter === 'has' || entitlementFilter === 'none' ? entitlementFilter : undefined, + sort: sortKey, + order: sortOrder, + }) .then((res) => { setItems(res.items as unknown as UserRow[]); setTotal(res.total); setPage(res.page); }) .catch((err: { message: string }) => setError(err.message)); - }, []); + }, [statusFilter, entitlementFilter, sortKey, sortOrder]); + // 筛选 / 排序一变,load 的引用就变,这里会带着当前关键词回到第 1 页重拉 + useEffect(() => { + load(1, keyword); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [load]); + + // 发放权益时用的课程下拉,只在首次进页面拉一次 useEffect(() => { - load(1, ''); api .listResource('courses', { page: 1, pageSize: 100 }) .then((res) => setCourses(res.items as unknown as CourseRow[])) .catch(() => setCourses([])); - }, [load]); + }, []); const loadEntitlements = (userId: number) => { api @@ -141,8 +160,48 @@ export default function UsersPage() { - + /* 筛选、排序、搜索排在一行;改任意一项都会从第 1 页重新拉 */ +
+
+ +
+
+ +
@@ -176,7 +235,7 @@ export default function UsersPage() { {u.status === 1 ? 正常 : 封禁} - {formatDate(u.last_login_at)} + {formatDateTime(u.last_login_at)}