feat: admin 初始化与基本界面

pull/70/head
moonrailgun 2 years ago
parent 8f587887ee
commit 96292a23ba

@ -31,5 +31,6 @@ module.exports = {
'react/prop-types': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/ban-types': 'off',
'react/react-in-jsx-scope': 'off',
},
};

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…
Cancel
Save