feat: 通知与个人页面改版(WIP) (#1455)

Co-authored-by: sealday <sealday@gmail.com>
Co-authored-by: wangjiahui <wwwjh0710@163.com>
Reviewed-on: daoyoucloud/tachybase#1455
This commit is contained in:
sealday 2024-09-14 18:52:36 +08:00
parent d78017ac40
commit bf2cae92b0
56 changed files with 2548 additions and 68 deletions

View File

@ -8,7 +8,7 @@ VERDACCIO_PORT=10104
################# TACHYBASE APPLICATION #################
# !!! When `APP_ENV=production`, opening http://localhost:13000/ will show "Not Found".
# !!! When `APP_ENV=production`, opening http://localhost:3000/ will show "Not Found".
# !!! It is recommended to use nginx to proxy static files. For example https://github.com/tachybase/tachybase/blob/main/docker/tachybase/tachybase.conf
APP_ENV=development
APP_PORT=20000

View File

@ -1,7 +1,7 @@
################# TACHYBASE APPLICATION #################
APP_ENV=development
APP_PORT=13000
APP_PORT=3000
APP_KEY=test-key
API_BASE_PATH=/api/

View File

@ -54,7 +54,7 @@ server {
}
location ^~ /api/ {
proxy_pass http://127.0.0.1:13000/api/;
proxy_pass http://127.0.0.1:3000/api/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
@ -67,7 +67,7 @@ server {
}
location ^~ /adapters/ {
proxy_pass http://127.0.0.1:13000/adapters/;
proxy_pass http://127.0.0.1:3000/adapters/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
@ -80,7 +80,7 @@ server {
}
location ^~ /static/plugins/ {
proxy_pass http://127.0.0.1:13000/static/plugins/;
proxy_pass http://127.0.0.1:3000/static/plugins/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
@ -93,7 +93,7 @@ server {
}
location /ws {
proxy_pass http://127.0.0.1:13000/ws;
proxy_pass http://127.0.0.1:3000/ws;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";

View File

@ -67,7 +67,7 @@ describe.skip('cli', () => {
test('install', async () => {
const database = await createDatabase();
const port = await getPortPromise({
port: 13000,
port: 3000,
});
console.log(process.env.DB_DIALECT, port);
const dbFile = `storage/tests/db/tachybase-${uid()}.sqlite`;
@ -97,7 +97,7 @@ describe.skip('cli', () => {
console.log(process.env.DB_DIALECT);
const database = await createDatabase();
const port = await getPortPromise({
port: 13000,
port: 3000,
});
const dbFile = `storage/tests/db/tachybase-${uid()}.sqlite`;
const env = {
@ -127,7 +127,7 @@ describe.skip('cli', () => {
console.log(process.env.DB_DIALECT);
const database = await createDatabase();
const port = await getPortPromise({
port: 13000,
port: 3000,
});
const dbFile = `storage/tests/db/tachybase-${uid()}.sqlite`;
const env = {
@ -153,7 +153,7 @@ describe.skip('cli', () => {
console.log(process.env.DB_DIALECT);
const database = await createDatabase();
const port = await getPortPromise({
port: 13000,
port: 3000,
});
const dbFile = `storage/tests/db/tachybase-${uid()}.sqlite`;
const env = {
@ -183,7 +183,7 @@ describe.skip('cli', () => {
console.log(process.env.DB_DIALECT);
const database = await createDatabase();
const port = await getPortPromise({
port: 13000,
port: 3000,
});
const dbFile = `storage/tests/db/tachybase-${uid()}.sqlite`;
const env = {
@ -208,7 +208,7 @@ describe.skip('cli', () => {
console.log(process.env.DB_DIALECT);
const database = await createDatabase();
const port = await getPortPromise({
port: 13000,
port: 3000,
});
const dbFile = `storage/tests/db/tachybase-${uid()}.sqlite`;
const env = {

View File

@ -320,7 +320,7 @@ export function initEnv() {
const env = {
APP_ENV: 'development',
APP_KEY: 'test-jwt-secret',
APP_PORT: 13000,
APP_PORT: 3000,
API_BASE_PATH: '/api/',
DB_DIALECT: 'sqlite',
DB_STORAGE: 'storage/db/tachybase.sqlite',

View File

@ -54,7 +54,6 @@
"react-hotkeys-hook": "^3.4.7",
"react-i18next": "^14.1.2",
"react-iframe": "~1.8.5",
"react-image-lightbox": "^5.1.4",
"react-intersection-observer": "^9.10.3",
"react-js-cron": "^3.1.0",
"react-quill": "^2.0.0",

View File

@ -28,6 +28,7 @@ import { ComponentTypeAndString, RouterManager, RouterOptions } from './RouterMa
import { SchemaInitializer, SchemaInitializerManager } from './schema-initializer';
import * as schemaInitializerComponents from './schema-initializer/components';
import { SchemaSettings, SchemaSettingsManager } from './schema-settings';
import { UserSettingOptions, UserSettingsManager } from './UserSettingsManager';
import { compose, normalizeContainer } from './utils';
import { defineGlobalDeps } from './utils/globalDeps';
import { getRequireJs, type RequireJS } from './utils/requirejs';
@ -36,6 +37,7 @@ import { WebSocketClient, WebSocketClientOptions } from './WebSocketClient';
declare global {
interface Window {
define: RequireJS['define'];
require: RequireJS['require'];
}
}
@ -53,6 +55,7 @@ export interface ApplicationOptions {
scopes?: Record<string, any>;
router?: RouterOptions;
pluginSettings?: Record<string, PluginSettingOptions>;
userSettings?: Record<string, UserSettingOptions>;
schemaSettings?: SchemaSettings[];
schemaInitializers?: SchemaInitializer[];
designable?: boolean;
@ -77,6 +80,7 @@ export class Application {
};
public pluginManager: PluginManager;
public pluginSettingsManager: PluginSettingsManager;
public userSettingsManager: UserSettingsManager;
public devDynamicImport: DevDynamicImport;
public requirejs: RequireJS;
public notification;
@ -129,6 +133,7 @@ export class Application {
this.addProviders(options.providers || []);
this.ws = new WebSocketClient(options.ws, this);
this.pluginSettingsManager = new PluginSettingsManager(options.pluginSettings, this);
this.userSettingsManager = new UserSettingsManager(options.userSettings, this);
this.addRoutes();
this.name = this.options.name || getSubAppName(options.publicPath) || 'main';
this.pluginContextMenu = new PluginContextMenu(options.pluginMenuItems, this);
@ -138,7 +143,7 @@ export class Application {
this.requirejs = getRequireJs();
defineGlobalDeps(this.requirejs);
window.define = this.requirejs.define;
window.require = this.requirejs.require;
window.require = this.requirejs.require as RequireJS['require'] & NodeRequire;
}
private addDefaultProviders() {

View File

@ -27,6 +27,10 @@ export class Plugin<T = any> {
return this.app.pluginSettingsManager;
}
get userSettingsManager() {
return this.app.userSettingsManager;
}
get schemaInitializerManager() {
return this.app.schemaInitializerManager;
}

View File

@ -0,0 +1,161 @@
import React, { createElement } from 'react';
import { set } from 'lodash';
import { Outlet } from 'react-router-dom';
import { Icon } from '../icon';
import type { Application } from './Application';
import type { RouteType } from './RouterManager';
export const USER_SETTINGS_KEY = 'admin.profilers.';
export const USER_SETTINGS_PATH = '/admin/profilers/';
export const SNIPPET_PREFIX = 'pm.';
export interface UserSettingOptions {
title: any;
/**
* @default Outlet
*/
Component?: RouteType['Component'];
icon?: string;
/**
* sort, the smaller the number, the higher the priority
* @default 0
*/
sort?: number;
aclSnippet?: string;
[index: string]: any;
}
export interface UserSettingsPageType {
label?: string | React.ReactElement;
title: string | React.ReactElement;
key: string;
icon: any;
path: string;
sort?: number;
name?: string;
isAllow?: boolean;
topLevelName?: string;
aclSnippet: string;
children?: UserSettingsPageType[];
[index: string]: any;
}
export class UserSettingsManager {
protected settings: Record<string, UserSettingOptions> = {};
protected aclSnippets: string[] = [];
public app: Application;
private cachedList = {};
constructor(_userSettings: Record<string, UserSettingOptions>, app: Application) {
this.app = app;
Object.entries(_userSettings || {}).forEach(([name, userSettingOptions]) => {
this.add(name, userSettingOptions);
});
}
setAclSnippets(aclSnippets: string[]) {
this.aclSnippets = aclSnippets;
}
getAclSnippet(name: string) {
const setting = this.settings[name];
return setting?.aclSnippet ? setting.aclSnippet : `${SNIPPET_PREFIX}${name}`;
}
getRouteName(name: string) {
return `${USER_SETTINGS_KEY}${name}`;
}
getRoutePath(name: string) {
return `${USER_SETTINGS_PATH}${name.replaceAll('.', '/')}`;
}
add(name: string, options: UserSettingOptions) {
const nameArr = name.split('.');
const topLevelName = nameArr[0];
this.settings[name] = {
...this.settings[name],
Component: Outlet,
...options,
name,
topLevelName: options.topLevelName || topLevelName,
};
// add children
if (nameArr.length > 1) {
set(this.settings, nameArr.join('.children.'), this.settings[name]);
}
// add route
this.app.router.add(this.getRouteName(name), {
path: this.getRoutePath(name),
Component: this.settings[name].Component,
});
}
remove(name: string) {
// delete self and children
Object.keys(this.settings).forEach((key) => {
if (key.startsWith(name)) {
delete this.settings[key];
this.app.router.remove(`${ADMIN_SETTINGS_KEY}${key}`);
}
});
}
hasAuth(name: string) {
if (this.aclSnippets.includes(`!${this.getAclSnippet('*')}`)) return false;
return this.aclSnippets.includes(`!${this.getAclSnippet(name)}`) === false;
}
getSetting(name: string) {
return this.settings[name];
}
has(name: string) {
const hasAuth = this.hasAuth(name);
if (!hasAuth) return false;
return !!this.getSetting(name);
}
get(name: string, filterAuth = true): UserSettingsPageType {
const isAllow = this.hasAuth(name);
const userSetting = this.getSetting(name);
if ((filterAuth && !isAllow) || !userSetting) return null;
const children = Object.keys(userSetting.children || {})
.sort((a, b) => a.localeCompare(b)) // sort by name
.map((key) => this.get(userSetting.children[key].name, filterAuth))
.filter(Boolean)
.sort((a, b) => (a.sort || 0) - (b.sort || 0));
const { title, icon, aclSnippet, ...others } = userSetting;
return {
...others,
aclSnippet: this.getAclSnippet(name),
title,
isAllow,
label: title,
icon: typeof icon === 'string' ? createElement(Icon, { type: icon }) : icon,
path: this.getRoutePath(name),
key: name,
children: children.length ? children : undefined,
};
}
getList(filterAuth = true): UserSettingsPageType[] {
const cacheKey = JSON.stringify(filterAuth);
if (this.cachedList[cacheKey]) return this.cachedList[cacheKey];
return (this.cachedList[cacheKey] = Array.from(
new Set(Object.values(this.settings).map((item) => item.topLevelName)),
)
.sort((a, b) => a.localeCompare(b)) // sort by name
.map((name) => this.get(name, filterAuth))
.filter(Boolean)
.sort((a, b) => (a.sort || 0) - (b.sort || 0)));
}
getAclSnippets() {
return Object.keys(this.settings).map((name) => this.getAclSnippet(name));
}
}

View File

@ -30,6 +30,8 @@ import { LocalePlugin } from './locale/LocalePlugin';
import { PinnedListPlugin } from './pinned-list';
import { PMPlugin } from './pm';
import { SystemSettingsPlugin } from './system-settings';
import { UserSettingsPlugin } from './user-settings';
import { PluginWebNotification } from './web-notification/PluginWebNotification';
export { AdminProvider, NoticeArea } from './admin-layout';
export * from './context-menu';
@ -352,7 +354,8 @@ export class BuiltInPlugin extends Plugin {
await this.app.pm.add(RemoteDocumentTitlePlugin, { name: 'remote-document-title' });
await this.app.pm.add(PMPlugin, { name: 'builtin-pm' });
await this.app.pm.add(CollectionPlugin, { name: 'builtin-collection' });
await this.app.pm.add(PluginContextMenu);
await this.app.pm.add(PluginAssistant);
await this.app.pm.add(PluginContextMenu, { name: 'context-menu' });
await this.app.pm.add(PluginAssistant, { name: 'drag-assistant' });
await this.app.pm.add(UserSettingsPlugin, { name: 'user-settings' });
}
}

View File

@ -2,6 +2,7 @@ import React from 'react';
import { ADMIN_SETTINGS_PATH } from '../../application';
import { Plugin } from '../../application/Plugin';
import { USER_SETTINGS_PATH } from '../../application/UserSettingsManager';
import { BlockTemplatesPane } from '../../schema-templates';
import { SystemSettingsPane } from '../system-settings';
import { PluginManager } from './PluginManager';

View File

@ -0,0 +1,7 @@
import React from 'react';
import { Outlet } from 'react-router';
export const UserSettingsLayout = () => {
return <Outlet />;
};

View File

@ -0,0 +1,14 @@
import React from 'react';
import { Plugin } from '../../application/Plugin';
import { USER_SETTINGS_PATH } from '../../application/UserSettingsManager';
import { UserSettingsLayout } from './UserSettingsLayout';
export class UserSettingsPlugin extends Plugin {
async load() {
this.app.router.add('admin.profilers', {
path: USER_SETTINGS_PATH,
element: <UserSettingsLayout />,
});
}
}

View File

@ -0,0 +1,17 @@
import { Plugin } from '../../application/Plugin';
export class PluginWebNotification extends Plugin {
async afterLoad() {
// ‌请求用户授予权限
if (Notification.permission !== 'denied') {
await Notification.requestPermission();
}
setTimeout(() => {
const notification = new Notification('通知标题:', {
body: '通知内容',
icon: 'https://www.example.com/icon.png', // 通知的图标 URL
});
}, 2000);
}
}

View File

@ -1,4 +1,5 @@
import React, { useEffect, useRef, useState } from 'react';
import { Lightbox } from '@tachybase/components';
import { connect, mapReadPretty } from '@tachybase/schema';
import { DeleteOutlined, DownloadOutlined, PlusOutlined } from '@ant-design/icons';
@ -6,9 +7,6 @@ import { Upload as AntdUpload, Button, Progress, Space, UploadFile } from 'antd'
import cls from 'classnames';
import { saveAs } from 'file-saver';
import { useTranslation } from 'react-i18next';
import Lightbox from 'react-image-lightbox';
import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app
import { ReadPretty } from '../upload/ReadPretty';
import { isImage, toFileList, useUploadProps } from '../upload/shared';

View File

@ -1,4 +1,5 @@
import React, { useState } from 'react';
import { Lightbox } from '@tachybase/components';
import { Field, useField } from '@tachybase/schema';
import { isString } from '@tachybase/utils/client';
@ -8,7 +9,6 @@ import useUploadStyle from 'antd/es/upload/style';
import cls from 'classnames';
import { saveAs } from 'file-saver';
import { useTranslation } from 'react-i18next';
import Lightbox from 'react-image-lightbox';
import { useRecord } from '../../../record-provider';
import { isImage, isPdf, toArr, toImages } from './shared';

View File

@ -1,4 +1,5 @@
import React, { useEffect, useRef, useState } from 'react';
import { Lightbox } from '@tachybase/components';
import { connect, mapProps, mapReadPretty } from '@tachybase/schema';
import { DeleteOutlined, DownloadOutlined, InboxOutlined, LoadingOutlined, PlusOutlined } from '@ant-design/icons';
@ -6,9 +7,6 @@ import { Upload as AntdUpload, Button, Modal, Progress, Space, UploadFile } from
import cls from 'classnames';
import { saveAs } from 'file-saver';
import { useTranslation } from 'react-i18next';
import LightBox from 'react-image-lightbox';
import 'react-image-lightbox/style.css'; // This only needs to be imported once in your app
import { withDynamicSchemaProps } from '../../../application/hoc/withDynamicSchemaProps';
import { useProps } from '../../hooks/useProps';
@ -201,7 +199,7 @@ Upload.Attachment = connect((props: UploadProps) => {
</div>
{/* 预览图片的弹框 */}
{visible && fileType === 'image' && (
<LightBox
<Lightbox
// discourageDownloads={true}
mainSrc={images[fileIndex]?.imageUrl}
nextSrc={images[(fileIndex + 1) % images.length]?.imageUrl}

View File

@ -23,6 +23,8 @@
"antd": "5.19.4",
"classnames": "^2.5.1",
"dayjs": "^1.11.11",
"prop-types": "^15.8.1",
"react-modal": "^3.16.1",
"react-sticky-box": "^1.0.2"
},
"devDependencies": {

View File

@ -35,3 +35,4 @@ export * from './tree-select';
export * from './upload';
export * from './__builtins__';
export * from './code-mirror';
export * from './lightbox';

View File

@ -0,0 +1,39 @@
// Min image zoom level
export const MIN_ZOOM_LEVEL = 0;
// Max image zoom level
export const MAX_ZOOM_LEVEL = 300;
// Size ratio between previous and next zoom levels
export const ZOOM_RATIO = 1.007;
// How much to increase/decrease the zoom level when the zoom buttons are clicked
export const ZOOM_BUTTON_INCREMENT_SIZE = 100;
// Used to judge the amount of horizontal scroll needed to initiate a image move
export const WHEEL_MOVE_X_THRESHOLD = 200;
// Used to judge the amount of vertical scroll needed to initiate a zoom action
export const WHEEL_MOVE_Y_THRESHOLD = 1;
export const KEYS = {
ESC: 27,
LEFT_ARROW: 37,
RIGHT_ARROW: 39,
};
// Actions
export const ACTION_NONE = 0;
export const ACTION_MOVE = 1;
export const ACTION_SWIPE = 2;
export const ACTION_PINCH = 3;
export const ACTION_ROTATE = 4;
// Events source
export const SOURCE_ANY = 0;
export const SOURCE_MOUSE = 1;
export const SOURCE_TOUCH = 2;
export const SOURCE_POINTER = 3;
// Minimal swipe distance
export const MIN_SWIPE_DISTANCE = 200;

View File

@ -0,0 +1,3 @@
import Lightbox from './react-image-lightbox';
export { Lightbox };

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,344 @@
@keyframes closeWindow {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
.ril__outer {
background-color: rgba(0, 0, 0, 0.85);
outline: none;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1000;
width: 100%;
height: 100%;
-ms-content-zooming: none;
-ms-user-select: none;
-ms-touch-select: none;
touch-action: none;
}
.ril__outerClosing {
opacity: 0;
}
.ril__inner {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.ril__image,
.ril__imagePrev,
.ril__imageNext {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
margin: auto;
max-width: none;
-ms-content-zooming: none;
-ms-user-select: none;
-ms-touch-select: none;
touch-action: none;
}
.ril__imageDiscourager {
background-repeat: no-repeat;
background-position: center;
background-size: contain;
}
.ril__navButtons {
border: none;
position: absolute;
top: 0;
bottom: 0;
width: 20px;
height: 34px;
padding: 40px 30px;
margin: auto;
cursor: pointer;
opacity: 0.7;
}
.ril__navButtons:hover {
opacity: 1;
}
.ril__navButtons:active {
opacity: 0.7;
}
.ril__navButtonPrev {
left: 0;
background: rgba(0, 0, 0, 0.2)
url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSIgd2lkdGg9IjIwIiBoZWlnaHQ9IjM0Ij48cGF0aCBkPSJtIDE5LDMgLTIsLTIgLTE2LDE2IDE2LDE2IDEsLTEgLTE1LC0xNSAxNSwtMTUgeiIgZmlsbD0iI0ZGRiIvPjwvc3ZnPg==')
no-repeat center;
}
.ril__navButtonNext {
right: 0;
background: rgba(0, 0, 0, 0.2)
url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSIgd2lkdGg9IjIwIiBoZWlnaHQ9IjM0Ij48cGF0aCBkPSJtIDEsMyAyLC0yIDE2LDE2IC0xNiwxNiAtMSwtMSAxNSwtMTUgLTE1LC0xNSB6IiBmaWxsPSIjRkZGIi8+PC9zdmc+')
no-repeat center;
}
.ril__downloadBlocker {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-image: url('data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7');
background-size: cover;
}
.ril__caption,
.ril__toolbar {
background-color: rgba(0, 0, 0, 0.5);
position: absolute;
left: 0;
right: 0;
display: flex;
justify-content: space-between;
}
.ril__caption {
bottom: 0;
max-height: 150px;
overflow: auto;
}
.ril__captionContent {
padding: 10px 20px;
color: #fff;
}
.ril__toolbar {
top: 0;
height: 50px;
}
.ril__toolbarSide {
height: 50px;
margin: 0;
}
.ril__toolbarLeftSide {
padding-left: 20px;
padding-right: 0;
flex: 0 1 auto;
overflow: hidden;
text-overflow: ellipsis;
}
.ril__toolbarRightSide {
padding-left: 0;
padding-right: 20px;
flex: 0 0 auto;
}
.ril__toolbarItem {
display: inline-block;
line-height: 50px;
padding: 0;
color: #fff;
font-size: 120%;
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.ril__toolbarItemChild {
vertical-align: middle;
}
.ril__builtinButton {
width: 40px;
height: 35px;
cursor: pointer;
border: none;
opacity: 0.7;
}
.ril__builtinButton:hover {
opacity: 1;
}
.ril__builtinButton:active {
outline: none;
}
.ril__builtinButtonDisabled {
cursor: default;
opacity: 0.5;
}
.ril__builtinButtonDisabled:hover {
opacity: 0.5;
}
.ril__closeButton {
background: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZlcnNpb249IjEuMSIgd2lkdGg9IjIwIiBoZWlnaHQ9IjIwIj48cGF0aCBkPSJtIDEsMyAxLjI1LC0xLjI1IDcuNSw3LjUgNy41LC03LjUgMS4yNSwxLjI1IC03LjUsNy41IDcuNSw3LjUgLTEuMjUsMS4yNSAtNy41LC03LjUgLTcuNSw3LjUgLTEuMjUsLTEuMjUgNy41LC03LjUgLTcuNSwtNy41IHoiIGZpbGw9IiNGRkYiLz48L3N2Zz4=')
no-repeat center;
}
.ril__zoomInButton {
background: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMCIgaGVpZ2h0PSIyMCI+PGcgc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCI+PHBhdGggZD0iTTEgMTlsNi02Ii8+PHBhdGggZD0iTTkgOGg2Ii8+PHBhdGggZD0iTTEyIDV2NiIvPjwvZz48Y2lyY2xlIGN4PSIxMiIgY3k9IjgiIHI9IjciIGZpbGw9Im5vbmUiIHN0cm9rZT0iI2ZmZiIgc3Ryb2tlLXdpZHRoPSIyIi8+PC9zdmc+')
no-repeat center;
}
.ril__zoomOutButton {
background: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyMCIgaGVpZ2h0PSIyMCI+PGcgc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCI+PHBhdGggZD0iTTEgMTlsNi02Ii8+PHBhdGggZD0iTTkgOGg2Ii8+PC9nPjxjaXJjbGUgY3g9IjEyIiBjeT0iOCIgcj0iNyIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjZmZmIiBzdHJva2Utd2lkdGg9IjIiLz48L3N2Zz4=')
no-repeat center;
}
.ril__outerAnimating {
animation-name: closeWindow;
}
@keyframes pointFade {
0%,
19.999%,
100% {
opacity: 0;
}
20% {
opacity: 1;
}
}
.ril__loadingCircle {
width: 60px;
height: 60px;
position: relative;
}
.ril__loadingCirclePoint {
width: 100%;
height: 100%;
position: absolute;
left: 0;
top: 0;
}
.ril__loadingCirclePoint::before {
content: '';
display: block;
margin: 0 auto;
width: 11%;
height: 30%;
background-color: #fff;
border-radius: 30%;
animation: pointFade 800ms infinite ease-in-out both;
}
.ril__loadingCirclePoint:nth-of-type(1) {
transform: rotate(0deg);
}
.ril__loadingCirclePoint:nth-of-type(7) {
transform: rotate(180deg);
}
.ril__loadingCirclePoint:nth-of-type(1)::before,
.ril__loadingCirclePoint:nth-of-type(7)::before {
animation-delay: -800ms;
}
.ril__loadingCirclePoint:nth-of-type(2) {
transform: rotate(30deg);
}
.ril__loadingCirclePoint:nth-of-type(8) {
transform: rotate(210deg);
}
.ril__loadingCirclePoint:nth-of-type(2)::before,
.ril__loadingCirclePoint:nth-of-type(8)::before {
animation-delay: -666ms;
}
.ril__loadingCirclePoint:nth-of-type(3) {
transform: rotate(60deg);
}
.ril__loadingCirclePoint:nth-of-type(9) {
transform: rotate(240deg);
}
.ril__loadingCirclePoint:nth-of-type(3)::before,
.ril__loadingCirclePoint:nth-of-type(9)::before {
animation-delay: -533ms;
}
.ril__loadingCirclePoint:nth-of-type(4) {
transform: rotate(90deg);
}
.ril__loadingCirclePoint:nth-of-type(10) {
transform: rotate(270deg);
}
.ril__loadingCirclePoint:nth-of-type(4)::before,
.ril__loadingCirclePoint:nth-of-type(10)::before {
animation-delay: -400ms;
}
.ril__loadingCirclePoint:nth-of-type(5) {
transform: rotate(120deg);
}
.ril__loadingCirclePoint:nth-of-type(11) {
transform: rotate(300deg);
}
.ril__loadingCirclePoint:nth-of-type(5)::before,
.ril__loadingCirclePoint:nth-of-type(11)::before {
animation-delay: -266ms;
}
.ril__loadingCirclePoint:nth-of-type(6) {
transform: rotate(150deg);
}
.ril__loadingCirclePoint:nth-of-type(12) {
transform: rotate(330deg);
}
.ril__loadingCirclePoint:nth-of-type(6)::before,
.ril__loadingCirclePoint:nth-of-type(12)::before {
animation-delay: -133ms;
}
.ril__loadingCirclePoint:nth-of-type(7) {
transform: rotate(180deg);
}
.ril__loadingCirclePoint:nth-of-type(13) {
transform: rotate(360deg);
}
.ril__loadingCirclePoint:nth-of-type(7)::before,
.ril__loadingCirclePoint:nth-of-type(13)::before {
animation-delay: 0ms;
}
.ril__loadingContainer {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
.ril__imagePrev .ril__loadingContainer,
.ril__imageNext .ril__loadingContainer {
display: none;
}
.ril__errorContainer {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
}
.ril__imagePrev .ril__errorContainer,
.ril__imageNext .ril__errorContainer {
display: none;
}
.ril__loadingContainer__icon {
color: #fff;
position: absolute;
top: 50%;
left: 50%;
transform: translateX(-50%) translateY(-50%);
}

View File

@ -0,0 +1,52 @@
/**
* Placeholder for future translate functionality
*/
export function translate(str, replaceStrings = null) {
if (!str) {
return '';
}
let translated = str;
if (replaceStrings) {
Object.keys(replaceStrings).forEach((placeholder) => {
translated = translated.replace(placeholder, replaceStrings[placeholder]);
});
}
return translated;
}
export function getWindowWidth() {
return typeof global.window !== 'undefined' ? global.window.innerWidth : 0;
}
export function getWindowHeight() {
return typeof global.window !== 'undefined' ? global.window.innerHeight : 0;
}
const isCrossOriginFrame = () => {
try {
return global.window.location.hostname !== global.window.parent.location.hostname;
} catch (e) {
return true;
}
};
// Get the highest window context that isn't cross-origin
// (When in an iframe)
export function getHighestSafeWindowContext(self = global.window.self) {
// If we reached the top level, return self
if (self === global.window.top) {
return self;
}
// If parent is the same origin, we can move up one context
// Reference: https://stackoverflow.com/a/21965342/1601953
if (!isCrossOriginFrame()) {
return getHighestSafeWindowContext(self.parent);
}
// If a different origin, we consider the current level
// as the top reachable one
return self;
}

View File

@ -130,7 +130,7 @@ class AppGenerator extends Generator {
dependencies: dependencies.join(`,\n `),
envs: envs.join(`\n`),
env: {
APP_PORT: 13000,
APP_PORT: 3000,
APP_ENV: 'development',
DB_DIALECT: dbDialect,
APP_KEY: crypto.randomBytes(256).toString('base64'),

View File

@ -1,6 +1,6 @@
################# TACHYBASE APPLICATION #################
# !!! When `APP_ENV=production`, opening http://localhost:13000/ will show "Not Found".
# !!! When `APP_ENV=production`, opening http://localhost:3000/ will show "Not Found".
# !!! It is recommended to use nginx to proxy static files. For example https://github.com/tachybase/tachybase/blob/main/docker/tachybase/tachybase.conf
APP_ENV=development
APP_PORT=20000

View File

@ -1,5 +1,5 @@
APP_ENV=development
APP_PORT=13000
APP_PORT=3000
APP_KEY=test-key
API_BASE_PATH=/api/

View File

@ -304,7 +304,7 @@ export class Gateway extends EventEmitter {
await this.watch();
const startOptions = this.getStartOptions();
const port = startOptions.port || process.env.APP_PORT || 13000;
const port = startOptions.port || process.env.APP_PORT || 3000;
const host = startOptions.host || process.env.APP_HOST || '0.0.0.0';
this.start({

View File

@ -5,7 +5,7 @@ import { Configuration } from './Configuration';
export class PluginAPIKeysClient extends Plugin {
async load() {
this.pluginSettingsManager.add(NAMESPACE, {
this.userSettingsManager.add(NAMESPACE, {
icon: 'KeyOutlined',
title: this.t('API keys'),
Component: Configuration,

View File

@ -11,7 +11,6 @@
"devDependencies": {
"@ant-design/icons": "^5.3.7",
"@hapi/topo": "^6.0.0",
"@koa/multer": "^3.0.2",
"@tachybase/components": "workspace:*",
"@types/archiver": "^5.3.1",
"@types/file-saver": "^2.0.7",

View File

@ -32,7 +32,7 @@ export const Options = () => {
'x-decorator': 'FormItem',
'x-decorator-props': {
tooltip: generateNTemplate(
'The domain is usually the address of your server, in local development, you can use the address of your local machine, such as: http://localhost:13000',
'The domain is usually the address of your server, in local development, you can use the address of your local machine, such as: http://localhost:3000',
),
},
required: true,

View File

@ -3,5 +3,5 @@
"User will be registered automatically if not exists.": "User will be registered automatically if not exists.",
"Sign up automatically when the user does not exist": "Sign up automatically when the user does not exist",
"Service domain": "Service domain",
"The domain is usually the address of your server, in local development, you can use the address of your local machine, such as: http://localhost:13000": "The domain is usually the address of your server, in local development, you can use the address of your local machine, such as: http://localhost:13000"
"The domain is usually the address of your server, in local development, you can use the address of your local machine, such as: http://localhost:3000": "The domain is usually the address of your server, in local development, you can use the address of your local machine, such as: http://localhost:3000"
}

View File

@ -3,6 +3,6 @@
"User will be registered automatically if not exists.": "사용자가 존재하지 않으면 자동으로 등록됩니다.",
"Sign up automatically when the user does not exist": "사용자가 존재하지 않을 경우 자동으로 가입",
"Service domain": "서비스 도메인",
"The domain is usually the address of your server, in local development, you can use the address of your local machine, such as: http://localhost:13000":
"도메인은 일반적으로 서버의 주소입니다. 로컬 개발에서는 로컬 머신의 주소를 사용할 수 있습니다. 예: http://localhost:13000"
"The domain is usually the address of your server, in local development, you can use the address of your local machine, such as: http://localhost:3000":
"도메인은 일반적으로 서버의 주소입니다. 로컬 개발에서는 로컬 머신의 주소를 사용할 수 있습니다. 예: http://localhost:3000"
}

View File

@ -3,5 +3,5 @@
"User will be registered automatically if not exists.": "用户不存在时将自动注册。",
"Sign up automatically when the user does not exist": "用户不存在时自动注册",
"Service domain": "服务域名",
"The domain is usually the address of your server, in local development, you can use the address of your local machine, such as: http://localhost:13000": "域名通常是你的服务器地址在本地开发时可以使用你本机的地址例如http://localhost:13000"
"The domain is usually the address of your server, in local development, you can use the address of your local machine, such as: http://localhost:3000": "域名通常是你的服务器地址在本地开发时可以使用你本机的地址例如http://localhost:3000"
}

View File

@ -12,7 +12,6 @@
"devDependencies": {
"@ant-design/icons": "^5.3.7",
"@aws-sdk/client-s3": "^3.245.0",
"@koa/multer": "^3.0.0",
"@tachybase/components": "workspace:*",
"@tachybase/schema": "workspace:*",
"@types/koa-multer": "^1.0.1",

View File

@ -4,7 +4,7 @@ import path from 'path';
import { getApp } from '.';
import { FILE_FIELD_NAME, STORAGE_TYPE_LOCAL } from '../constants';
const { LOCAL_STORAGE_BASE_URL, LOCAL_STORAGE_DEST = 'storage/uploads', APP_PORT = '13000' } = process.env;
const { LOCAL_STORAGE_BASE_URL, LOCAL_STORAGE_DEST = 'storage/uploads', APP_PORT = '3000' } = process.env;
const DEFAULT_LOCAL_BASE_URL = LOCAL_STORAGE_BASE_URL || `/storage/uploads`;

View File

@ -40,6 +40,6 @@ export default class extends Migration {
}
const c = this.db.getCollection('attachments');
const table = c.getTableNameWithSchemaAsString();
await this.db.sequelize.query(`update ${table} set url = replace(url, 'http://localhost:13000', '')`);
await this.db.sequelize.query(`update ${table} set url = replace(url, 'http://localhost:3000', '')`);
}
}

View File

@ -10,7 +10,6 @@
"main": "./dist/server/index.js",
"devDependencies": {
"@ant-design/icons": "~5.3.7",
"@koa/multer": "^3.0.2",
"@tachybase/components": "workspace:*",
"@tachybase/schema": "workspace:*",
"@types/node-xlsx": "^0.15.1",

View File

@ -0,0 +1,2 @@
/node_modules
/src

View File

@ -0,0 +1 @@
# @tachybase/plugin-messages

View File

@ -0,0 +1,2 @@
export * from './dist/client';
export { default } from './dist/client';

View File

@ -0,0 +1 @@
module.exports = require('./dist/client/index.js');

View File

@ -0,0 +1,12 @@
{
"name": "@tachybase/plugin-messages",
"version": "0.21.82",
"main": "dist/server/index.js",
"dependencies": {},
"peerDependencies": {
"@tachybase/client": "workspace:*",
"@tachybase/database": "workspace:*",
"@tachybase/server": "workspace:*",
"@tachybase/test": "workspace:*"
}
}

View File

@ -0,0 +1,2 @@
export * from './dist/server';
export { default } from './dist/server';

View File

@ -0,0 +1 @@
module.exports = require('./dist/server/index.js');

View File

@ -0,0 +1,21 @@
import { Plugin } from '@tachybase/client';
export class PluginMessagesClient extends Plugin {
async afterAdd() {
// await this.app.pm.add()
}
async beforeLoad() {}
// You can get and modify the app instance here
async load() {
console.log(this.app);
// this.app.addComponents({})
// this.app.addScopes({})
// this.app.addProvider()
// this.app.addProviders()
// this.app.router.add()
}
}
export default PluginMessagesClient;

View File

@ -0,0 +1,2 @@
export * from './server';
export { default } from './server';

View File

@ -0,0 +1,79 @@
import { defineCollection } from '@tachybase/database';
export default defineCollection({
dumpRules: 'required',
name: 'messages',
sortable: true,
createdBy: true,
updatedBy: true,
logging: true,
fields: [
{
name: 'id',
type: 'bigInt',
autoIncrement: true,
primaryKey: true,
allowNull: false,
interface: 'id',
},
{
interface: 'input',
type: 'string',
name: 'title',
allowNull: false,
unique: false,
uiSchema: {
type: 'string',
title: '{{t("Title")}}',
'x-component': 'Input',
required: true,
},
},
{
interface: 'input',
type: 'string',
name: 'content',
allowNull: false,
unique: false,
uiSchema: {
type: 'string',
title: '{{t("Content")}}',
'x-component': 'Input',
required: true,
},
},
{
name: 'read',
type: 'boolean',
interface: 'checkbox',
description: '{{ t("Read") }}',
uiSchema: {
'x-component-props': { showUnchecked: true },
type: 'boolean',
'x-component': 'Checkbox',
title: '{{ t("Read") }}',
},
},
{
name: 'userId',
type: 'bigInt',
interface: 'integer',
isForeignKey: true,
uiSchema: { type: 'number', title: 'userId', 'x-component': 'InputNumber', 'x-read-pretty': true },
},
{
foreignKey: 'userId',
onDelete: 'CASCADE',
name: 'user',
type: 'belongsTo',
interface: 'm2o',
uiSchema: {
'x-component': 'AssociationField',
'x-component-props': { multiple: false, fieldNames: { label: 'id', value: 'id' } },
title: '用户',
},
target: 'users',
targetKey: 'id',
},
],
});

View File

@ -0,0 +1,14 @@
import { extendCollection } from '@tachybase/database';
export default extendCollection({
name: 'users',
fields: [
{
type: 'json',
// 订阅偏好设置
name: 'subPrefs',
allowNull: false,
defaultValue: {},
},
],
});

View File

@ -0,0 +1 @@
export { default } from './plugin';

View File

@ -0,0 +1,19 @@
import { Plugin } from '@tachybase/server';
export class PluginMessagesServer extends Plugin {
async afterAdd() {}
async beforeLoad() {}
async load() {}
async install() {}
async afterEnable() {}
async afterDisable() {}
async remove() {}
}
export default PluginMessagesServer;

View File

@ -10,7 +10,7 @@ English | [中文](./README.zh-CN.md)
### 1、启用主题插件
首先把 TachyBase 更新到最新版本,然后会在[插件管理页面](http://localhost:13000/admin/pm/list/local/)看到 `theme-editor` 的卡片,点击卡片右下角的 `启用` 按钮等待页面刷新。
首先把 TachyBase 更新到最新版本,然后会在[插件管理页面](http://localhost:3000/admin/pm/list/local/)看到 `theme-editor` 的卡片,点击卡片右下角的 `启用` 按钮等待页面刷新。
<a href="https://sm.ms/image/EkiMxUpngzAb8yo" target="_blank"><img src="https://s2.loli.net/2023/07/19/EkiMxUpngzAb8yo.png" height="400" style="object-fit: contain;"></a>

View File

@ -10,7 +10,7 @@
### 1、启用主题插件
首先把 TachyBase 更新到最新版本,然后会在[插件管理页面](http://localhost:13000/admin/pm/list/local/)看到 `theme-editor` 的卡片,点击卡片右下角的 `启用` 按钮等待页面刷新。
首先把 TachyBase 更新到最新版本,然后会在[插件管理页面](http://localhost:3000/admin/pm/list/local/)看到 `theme-editor` 的卡片,点击卡片右下角的 `启用` 按钮等待页面刷新。
<a href="https://sm.ms/image/EkiMxUpngzAb8yo" target="_blank"><img src="https://s2.loli.net/2023/07/19/EkiMxUpngzAb8yo.png" height="400" style="object-fit: contain;"></a>

View File

@ -37,6 +37,7 @@
"@tachybase/plugin-localization-management": "workspace:*",
"@tachybase/plugin-logger": "workspace:*",
"@tachybase/plugin-map": "workspace:*",
"@tachybase/plugin-messages": "workspace:*",
"@tachybase/plugin-mobile-client": "workspace:*",
"@tachybase/plugin-mock-collections": "workspace:*",
"@tachybase/plugin-multi-app-manager": "workspace:*",

View File

@ -16,6 +16,7 @@ export class PresetTachyBase extends Plugin {
'verification',
'users',
'acl',
'messages',
// optional plugins, default enabled
'action-bulk-edit',
'action-bulk-update',

View File

@ -614,9 +614,6 @@ importers:
react-iframe:
specifier: ~1.8.5
version: 1.8.5(react@18.3.1)
react-image-lightbox:
specifier: ^5.1.4
version: 5.1.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-intersection-observer:
specifier: ^9.10.3
version: 9.10.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@ -726,6 +723,12 @@ importers:
dayjs:
specifier: 1.11.10
version: 1.11.10
prop-types:
specifier: ^15.8.1
version: 15.8.1
react-modal:
specifier: ^3.16.1
version: 3.16.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-sticky-box:
specifier: ^1.0.2
version: 1.0.2(react@18.3.1)
@ -2171,9 +2174,6 @@ importers:
'@hapi/topo':
specifier: ^6.0.0
version: 6.0.2
'@koa/multer':
specifier: ^3.0.2
version: 3.0.2(multer@1.4.5-lts.1)
'@tachybase/components':
specifier: workspace:*
version: link:../../../core/components
@ -2873,9 +2873,6 @@ importers:
'@aws-sdk/client-s3':
specifier: ^3.245.0
version: 3.474.0
'@koa/multer':
specifier: ^3.0.0
version: 3.0.2(multer@1.4.5-lts.1)
'@tachybase/components':
specifier: workspace:*
version: link:../../../core/components
@ -3129,9 +3126,6 @@ importers:
'@ant-design/icons':
specifier: ~5.3.7
version: 5.3.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@koa/multer':
specifier: ^3.0.2
version: 3.0.2(multer@1.4.5-lts.1)
'@tachybase/components':
specifier: workspace:*
version: link:../../../core/components
@ -3356,6 +3350,21 @@ importers:
specifier: ^6.25.1
version: 6.25.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
packages/plugins/@tachybase/plugin-messages:
dependencies:
'@tachybase/client':
specifier: workspace:*
version: link:../../../core/client
'@tachybase/database':
specifier: workspace:*
version: link:../../../core/database
'@tachybase/server':
specifier: workspace:*
version: link:../../../core/server
'@tachybase/test':
specifier: workspace:*
version: link:../../../core/test
packages/plugins/@tachybase/plugin-mobile-client:
dependencies:
'@tachybase/client':
@ -4464,6 +4473,9 @@ importers:
'@tachybase/plugin-map':
specifier: workspace:*
version: link:../../plugins/@tachybase/plugin-map
'@tachybase/plugin-messages':
specifier: workspace:*
version: link:../../plugins/@tachybase/plugin-messages
'@tachybase/plugin-mobile-client':
specifier: workspace:*
version: link:../../plugins/@tachybase/plugin-mobile-client
@ -17408,13 +17420,6 @@ packages:
peerDependencies:
react: '>=16.x.x'
react-image-lightbox@5.1.4:
resolution: {integrity: sha512-kTiAODz091bgT7SlWNHab0LSMZAPJtlNWDGKv7pLlLY1krmf7FuG1zxE0wyPpeA8gPdwfr3cu6sPwZRqWsc3Eg==}
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
peerDependencies:
react: 16.x || 17.x
react-dom: 16.x || 17.x
react-intersection-observer@9.10.3:
resolution: {integrity: sha512-9NYfKwPZRovB6QJee7fDg0zz/SyYrqXtn5xTZU0vwLtLVBtfu9aZt1pVmr825REE49VPDZ7Lm5SNHjJBOTZHpA==}
peerDependencies:
@ -36110,13 +36115,6 @@ snapshots:
object-assign: 4.1.1
react: 18.3.1
react-image-lightbox@5.1.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
prop-types: 15.8.1
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react-modal: 3.16.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-intersection-observer@9.10.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
react: 18.3.1