diff --git a/shared/config.ts b/shared/config.ts index fbbf9d2f..d5481e45 100644 --- a/shared/config.ts +++ b/shared/config.ts @@ -4,4 +4,22 @@ export const config = { serverUrl: 'http://127.0.0.1:11000', + color: [ + '#333333', + '#2c3e50', + '#8e44ad', + '#2980b9', + '#27ae60', + '#16a085', + '#f39c12', + '#d35400', + '#c0392b', + '#3498db', + '#9b59b6', + '#2ecc71', + '#1abc9c', + '#f1c40f', + '#e74c3c', + '#e67e22', + ], }; diff --git a/shared/index.tsx b/shared/index.tsx index 4a6bb5d4..c14fdcb6 100644 --- a/shared/index.tsx +++ b/shared/index.tsx @@ -31,3 +31,6 @@ export { loginWithEmail, registerWithEmail } from './model/user'; export { setupRedux } from './redux/setup'; export { createStore } from './redux/store'; export type { AppStore, AppDispatch } from './redux/store'; + +// utils +export { getTextColorHex } from './utils/string-helper'; diff --git a/shared/package.json b/shared/package.json index 6b18c6c7..41ec597e 100644 --- a/shared/package.json +++ b/shared/package.json @@ -13,6 +13,8 @@ "lodash": "^4.17.21", "react-native-storage": "npm:@trpgengine/react-native-storage@^1.0.1", "redux": "^4.1.0", + "str2int": "^1.0.0", + "url-regex": "^5.0.0", "yup": "^0.32.9" }, "devDependencies": { diff --git a/shared/utils/__tests__/string-helper.spec.ts b/shared/utils/__tests__/string-helper.spec.ts new file mode 100644 index 00000000..1beba681 --- /dev/null +++ b/shared/utils/__tests__/string-helper.spec.ts @@ -0,0 +1,31 @@ +import { isAvailableString, isUrl } from '../string-helper'; + +describe('string-helper', () => { + describe('isAvailableString', () => { + test.each<[any, boolean]>([ + ['any string', true], + ['', false], + [1, false], + [() => {}, false], + [{}, false], + [[], false], + [undefined, false], + [null, false], + ])('%p => %p', (url, res) => { + expect(isAvailableString(url)).toBe(res); + }); + }); + + describe('isUrl', () => { + test.each<[string, boolean]>([ + ['http://baidu.com', true], + ['https://baidu.com', true], + ['ws://baidu.com', true], + ['wss://baidu.com', true], + ['baidu.com', false], + ['baidu', false], + ])('%s => %p', (url, res) => { + expect(isUrl(url)).toBe(res); + }); + }); +}); diff --git a/shared/utils/string-helper.ts b/shared/utils/string-helper.ts new file mode 100644 index 00000000..bea3b19a --- /dev/null +++ b/shared/utils/string-helper.ts @@ -0,0 +1,57 @@ +import _isString from 'lodash/isString'; +import str2int from 'str2int'; +import urlRegex from 'url-regex'; +import { config } from '../config'; + +/** + * 判断一个字符串是否可用() + * @param str 要判断的字符串 + */ +export function isAvailableString(str: any): boolean { + return typeof str === 'string' && str.length > 0; +} + +/** + * 判断一个字符串是否是url + * @param str 要判断的字符串 + */ +export function isUrl(str: string) { + return urlRegex({ exact: true }).test(str); +} + +/** + * 判断字符串是否是一个blobUrl + * @param str url字符串 + */ +export const isBlobUrl = (str: string) => { + return _isString(str) && str.startsWith('blob:'); +}; + +/** + * 获取一段字符串中的所有url + * @param str 字符串 + */ +export const getUrls = (str: string): string[] => { + return str.match(urlRegex()) ?? []; +}; + +/** + * 用于判定环境变量的值 + */ +export function is(it: string) { + return !!it && it !== '0' && it !== 'false'; +} + +/** + * 根据文本内容返回一个内置色卡的颜色 + * @param text 文本 + */ +export function getTextColorHex(text: string): string { + if (!text || !_isString(text)) { + return '#ffffff'; // 如果获取不到文本,则返回白色 + } + + const color = config.color; + const id = str2int(text); + return color[id % color.length]; +} diff --git a/web/src/components/Avatar.tsx b/web/src/components/Avatar.tsx new file mode 100644 index 00000000..f8ba71f2 --- /dev/null +++ b/web/src/components/Avatar.tsx @@ -0,0 +1,52 @@ +import React, { useMemo } from 'react'; +import { Avatar as AntdAvatar } from 'antd'; +import _head from 'lodash/head'; +import _upperCase from 'lodash/upperCase'; +import _isNil from 'lodash/isNil'; +import _isEmpty from 'lodash/isEmpty'; +import _isNumber from 'lodash/isNumber'; +import type { AvatarProps as AntdAvatarProps } from 'antd/lib/avatar'; +import { getTextColorHex } from 'pawchat-shared'; + +interface AvatarProps extends AntdAvatarProps { + name?: string; +} +export const Avatar: React.FC = React.memo((props) => { + const src = typeof props.src !== 'string' ? props.src : undefined; + + const name = useMemo(() => _upperCase(_head(props.name)), [props.name]); + + const color = useMemo( + () => + // 如果src为空 且 icon为空 则给个固定颜色 + _isEmpty(src) && _isNil(props.icon) + ? getTextColorHex(props.name!) + : undefined, + [src, props.icon, props.name] + ); + + const style: React.CSSProperties = useMemo( + () => ({ + cursor: 'inherit', + userSelect: 'none', + ...props.style, + backgroundColor: color, + }), + [props.style, color] + ); + + if (_isNumber(props.size) && typeof style.fontSize === 'undefined') { + // 如果props.size是数字且没有指定文字大小 + // 则自动增加fontSize大小 + style.fontSize = props.size * 0.4; + } + + return ( + + {name} + + ); +}); +Avatar.displayName = 'Avatar'; + +export default Avatar; diff --git a/web/src/routes/Main.tsx b/web/src/routes/Main/index.tsx similarity index 100% rename from web/src/routes/Main.tsx rename to web/src/routes/Main/index.tsx diff --git a/yarn.lock b/yarn.lock index 9326327c..341efe1f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3679,6 +3679,11 @@ ip-regex@^2.1.0: resolved "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9" integrity sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk= +ip-regex@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-4.3.0.tgz#687275ab0f57fa76978ff8f4dddc8a23d5990db5" + integrity sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q== + ip@^1.1.0, ip@^1.1.5: version "1.1.5" resolved "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" @@ -6825,6 +6830,11 @@ static-extend@^0.1.1: resolved "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= +str2int@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/str2int/-/str2int-1.0.0.tgz#2bff996bdd7c8aa16891b5677b865608a0330dff" + integrity sha512-rhRV56wlT7fE+zUtVUAgnr3LCDLfkmmeoVCtggjHezi0JbXmtdcbAXp6KV3LegP5nFXSqBaGy9VDvc3vvHb/Sg== + string-convert@^0.2.0: version "0.2.1" resolved "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz#6982cc3049fbb4cd85f8b24568b9d9bf39eeff97" @@ -7098,6 +7108,11 @@ tiny-warning@^1.0.0, tiny-warning@^1.0.2, tiny-warning@^1.0.3: resolved "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== +tlds@^1.203.0: + version "1.221.1" + resolved "https://registry.yarnpkg.com/tlds/-/tlds-1.221.1.tgz#6cf6bff5eaf30c5618c5801c3f425a6dc61ca0ad" + integrity sha512-N1Afn/SLeOQRpxMwHBuNFJ3GvGrdtY4XPXKPFcx8he0U9Jg9ZkvTKE1k3jQDtCmlFn44UxjVtouF6PT4rEGd3Q== + tmp@^0.0.33: version "0.0.33" resolved "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" @@ -7346,6 +7361,14 @@ url-parse@^1.4.3, url-parse@^1.5.1: querystringify "^2.1.1" requires-port "^1.0.0" +url-regex@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/url-regex/-/url-regex-5.0.0.tgz#8f5456ab83d898d18b2f91753a702649b873273a" + integrity sha512-O08GjTiAFNsSlrUWfqF1jH0H1W3m35ZyadHrGv5krdnmPPoxP27oDTqux/579PtaroiSGm5yma6KT1mHFH6Y/g== + dependencies: + ip-regex "^4.1.0" + tlds "^1.203.0" + url@^0.11.0: version "0.11.0" resolved "https://registry.npmjs.org/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1"