mirror of https://github.com/msgbyte/tailchat
feat: admin 初始化与基本界面
parent
8f587887ee
commit
96292a23ba
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,6 @@
|
||||
node_modules
|
||||
|
||||
/.cache
|
||||
/build
|
||||
/public/build
|
||||
.env
|
@ -0,0 +1,53 @@
|
||||
# Welcome to Remix!
|
||||
|
||||
- [Remix Docs](https://remix.run/docs)
|
||||
|
||||
## Development
|
||||
|
||||
From your terminal:
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
|
||||
This starts your app in development mode, rebuilding assets on file changes.
|
||||
|
||||
## Deployment
|
||||
|
||||
First, build your app for production:
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
```
|
||||
|
||||
Then run the app in production mode:
|
||||
|
||||
```sh
|
||||
npm start
|
||||
```
|
||||
|
||||
Now you'll need to pick a host to deploy it to.
|
||||
|
||||
### DIY
|
||||
|
||||
If you're familiar with deploying node applications, the built-in Remix app server is production-ready.
|
||||
|
||||
Make sure to deploy the output of `remix build`
|
||||
|
||||
- `build/`
|
||||
- `public/build/`
|
||||
|
||||
### Using a Template
|
||||
|
||||
When you ran `npx create-remix@latest` there were a few choices for hosting. You can run that again to create a new project, then copy over your `app/` folder to the new project that's pre-configured for your target server.
|
||||
|
||||
```sh
|
||||
cd ..
|
||||
# create a new project, and pick a pre-configured host
|
||||
npx create-remix@latest
|
||||
cd my-new-remix-app
|
||||
# remove the new project's app (not the old one!)
|
||||
rm -rf app
|
||||
# copy your app over
|
||||
cp -R ../my-old-remix-app/app app
|
||||
```
|
@ -0,0 +1,22 @@
|
||||
import { RemixBrowser } from '@remix-run/react';
|
||||
import { startTransition, StrictMode } from 'react';
|
||||
import { hydrateRoot } from 'react-dom/client';
|
||||
|
||||
function hydrate() {
|
||||
startTransition(() => {
|
||||
hydrateRoot(
|
||||
document,
|
||||
<StrictMode>
|
||||
<RemixBrowser />
|
||||
</StrictMode>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof requestIdleCallback === 'function') {
|
||||
requestIdleCallback(hydrate);
|
||||
} else {
|
||||
// Safari doesn't support requestIdleCallback
|
||||
// https://caniuse.com/requestidlecallback
|
||||
setTimeout(hydrate, 1);
|
||||
}
|
@ -0,0 +1,111 @@
|
||||
import { PassThrough } from 'stream';
|
||||
import type { EntryContext } from '@remix-run/node';
|
||||
import { Response } from '@remix-run/node';
|
||||
import { RemixServer } from '@remix-run/react';
|
||||
import isbot from 'isbot';
|
||||
import { renderToPipeableStream } from 'react-dom/server';
|
||||
|
||||
const ABORT_DELAY = 5000;
|
||||
|
||||
export default function handleRequest(
|
||||
request: Request,
|
||||
responseStatusCode: number,
|
||||
responseHeaders: Headers,
|
||||
remixContext: EntryContext
|
||||
) {
|
||||
return isbot(request.headers.get('user-agent'))
|
||||
? handleBotRequest(
|
||||
request,
|
||||
responseStatusCode,
|
||||
responseHeaders,
|
||||
remixContext
|
||||
)
|
||||
: handleBrowserRequest(
|
||||
request,
|
||||
responseStatusCode,
|
||||
responseHeaders,
|
||||
remixContext
|
||||
);
|
||||
}
|
||||
|
||||
function handleBotRequest(
|
||||
request: Request,
|
||||
responseStatusCode: number,
|
||||
responseHeaders: Headers,
|
||||
remixContext: EntryContext
|
||||
) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let didError = false;
|
||||
|
||||
const { pipe, abort } = renderToPipeableStream(
|
||||
<RemixServer context={remixContext} url={request.url} />,
|
||||
{
|
||||
onAllReady() {
|
||||
const body = new PassThrough();
|
||||
|
||||
responseHeaders.set('Content-Type', 'text/html');
|
||||
|
||||
resolve(
|
||||
new Response(body, {
|
||||
headers: responseHeaders,
|
||||
status: didError ? 500 : responseStatusCode,
|
||||
})
|
||||
);
|
||||
|
||||
pipe(body);
|
||||
},
|
||||
onShellError(error: unknown) {
|
||||
reject(error);
|
||||
},
|
||||
onError(error: unknown) {
|
||||
didError = true;
|
||||
|
||||
console.error(error);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
setTimeout(abort, ABORT_DELAY);
|
||||
});
|
||||
}
|
||||
|
||||
function handleBrowserRequest(
|
||||
request: Request,
|
||||
responseStatusCode: number,
|
||||
responseHeaders: Headers,
|
||||
remixContext: EntryContext
|
||||
) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let didError = false;
|
||||
|
||||
const { pipe, abort } = renderToPipeableStream(
|
||||
<RemixServer context={remixContext} url={request.url} />,
|
||||
{
|
||||
onShellReady() {
|
||||
const body = new PassThrough();
|
||||
|
||||
responseHeaders.set('Content-Type', 'text/html');
|
||||
|
||||
resolve(
|
||||
new Response(body, {
|
||||
headers: responseHeaders,
|
||||
status: didError ? 500 : responseStatusCode,
|
||||
})
|
||||
);
|
||||
|
||||
pipe(body);
|
||||
},
|
||||
onShellError(err: unknown) {
|
||||
reject(err);
|
||||
},
|
||||
onError(error: unknown) {
|
||||
didError = true;
|
||||
|
||||
console.error(error);
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
setTimeout(abort, ABORT_DELAY);
|
||||
});
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
import {
|
||||
Admin,
|
||||
Resource,
|
||||
ListGuesser,
|
||||
fetchUtils,
|
||||
EditGuesser,
|
||||
ShowGuesser,
|
||||
} from 'react-admin';
|
||||
import jsonServerProvider from 'ra-data-json-server';
|
||||
import { authProvider } from './authProvider';
|
||||
import { UserList } from './resources/user';
|
||||
import React from 'react';
|
||||
|
||||
const httpClient: typeof fetchUtils.fetchJson = (url, options = {}) => {
|
||||
try {
|
||||
if (!options.headers) {
|
||||
options.headers = new Headers({ Accept: 'application/json' });
|
||||
}
|
||||
const { token } = JSON.parse(window.localStorage.getItem('auth') ?? '');
|
||||
(options.headers as Headers).set('Authorization', `Bearer ${token}`);
|
||||
|
||||
return fetchUtils.fetchJson(url, options);
|
||||
} catch (err) {
|
||||
return Promise.reject();
|
||||
}
|
||||
};
|
||||
|
||||
const dataProvider = jsonServerProvider(
|
||||
// 'https://jsonplaceholder.typicode.com'
|
||||
'/admin/api'
|
||||
// httpClient
|
||||
);
|
||||
|
||||
export const App = () => (
|
||||
<Admin
|
||||
basename="/admin"
|
||||
authProvider={authProvider}
|
||||
dataProvider={dataProvider}
|
||||
>
|
||||
<Resource
|
||||
name="users"
|
||||
options={{ label: '用户管理' }}
|
||||
list={UserList}
|
||||
show={ShowGuesser}
|
||||
/>
|
||||
</Admin>
|
||||
);
|
@ -0,0 +1,33 @@
|
||||
import { AuthProvider } from 'react-admin';
|
||||
|
||||
export const authProvider: AuthProvider = {
|
||||
login: ({ username, password }) => {
|
||||
// TODO
|
||||
if (username !== 'admin' || password !== 'admin') {
|
||||
return Promise.reject();
|
||||
}
|
||||
localStorage.setItem('username', username);
|
||||
return Promise.resolve();
|
||||
},
|
||||
logout: () => {
|
||||
localStorage.removeItem('username');
|
||||
return Promise.resolve();
|
||||
},
|
||||
checkAuth: () =>
|
||||
localStorage.getItem('username') ? Promise.resolve() : Promise.reject(),
|
||||
checkError: (error) => {
|
||||
const status = error.status;
|
||||
if (status === 401 || status === 403) {
|
||||
localStorage.removeItem('username');
|
||||
return Promise.reject();
|
||||
}
|
||||
// other error code (404, 500, etc): no need to log out
|
||||
return Promise.resolve();
|
||||
},
|
||||
getIdentity: () =>
|
||||
Promise.resolve({
|
||||
id: 'user',
|
||||
fullName: 'John Doe',
|
||||
}),
|
||||
getPermissions: () => Promise.resolve(''),
|
||||
};
|
@ -0,0 +1,40 @@
|
||||
import {
|
||||
BooleanField,
|
||||
Datagrid,
|
||||
DateField,
|
||||
EmailField,
|
||||
List,
|
||||
TextField,
|
||||
ShowButton,
|
||||
EditButton,
|
||||
SearchInput,
|
||||
} from 'react-admin';
|
||||
import React from 'react';
|
||||
import { Box } from '@mui/material';
|
||||
|
||||
const PostListActionToolbar = ({ children, ...props }) => (
|
||||
<Box sx={{ alignItems: 'center', display: 'flex' }}>{children}</Box>
|
||||
);
|
||||
|
||||
export const UserList: React.FC = () => (
|
||||
<List filters={[<SearchInput key="search" source="q" alwaysOn />]}>
|
||||
<Datagrid>
|
||||
<TextField source="id" />
|
||||
<EmailField source="email" />
|
||||
<TextField source="nickname" />
|
||||
<TextField source="discriminator" />
|
||||
<BooleanField source="temporary" />
|
||||
<TextField source="avatar" />
|
||||
<TextField source="type" />
|
||||
<TextField source="password" />
|
||||
<TextField source="settings" />
|
||||
<DateField source="createdAt" />
|
||||
<DateField source="updatedAt" />
|
||||
<PostListActionToolbar>
|
||||
<ShowButton />
|
||||
<EditButton />
|
||||
</PostListActionToolbar>
|
||||
</Datagrid>
|
||||
</List>
|
||||
);
|
||||
UserList.displayName = 'UserList';
|
@ -0,0 +1,32 @@
|
||||
import type { MetaFunction } from '@remix-run/node';
|
||||
import {
|
||||
Links,
|
||||
LiveReload,
|
||||
Meta,
|
||||
Outlet,
|
||||
Scripts,
|
||||
ScrollRestoration,
|
||||
} from '@remix-run/react';
|
||||
|
||||
export const meta: MetaFunction = () => ({
|
||||
charset: 'utf-8',
|
||||
title: 'Tailchat Admin',
|
||||
viewport: 'width=device-width,initial-scale=1',
|
||||
});
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<html lang="en">
|
||||
<head>
|
||||
<Meta />
|
||||
<Links />
|
||||
</head>
|
||||
<body>
|
||||
<Outlet />
|
||||
<ScrollRestoration />
|
||||
<Scripts />
|
||||
<LiveReload />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
import { App } from '~/ra/App';
|
||||
import styles from '~/styles/app.css';
|
||||
|
||||
export function links() {
|
||||
return [{ rel: 'stylesheet', href: styles }];
|
||||
}
|
||||
|
||||
export default App;
|
@ -0,0 +1,8 @@
|
||||
import { App } from '~/ra/App';
|
||||
import styles from '~/styles/app.css';
|
||||
|
||||
export function links() {
|
||||
return [{ rel: 'stylesheet', href: styles }];
|
||||
}
|
||||
|
||||
export default App;
|
@ -0,0 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function Index() {
|
||||
// eslint-disable-next-line react/no-unescaped-entities
|
||||
return <div>Please visit '/admin'</div>;
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
html, body {
|
||||
margin: 0;
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
{
|
||||
"verbose": true,
|
||||
"watch": ["./server.ts"],
|
||||
"ext": "ts",
|
||||
"delay": 1000,
|
||||
"exec": "ts-node ./server.ts"
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "tailchat-admin",
|
||||
"private": true,
|
||||
"sideEffects": false,
|
||||
"scripts": {
|
||||
"build": "remix build",
|
||||
"dev": "remix build && run-p \"dev:*\"",
|
||||
"dev:node": "cross-env NODE_ENV=development nodemon",
|
||||
"dev:remix": "remix watch",
|
||||
"start": "cross-env NODE_ENV=production node ./server.js",
|
||||
"typecheck": "tsc -b"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mui/material": "^5.11.3",
|
||||
"@remix-run/express": "^1.9.0",
|
||||
"@remix-run/node": "^1.9.0",
|
||||
"@remix-run/react": "^1.9.0",
|
||||
"@typegoose/typegoose": "9.3.1",
|
||||
"compression": "^1.7.4",
|
||||
"express": "^4.18.2",
|
||||
"express-mongoose-ra-json-server": "^0.1.0",
|
||||
"isbot": "^3.6.5",
|
||||
"morgan": "^1.10.0",
|
||||
"ra-data-json-server": "^4.6.3",
|
||||
"react": "^18.2.0",
|
||||
"react-admin": "^4.6.3",
|
||||
"react-dom": "^18.2.0",
|
||||
"ts-node": "^10.9.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@remix-run/dev": "^1.9.0",
|
||||
"@types/compression": "^1.7.2",
|
||||
"@types/express": "^4.17.15",
|
||||
"@types/morgan": "^1.9.4",
|
||||
"@types/react": "^18.0.25",
|
||||
"@types/react-dom": "^18.0.8",
|
||||
"cross-env": "^7.0.3",
|
||||
"dotenv": "^16.0.3",
|
||||
"nodemon": "^2.0.20",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"typescript": "^4.8.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 17 KiB |
@ -0,0 +1,8 @@
|
||||
/** @type {import('@remix-run/dev').AppConfig} */
|
||||
module.exports = {
|
||||
ignoredRouteFiles: ['**/.*'],
|
||||
// appDirectory: "app",
|
||||
// assetsBuildDirectory: "public/build",
|
||||
// serverBuildPath: "build/index.js",
|
||||
// publicPath: "/build/",
|
||||
};
|
@ -0,0 +1,2 @@
|
||||
/// <reference types="@remix-run/dev" />
|
||||
/// <reference types="@remix-run/node" />
|
@ -0,0 +1,85 @@
|
||||
import path from 'path';
|
||||
import express, { Router } from 'express';
|
||||
import compression from 'compression';
|
||||
import morgan from 'morgan';
|
||||
import { createRequestHandler } from '@remix-run/express';
|
||||
import raExpressMongoose from 'express-mongoose-ra-json-server';
|
||||
import mongoose from 'mongoose';
|
||||
import dotenv from 'dotenv';
|
||||
dotenv.config();
|
||||
|
||||
// 链接数据库
|
||||
mongoose.connect(process.env.MONGO_URL!, (error: any) => {
|
||||
if (!error) {
|
||||
return console.info('Mongo connected');
|
||||
}
|
||||
console.error(error);
|
||||
});
|
||||
|
||||
const BUILD_DIR = path.join(process.cwd(), 'build');
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(compression());
|
||||
|
||||
// http://expressjs.com/en/advanced/best-practice-security.html#at-a-minimum-disable-x-powered-by-header
|
||||
app.disable('x-powered-by');
|
||||
|
||||
// Remix fingerprints its assets so we can cache forever.
|
||||
app.use(
|
||||
'/build',
|
||||
express.static('public/build', { immutable: true, maxAge: '1y' })
|
||||
);
|
||||
|
||||
// Everything else (like favicon.ico) is cached for an hour. You may want to be
|
||||
// more aggressive with this caching.
|
||||
app.use(express.static('public', { maxAge: '1h' }));
|
||||
|
||||
app.use(morgan('tiny'));
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use(
|
||||
'/users',
|
||||
raExpressMongoose(require('../models/user/user').default, {
|
||||
q: ['nickname', 'email'],
|
||||
})
|
||||
);
|
||||
|
||||
app.use('/admin/api', router);
|
||||
|
||||
app.all(
|
||||
'/admin/*',
|
||||
process.env.NODE_ENV === 'development'
|
||||
? (req, res, next) => {
|
||||
purgeRequireCache();
|
||||
|
||||
return createRequestHandler({
|
||||
build: require(BUILD_DIR),
|
||||
mode: process.env.NODE_ENV,
|
||||
})(req, res, next);
|
||||
}
|
||||
: createRequestHandler({
|
||||
build: require(BUILD_DIR),
|
||||
mode: process.env.NODE_ENV,
|
||||
})
|
||||
);
|
||||
|
||||
const port = process.env.PORT || 3000;
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`Express server listening on port ${port}`);
|
||||
});
|
||||
|
||||
function purgeRequireCache() {
|
||||
// purge require cache on requests for "server side HMR" this won't let
|
||||
// you have in-memory objects between requests in development,
|
||||
// alternatively you can set up nodemon/pm2-dev to restart the server on
|
||||
// file changes, but then you'll have to reconnect to databases/etc on each
|
||||
// change. We prefer the DX of this, so we've included it for you by default
|
||||
for (const key in require.cache) {
|
||||
if (key.startsWith(BUILD_DIR)) {
|
||||
delete require.cache[key];
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx", "../models/**/*.ts"],
|
||||
"compilerOptions": {
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2019"],
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"target": "ES2019",
|
||||
"allowJs": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"experimentalDecorators": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~/*": ["./app/*"]
|
||||
},
|
||||
"jsx": "react-jsx",
|
||||
|
||||
// Remix takes care of building everything in `remix build`.
|
||||
"noEmit": true
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue