增加了token/me等几个公共页面,context中增加了scene
This commit is contained in:
parent
48767766a7
commit
ff2a97960a
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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'>;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'));
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
|
@ -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>;
|
||||
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import addressCheckers from './address';
|
||||
import tokenCheckers from './token';
|
||||
|
||||
export default [...addressCheckers];
|
||||
export default [...addressCheckers, ...tokenCheckers];
|
||||
|
|
@ -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;
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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: '',
|
||||
};
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
// index.ts
|
||||
|
||||
OakPage({
|
||||
path: 'address-list',
|
||||
path: 'address:list',
|
||||
entity: 'address',
|
||||
projection: {
|
||||
id: 1,
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"navigationBarTitleText": "手机号",
|
||||
"usingComponents": {
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -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>
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
|
||||
OakPage({
|
||||
path: 'area-picker',
|
||||
path: 'area:picker',
|
||||
entity: 'area',
|
||||
projection: {
|
||||
id: 1,
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"navigationBarTitleText": "个人信息",
|
||||
"usingComponents": {
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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',
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"navigationBarTitleText": "用户管理",
|
||||
"usingComponents": {
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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: {
|
||||
}
|
||||
});
|
||||
|
|
@ -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>
|
||||
|
|
@ -147,6 +147,7 @@
|
|||
flex: 1;
|
||||
overflow: hidden;
|
||||
color: @text-color;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue