Merge pull request 'ssr分支' (#606) from Eeeros/forgeplus-react:gitlink_ssr_test into gitlink_ssr

1
This commit is contained in:
Eeeros 2024-01-15 12:34:17 +08:00
commit 1dd20d3d26
46 changed files with 6258 additions and 1018 deletions

View File

@ -1,16 +1,28 @@
{
"presets": [
"env",
"react",
"stage-2"
// 移除 "es2015" 预设,因为它已经被 "env" 预设覆盖
["@babel/preset-env" ],
"@babel/preset-react" // 使用新的插件命名
],
"plugins": [[
"transform-runtime",
{
"helpers": false,
"polyfill": false,
"regenerator": true,
"moduleName": "babel-runtime"
}
],["transform-decorators-legacy"]]
"plugins": [
["import", { "libraryName": "antd", "libraryDirectory": "lib"}],
// 使用新的插件命名
["@babel/plugin-transform-runtime", {
"corejs": 3,
"helpers": true,
"regenerator": true,
"useESModules": false
}],
["@babel/plugin-proposal-decorators", {
"legacy": true
}],
["@babel/plugin-syntax-dynamic-import"],
["@babel/plugin-transform-class-properties"], // 使用新的插件命名
"@babel/plugin-syntax-import-meta",
"@babel/plugin-proposal-json-strings",
"@babel/plugin-proposal-function-sent",
"@babel/plugin-proposal-export-namespace-from",
"@babel/plugin-proposal-numeric-separator",
"@babel/plugin-proposal-throw-expressions"
]
}

1
.gitignore vendored
View File

@ -38,6 +38,7 @@ bower_components
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/
buildserver/
src/.umi/
# Dependency directories
node_modules/

View File

@ -1,3 +1,42 @@
# gitlink-ssr版本
#### node版本
12.x
#### ng配置
```
if ($http_user_agent ~* "googlebot|Mediapartners-Google|bingbot|google-structured-data-testing-tool|baiduspider|360Spider|Sogou Spider|Yahoo! Slurp China|^$")
{#判断如果是网络爬虫转发到node服务器下
proxy_pass http://www.test.com:3000;
break;
}
```
#### 依赖安装、运行
```bash
// 依赖
npm install
npm install pm2 -g
// 打包
npm run build
npm run build:server
// 启动服务
npm run pm2
```
或者
```bash
./build.sh
```
<h3>前端react环境安装</h3>
<p>1、 安装node v6.9.x此安装包含了node和npm。</p>
<p>2、 安装cnpm命令行 npm install -g cnpm --registry=https://registry.npm.taobao.org</p>

19
Routes.js Normal file
View File

@ -0,0 +1,19 @@
import app from './src/App'
import users from './src/forge/users/Index';
import Detail from "./src/forge/Main/Detail";
export default [
{
key:"detail",
path: "/:owner/:projectsId",
component: app,
}
];
export const deepRoutes = [
{
key:"detail",
path: "/:owner/:projectsId",
component: Detail,
}
]

16
build.sh Normal file
View File

@ -0,0 +1,16 @@
#!/bin/bash
# 确保脚本在当前目录执行
cd "$(dirname "$0")"
# 安装依赖
npm install
# 构建项目
npm run build
# 构建服务器端
npm run build:server
# 使用PM2启动应用程序
npm run pm2

View File

@ -47,6 +47,7 @@ module.exports = {
appIndexJs: resolveApp('src/index.js'),
appPackageJson: resolveApp('package.json'),
appSrc: resolveApp('src'),
serverSrc: resolveApp('server'),
yarnLockFile: resolveApp('yarn.lock'),
testsSetup: resolveApp('src/setupTests.js'),
appNodeModules: resolveApp('node_modules'),

1
config/ssrUrl.js Normal file
View File

@ -0,0 +1 @@
export const url = process.env.NODE_ENV === 'production' ? 'https://www.gitlink.org.cn' : 'http://172.20.32.202:4000'

View File

@ -273,12 +273,11 @@ module.exports = {
reactPath:'react.production.min.js',
}),
new InterpolateHtmlPlugin(HtmlWebpackPlugin, env.raw),
// Add module names to factory functions so they appear in browser profiler.
new webpack.NamedModulesPlugin(),
// Makes some environment variables available to the JS code, for example:
// if (process.env.NODE_ENV === 'development') { ... }. See `./env.js`.
new webpack.DefinePlugin(env.stringified),
new webpack.DefinePlugin({ ...env.stringified, __SERVER__: 'false',__CLIENT__: 'true' }),
// This is necessary to emit hot updates (currently CSS only):
new webpack.HotModuleReplacementPlugin(),
// Watcher doesn't work well if you mistype casing in a path so we use

View File

@ -1,6 +1,7 @@
"use strict";
const autoprefixer = require("autoprefixer");
const path = require("path");
import { ReactLoadablePlugin } from 'react-loadable/webpack';
const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
@ -16,8 +17,9 @@ const TerserWebpackPlugin = require('terser-webpack-plugin');
const paths = require("./paths");
const getClientEnvironment = require("./env");
let publicPath = "/react/build/";
let publicPath = "/build/";
const publicUrl = publicPath.slice(0, -1);
// const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== "false";
const shouldUseSourceMap = process.env.NODE_ENV !== "production";
const env = getClientEnvironment(publicPath,'production.min');
@ -116,6 +118,9 @@ module.exports = {
// please link the files into your node_modules/ and let module-resolution kick in.
// Make sure your source files are compiled, as they will not be processed in any way.
new ModuleScopePlugin(paths.appSrc, [paths.appPackageJson]),
new ReactLoadablePlugin({
filename: './react-loadable.json',
}),
// ["transform-remove-console"]
],
},
@ -294,12 +299,12 @@ module.exports = {
minifyURLs: true,
},
}),
new webpack.DefinePlugin({ ...env.stringified, __SERVER__: 'false',__CLIENT__: 'true' }),
new InterpolateHtmlPlugin(HtmlWebpackPlugin, env.raw),
// Makes some environment variables available to the JS code, for example:
// if (process.env.NODE_ENV === 'production') { ... }. See `./env.js`.
// It is absolutely essential that NODE_ENV was set to production here.
// Otherwise React will be compiled in the very slow development mode.
new webpack.DefinePlugin(env.stringified),
new MiniCssExtractPlugin({
filename: "static/css/[name].[contenthash:8].css",

243
config/webpack.server.js Normal file
View File

@ -0,0 +1,243 @@
const autoprefixer = require("autoprefixer");
const path = require("path");
const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const ManifestPlugin = require("webpack-manifest-plugin");
const InterpolateHtmlPlugin = require("react-dev-utils/InterpolateHtmlPlugin");
const SWPrecacheWebpackPlugin = require("sw-precache-webpack-plugin");
const eslintFormatter = require("react-dev-utils/eslintFormatter");
const ModuleScopePlugin = require("react-dev-utils/ModuleScopePlugin");
const MonacoWebpackPlugin = require("monaco-editor-webpack-plugin");
const TerserJSPlugin = require("terser-webpack-plugin");
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");
const TerserWebpackPlugin = require('terser-webpack-plugin');
const paths = require("./paths");
const getClientEnvironment = require("./env");
let publicPath = "/react/buildserver/";
// const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== "false";
const shouldUseSourceMap = true;
const env = getClientEnvironment(publicPath,'development');
const nodeExternals = require("webpack-node-externals");
const serverConfig = {
target:"node", //由于输出代码的运行环境是node源码中依赖的node原生模块没必要打包进去为了不把nodejs内置模块打包进输出文件中例如 fs net模块等
mode: "development",
entry: path.resolve(__dirname,"../server/index.js"),
output:{
filename:"bundle.js",
path: path.resolve(__dirname,"../buildserver")
},
externals: [
nodeExternals(), // 忽略 Node.js 核心模块
// 自定义的外部模块
{ "react": "React", "react-dom": "ReactDOM" }
],
resolve: {
// This allows you to set a fallback for where Webpack should look for modules.
// We placed these paths second because we want `node_modules` to "win"
// if there are any conflicts. This matches Node resolution mechanism.
// https://github.com/facebookincubator/create-react-app/issues/253
modules: ["node_modules", paths.appNodeModules].concat(
// It is guaranteed to exist because we tweak it in `env.js`
process.env.NODE_PATH.split(path.delimiter).filter(Boolean)
),
// These are the reasonable defaults supported by the Node ecosystem.
// We also include JSX as a common component filename extension to support
// some tools, although we do not recommend using it, see:
// https://github.com/facebookincubator/create-react-app/issues/290
// `web` extension prefixes have been added for better support
// for React Native Web.
extensions: [".web.js", ".mjs", ".js", ".json", ".web.jsx", ".jsx"],
alias: {
educoder: __dirname + "/../src/common/educoder.js",
// Support React Native Web
// https://www.smashingmagazine.com/2016/08/a-glimpse-into-the-future-with-react-native-for-web/
"react-native": "react-native-web",
},
plugins: [
// Prevents users from importing files from outside of src/ (or node_modules/).
// This often causes confusion because we only process files within src/ with babel.
// To fix this, we prevent you from importing files out of src/ -- if you'd like to,
// please link the files into your node_modules/ and let module-resolution kick in.
// Make sure your source files are compiled, as they will not be processed in any way.
new ModuleScopePlugin(paths.appSrc, [paths.appPackageJson]),
// ["transform-remove-console"]
],
},
module: {
strictExportPresence: true,
rules: [
{
test: /\.(js|jsx|mjs)$/,
enforce: "pre",
use: [
{
options: {
formatter: eslintFormatter,
eslintPath: require.resolve("eslint"),
},
loader: require.resolve("eslint-loader"),
},
],
include: [paths.appSrc, paths.serverSrc],
},
{
// "oneOf" will traverse all following loaders until one will
// match the requirements. When no loader matches it will fall
// back to the "file" loader at the end of the loader list.
oneOf: [
// "url" loader works just like "file" loader but it also embeds
// assets smaller than specified size as data URLs to avoid requests.
{
test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
loader: require.resolve("url-loader"),
options: {
limit: 10000,
name: "static/media/[name].[hash:8].[ext]",
},
},
{
test: /\.(js|jsx|mjs)$/,
include: [paths.appSrc, paths.serverSrc],
// exclude: /node_modules/,
loader: "babel-loader",
options: {
cacheDirectory: true,
presets: [
require.resolve('@babel/preset-env'),
require.resolve('@babel/preset-react'),
],
plugins: [
require.resolve('@babel/plugin-transform-async-to-generator'),
require.resolve('@babel/plugin-syntax-dynamic-import'),
require.resolve('@babel/plugin-proposal-class-properties'),
require.resolve('@babel/plugin-proposal-export-default-from'),
require.resolve('@babel/plugin-transform-runtime'),
require.resolve('@babel/plugin-transform-modules-commonjs'),
require.resolve('babel-plugin-dynamic-import-webpack'),
]
}
},
{
test: /\.css$/,
use: [
{
loader: MiniCssExtractPlugin.loader,
options: {
publicPath,
},
},
{
loader: require.resolve("css-loader"),
options: {
importLoaders: 1,
sourceMap: shouldUseSourceMap,
},
},
{
loader: require.resolve("postcss-loader"),
options: {
ident: "postcss",
plugins: () => [
require("postcss-flexbugs-fixes"),
autoprefixer({
browsers: [
">1%",
"last 4 versions",
"Firefox ESR",
"not ie < 9", // React doesn't support IE8 anyway
],
flexbox: "no-2009",
}),
],
},
},
],
},
{
test: /\.scss$/,
use: [
{
loader: MiniCssExtractPlugin.loader,
options: {
publicPath,
},
},
{
loader: require.resolve("css-loader"),
options: {
importLoaders: 1,
sourceMap: shouldUseSourceMap,
},
},
{
loader: require.resolve("sass-loader"),
},
{
loader: 'sass-resources-loader',
options: {
resources: ['src/global.scss']
}
}
],
},
{
test: /\.less$/,
use: [{
loader: 'isomorphic-style-loader',
}, {
loader: 'css-loader', // translates CSS into CommonJS
}, {
loader: 'less-loader', // compiles Less to CSS
options: {
modifyVars: {
'primary-color': '#466aff',
'link-color': '#466aff',
},
javascriptEnabled: true,
},
}]
},
// "file" loader makes sure assets end up in the `build` folder.
// When you `import` an asset, you get its filename.
// This loader doesn't use a "test" so it will catch all modules
// that fall through the other loaders.
{
loader: require.resolve("file-loader"),
// Exclude `js` files to keep "css" loader working as it injects
// it's runtime that would otherwise processed through "file" loader.
// Also exclude `html` and `json` extensions so they get processed
// by webpacks internal loaders.
exclude: [/\.(js|jsx|mjs)$/, /\.html$/, /\.json$/],
options: {
name: "static/media/[name].[contenthash:8].[ext]",
},
},
// ** STOP ** Are you adding a new loader?
// Make sure to add the new loader(s) before the "file" loader.
],
},
],
},
plugins: [
new webpack.DefinePlugin({ ...env.stringified, __SERVER__: 'true',__CLIENT__: 'false' }),
new MiniCssExtractPlugin({
filename: "static/css/[name].[contenthash:8].css",
chunkFilename: "static/css/[name].[contenthash:8].chunk.css",
}),
new ManifestPlugin({
fileName: "asset-manifest.json",
}),
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
new MonacoWebpackPlugin({
features: ["coreCommands", "find"],
languages: ['plaintext','apex', 'azcli', 'bat', 'clojure', 'coffee', 'cpp', 'csharp', 'csp', 'css', 'dockerfile', 'fsharp', 'go', 'handlebars', 'html', 'ini', 'java', 'javascript', 'json', 'less', 'lua', 'markdown', 'msdax', 'mysql', 'objective', 'perl', 'pgsql', 'php', 'postiats', 'powerquery', 'powershell', 'pug', 'python', 'r', 'razor', 'redis', 'redshift', 'ruby', 'rust', 'sb', 'scheme', 'scss', 'shell', 'solidity', 'sql', 'st', 'swift', 'typescript', 'vb', 'xml', 'yaml']
}),
new webpack.NamedChunksPlugin(),
new webpack.HashedModuleIdsPlugin(),
],
};
module.exports = serverConfig;

5763
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -3,6 +3,24 @@
"version": "3.1.0",
"private": true,
"dependencies": {
"@babel/core": "^7.0.0",
"@babel/plugin-proposal-class-properties": "^7.0.0",
"@babel/plugin-proposal-decorators": "^7.0.0",
"@babel/plugin-proposal-export-default-from": "^7.23.3",
"@babel/plugin-proposal-export-namespace-from": "^7.0.0",
"@babel/plugin-proposal-function-sent": "^7.0.0",
"@babel/plugin-proposal-json-strings": "^7.0.0",
"@babel/plugin-proposal-numeric-separator": "^7.0.0",
"@babel/plugin-proposal-throw-expressions": "^7.0.0",
"@babel/plugin-syntax-dynamic-import": "^7.0.0",
"@babel/plugin-syntax-import-meta": "^7.0.0",
"@babel/plugin-transform-async-to-generator": "^7.0.0",
"@babel/plugin-transform-runtime": "^7.23.6",
"@babel/preset-env": "^7.23.6",
"@babel/preset-react": "^7.23.3",
"@babel/preset-stage-2": "^7.8.3",
"@babel/runtime": "^7.0.0-beta.46",
"@babel/runtime-corejs3": "^7.23.6",
"@monaco-editor/react": "^2.3.0",
"@novnc/novnc": "^1.1.0",
"@wangeditor/editor": "^5.1.23",
@ -13,6 +31,10 @@
"array-flatten": "^2.1.2",
"autoprefixer": "7.1.6",
"axios": "^0.24.0",
"babel-core": "^7.0.0-bridge.0",
"babel-jest": "^23.4.2",
"babel-plugin-antd": "^0.5.1",
"babel-plugin-dynamic-import-webpack": "^1.1.0",
"bizcharts": "^3.5.8",
"bundle-loader": "^0.5.6",
"chalk": "1.1.3",
@ -20,22 +42,29 @@
"clipboard": "^2.0.8",
"code-prettify": "^0.1.0",
"codemirror": "^5.64.0",
"connect-timeout": "^1.9.0",
"connected-react-router": "4.4.1",
"core-js": "^3.34.0",
"dompurify": "^2.3.3",
"dotenv": "4.0.0",
"dotenv-expand": "4.2.0",
"echarts": "^4.9.0",
"echarts-wordcloud": "^2.0.0",
"editor.md": "^1.5.0",
"express": "^4.18.2",
"flv.js": "^1.5.0",
"fs-extra": "3.0.1",
"http-proxy-middleware": "^2.0.6",
"i18next": "^23.4.5",
"immutability-helper": "^2.6.6",
"install": "^0.12.2",
"intersection-observer": "^0.12.2",
"isomorphic-style-loader": "^5.3.2",
"jest": "20.0.4",
"jquery": "^3.7.1",
"js-base64": "^2.5.2",
"js2wordcloud": "^1.1.12",
"jsdom": "^15.2.1",
"katex": "^0.11.1",
"less": "^3.13.1",
"localforage": "^1.10.0",
@ -88,6 +117,7 @@
"react-redux": "5.0.7",
"react-resizable": "^1.10.1",
"react-router": "^4.2.0",
"react-router-config": "^5.1.1",
"react-router-dom": "^4.2.2",
"react-slick": "^0.28.1",
"react-split-pane": "^0.1.91",
@ -104,23 +134,31 @@
"slick-carousel": "^1.8.1",
"store": "^2.0.12",
"styled-components": "^4.4.1",
"webpack-node-externals": "^3.0.0",
"weixin-js-sdk": "^1.6.0",
"whatwg-fetch": "2.0.3",
"winston": "^3.11.0",
"winston-daily-rotate-file": "^4.7.1",
"wrap-md-editor": "^0.2.20",
"xss": "^1.0.14",
"xterm": "4.8.1",
"xterm-addon-fit": "0.4.0"
},
"scripts": {
"start": "node --max_old_space_size=15360 scripts/start.js",
"build": "cross-env NODE_ENV=production node --max_old_space_size=15360 scripts/build.js",
"start": "node scripts/start.js",
"build": "cross-env NODE_ENV=production babel-node --max_old_space_size=15360 scripts/build.js",
"build:server": "cross-env NODE_ENV=production webpack --config config/webpack.server.js",
"build:dll": "webpack --config=./config/webpack.dll.config.js",
"test-build": "cross-env NODE_ENV=testBuild node --max_old_space_size=15360 scripts/build.js",
"pre-build": "NODE_ENV=preBuild node --max_old_space_size=15360 scripts/build.js",
"gen_stats": "NODE_ENV=production webpack --profile --config=./config/webpack.config.prod.js --json > stats.json",
"ana": "webpack-bundle-analyzer ./stats.json",
"analyze": "npm run build -- --stats && webpack-bundle-analyzer build/bundle-stats.json",
"analyz": "NODE_ENV=production npm_config_report=true npm run build"
"analyz": "NODE_ENV=production npm_config_report=true npm run build",
"dev": "npm-run-all --parallel dev:**",
"dev:server": "cross-env NODE_ENV=production nodemon --exec babel-node \"./buildserver/bundle.js\" --watch config --watch server",
"dev:build:server": "cross-env NODE_ENV=production webpack --config config/webpack.server.js",
"pm2": "pm2 start pm2.json"
},
"jest": {
"collectCoverageFrom": [
@ -156,46 +194,21 @@
"node"
]
},
"babel": {
"presets": [
"react",
"react-app"
],
"plugins": [
[
"import",
{
"libraryName": "antd",
"libraryDirectory": "lib",
"style": "css"
},
"ant"
],
"syntax-dynamic-import"
]
},
"eslintConfig": {
"extends": "react-app"
},
"proxy": "http://172.20.32.202:4000",
"port": "3007",
"devDependencies": {
"@babel/runtime": "7.0.0-beta.51",
"babel-cli": "^6.26.0",
"babel-core": "^6.26.0",
"babel-eslint": "7.2.3",
"babel-jest": "20.0.3",
"babel-loader": "7.1.2",
"@babel/core": "^7.23.6",
"@babel/node": "^7.0.0",
"@babel/plugin-proposal-decorators": "^7.0.0",
"@babel/polyfill": "^7.0.0",
"@babel/preset-react": "^7.0.0",
"babel-loader": "^8.3.0",
"babel-plugin-import": "^1.13.0",
"babel-plugin-syntax-dynamic-import": "^6.18.0",
"babel-plugin-transform-decorators-legacy": "^1.3.5",
"babel-plugin-transform-runtime": "^6.23.0",
"babel-polyfill": "^6.26.0",
"babel-preset-es2015": "^6.24.1",
"babel-preset-react": "^6.24.1",
"babel-preset-react-app": "^3.1.1",
"babel-preset-stage-2": "^6.24.1",
"babel-runtime": "6.26.0",
"babel-upgrade": "^1.0.1",
"case-sensitive-paths-webpack-plugin": "2.1.1",
"compression-webpack-plugin": "^1.1.12",
"concat": "^1.0.3",
@ -214,6 +227,7 @@
"less-loader": "^4.1.0",
"mockjs": "^1.1.0",
"node-sass": "^4.14.1",
"npm-run-all": "^4.1.5",
"optimize-css-assets-webpack-plugin": "^5.0.3",
"postcss-loader": "2.0.8",
"purgecss": "^2.1.2",

14
pm2.json Normal file
View File

@ -0,0 +1,14 @@
{
"apps": [{
"name": "gitlink-ssr",
"script": "buildserver/bundle.js",
"watch": false,
"log_date_format": "YYYY-MM-DD HH:mm Z",
"exec_mode": "cluster",
"max_memory_restart": "500M",
"env": {
"NODE_ENV": "production"
},
"instances": 4
}]
}

44
server/index.js Normal file
View File

@ -0,0 +1,44 @@
import express from "express";
import "./window"
import {render} from "./render";
import { url } from "../config/ssrUrl";
const { createProxyMiddleware } = require('http-proxy-middleware');
const app = express();
app.use('/build', express.static('build'));
const targetServer = url; // Java 服务器的地址
const options = {
target: targetServer,
changeOrigin: true, // 允许在请求头中更改主机
timeout: 30000
};
// 设置代理
app.use(['/api', '/images', '/system', '/favicon'], (req, res) => {
const proxy = createProxyMiddleware(options);
proxy(req, res);
});
// 中间件,用于排除特定的路径
function excludePath(req, res, next) {
// 检查请求的路径是否需要被排除
if (req.path.includes('/build/')) {
// 如果是,直接结束请求,不调用下一个中间件或路由处理器
res.status(404).send('This path is excluded.');
} else {
// 如果不是,继续执行下一个中间件或路由处理器
next();
}
}
// 应用中间件到所有路由
app.use(excludePath);
app.get('*',function (req,res) {
render(req,res);
})
const port = 3000
console.log(`\n==> 🌎 Listening on port ${port}. Open up http://localhost:${port}/ in your browser.\n`)
app.listen(port, '0.0.0.0');

28
server/log.js Normal file
View File

@ -0,0 +1,28 @@
const { createLogger, format, transports } = require("winston");
require("winston-daily-rotate-file");
const customFormat = format.combine(
format.timestamp({ format: "MMM-DD-YYYY HH:mm:ss" }),
format.align(),
format.printf((i) => `${i.level}: ${[i.timestamp]}: ${i.message}`)
);
const defaultOptions = {
format: customFormat,
datePattern: "YYYY-MM-DD",
zippedArchive: true,
maxSize: "20m",
maxFiles: "14d",
};
const Logger = createLogger({
format: customFormat,
transports: [
new transports.DailyRotateFile({
filename: "logs/info-%DATE%.log",
level: "info",
...defaultOptions,
}),
],
});
export default Logger

93
server/render.js Normal file
View File

@ -0,0 +1,93 @@
import React from "react";
import {renderToString} from "react-dom/server";
import {StaticRouter, matchPath} from "react-router-dom";
import Routes, { deepRoutes } from "../Routes";
import { Provider } from "react-redux";
import { serverStore } from "../src/redux/stores/configureStore";
import App from "../src/App";
import { Route } from 'react-router-dom';
import { setDefaultMeta } from '../src/common/UrlTool'
import Logger from "./log";
export const render = async (req,res)=>{
let _route = null,
_match = null;
Logger.info(`${req.url} UA:${req.headers['user-agent']}`)
let store = serverStore();
function matchSubRoutes(routes, url) {
for (const route of routes) {
// 检查当前路由是否匹配
let match = matchPath(url, route.path);
if (match) {
// 如果当前路由匹配,并且它有子路由,递归检查子路由
if (route.routes) {
// 递归调用匹配子路由
const subMatch = matchSubRoutes(route.routes, url);
if (subMatch) {
_route = route;
_match = subMatch;
return true;
}
}
// 如果没有子路由,或者子路由不匹配,但当前路由匹配,返回当前路由
_route = route;
_match = match;
return true;
}
}
// 如果没有找到匹配的路由返回false
return false;
}
matchSubRoutes(deepRoutes, req.url.split("?")[0])
let context = {
code: 200,
};
if (_route && _route.component && _route.component.preFetch) {
context = await _route.component.preFetch({
store,
match: _match,
query: req.path,
});
}
const content = renderToString((
<Provider store={store}>
<StaticRouter location={req.path} context={context}>
<Route path='/:owner/:projectsId' exact component={App}></Route>
</StaticRouter>
</Provider>
))
if (!content) {
// 未匹配到详情页恢复默认header
setDefaultMeta()
} else {
// 匹配到服务端渲染页面注入state
let script = domObj.window.document.getElementById('initState')
if (!script) {
script = domObj.window.document.createElement('script')
script.setAttribute('id', 'initState')
}
script.textContent = `window.__initState__ = ${ JSON.stringify(store.getState()) }`
domObj.window.document.head.appendChild(script);
}
let html = domObj.serialize()
const prepHTML=(data,rootString)=>{
data=data.replace('<div id="root" class="page -layout-v -fit widthunit"></div>',`<div id="root" class="page -layout-v -fit widthunit">${rootString}</div>`);
return data;
}
res.send(prepHTML(html, content))
store = null
}

33
server/service.js Normal file
View File

@ -0,0 +1,33 @@
import axios from "axios";
const instance = axios.create({
baseURL: 'https://www.gitlink.org.cn', // 你的API基础URL
timeout: 20000, // 请求超时时间
headers: {
'Content-Type': 'application/json',
},
});
// 封装GET请求
function get(url, params) {
return instance.get(url, { params }).then(response => response.data);
}
// 封装POST请求
function post(url, data) {
return instance.post(url, data).then(response => response.data);
}
// 封装PUT请求
function put(url, data) {
return instance.put(url, data).then(response => response.data);
}
// 封装DELETE请求
function deleteRequest(url) {
return instance.delete(url).then(response => response.data);
}
export const getPathType = (pathname) => {
return get(`/api/owners/${pathname}.json`)
}

30
server/window.js Normal file
View File

@ -0,0 +1,30 @@
const jsdom = require("jsdom");
import fs from 'fs'
import path from 'path';
import axios from "axios";
const { JSDOM } = jsdom;
const html = fs.readFileSync(path.join(path.resolve('./build'),'index.html'),'utf-8');
const dom = new JSDOM(html);
const { window } = dom;
const $ = require( "jquery" )( window )
global.window = window;
global.domObj = dom;
global.document = window.document;
global.navigator = window.navigator;
global.localStorage = {}
// axios.get('https://gw.alipayobjects.com/os/lib/alipay/alex/2.0.19/bundle/alex.all.global.min.js')
// .then(response => {
// // 处理响应
// if (response) {
// // 这里你可以直接使用response.data而不是eval
// eval(response);
// }
// })
// .catch(error => {
// console.error('Error fetching data:', error);
// });

View File

@ -8,7 +8,7 @@ import {
} from 'react-router-dom';
import axios from 'axios';
import LoginDialog from './modules/login/LoginDialog';
import 'babel-polyfill';
// import 'babel-polyfill';
import Loading from './Loading';
import Loadable from 'react-loadable';
@ -22,11 +22,10 @@ import SiderBarHelp from './glcc/siderBarHelp';
import { SnackbarHOC } from 'educoder';
import { initAxiosInterceptors } from './AppConfig'
import { Provider } from 'react-redux';
import configureStore from './redux/stores/configureStore';
import cookie from 'react-cookies';
import InfosIndex from './forge/users/Index'
import OrganizeIndex from './forge/Team/Index'
const store = configureStore();
window.marked = marked;
const theme = createMuiTheme({
palette: {
@ -77,15 +76,15 @@ const http500 = Loadable({
loader: () => import('./modules/500/http500'),
loading: Loading,
})
const InfosIndex = Loadable({
loader: () => import('./forge/users/Index'),
loading: Loading,
})
// const InfosIndex = Loadable({
// loader: () => import('./forge/users/Index'),
// loading: Loading,
// })
// 组织
const OrganizeIndex = Loadable({
loader: () => import('./forge/Team/Index'),
loading: Loading,
})
// const OrganizeIndex = Loadable({
// loader: () => import('./forge/Team/Index'),
// loading: Loading,
// })
const Search = Loadable({
loader: () => import('./modules/search/'),
@ -328,7 +327,6 @@ class App extends Component {
render() {
const { pathType, pathName, mygetHelmetapi } = this.state;
return (
<Provider store={store}>
<ConfigProvider locale={zhCN}>
<MuiThemeProvider theme={theme}>
<LoginDialog {...this.props} {...this.state} Modifyloginvalue={() => this.Modifyloginvalue()}></LoginDialog>
@ -536,7 +534,6 @@ class App extends Component {
</Switch>
</MuiThemeProvider>
</ConfigProvider>
</Provider>
);
}
}

View File

@ -18,14 +18,14 @@ function locationurl(list) {
// TODO 开发期多个身份切换
let debugType = ""
if (isDev) {
const _search = window.location.search;
let parsed = {};
if (_search) {
parsed = queryString.parse(_search);
}
debugType = window.location.search.indexOf('debug=t') !== -1 ? 'teacher' :
window.location.search.indexOf('debug=s') !== -1 ? 'student' :
window.location.search.indexOf('debug=a') !== -1 ? 'admin' : parsed.debug || 'admin'
// const _search = window.location.search;
// let parsed = {};
// if (_search) {
// parsed = queryString.parse(_search);
// }
// debugType = window.location.search.indexOf('debug=t') !== -1 ? 'teacher' :
// window.location.search.indexOf('debug=s') !== -1 ? 'student' :
// window.location.search.indexOf('debug=a') !== -1 ? 'admin' : parsed.debug || 'admin'
}
window._debugType = debugType;
export function initAxiosInterceptors(props) {
@ -63,7 +63,7 @@ export function initAxiosInterceptors(props) {
});
axios.interceptors.response.use(function (response) {
if (response === undefined) {
if (response === undefined || !response.data) {
return
}
const config = response.config;
@ -94,13 +94,13 @@ export function initAxiosInterceptors(props) {
}
if (response.data.status === 404) {
let responseURL = response.request ? response.request.responseURL:'';
let responseURL = (response && response.request) ? response.request.responseURL:'';
// 组织和个人的拥有情况404不跳转
if (responseURL.indexOf('/api/users/') === -1 && responseURL.indexOf('/api/organizations/') === -1 ) {
// 邀请页面不进行404跳转
if( window.location.pathname.includes('/invite') && (responseURL.includes('/simple.json')||responseURL.includes('/detail.json')||responseURL.includes('/menu_list.json'))){
}else{
locationurl('/nopage');
// locationurl('/nopage');
}
}
}

View File

@ -3,9 +3,9 @@ import md5 from 'md5';
import {Input} from "antd";
const { Search } = Input;
const $ = window.$;
const isDev = window.location.port == 3007;
const isdev2= window.location.hostname ==='www.educoder.net'
// const $ = window.$;
const isDev = __SERVER__ ? false : window.location.port == 3007;
const isdev2= __SERVER__ ? false : window.location.hostname ==='www.educoder.net'
export const TEST_HOST = "https://testforgeplus.trustie.net/"
export function getImageUrl(path) {
// https://www.educoder.net
@ -40,7 +40,7 @@ export function getImageUrlAbsolute(path) {
// const local = 'http://localhost:3000'
path && !path.startsWith('/') && !path.startsWith('http') && (path = '/'.concat(path));
const local = 'https://testforgeplus.trustie.net';
const prod = window.location.origin;
const prod = __SERVER__ ? local : window.location.origin;
if (isDev) {
return `${local}${path}`
}else{
@ -198,7 +198,7 @@ function railsgettimess(proxy) {
}
}})
window.setTimeout(function () {
setTimeout(function () {
checkSubmitFlgs=false;
}, 2500);
}
@ -295,13 +295,13 @@ export function turnbar(str){
let s = str;
if(s && s.length>0){
if(s.indexOf("%")>-1){
s = s.replaceAll('%','_25');
s = s.split('%').join('_25');
}
if(s.indexOf("#")>-1){
s = s.replaceAll('#','%23');
s = s.split('#').join('%23');
}
if(s.indexOf("/")>-1){
s = s.replaceAll('/','%2F');
s = s.split('/').join('%2F');
}
}
return s;
@ -310,13 +310,13 @@ export function returnbar(str){
let s = str;
if(s && s.length>0){
if(str.indexOf("_25")>-1){
s = s.replaceAll('_25','%');
s = s.split('_25').join('%');
}
if(s.indexOf("%23")>-1){
s = s.replaceAll('%23','#');
s = s.split('%23').join('#');
}
if(s.indexOf("%2F")>-1){
s = s.replaceAll('%2F','/');
s = s.split('%2F').join('/');
}
}
return s;
@ -348,6 +348,9 @@ export function setSeoMeta(keyWords, title, description, url, owner, projectId)
if(owner)keyStatement += owner;
if(projectId)keyStatement += `/${projectId}`;
if (domObj) {
document = domObj.window.document
}
document.querySelector(`meta[property='og:title']`).content = title + keyStatement + ' for gitlink' + keyStatement + ' for git';
document.querySelector(`meta[property='og:url']`).content = window.location.origin + url;
document.querySelector(`meta[property='og:description']`).content = description + ' - ' + title + ' for gitlink' + keyStatement + ' for git';
@ -364,5 +367,27 @@ export function setSeoMeta(keyWords, title, description, url, owner, projectId)
document.querySelector(`meta[name='twitter:description']`).content = description + ' - ' + title + keyStatement + ' for gitlink' + keyStatement + ' for git';
document.querySelector(`link[rel='canonical']`).href = window.location.origin + url;
if (domObj) {
domObj.window.document.querySelector('meta[name="Keywords"]')
}
}
export function setDefaultMeta() {
if (domObj) {
document = domObj.window.document
}
document.querySelector(`meta[property='og:title']`).content = 'GitLink | 确实开源';
document.querySelector(`meta[property='og:url']`).content = 'https://gitlink.org.cn/';
document.querySelector(`meta[property='og:description']`).content = 'GitLink,新一代开源创新服务平台 分布式协作开发 一站式过程管理 高效流水线运维 多层次代码分析 多维度用户画像 分布式协作开发 基于Git打造分布式代码托管环境';
document.querySelector(`meta[property='og:image:alt']`).content = 'GitLink | 确实开源';
document.querySelector('meta[name="Keywords"]').content= 'gitLink,GitLink,gitlink,git,trustie,trustieforge,forge,开源,确实开源,代码托管,Git,开源,内源,项目管理,版本控制,开源代码,代码分享,项目协作,开源项目托管,免费代码托管,Git代码托管,Git托管服务,确实让创建更美好,协同开发平台';
document.querySelector(`meta[name='description']`).content = 'GitLink,新一代开源创新服务平台 分布式协作开发 一站式过程管理 高效流水线运维 多层次代码分析 多维度用户画像 分布式协作开发 基于Git打造分布式代码托管环境';
document.querySelector(`meta[name='go-import']`).content ='gitlink.org.cn git https://gitlink.org.cn';
document.querySelector(`meta[name='octolytics-dimension-user_login']`).content = 'GitLink';
document.querySelector(`meta[name='octolytics-dimension-repository_nwo']`).content = 'GitLink';
document.querySelector(`meta[name='octolytics-dimension-repository_network_root_nwo']`).content = 'GitLink';
document.querySelector(`meta[name='twitter:title']`).content = 'GitLink | 确实开源';
document.querySelector(`meta[name='twitter:description']`).content = 'GitLink,新一代开源创新服务平台 分布式协作开发 一站式过程管理 高效流水线运维 多层次代码分析 多维度用户画像 分布式协作开发 基于Git打造分布式代码托管环境';
document.querySelector(`link[rel='canonical']`).href = 'https://gitlink.org.cn';
}

View File

@ -1,4 +1,4 @@
const queryString = {
export const queryString = {
stringify: function(params) {
let paramsUrl = '';
for (let key in params) {
@ -47,5 +47,4 @@ const queryString = {
./node_modules/_query-string@6.1.0@query-string/index.js:8
Read more here: http://bit.ly/2tRViJ9
*/
module.exports = queryString
*/

View File

@ -9,7 +9,7 @@
import './index.scss';
import React, { useState } from 'react';
import { Form, Button, Input } from 'antd';
import QuillForEditor from '../../quillForEditor';
// import QuillForEditor from '../../quillForEditor';
const FormItem = Form.Item;
function CommentForm(props) {
@ -116,7 +116,7 @@ function CommentForm(props) {
)
}
<QuillForEditor
{/* <QuillForEditor
imgAttrs={{ width: '60px', height: '30px' }}
wrapStyle={{
height: showQuill ? 'auto' : '0px',
@ -131,7 +131,7 @@ function CommentForm(props) {
value={ctx}
showUploadImage={handleShowImage}
onContentChange={handleContentChange}
/>
/> */}
</FormItem>
<FormItem style={{ textAlign: 'right', display: showQuill ? 'block' : 'none' }}>
<Button onClick={handleCancle}>取消</Button>

View File

@ -11,7 +11,7 @@ export {
} from './UrlTool';
export { setmiyah as setmiyah } from './Component';
export { default as queryString } from './UrlTool2';
export { queryString } from './UrlTool2';
export { SnackbarHOC as SnackbarHOC } from './SnackbarHOC';
@ -68,7 +68,7 @@ export { default as ActionBtn } from './course/ActionBtn'
export { default as MarkdownToHtml } from './components/markdown/MarkdownToHtml'
export { default as QuillForEditor } from './quillForEditor'
// export { default as QuillForEditor } from './quillForEditor'
export { default as Clappr } from './components/media/Clappr'
export { default as AliyunUploader } from './components/media/AliyunUploader'

View File

@ -1,7 +1,7 @@
import './index.scss'
import 'quill/dist/quill.core.css' // 核心样式
import 'quill/dist/quill.snow.css' // 有工具栏
import 'quill/dist/quill.bubble.css' // 无工具栏
// import 'quill/dist/quill.core.css' // 核心样式
// import 'quill/dist/quill.snow.css' // 有工具栏
// import 'quill/dist/quill.bubble.css' // 无工具栏
import './font.css'
import React, { useState, useRef, useEffect } from 'react'
import Quill from 'quill'

View File

@ -1,5 +1,5 @@
import React, { useEffect, useRef, useMemo , useState } from 'react'
import 'katex/dist/katex.min.css';
// import 'katex/dist/katex.min.css';
import marked, { getTocContent, cleanToc, getMathExpressions, resetMathExpressions } from '../common/marked';
import 'code-prettify';
import dompurify from 'dompurify';
@ -93,7 +93,7 @@ export default ({
for(var x=0;x<issues.length;x++){
let item = issues[x];
let content = item.id ? `<a href="`+`/${owner}/${projectsId}/issues/${item.project_issues_index}`+`">#${item.project_issues_index}:${item.subject}</a>` : `<span>#${item.project_issues_index}(已删除)</span>`;
rs = rs.replaceAll(`<a href="`+`/${owner}/${projectsId}/issues/${item.project_issues_index}`+`">#${item.project_issues_index}</a>`,content);
rs = rs.split(`<a href="`+`/${owner}/${projectsId}/issues/${item.project_issues_index}`+`">#${item.project_issues_index}</a>`).join(content);
}
}
@ -102,7 +102,7 @@ export default ({
return renderToString(_unescape(expression) || '', { displayMode: type === 'block', throwOnError: false, output: 'html' })
})
rs = dompurify.sanitize(rs.replace(/▁/g, "▁▁▁"))
rs = rs.replaceAll('<img ', `<img onerror="javascript:this.src='${ imgError }';"`)
rs = rs.split('<img ').join(`<img onerror="javascript:this.src='${ imgError }';"`) // replaceall
resetMathExpressions()
return rs
}, [str,issues]);

View File

@ -3,6 +3,7 @@ import { WhiteBack , Box , LongWidth , ShortWidth , Gap , AlignCenter , FlexAJ
import { Dropdown , Menu , Divider , Spin, Button , Typography } from 'antd';
import { getImageUrl , turnbar , returnbar } from "educoder";
import { Link } from 'react-router-dom';
import { connect } from 'react-redux';
import { truncateCommitId } from "../common/util";
import CloneAddress from '../Branch/CloneAddress';
@ -12,7 +13,7 @@ import axios from 'axios';
import Path from './CoderDepotPath';
import Catalogue from './CoderDepotCatalogue';
import ReadMe from './CoderDepotReadme';
import CoderRootFileDetail from './CoderRootFileDetail';
// import CoderRootFileDetail from './CoderRootFileDetail';
import './Index.scss';
import Releases from '../Component/Releases';
import Contributors from '../Component/Contributors';
@ -28,30 +29,34 @@ import RenderHtml from '../../components/render-html';
*/
function CoderDepot(props){
// const [ projectDetail , setProjectDetail ]= useState(undefined);
const [ inviteCode , setInviteCode ] = useState(undefined);
let { platform , mirror_status , bannerList , projectDetail, projectEntries, projectReadMe } = props ;
projectDetail = props.defaultDetail || projectDetail || {}
const [ inviteCode , setInviteCode ] = useState(projectDetail.invite_code || undefined);
const [ treeValue , setTreeValue ] = useState(undefined);
const [ treeValuePath , setTreeValuePath ] = useState(undefined);
const [ lastCommit,setLastCommit ] = useState(undefined);
const [ lastCommitAuthor,setLastCommitAuthor ] = useState(undefined);
const [ lastCommit,setLastCommit ] = useState((projectEntries.last_commit && projectEntries.last_commit.commit) || undefined);
const [ lastCommitAuthor,setLastCommitAuthor ] = useState((projectEntries.last_commit && projectEntries.last_commit.author) || undefined);
const [ type ,setType ] = useState('dir');
const [ hide , setHide ] = useState(true);
const [ hideBtn , setHideBtn ] = useState(false);
const [ commitCount ,setCommitCount ] = useState(0);
const [ dirInfo ,setDirInfo ] = useState(undefined);//
const [ commitCount ,setCommitCount ] = useState(projectEntries.commits_count || 0);
const [ dirInfo ,setDirInfo ] = useState(projectEntries.entries || undefined);//
const [ fileInfo ,setFileInfo ] = useState(undefined);//
const [ zip_url , setZip_url ] = useState(undefined);
const [ tar_url , setTar_url ] = useState(undefined);
const [ zip_url , setZip_url ] = useState(projectEntries.zip_url || undefined);
const [ tar_url , setTar_url ] = useState(projectEntries.tar_url || undefined);
const [ readOnly , setReadOnly] = useState(undefined);
const [ isSpin , setIsSpin] = useState(false);
const [ visible ,setVisible ] = useState(false);
const [ mainFlag ,setMainFlag ] = useState(false);
const [ openModal , setOpenModal ] = useState(false);
const [ desc , setDesc ] = useState(undefined);
const [ website , setWebsite ] = useState(undefined);
const [ lesson_url , setLessonUrl ] = useState(undefined);
const [ topics, setTopics] = useState(undefined);
const [ readme , setReadme ] = useState(undefined);
const [ defaultBranch , setDefaultBranch ] = useState(undefined);
const [ desc , setDesc ] = useState(projectDetail.description || undefined);
const [ website , setWebsite ] = useState(projectDetail.website ||undefined);
const [ lesson_url , setLessonUrl ] = useState(projectDetail.lesson_url || undefined);
const [ topics, setTopics] = useState(projectDetail.topics || undefined);
const [ readme , setReadme ] = useState(projectReadMe || undefined);
const [ defaultBranch , setDefaultBranch ] = useState(projectDetail.default_branch || undefined);
const [ editReadme , setEditReadme ] = useState(false);
const [ pullsFlag , setPullsFlag ] = useState(true);
const [ issuesFlag , setIssuesFlag ] = useState(true);
@ -63,7 +68,6 @@ function CoderDepot(props){
branchName = returnbar(branchName);
let pathname = props.history.location.pathname;
let search = props.history.location.search
const { platform , mirror_status , bannerList , projectDetail } = props ;
//distribution
const distribution = projectDetail && projectDetail.type ===0 && ( projectDetail.permission && projectDetail.permission !== "Reporter");
@ -109,9 +113,9 @@ function CoderDepot(props){
const { author, name, description, default_branch} = projectDetail;
if(branchName && branchName !== default_branch){
// /
document.title = `${author.name}/${name}-${branchName}-for gitlink;for git`;
document.title = `${author && author.name}/${name}-${branchName}-for gitlink;for git`;
}else{
document.title = `${author.name}/${name}${description?': '+description:''}-for gitlink;for git` ;
document.title = `${author && author.name}/${name}${description?': '+description:''}-for gitlink;for git` ;
}
}
}, [treeValuePath, projectDetail, branchName])
@ -134,7 +138,9 @@ function CoderDepot(props){
setType("file");
}else{
setTreeValue(undefined);
getDirInfo(branchName || defaultBranch);
if (!projectEntries) {
getDirInfo(branchName || defaultBranch);
}
setType("dir");
}
}
@ -142,8 +148,8 @@ function CoderDepot(props){
//
useEffect(()=>{
if (projectsId && owner && defaultBranch && mirror_status===0 && !search){
let b = turnbar(branchName) ;
let b = turnbar(branchName) ;
if (projectsId && owner && defaultBranch && mirror_status===0 && !search && pathname.indexOf(`/tree/${b}/`) > -1){
let url = pathname.split(`/tree/${b}/`)[1];
getFileInfo(url,branchName);
}
@ -565,18 +571,18 @@ function CoderDepot(props){
})
}
{
fileInfo &&
<CoderRootFileDetail
{...props}
detail={fileInfo}
readOnly={readOnly}
md={mdFlag}
onEdit={onEdit}
currentBranch={branchName || defaultBranch}
branch={branchName || defaultBranch}
type={projectDetail.type}
treeValuePath={treeValuePath}
></CoderRootFileDetail>
// fileInfo &&
// <CoderRootFileDetail
// {...props}
// detail={fileInfo}
// readOnly={readOnly}
// md={mdFlag}
// onEdit={onEdit}
// currentBranch={branchName || defaultBranch}
// branch={branchName || defaultBranch}
// type={projectDetail.type}
// treeValuePath={treeValuePath}
// ></CoderRootFileDetail>
}
</ul>
</div>
@ -671,4 +677,20 @@ function CoderDepot(props){
</WhiteBack>
)
}
export default CoderDepot;
const mapStateToProps = (state) => {
const {
defaultDetail,
projectEntries,
projectReadMe
} = state.projectReducer;
return {
defaultDetail,
projectEntries,
projectReadMe
};
}
export default connect(
mapStateToProps
)(CoderDepot);

View File

@ -8,7 +8,7 @@ const $ = window.$;
function CoderDepotReadme({ operate , history , readme , ChangeFile }){
const [ menuList ,setMenuList ] = useState(undefined);
const [ content ,setContent ] = useState(undefined);
const [ content ,setContent ] = useState( (readme && readme.replace_content) || undefined );
useEffect(()=>{
if(readme && readme.replace_content){

View File

@ -2,21 +2,26 @@ import React, { Component } from 'react';
import { Spin, Tooltip } from 'antd';
import { Link, Route, Switch } from 'react-router-dom';
import { withRouter } from "react-router";
import { connect } from 'react-redux';
import { Content, AlignTop } from '../Component/layout';
import DetailBanner from './sub/DetailBanner';
import CoderDepot from './CoderDepot'
import cookie from 'react-cookies';
import { setSeoMeta } from 'educoder';
import { Base64 } from 'js-base64';
import { setProject, setProjectDetail, setProjectEntries, setProjectMenuList, setProjectReadMe } from '../../redux/actions/server';
import actions from '../../redux/actions';
import types from '../../redux/actions/actionTypes'
import { getProjectFunc, getProjectDetailFunc } from '../../services/project'
import '../css/index.scss'
import './list.scss';
import { ImageLayerOfCommentHOC } from "../../modules/page/layers/ImageLayerOfCommentHOC";
import Loadable from 'react-loadable';
import Loading from '../../Loading';
import axios from 'axios';
import { async } from 'q';
const Setting = Loadable({
loader: () => import('../Settings/Index'),
@ -90,10 +95,10 @@ const CoderRootCommit = Loadable({
loader: () => import('./CoderRootCommit'),
loading: Loading,
})
const CoderDepot = Loadable({
loader: () => import('./CoderDepot'),
loading: Loading,
})
// const CoderDepot = Loadable({
// loader: () => import('./CoderDepot'),
// loading: Loading,
// })
const TrendsIndex = Loadable({
loader: () => import('../Activity/Activity'),
@ -165,11 +170,12 @@ function checkPathname(projectsId, owner, pathname) {
}
return name;
}
class Detail extends Component {
constructor(props) {
super(props);
this.state = {
projectDetail: undefined,
projectDetail: props.defaultDetail || undefined,
isManager: false,
isReporter: false,
isDeveloper: false,
@ -182,7 +188,7 @@ class Detail extends Component {
http_url: undefined,
branchs: undefined,
branchList: undefined,
project: null,
project: props.projectBase || undefined,
firstSync: false,
secondSync: false,
open_devops: false,
@ -192,22 +198,33 @@ class Detail extends Component {
// 非本平台项目
platform: false,
mirror_status:2
mirror_status:2,
bannerList: props.projectMenu || []
}
}
componentDidMount = () => {
this.getProject();
// this.getProject();
let history = this.props.location;
this.clearIssueCookies(history);
if (this.props.clearProject) {
const typeList = [ types.GET_PROJECT_BASE, types.GET_PROJECT_DETAIL, types.GET_PROJECT_ENTRIES, types.GET_PROJECT_MENU, types.GET_PROJECT_README ]
typeList.forEach(e => {
this.props.clearProject(e)
})
}
}
componentDidUpdate = (prevState) => {
componentDidUpdate = async (prevState) => {
let prevParam = prevState.match.params;
let propsParam = this.props.match.params;
if (prevState && this.props && (prevParam.projectsId !== propsParam.projectsId || prevParam.owner !== propsParam.owner)) {
this.getProject();
const { projectsId, owner } = this.props.match.params;
const data = await getProjectFunc(owner, projectsId);
if (data.data) {
this.getProject(data.data)
}
}
this.props.history.listen((history) => {
@ -225,56 +242,71 @@ class Detail extends Component {
cookie.save('issuestates', undefined, { expires: 0, path: `/` });
}
}
async componentWillMount() {
const { projectsId, owner } = this.props.match.params;
if (!this.props.projectBase) {
const data = await getProjectFunc(owner, projectsId);
if (data.data) {
this.getProject(data.data)
}
} else {
this.getProject(this.props.projectBase)
}
}
componentWillUnmount() {
this.timerChannel && clearTimeout(this.timerChannel);
}
getProject = (num) => {
getProject = async (data) => {
const { projectsId, owner } = this.props.match.params;
const url = `/${owner}/${projectsId}/simple.json`;
axios.get(url).then((result) => {
if (result && result.data) {
this.setState({
project: result.data,
open_devops: result.data.open_devops,
platform: result.data.platform && result.data.platform !== 'educoder'
})
if (result.data.type !== 0 && result.data.mirror_status === 1) {
console.log("--------start channel --------");
// 是镜像项目,且未完成迁移
this.canvasChannel();
if (num) {
this.setState({
secondSync: true,
firstSync: false
})
} else {
this.setState({
firstSync: true,
secondSync: false
})
}
this.setState({
mirror_status:1
})
} else if (result.data.mirror_status === 2) {
this.setState({
mirror_status:2,
firstSync: false
})
this.deleteProjectBack();
} else {
this.setState({
firstSync: false,
secondSync: false,
mirror_status:0
})
this.getBanner();
this.getDetail();
}
}
this.setState({
project: data,
open_devops: data.open_devops,
platform: data.platform && data.platform !== 'educoder'
})
if (data.type !== 0 && data.mirror_status === 1) {
console.log("--------start channel --------");
// 是镜像项目,且未完成迁移
this.canvasChannel();
if (num) {
this.setState({
secondSync: true,
firstSync: false
})
} else {
this.setState({
firstSync: true,
secondSync: false
})
}
this.setState({
mirror_status:1
})
} else if (data.mirror_status === 2) {
this.setState({
mirror_status:2,
firstSync: false
})
this.deleteProjectBack();
} else {
this.setState({
firstSync: false,
secondSync: false,
mirror_status:0
})
this.getBanner(this.props.projectMenu)
let projectdata
if (this.props.defaultDetail) {
projectdata = this.props.defaultDetail
} else {
projectdata = await getProjectDetailFunc(owner, projectsId, !window.location.host).data;
}
if (projectdata) {
this.getDetail(projectdata)
}
}
}
// 工作流激活后修改状态
@ -358,58 +390,67 @@ class Detail extends Component {
});
}
getDetail = () => {
getDetail = async (data) => {
const { projectsId, owner } = this.props.match.params;
const url = `/${owner}/${projectsId}/detail.json`;
axios.get(url).then((result) => {
if (result && result.data) {
if (result.data.status === 404) {
if (window.location.pathname.includes('/invite')) {
let inviteString = window.location.search && window.location.search.split('?invite=')[1];
let data = inviteString && JSON.parse(Base64.decode(inviteString));
const { project = {}, projectDetail = {} } = this.state
this.setState({
project: Object.assign(project, { author: { name: data.ownerName } }),
projectDetail: Object.assign(projectDetail, { name: data.projectName })
});
} else {
this.props.history.push('/nopage');
}
} else {
this.setState({
projectDetail: result.data,
project_id: result.data.project_id,
isManager: result.data.permission && (result.data.permission === "Manager" || result.data.permission === "Admin" || result.data.permission === "Owner"),
isReporter: result.data.permission && result.data.permission === "Reporter",
isDeveloper: result.data.permission && result.data.permission === "Developer",
http_url: result.data.clone_url,
praised: result.data.praised,
watched: result.data.watched,
watchers_count: result.data.watchers_count,
praises_count: result.data.praises_count,
forked_count: result.data.forked_count,
defaultBranch: result.data.default_branch
});
// seo优化设置
let keyWords=`${owner},${projectsId},${result.data.author.name},`;
let title= `${owner}/${projectsId}${result.data.description?''+result.data.description:''}`;
setSeoMeta(keyWords,title,result.data.description,`/${owner}/${projectsId}`,owner,projectsId);
}
if (!data) {
const url = `/${owner}/${projectsId}/detail.json`;
const res = await axios.get(url)
if (res.data) {
data = res.data
}
}).catch((error) => { })
}
if (data) {
if (data.status === 404) {
if (window.location.pathname.includes('/invite')) {
let inviteString = window.location.search && window.location.search.split('?invite=')[1];
let inviteData = inviteString && JSON.parse(Base64.decode(inviteString));
const { project = {}, projectDetail = {} } = this.state
this.setState({
project: Object.assign(project, { author: { name: inviteData.ownerName } }),
projectDetail: Object.assign(projectDetail, { name: inviteData.projectName })
});
} else {
this.props.history.push('/nopage');
}
} else {
this.setState({
projectDetail: data,
project_id: data.project_id,
isManager: data.permission && (data.permission === "Manager" || data.permission === "Admin" || data.permission === "Owner"),
isReporter: data.permission && data.permission === "Reporter",
isDeveloper: data.permission && data.permission === "Developer",
http_url: data.clone_url,
praised: data.praised,
watched: data.watched,
watchers_count: data.watchers_count,
praises_count: data.praises_count,
forked_count: data.forked_count,
defaultBranch: data.default_branch
});
// seo优化设置
let keyWords=`${owner},${projectsId},${data.author.name},`;
let title= `${owner}/${projectsId}${data.description?''+data.description:''}`;
setSeoMeta(keyWords,title,data.description,`/${owner}/${projectsId}`,owner,projectsId);
}
}
}
// 获取动态导航栏菜单
getBanner() {
async getBanner(data) {
const { projectsId, owner } = this.props.match.params;
const url = `/${owner}/${projectsId}/menu_list.json`;
axios.get(url).then(result => {
if (result) {
this.setState({
bannerList: result.data
})
if (!data) {
const url = `/${owner}/${projectsId}/menu_list.json`;
const res = await axios.get(url)
if (res.data) {
data = res.data
}
}).catch(error => { })
}
if (data) {
this.setState({
bannerList: data
})
}
}
// 关注和取消关注
@ -879,7 +920,42 @@ class Detail extends Component {
}
}
export default withRouter(ImageLayerOfCommentHOC({
imgSelector: ".imageLayerParent img, .imageLayerParent .imageTarget",
parentSelector: ".newContainer",
})(Detail));
const mapStateToProps = (state) => {
const {
projectBase,
defaultDetail,
projectMenu,
} = state.projectReducer;
return {
projectBase,
defaultDetail,
projectMenu,
};
}
const mapDispatchToProps = (dispatch) => ({
clearProject: (type) => dispatch(actions.clearProject(type)),
});
Detail.preFetch = ({ store, match, query }) => {
return new Promise(async function(resolve, reject) {
let { dispatch, getState } = store;
// const { owner,projectsId } = match.params
const promises = [
dispatch(setProjectDetail(match.params)),
dispatch(setProject(match.params)),
dispatch(setProjectEntries({...match.params, branch: 'master'})),
dispatch(setProjectMenuList(match.params)),
dispatch(setProjectReadMe({...match.params, branch: 'master'}))
];
const data = await Promise.all(promises);
resolve({
code: 200,
});
});
};
export default withRouter(connect(
mapStateToProps,
mapDispatchToProps
)(Detail));

View File

@ -28,16 +28,16 @@ class IndexItem extends Component {
<img className="p-r-photo" alt="" src={item.author && item.author.image_url} ></img>
</a>
:
<Link to={`/${item.author && item.author.login}`} className="show-user-link">
<a href="javascript:void(0)" onClick={() => { window.location.href = `/${item.author && item.author.login}/${item.identifier}` }} className="show-user-link">
<img className="p-r-photo" alt="" src={getImageUrl(`/${item.author && item.author.image_url}`)} ></img>
</Link>
</a>
}
<div className="p-r-Infos">
<div className="p-r-name">
<AlignCenter>
<Link to={`/${item.author.login}/${item.identifier}`} title={`${item.author.name}/${item.name}`} className="color-grey-3 font-18 task-hide " style={{maxWidth: 470 }}>
<a href="javascript:void(0)" onClick={() => { window.location.href = `/${item.author && item.author.login}/${item.identifier}` }} title={`${item.author.name}/${item.name}`} className="color-grey-3 font-18 task-hide " style={{maxWidth: 470 }}>
{item.author.name}/{item.name}
</Link>
</a>
{ !item.is_public && <span className="privateTag">私有</span> }
{
item.forked_from_project_id ?

View File

@ -114,7 +114,7 @@ function Index() {
}
<div className="itemTitle">
<div className="item-title-infos">
<Link to={`/${i.author && i.author.login}/${i.identifier}`} className="infotitle task-hide">{i.author && i.author.name}/{i.name}</Link>
<a href="javascript:void(0)" onClick={() => { window.location.href = `/${i.author && i.author.login}/${i.identifier}` }} className="infotitle task-hide">{i.author && i.author.name}/{i.name}</a>
{i.praises_count > 0 ? <span><i className="iconfont icon-xingzhuang mr3 font-14"></i>{i.praises_count}</span> :"" }
{i.forked_count > 0 ? <span><i className="iconfont icon-yifuke_icon mr3 font-14"></i>{i.forked_count}</span>:""}
</div>

View File

@ -39,7 +39,7 @@ const ProjectDetail = Loadable({
loader: () => import("../Main/Detail"),
loading: Loading,
});
export default withRouter(CNotificationHOC()(SnackbarHOC()(TPMIndexHOC(
const team = CNotificationHOC()(SnackbarHOC()(TPMIndexHOC(
((props)=>{
return (
<div className="newMain">
@ -114,4 +114,8 @@ export default withRouter(CNotificationHOC()(SnackbarHOC()(TPMIndexHOC(
</div>
)
})
))))
)))
team.preFetch = ProjectDetail.preFetch
export default withRouter(team)

View File

@ -87,7 +87,7 @@ function ConcentrateProject({
list.map((i,k)=>{
return(
<li key={i.id}>
<Link to={`/${i.author && i.author.login}/${i.identifier}`} className="name task-hide">{i.name}</Link>
<a href="javascript:void(0)" onClick={() => { window.location.href = `/${i.author && i.author.login}/${i.identifier}`}}className="name task-hide">{i.name}</a>
<p className="task-hide desc">{i.description}</p>
{/* 项目标签 */}
{i.topics && <div className='viewProListTopics mt10'>

View File

@ -1,4 +1,4 @@
import React from 'react';
import React, { Component } from 'react';
import { Route, Switch } from "react-router-dom";
import Loadable from "react-loadable";
import Loading from "../../Loading";
@ -15,32 +15,89 @@ const ProjectDetail = Loadable({
loader: () => import("../Main/Detail"),
loading: Loading,
});
export default withRouter(
(CNotificationHOC()(SnackbarHOC()(TPMIndexHOC((props) => {
class DetailTop extends Component {
constructor(props) {
super(props);
this.state = {}
}
getProject = (num) => {
const { projectsId, owner } = this.props.match.params;
const url = `https://www.gitlink.org.cn/${owner}/${projectsId}/simple.json`;
axios.get(url).then((result) => {
if (result && result.data) {
this.setState({
project: result.data,
open_devops: result.data.open_devops,
platform: result.data.platform && result.data.platform !== 'educoder'
})
if (result.data.type !== 0 && result.data.mirror_status === 1) {
console.log("--------start channel --------");
//
this.canvasChannel();
if (num) {
this.setState({
secondSync: true,
firstSync: false
})
} else {
this.setState({
firstSync: true,
secondSync: false
})
}
this.setState({
mirror_status:1
})
} else if (result.data.mirror_status === 2) {
this.setState({
mirror_status:2,
firstSync: false
})
this.deleteProjectBack();
} else {
this.setState({
firstSync: false,
secondSync: false,
mirror_status:0
})
this.getBanner();
this.getDetail();
}
}
})
}
render() {
//
let secondRouter = '';
if (props.location.pathname) {
secondRouter = props.location.pathname.split('/')[2];
if (this.props.location.pathname) {
secondRouter = this.props.location.pathname.split('/')[2];
}
let userRouterArr = ['statistics', 'projects', 'notice', 'devops', 'organizes', 'info', 'following', 'followers', 'password' , "general", "blockchain"];
return (
<Switch>
{secondRouter && (!userRouterArr.includes(secondRouter)) ? <Route
path="/:owner/:projectsId"
render={(p) => (
<ProjectDetail {...props} {...p} />
<ProjectDetail {...this.props} {...p} />
)}
></Route> : <Route
path="/:username"
render={(p) => (
<Infos {...props} {...p} />
<Infos {...this.props} {...p} />
)}
></Route>}
</Switch>
)
}))))
)
}
}
const user = (CNotificationHOC()(SnackbarHOC()(TPMIndexHOC(DetailTop))))
user.preFetch = ProjectDetail.preFetch
export default withRouter(user)

View File

@ -7,20 +7,27 @@ import './index.css';
import App from './App';
import { configureUrlQuery } from 'react-url-query';
import history from './history';
import configureStore from './redux/stores/configureStore';
import { Provider } from 'react-redux';
configureUrlQuery({ history });
const store = configureStore();
window.__useKindEditor = false;
const render = (Component) => {
ReactDOM.render(
<AppContainer >
ReactDOM.hydrate(
<AppContainer>
{/* <Component /> */}
<Provider store={store}>
<BrowserRouter basename='/'>
<Route path={`/`} component={App}></Route>
<Route path='/' component={App}></Route>
</BrowserRouter>
</Provider>
</AppContainer>,
document.getElementById('root')
)

View File

@ -6,9 +6,9 @@
* @LastEditors: tangjiang
* @LastEditTime: 2019-12-02 16:33:35
*/
import 'quill/dist/quill.core.css';
import 'quill/dist/quill.bubble.css';
import 'quill/dist/quill.snow.css';
// import 'quill/dist/quill.core.css';
// import 'quill/dist/quill.bubble.css';
// import 'quill/dist/quill.snow.css';
import './index.scss';
import React, { useState, useImperativeHandle, useRef, useEffect } from 'react';
import { Form, Input, InputNumber, Button, Select } from 'antd';

View File

@ -11,7 +11,7 @@
// import 'quill/dist/quill.snow.css';
// import 'katex/dist/katex.css';
import './index.scss';
import 'katex/dist/katex.min.css';
// import 'katex/dist/katex.min.css';
import React from 'react';
import katex from 'katex';
const Quill = require('quill');

View File

@ -331,7 +331,7 @@ class LoginDialog extends Component {
this.setState({
isphone: flag,
//查询第三方登录信息
settings: JSON.parse(localStorage.getItem("chromesetting")),
settings: JSON.parse(localStorage.getItem("chromesetting") || '{}'),
})
if (this.props.isRender != undefined) {

View File

@ -17,7 +17,7 @@ export function TPMIndexHOC(WrappedComponent) {
return class II extends React.Component {
constructor(props) {
super(props);
window.$('#root').css('position', 'relative');
// window.$('#root').css('position', 'relative');
this.state = {
tpmLoading: true,
@ -61,7 +61,13 @@ export function TPMIndexHOC(WrappedComponent) {
window.removeEventListener('keyup', this.keyupListener)
}
componentWillMount() {
this.fetchUsers();
if (!__SERVER__) {
this.fetchUsers();
} else {
this.setState({
current_user: {}
})
}
}
componentDidMount() {

View File

@ -106,7 +106,13 @@ const types = {
IS_SHOW_WXCODE_TEST_CASES: 'IS_SHOW_WXCODE_TEST_CASES',
SHOW_WX_CODE_LOADING: 'SHOW_WX_CODE_LOADING',
SHOW_WX_CODE_DIALOG: 'SHOW_WX_CODE_DIALOG',
SET_GOLD_AND_EXPERIENCE: 'SET_GOLD_AND_EXPERIENCE'
SET_GOLD_AND_EXPERIENCE: 'SET_GOLD_AND_EXPERIENCE',
/** 服务端渲染 */
GET_PROJECT_BASE: 'GET_PROJECT_BASE',
GET_PROJECT_DETAIL: 'GET_PROJECT_DETAIL',
GET_PROJECT_ENTRIES: 'GET_PROJECT_ENTRIES',
GET_PROJECT_MENU: 'GET_PROJECT_MENU',
GET_PROJECT_README: 'GET_PROJECT_README',
}
export default types;

View File

@ -126,6 +126,16 @@ import {
changeWXCodeEvaluateLoading,
changeWXCodeEvaluateDialog
} from './wxCode';
import {
setProject,
clearProject,
setProjectDetail,
setProjectEntries,
setProjectMenuList,
setProjectReadMe
} from './server.js';
export default {
toggleTodo,
getOJList,
@ -223,5 +233,12 @@ export default {
evaluateWxCode,
showWXCodeTextCase,
changeWXCodeEvaluateLoading,
changeWXCodeEvaluateDialog
changeWXCodeEvaluateDialog,
// 服务端渲染
setProject,
clearProject,
setProjectDetail,
setProjectEntries,
setProjectMenuList,
setProjectReadMe
}

View File

@ -0,0 +1,79 @@
import types from './actionTypes';
import { getProjectFunc, getProjectDetailFunc, getProjectEntriesFunc, getBannerFunc, getProjectReadMe } from '../../services/project'
// 获取项目信息
export const setProject = ({ owner,projectsId }) => {
return (dispatch) => {
return getProjectFunc(owner, projectsId).then(res => {
dispatch({
type: types.GET_PROJECT_BASE,
payload: res.data
});
return res.data
})
}
}
export const clearProject = (type) => {
return {
type: type,
payload: ''
}
}
// 获取项目详情信息
export const setProjectDetail = ({ owner,projectsId }) => {
return (dispatch) => {
return getProjectDetailFunc(owner, projectsId).then(res => {
dispatch({
type: types.GET_PROJECT_DETAIL,
payload: res.data
});
return res.data
})
}
}
// 获取文件列表
export const setProjectEntries = ({ owner,projectsId, branch }) => {
return (dispatch) => {
return getProjectEntriesFunc(owner, projectsId, branch).then(res => {
dispatch({
type: types.GET_PROJECT_ENTRIES,
payload: res.data
});
return res.data
})
}
}
// 获取banner
export const setProjectMenuList = ({ owner,projectsId }) => {
return (dispatch) => {
return getBannerFunc(owner, projectsId).then(res => {
dispatch({
type: types.GET_PROJECT_MENU,
payload: res.data
});
return res.data
})
}
}
// 获取readMe
export const setProjectReadMe = ({ owner,projectsId, branch }) => {
return (dispatch) => {
return getProjectReadMe(owner, projectsId, branch).then(res => {
if (res && res.data) {
dispatch({
type: types.GET_PROJECT_README,
payload: res.data
});
return res.data
}
})
}
}

View File

@ -18,6 +18,7 @@ import commentReducer from './commentReducer';
import tpiReducer from './tpiReducer';
import staticReducer from './staticReducer';
import wxcodeReducer from './wxcodeReducer';
import projectReducer from './projectReducer'
export default combineReducers({
testReducer,
@ -30,5 +31,6 @@ export default combineReducers({
commentReducer,
tpiReducer,
staticReducer,
wxcodeReducer
wxcodeReducer,
projectReducer
});

View File

@ -0,0 +1,48 @@
import types from "../actions/actionTypes";
const initialState = {
defaultDetail: '', // 当前项目基本信息
projectBase: '', // 当前项目详情
projectEntries: '', // 当前文件列表
projectMenu: '', // 当前menu
projectReadMe: '', // 当前readme
};
const projectReducer = (state = initialState, action) => {
switch (action.type) {
case types.GET_PROJECT_DETAIL:
return {
...state,
defaultDetail: action.payload
}
case types.GET_PROJECT_BASE:
return {
...state,
projectBase: action.payload
}
case types.GET_PROJECT_ENTRIES:
return {
...state,
projectEntries: action.payload
}
case types.GET_PROJECT_MENU:
return {
...state,
projectMenu: action.payload
}
case types.GET_PROJECT_README:
return {
...state,
projectReadMe: action.payload
}
default:
return {
...state
}
}
}
export default projectReducer;
export {
projectReducer
};

View File

@ -12,7 +12,13 @@ import rootReducer from '../reducers';
const configureStore = () => createStore(
rootReducer,
window.__initState__,
applyMiddleware(thunk)
);
export default configureStore;
export const serverStore = ()=>{
return createStore(rootReducer, applyMiddleware(thunk));
}

30
src/services/project.js Normal file
View File

@ -0,0 +1,30 @@
import axios from 'axios';
import { url } from '../../config/ssrUrl';
const baseUrl = `${url}/api`
//
export async function getProjectFunc (owner, projectsId) {
const url = `${baseUrl}/${owner}/${projectsId}/simple.json`;
return axios.get(url);
}
export async function getProjectDetailFunc (owner, projectsId) {
const url = `${baseUrl}/${owner}/${projectsId}/detail.json`;
return axios.get(url);
}
export async function getProjectEntriesFunc (owner, projectsId, branch) {
const url = `${baseUrl}/${owner}/${projectsId}/entries.json`;
return axios.get(url, {params: { ref: branch }});
}
export async function getBannerFunc (owner, projectsId) {
const url = `${baseUrl}/${owner}/${projectsId}/menu_list.json`;
return axios.get(url);
}
export async function getProjectReadMe (owner, projectsId, branch) {
const url = `${baseUrl}/${owner}/${projectsId}/readme.json`;
return axios.get(url, {params: { ref: branch }});
}