增加了token/me等几个公共页面,context中增加了scene

This commit is contained in:
Xu Chang 2022-05-06 10:16:56 +08:00
parent 48767766a7
commit ff2a97960a
42 changed files with 880 additions and 48 deletions

View File

@ -2,9 +2,9 @@ import { UniversalContext } from 'oak-domain/lib/store/UniversalContext';
import { EntityDict } from 'oak-app-domain';
import { RowStore } from 'oak-domain/lib/types';
export declare abstract class GeneralRuntimeContext<ED extends EntityDict> extends UniversalContext<ED> {
applicationId: string;
token?: string;
constructor(store: RowStore<ED, GeneralRuntimeContext<ED>>, appId: string, token?: string);
private applicationId;
private getTokenFn;
constructor(store: RowStore<ED, GeneralRuntimeContext<ED>>, appId: string, getToken: () => Promise<string | undefined>);
getApplication(): Promise<import("oak-domain/lib/types").SelectRowShape<import("oak-app-domain/Application/Schema").Schema, {
id: 1;
name: 1;

View File

@ -4,11 +4,11 @@ exports.GeneralRuntimeContext = void 0;
const UniversalContext_1 = require("oak-domain/lib/store/UniversalContext");
class GeneralRuntimeContext extends UniversalContext_1.UniversalContext {
applicationId;
token;
constructor(store, appId, token) {
getTokenFn;
constructor(store, appId, getToken) {
super(store);
this.applicationId = appId;
this.token = token;
this.getTokenFn = getToken;
}
async getApplication() {
const { result: [application] } = await this.rowStore.select('application', {
@ -26,7 +26,8 @@ class GeneralRuntimeContext extends UniversalContext_1.UniversalContext {
return application;
}
async getToken() {
if (this.token) {
const tokenValue = await this.getTokenFn();
if (tokenValue) {
const { result: [token] } = await this.rowStore.select('token', {
data: {
id: 1,
@ -34,7 +35,7 @@ class GeneralRuntimeContext extends UniversalContext_1.UniversalContext {
playerId: 1,
},
filter: {
id: this.token,
id: tokenValue,
ableState: 'enabled',
}
}, this);

View File

@ -1,6 +1,7 @@
import { loginByPassword, loginMp, loginWechatMp } from './token';
import { loginByPassword, loginMp, loginWechatMp, syncUserInfoWechatMp } from './token';
export declare const aspectDict: {
loginByPassword: typeof loginByPassword;
loginMp: typeof loginMp;
loginWechatMp: typeof loginWechatMp;
syncUserInfoWechatMp: typeof syncUserInfoWechatMp;
};

View File

@ -6,5 +6,6 @@ exports.aspectDict = {
loginByPassword: token_1.loginByPassword,
loginMp: token_1.loginMp,
loginWechatMp: token_1.loginWechatMp,
syncUserInfoWechatMp: token_1.syncUserInfoWechatMp,
};
// export type AspectDict<ED extends EntityDict & BaseEntityDict> = TokenAD<ED> & CrudAD<ED>;

View File

@ -12,3 +12,15 @@ export declare function loginWechatMp<ED extends EntityDict, Cxt extends General
code: string;
env: WechatMpEnv;
}, context: Cxt): Promise<string>;
/**
* wx.getUserProfile拿到的用户信息
* @param param0
* @param context
*/
export declare function syncUserInfoWechatMp<ED extends EntityDict, Cxt extends GeneralRuntimeContext<ED>>({ nickname, avatarUrl, encryptedData, iv, signature }: {
nickname: string;
avatarUrl: string;
encryptedData: string;
iv: string;
signature: string;
}, context: Cxt): Promise<void>;

View File

@ -3,9 +3,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.loginWechatMp = exports.loginByPassword = exports.loginMp = void 0;
const assert_1 = __importDefault(require("assert"));
exports.syncUserInfoWechatMp = exports.loginWechatMp = exports.loginByPassword = exports.loginMp = void 0;
const oak_wechat_sdk_1 = __importDefault(require("oak-wechat-sdk"));
const assert_1 = __importDefault(require("assert"));
const lodash_1 = require("lodash");
async function loginMp(params, context) {
const { rowStore } = context;
@ -215,6 +215,98 @@ async function loginWechatMp({ code, env }, context) {
return id;
}
exports.loginWechatMp = loginWechatMp;
/**
* 同步从wx.getUserProfile拿到的用户信息
* @param param0
* @param context
*/
async function syncUserInfoWechatMp({ nickname, avatarUrl, encryptedData, iv, signature }, context) {
const { rowStore } = context;
const { userId } = (await context.getToken());
const application = await context.getApplication();
const { result: [{ sessionKey, user }] } = await rowStore.select('wechatUser', {
data: {
id: 1,
sessionKey: 1,
user: {
id: 1,
nickname: 1,
extraFile$entity: {
$entity: 'extraFile',
data: {
id: 1,
tag1: 1,
origin: 1,
bucket: 1,
objectId: 1,
filename: 1,
},
filter: {
tag1: 'avatar',
},
}
}
},
filter: {
userId: userId,
applicationId: application.id,
}
}, context);
const { type, config } = application;
(0, assert_1.default)(type === 'wechatMp' || config.type === 'wechatMp');
const config2 = config;
const { appId, appSecret } = config2;
const wechatInstance = oak_wechat_sdk_1.default.getInstance(appId, appSecret, 'wechatMp');
console.log(avatarUrl);
// const result = wechatInstance.decryptData(sessionKey as string, encryptedData, iv, signature);
// 实测发现解密出来的和userInfo完全一致……
// console.log(result);
const { id, nickname: originNickname, extraFile$entity } = user;
const updateData = {};
if (nickname !== originNickname) {
Object.assign(updateData, {
nickname,
});
}
/* if (extraFile$entity?.length === 0 || composeFileUrl(extraFile$entity![0]) !== avatarUrl) {
// 需要更新新的avatar extra file
const extraFileOperations: ExtraFileOperation['data'][] = [
{
action: 'create',
data: assign({
id: await generateNewId(),
tag1: 'avatar',
userId,
}, decomposeFileUrl(avatarUrl))
}
];
if (extraFile$entity!.length > 0) {
extraFileOperations.push(
{
action: 'remove',
data: {},
filter: {
id: extraFile$entity![0].id,
}
}
);
}
assign(updateData, {
extraFile$entity: extraFileOperations,
});
}
if (keys(updateData).length > 0) {
await rowStore.operate('user', {
action: 'update',
data: updateData,
filter: {
id,
}
}, context);
} */
}
exports.syncUserInfoWechatMp = syncUserInfoWechatMp;
/* export type AspectDict<ED extends EntityDict> = {
loginMp: (params: { code: string }, context: GeneralRuntimeContext<ED>) => Promise<string>;
loginByPassword: (params: { password: string, mobile: string }, context: GeneralRuntimeContext<ED>) => Promise<string>;

View File

@ -1,7 +1,7 @@
import { String, Text } from 'oak-domain/lib/types/DataType';
import { EntityShape } from 'oak-domain/lib/types/Entity';
export interface Schema extends EntityShape {
origin: 'qiniu';
origin: 'qiniu' | 'unknown';
type: 'image' | 'pdf' | 'video' | 'audio' | 'file';
bucket: String<16>;
objectId: String<64>;
@ -11,4 +11,5 @@ export interface Schema extends EntityShape {
md5: Text;
entity: String<32>;
entityId: String<64>;
extra1?: Text;
}

View File

@ -4,8 +4,11 @@ import { Aspect, Context } from 'oak-domain/lib/types';
import { WechatMpEnv } from 'oak-app-domain/Token/Schema';
export declare class Token<ED extends EntityDict, Cxt extends Context<ED>, AD extends Record<string, Aspect<ED, Cxt>>> extends Feature<ED, Cxt, AD> {
private token?;
private rwLock;
constructor();
loginByPassword(mobile: string, password: string): Promise<void>;
loginWechatMp(code: string, env: WechatMpEnv): Promise<void>;
syncUserInfoWechatMp(): Promise<void>;
logout(): Promise<void>;
getToken(): string | undefined;
getToken(): Promise<string | undefined>;
}

View File

@ -8,22 +8,48 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
Object.defineProperty(exports, "__esModule", { value: true });
exports.Token = void 0;
const oak_frontend_base_1 = require("oak-frontend-base");
const concurrent_1 = require("oak-domain/lib/utils/concurrent");
class Token extends oak_frontend_base_1.Feature {
token;
rwLock;
constructor() {
super();
this.rwLock = new concurrent_1.RWLock();
}
async loginByPassword(mobile, password) {
await this.rwLock.acquire('X');
this.token = await this.getAspectProxy().loginByPassword({ password, mobile });
this.rwLock.release();
}
async loginWechatMp(code, env) {
await this.rwLock.acquire('X');
this.token = await this.getAspectProxy().loginWechatMp({
code,
env,
});
this.rwLock.release();
}
async syncUserInfoWechatMp() {
const info = await wx.getUserProfile({
desc: '同步微信昵称和头像信息',
});
const { userInfo: { nickName: nickname, avatarUrl }, encryptedData, signature, iv } = info;
await this.getAspectProxy().syncUserInfoWechatMp({
nickname,
avatarUrl,
encryptedData,
signature,
iv,
});
}
async logout() {
this.token = undefined;
}
getToken() {
return this.token;
async getToken() {
await this.rwLock.acquire('S');
const token = this.token;
this.rwLock.release();
return token;
}
}
__decorate([
@ -32,6 +58,9 @@ __decorate([
__decorate([
oak_frontend_base_1.Action
], Token.prototype, "loginWechatMp", null);
__decorate([
oak_frontend_base_1.Action
], Token.prototype, "syncUserInfoWechatMp", null);
__decorate([
oak_frontend_base_1.Action
], Token.prototype, "logout", null);

View File

@ -8,7 +8,7 @@ const triggers = [
entity: 'user',
action: 'create',
when: 'before',
fn: async ({ operation }, context) => {
fn: async ({ operation }, context, params) => {
if (NO_ANY_USER) {
const { rowStore } = context;
const { result } = await rowStore.select('user', {
@ -22,7 +22,7 @@ const triggers = [
},
indexFrom: 0,
count: 1,
}, context);
}, context, params);
if (result.length === 0) {
const { data } = operation;
const userData = data instanceof Array ? data[0] : data;

3
lib/utils/extraFile.d.ts vendored Normal file
View File

@ -0,0 +1,3 @@
import { OpSchema as ExtraFile } from '../base-app-domain/ExtraFile/Schema';
export declare function composeFileUrl(extraFile: ExtraFile): string;
export declare function decomposeFileUrl(url: string): Pick<ExtraFile, 'bucket' | 'filename' | 'origin' | 'type'>;

10
lib/utils/extraFile.js Normal file
View File

@ -0,0 +1,10 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.decomposeFileUrl = exports.composeFileUrl = void 0;
function composeFileUrl(extraFile) {
return ``;
}
exports.composeFileUrl = composeFileUrl;
function decomposeFileUrl(url) {
}
exports.decomposeFileUrl = decomposeFileUrl;

View File

@ -35,7 +35,8 @@
"scripts": {
"prebuild": "ts-node ./scripts/buildBaseEntityDict && npm link ./src/base-app-domain",
"build": "tsc",
"test": "ts-node ./scripts/getAmapArea.ts"
"test": "ts-node ./scripts/getAmapArea.ts",
"clean:mp": "ts-node ./scripts/cleanDtsInWechatMp"
},
"main": "src/index"
}

View File

@ -0,0 +1,22 @@
import { removeSync, readdirSync, statSync } from 'fs-extra';
import { join } from 'path';
function removeDts(path: string) {
const files = readdirSync(path);
files.forEach(
(file) => {
const stat = statSync(join(path, file));
if (stat.isDirectory()) {
removeDts(join(path, file));
}
else if (file.endsWith('.d.ts') || file.endsWith('.js')) {
removeSync(join(path, file));
console.log(`remove ${join(path, file)}`);
}
}
);
}
removeDts(join(process.cwd(), 'wechatMp'));

View File

@ -6,12 +6,15 @@ import { RowStore } from 'oak-domain/lib/types';
export abstract class GeneralRuntimeContext<ED extends EntityDict> extends UniversalContext<ED> {
applicationId: string;
getTokenFn: () => Promise<string | undefined>;
constructor(store: RowStore<ED, GeneralRuntimeContext<ED>>, appId: string, getToken: () => Promise<string | undefined>) {
private applicationId: string;
private getTokenFn: () => Promise<string | undefined>;
private scene: string;
constructor(store: RowStore<ED, GeneralRuntimeContext<ED>>, appId: string, getToken: () => Promise<string | undefined>, scene: string) {
super(store);
this.applicationId = appId;
this.getTokenFn = getToken;
this.scene = scene;
}
async getApplication () {
@ -49,4 +52,8 @@ export abstract class GeneralRuntimeContext<ED extends EntityDict> extends Unive
return token;
}
}
getScene() {
return this.scene;
}
};

View File

@ -1,11 +1,10 @@
import { EntityDict } from 'oak-domain/lib/types';
import { EntityDict as BaseEntityDict } from 'oak-app-domain/EntityDict';
import { loginByPassword, loginMp, loginWechatMp } from './token';
import { loginByPassword, loginMp, loginWechatMp, syncUserInfoWechatMp } from './token';
export const aspectDict = {
loginByPassword,
loginMp,
loginWechatMp,
syncUserInfoWechatMp,
};
// export type AspectDict<ED extends EntityDict & BaseEntityDict> = TokenAD<ED> & CrudAD<ED>;

View File

@ -1,13 +1,15 @@
import { GeneralRuntimeContext } from '../RuntimeContext';
import { EntityDict } from 'oak-app-domain';
import WechatSDK from 'oak-wechat-sdk';
import assert from 'assert';
import { WechatMpConfig } from 'oak-app-domain/Application/Schema';
import WechatSDK from 'oak-wechat-sdk';
import { CreateOperationData as CreateToken, WechatMpEnv } from 'oak-app-domain/Token/Schema';
import { CreateOperationData as CreateWechatUser } from 'oak-app-domain/WechatUser/Schema';
import { CreateOperationData as CreateUser } from 'oak-app-domain/User/Schema';
import { assign, isEqual } from 'lodash';
import { CreateOperationData as CreateUser, Schema as User } from 'oak-app-domain/User/Schema';
import { Operation as ExtraFileOperation } from 'oak-app-domain/ExtraFile/Schema';
import { assign, isEqual, keys } from 'lodash';
import { SelectRowShape } from 'oak-domain/lib/types';
import { composeFileUrl, decomposeFileUrl } from '../utils/extraFile';
export async function loginMp<ED extends EntityDict, Cxt extends GeneralRuntimeContext<ED>>(params: { code: string }, context: Cxt): Promise<string> {
const { rowStore } = context;
@ -243,6 +245,105 @@ export async function loginWechatMp<ED extends EntityDict, Cxt extends GeneralRu
return id;
}
/**
* wx.getUserProfile拿到的用户信息
* @param param0
* @param context
*/
export async function syncUserInfoWechatMp<ED extends EntityDict, Cxt extends GeneralRuntimeContext<ED>>({
nickname, avatarUrl, encryptedData, iv, signature
}: {nickname: string, avatarUrl: string, encryptedData: string, iv: string, signature: string}, context: Cxt) {
const { rowStore } = context;
const { userId } = (await context.getToken())!;
const application = await context.getApplication();
const { result: [{ sessionKey, user }]} = await rowStore.select('wechatUser', {
data: {
id: 1,
sessionKey: 1,
user: {
id: 1,
nickname: 1,
extraFile$entity: {
$entity: 'extraFile',
data: {
id: 1,
tag1: 1,
origin: 1,
bucket: 1,
objectId: 1,
filename: 1,
extra1: 1,
},
filter: {
tag1: 'avatar',
},
}
}
},
filter: {
userId: userId!,
applicationId: application.id,
}
}, context);
// console.log(avatarUrl);
// const { type, config } = application;
// assert(type === 'wechatMp' || config.type === 'wechatMp');
// const config2 = config as WechatMpConfig;
// const { appId, appSecret } = config2;
// const wechatInstance = WechatSDK.getInstance(appId, appSecret, 'wechatMp');
// const result = wechatInstance.decryptData(sessionKey as string, encryptedData, iv, signature);
// 实测发现解密出来的和userInfo完全一致……
// console.log(result);
const { nickname: originNickname, extraFile$entity } = user as User;
const updateData = {};
if (nickname !== originNickname) {
Object.assign(updateData, {
nickname,
});
}
if (extraFile$entity?.length === 0 || composeFileUrl(extraFile$entity![0]) !== avatarUrl) {
// 需要更新新的avatar extra file
const extraFileOperations: ExtraFileOperation['data'][] = [
{
action: 'create',
data: assign({
id: await generateNewId(),
tag1: 'avatar',
entity: 'user',
entityId: userId,
}, decomposeFileUrl(avatarUrl))
}
];
if (extraFile$entity!.length > 0) {
extraFileOperations.push(
{
action: 'remove',
data: {},
filter: {
id: extraFile$entity![0].id,
}
}
);
}
assign(updateData, {
extraFile$entity: extraFileOperations,
});
}
if (keys(updateData).length > 0) {
await rowStore.operate('user', {
action: 'update',
data: updateData,
filter: {
id: userId!,
}
}, context);
}
}
/* export type AspectDict<ED extends EntityDict> = {
loginMp: (params: { code: string }, context: GeneralRuntimeContext<ED>) => Promise<string>;
loginByPassword: (params: { password: string, mobile: string }, context: GeneralRuntimeContext<ED>) => Promise<string>;

View File

@ -1,3 +1,4 @@
import addressCheckers from './address';
import tokenCheckers from './token';
export default [...addressCheckers];
export default [...addressCheckers, ...tokenCheckers];

39
src/checkers/token.ts Normal file
View File

@ -0,0 +1,39 @@
import { Checker, OakUserUnpermittedException } from "oak-domain/lib/types";
import { EntityDict } from 'oak-app-domain';
import { GeneralRuntimeContext } from '../RuntimeContext';
import { OakUnloggedInException } from "../types/Exceptions";
import { assign } from "lodash";
import { combineFilters } from "oak-domain/lib/store/filter";
const checkers: Checker<EntityDict, 'token', GeneralRuntimeContext<EntityDict>> [] = [
{
type: 'user',
action: 'select',
entity: 'token',
checker: async ({ operation }, context) => {
const scene = context.getScene();
const { filter } = operation;
if (scene === 'token:me') {
if (!filter || !filter.id) {
const token = await context.getToken();
if (!token) {
throw new OakUnloggedInException();
}
const { id } = token;
assign(operation, {
filter: combineFilters([filter, { id }]),
});
return 1;
}
return 0;
}
if (['app:onLaunch', 'token:me', 'token:login'].includes(scene)) {
return 0;
}
// 对获取token的权限进行精细化控制除了root
throw new OakUserUnpermittedException();
},
}
];
export default checkers;

View File

@ -5,6 +5,7 @@ import { ROOT_ROLE_ID, ROOT_USER_ID } from '../constants';
export const users: Array<UserCreate> = [
{
password: 'oak@2022',
nickname: 'root',
name: 'root',
id: ROOT_USER_ID,
}

View File

@ -2,7 +2,7 @@ import { String, Int, Text, Image } from 'oak-domain/lib/types/DataType';
import { EntityShape } from 'oak-domain/lib/types/Entity';
export interface Schema extends EntityShape {
origin: 'qiniu';
origin: 'qiniu' | 'unknown';
type: 'image' | 'pdf' | 'video' | 'audio' | 'file';
bucket: String<16>;
objectId: String<64>;
@ -12,4 +12,5 @@ export interface Schema extends EntityShape {
md5: Text;
entity: String<32>;
entityId: String<64>;
extra1?: Text;
};

View File

@ -1,3 +1,4 @@
import { pick } from 'lodash';
import { EntityDict } from 'oak-app-domain';
import { Action, Feature } from 'oak-frontend-base';
import { Aspect, Context } from 'oak-domain/lib/types';
@ -14,20 +15,68 @@ export class Token<ED extends EntityDict, Cxt extends Context<ED>, AD extends Re
}
@Action
async loginByPassword(mobile: string, password: string) {
async loginByPassword(mobile: string, password: string, scene: string) {
await this.rwLock.acquire('X');
this.token = await this.getAspectProxy().loginByPassword({ password, mobile });
this.rwLock.release();
try {
this.token = await this.getAspectProxy().loginByPassword({ password, mobile }, scene);
this.rwLock.release();
}
catch (err) {
this.rwLock.release();
throw err;
}
}
@Action
async loginWechatMp(code: string, env: WechatMpEnv) {
async loginWechatMp(scene: string) {
await this.rwLock.acquire('X');
this.token = await this.getAspectProxy().loginWechatMp({
code,
env,
try {
const { code } = await wx.login();
const env = await wx.getSystemInfo();
const env2 = pick(env, [
'brand',
'model',
'pixelRatio',
'screenWidth',
'screenHeight',
'windowWidth',
'windowHeight',
'statusBarHeight',
'language',
'version',
'system',
'platform',
'fontSizeSetting',
'SDKVersion'
]);
this.token = await this.getAspectProxy().loginWechatMp({
code,
env: Object.assign(env2, { type: 'wechatMp' }) as WechatMpEnv,
}, scene);
this.rwLock.release();
}
catch(err) {
this.rwLock.release();
throw err;
}
}
@Action
async syncUserInfoWechatMp(scene: string) {
const info = await wx.getUserProfile({
desc: '同步微信昵称和头像信息',
});
this.rwLock.release();
const { userInfo: { nickName: nickname, avatarUrl }, encryptedData, signature, iv } = info;
await this.getAspectProxy().syncUserInfoWechatMp({
nickname,
avatarUrl,
encryptedData,
signature,
iv,
}, scene);
}
@Action
@ -37,8 +86,14 @@ export class Token<ED extends EntityDict, Cxt extends Context<ED>, AD extends Re
async getToken() {
await this.rwLock.acquire('S');
const token = this.token;
this.rwLock.release();
return token;
try {
const token = this.token;
this.rwLock.release();
return token;
}
catch (err) {
this.rwLock.release();
throw err;
}
}
}

View File

@ -5,6 +5,9 @@ import { CreateOperationData as UserRole } from 'oak-app-domain/UserRole/Schema'
import assert from 'assert';
import { ROOT_ROLE_ID, ROOT_USER_ID } from '../constants';
import { DeduceCreateOperationData } from 'oak-domain/lib/types';
import { OakUnloggedInException } from '../types/Exceptions';
import { assign } from 'lodash';
import { combineFilters } from 'oak-domain/lib/store/filter';
let NO_ANY_USER = true;
const triggers: Trigger<EntityDict, 'user', GeneralRuntimeContext<EntityDict>>[] = [
@ -53,7 +56,7 @@ const triggers: Trigger<EntityDict, 'user', GeneralRuntimeContext<EntityDict>>[]
}
return 0;
}
}
},
];
export default triggers;

20
src/utils/extraFile.ts Normal file
View File

@ -0,0 +1,20 @@
import { OpSchema as ExtraFile } from '../base-app-domain/ExtraFile/Schema';
export function composeFileUrl(extraFile: ExtraFile) {
const { type, bucket, filename, origin, extra1 } = extraFile;
if (origin === 'unknown') {
// 未知第三方源
return extra1!;
}
throw new Error('not implemented yet');
}
export function decomposeFileUrl(url: string): Pick<ExtraFile, 'bucket' | 'filename' | 'origin' | 'type' | 'extra1'> {
return {
origin: 'unknown',
extra1: url,
type: 'file',
filename: '',
bucket: '',
};
}

View File

@ -1,7 +1,7 @@
// index.ts
OakPage({
path: 'address-list',
path: 'address:list',
entity: 'address',
projection: {
id: 1,

View File

@ -1,6 +1,6 @@
OakPage({
path: 'address-upsert',
path: 'address:upsert',
entity: 'address',
projection: {
id: 1,
@ -34,7 +34,7 @@ OakPage({
}, {
methods: {
afterUpsert() {
if (this.data.oakFrom === 'address-list') {
if (this.data.oakFrom === 'address:list') {
wx.navigateBack();
}
}

View File

@ -0,0 +1,5 @@
{
"navigationBarTitleText": "手机号",
"usingComponents": {
}
}

View File

@ -0,0 +1,34 @@
/** index.wxss **/
@import "../../../styles/base.less";
@import "../../../styles/cpn.less";
.page-body {
height: 100vh;
display: flex;
flex: 1;
flex-direction: column;
background-color: @background-color-base;
}
.userInfo {
padding: 20rpx;
display: flex;
flex-direction: column;
align-items: center;
background-color: @background-color-base;
}
.avatar {
font-size: 188rpx;
color: @text-color;
height: 200rpx;
width: 200rpx;
border-radius: 20rpx;
}
.nickname {
font-size: @size-font-large;
color: @text-color;
padding-top: @size-spacing-small;
padding-bottom: @size-spacing-base;
}

View File

@ -0,0 +1,32 @@
import { composeFileUrl } from '../../../../src/utils/extraFile';
OakPage({
path: 'mobile:me',
entity: 'mobile',
isList: true,
projection: {
id: 1,
mobile: 1,
userId: 1,
},
formData: async (mobiles) => ({
mobiles,
}),
}, {
methods: {
async onRefreshMobile(e: any) {
this.setData({
refreshing: true,
});
try {
console.log(e.detail.code);
}
catch (err) {
console.error(err);
}
this.setData({
refreshing: false,
});
}
}
});

View File

@ -0,0 +1,24 @@
<!-- index.wxml -->
<view class="page-body">
<block wx:if="{{mobiles && mobiles.length > 0}}">
<view class="g-form" style="flex: 1">
<view wx:for="{{mobiles}}" wx:key="index">
<view class="g-form-item">
<view class="g-form-item-label">
{{mobile.mobile}}
</view>
</view>
</view>
</view>
</block>
<block wx:else>
<view style="flex:1; display:flex; align-items:center;justify-content:center">
尚未授权手机号
</view>
</block>
<view class="btn-box">
<button open-type="getPhoneNumber" class="g-btn g-btn-primary g-btn-fullWidth" type="primary" bindgetphonenumber="onRefreshMobile">
授权手机号
</button>
</view>
</view>

View File

@ -1,6 +1,6 @@
OakPage({
path: 'area-picker',
path: 'area:picker',
entity: 'area',
projection: {
id: 1,

View File

@ -1,7 +1,7 @@
import { WechatMpEnv } from "oak-app-domain/Token/Schema";
OakPage({
path: 'token-login',
path: 'token:login',
entity: 'token',
projection: {
id: 1,
@ -28,7 +28,7 @@ OakPage({
async onLoginClicked(options: WechatMiniprogram.Touch) {
const { code } = await wx.login();
const env = await wx.getSystemInfo();
await this.features.token.loginWechatMp(code, Object.assign(env, { type: 'wechatMp' }) as WechatMpEnv);
await this.features.token.loginWechatMp('token:login');
},
onReturnClicked() {

View File

@ -1,6 +1,6 @@
<!-- index.wxml -->
<view class="page-body">
<block wx:if={{!loggedIn}}>
<block wx:if="{{!loggedIn}}">
<view class="g-cell">
<button class="g-btn g-btn-fullWidth" size="default" bindtap="onLoginClicked">登录</button>
</view>

View File

@ -0,0 +1,5 @@
{
"navigationBarTitleText": "个人信息",
"usingComponents": {
}
}

View File

@ -0,0 +1,34 @@
/** index.wxss **/
@import "../../../styles/base.less";
@import "../../../styles/cpn.less";
.page-body {
height: 100vh;
display: flex;
flex: 1;
flex-direction: column;
background-color: @background-color-base;
}
.userInfo {
padding: 20rpx;
display: flex;
flex-direction: column;
align-items: center;
background-color: @background-color-base;
}
.avatar {
font-size: 188rpx;
color: @text-color;
height: 200rpx;
width: 200rpx;
border-radius: 20rpx;
}
.nickname {
font-size: @size-font-large;
color: @text-color;
padding-top: @size-spacing-small;
padding-bottom: @size-spacing-base;
}

View File

@ -0,0 +1,117 @@
import { ROOT_ROLE_ID } from '../../../../src/constants';
import { composeFileUrl } from '../../../../src/utils/extraFile';
OakPage({
path: 'token:me',
entity: 'token',
isList: true,
projection: {
id: 1,
userId: 1,
playerId: 1,
user: {
id: 1,
nickname: 1,
name: 1,
extraFile$entity: {
$entity: 'extraFile',
data: {
id: 1,
tag1: 1,
origin: 1,
bucket: 1,
objectId: 1,
filename: 1,
extra1: 1,
},
filter: {
tag1: 'avatar',
},
indexFrom: 0,
count: 1,
},
mobile$user: {
$entity: 'mobile',
data: {
id: 1,
mobile: 1,
},
},
},
player: {
id: 1,
userRole$user: {
$entity: 'userRole',
data: {
id: 1,
userId: 1,
roleId: 1,
}
},
}
},
formData: async ([token]) => {
const user = token?.user;
const player = token?.player;
const avatarFile = user && user.extraFile$entity && user.extraFile$entity[0];
const avatar = avatarFile && composeFileUrl(avatarFile);
const nickname = user && user.nickname;
const mobileData = user && user.mobile$user && user.mobile$user[0];
const { mobile } = mobileData || {};
const mobileCount = user?.mobile$user?.length || 0;
const isLoggedIn = !!token;
const isPlayingAnother = token && token.userId !== token.playerId;
const isRoot = player?.userRole$user && player.userRole$user[0].roleId === ROOT_ROLE_ID;
return {
avatar,
nickname,
mobile,
mobileCount,
isLoggedIn,
isPlayingAnother,
isRoot,
};
},
}, {
methods: {
async onRefresh() {
this.setData({
refreshing: true,
});
try {
await this.features.token.syncUserInfoWechatMp('token:me');
}
catch (err) {
console.error(err);
}
this.setData({
refreshing: false,
});
},
async doLogin() {
this.setData({
refreshing: true,
});
try {
await this.features.token.loginWechatMp('token:me');
}
catch (err) {
console.error(err);
}
this.setData({
refreshing: false,
});
},
goMyMobile() {
this.navigateTo({
url: '../../mobile/me/index',
});
},
goUserManage() {
this.navigateTo({
url: '../../user/manage/index',
});
}
}
});

View File

@ -0,0 +1,43 @@
<!-- index.wxml -->
<view class="page-body">
<view class="userInfo">
<block wx:if="{{avatar}}">
<image class="avatar" src="{{avatar}}" mode="aspectFit" />
</block>
<block wx:else>
<span class="material-icons avatar">person_outline</span>
</block>
<span class="nickname">{{ nickname || '未设置'}}</span>
<block wx:if="{{isLoggedIn && !isPlayingAnother}}">
<button class="g-btn-info" style="font-size: 26rpx" size="mini" bind:tap="onRefresh" disabled="{{refreshing}}">
更新
</button>
</block>
<block wx:elif="{{!isLoggedIn}}">
<button class="g-btn-info" style="font-size: 26rpx" size="mini" bind:tap="doLogin" disabled="{{refreshing}}">
登录
</button>
</block>
</view>
<view class="g-form" style="flex: 1">
<view class="g-form-item" bind:tap="goMyMobile">
<view class="g-form-item-label">手机号</view>
<view class="g-form-item-content">
<view wx:if="{{mobileCount > 1}}">{{mobileCount}}条手机号</view>
<view wx:elif="{{mobileCount === 1}}">{{mobile}}</view>
<view wx:else>未设置</view>
<span class="material-icons" color="#495060">chevron_right</span>
</view>
</view>
<view class="g-form-item">
<view class="g-form-item-label">收货地址</view>
</view>
<view style="flex:1" />
<view wx:if="{{isRoot}}" class="g-form-item" bind:tap="goUserManage">
<view class="g-form-item-label">用户管理</view>
<view class="g-form-item-content">
<span class="material-icons" color="#495060">chevron_right</span>
</view>
</view>
</view>
</view>

View File

@ -0,0 +1,6 @@
{
"navigationBarTitleText": "用户管理",
"usingComponents": {
}
}

View File

@ -0,0 +1,36 @@
/** index.wxss **/
@import "../../../styles/base.less";
@import "../../../styles/cpn.less";
.page-body {
height: 100vh;
display: flex;
flex: 1;
flex-direction: column;
background-color: @background-color-base;
}
.avatar {
font-size: 128rpx;
color: @text-color;
height: 140rpx;
width: 140rpx;
border-radius: 14rpx;
}
.nickname {
font-size: @size-font-large;
font-weight: bold;
}
.name {
display: flex;
align-items: center;
font-size: @size-font-base;
}
.mobile {
display: flex;
align-items: center;
font-size: @size-font-small;
}

View File

@ -0,0 +1,60 @@
// index.ts
import { composeFileUrl } from "../../../../src/utils/extraFile";
OakPage({
path: 'user:manage',
entity: 'user',
projection: {
id: 1,
nickname: 1,
name: 1,
extraFile$entity: {
$entity: 'extraFile',
data: {
id: 1,
tag1: 1,
origin: 1,
bucket: 1,
objectId: 1,
filename: 1,
extra1: 1,
},
filter: {
tag1: 'avatar',
},
indexFrom: 0,
count: 1,
},
mobile$user: {
$entity: 'mobile',
data: {
id: 1,
mobile: 1,
},
},
},
isList: true,
formData: async (users) => {
const userData = users.map(
(user) => {
const { id, nickname, name, mobile$user, extraFile$entity } = user;
const mobile = mobile$user && mobile$user[0]?.mobile;
const avatar = extraFile$entity && extraFile$entity[0] && composeFileUrl(extraFile$entity[0]);
return {
id,
nickname,
name,
mobile,
avatar,
};
}
);
return {
userData,
};
},
}, {
methods: {
}
});

View File

@ -0,0 +1,32 @@
<!-- index.wxml -->
<view class="page-body">
<view class="g-form" style="flex: 1">
<view wx:for="{{userData}}" wx:key="index">
<view class="g-form-item">
<block wx:if="{{item.avatar}}">
<image class="avatar" src="{{item.avatar}}" mode="aspectFit" />
</block>
<block wx:else>
<span class="material-icons avatar">person_outline</span>
</block>
<view class="g-form-item-content" style="flex-direction:column; margin-left: 22rpx">
<view class="nickname">{{item.nickname || '未设置'}}</view>
<view class="name">
<span class="material-icons" style="font-size:28rpx;margin-right:8rpx" color="#495060">tag_faces</span>
{{item.name || '未设置'}}
</view>
<view class="mobile">
<span class="material-icons" style="font-size:28rpx;margin-right:8rpx" color="#495060">phone_android</span>
{{item.mobile || '未设置'}}
</view>
</view>
<span class="material-icons" color="#495060">chevron_right</span>
</view>
</view>
</view>
<view class="btn-box">
<button class="g-btn g-btn-primary g-btn-fullWidth" type="primary" bind:tap="goNewAddress" size="default">
新建
</button>
</view>
</view>

View File

@ -147,6 +147,7 @@
flex: 1;
overflow: hidden;
color: @text-color;
justify-content: flex-end;
}
}
}