diff --git a/config/config.ts b/config/config.ts index 7c27b33..6416cc7 100644 --- a/config/config.ts +++ b/config/config.ts @@ -1,159 +1,159 @@ -// https://umijs.org/config/ -import { defineConfig } from '@umijs/max'; -import { join } from 'path'; -import defaultSettings from './defaultSettings'; -import proxy from './proxy'; -import routes from './routes'; - -const { REACT_APP_ENV = 'dev' } = process.env; - -export default defineConfig({ - /** - * @name 开启 hash 模式 - * @description 让 build 之后的产物包含 hash 后缀。通常用于增量发布和避免浏览器加载缓存。 - * @doc https://umijs.org/docs/api/config#hash - */ - hash: true, - - /** - * @name 兼容性设置 - * @description 设置 ie11 不一定完美兼容,需要检查自己使用的所有依赖 - * @doc https://umijs.org/docs/api/config#targets - */ - // targets: { - // ie: 11, - // }, - /** - * @name 路由的配置,不在路由中引入的文件不会编译 - * @description 只支持 path,component,routes,redirect,wrappers,title 的配置 - * @doc https://umijs.org/docs/guides/routes - */ - // umi routes: https://umijs.org/docs/routing - routes, - /** - * @name 主题的配置 - * @description 虽然叫主题,但是其实只是 less 的变量设置 - * @doc antd的主题设置 https://ant.design/docs/react/customize-theme-cn - * @doc umi 的theme 配置 https://umijs.org/docs/api/config#theme - */ - theme: { - // 如果不想要 configProvide 动态设置主题需要把这个设置为 default - // 只有设置为 variable, 才能使用 configProvide 动态设置主色调 - 'root-entry-name': 'variable', - }, - /** - * @name moment 的国际化配置 - * @description 如果对国际化没有要求,打开之后能减少js的包大小 - * @doc https://umijs.org/docs/api/config#ignoremomentlocale - */ - ignoreMomentLocale: true, - /** - * @name 代理配置 - * @description 可以让你的本地服务器代理到你的服务器上,这样你就可以访问服务器的数据了 - * @see 要注意以下 代理只能在本地开发时使用,build 之后就无法使用了。 - * @doc 代理介绍 https://umijs.org/docs/guides/proxy - * @doc 代理配置 https://umijs.org/docs/api/config#proxy - */ - proxy: proxy[REACT_APP_ENV as keyof typeof proxy], - /** - * @name 快速热更新配置 - * @description 一个不错的热更新组件,更新时可以保留 state - */ - fastRefresh: true, - //============== 以下都是max的插件配置 =============== - /** - * @name 数据流插件 - * @@doc https://umijs.org/docs/max/data-flow - */ - model: {}, - /** - * 一个全局的初始数据流,可以用它在插件之间共享数据 - * @description 可以用来存放一些全局的数据,比如用户信息,或者一些全局的状态,全局初始状态在整个 Umi 项目的最开始创建。 - * @doc https://umijs.org/docs/max/data-flow#%E5%85%A8%E5%B1%80%E5%88%9D%E5%A7%8B%E7%8A%B6%E6%80%81 - */ - initialState: {}, - /** - * @name layout 插件 - * @doc https://umijs.org/docs/max/layout-menu - */ - title: '青岛智慧就业服务系统', - layout: { - locale: false, - ...defaultSettings, - }, - /** - * @name moment2dayjs 插件 - * @description 将项目中的 moment 替换为 dayjs - * @doc https://umijs.org/docs/max/moment2dayjs - */ - moment2dayjs: { - preset: 'antd', - plugins: ['duration'], - }, - /** - * @name 国际化插件 - * @doc https://umijs.org/docs/max/i18n - */ - locale: { - // default zh-CN - default: 'zh-CN', - antd: false, - // default true, when it is true, will use `navigator.language` overwrite default - baseNavigator: false, - }, - /** - * @name antd 插件 - * @description 内置了 babel import 插件 - * @doc https://umijs.org/docs/max/antd#antd - */ - antd: {}, - /** - * @name 网络请求配置 - * @description 它基于 axios 和 ahooks 的 useRequest 提供了一套统一的网络请求和错误处理方案。 - * @doc https://umijs.org/docs/max/request - */ - request: {}, - /** - * @name 权限插件 - * @description 基于 initialState 的权限插件,必须先打开 initialState - * @doc https://umijs.org/docs/max/access - */ - access: {}, - /** - * @name 中额外的 script - * @description 配置 中额外的 script - */ - headScripts: [ - // 解决首次加载时白屏的问题 - { src: '/qingdao/scripts/loading.js', async: true }, - ], - //================ pro 插件配置 ================= - presets: ['umi-presets-pro'], - /** - * @name openAPI 插件的配置 - * @description 基于 openapi 的规范生成serve 和mock,能减少很多样板代码 - * @doc https://pro.ant.design/zh-cn/docs/openapi/ - */ - openAPI: [ - { - requestLibPath: "import { request } from '@umijs/max'", - // 或者使用在线的版本 - // schemaPath: "https://gw.alipayobjects.com/os/antfincdn/M%24jrzTTYJN/oneapi.json" - schemaPath: join(__dirname, 'oneapi.json'), - mock: false, - }, - { - requestLibPath: "import { request } from '@umijs/max'", - schemaPath: 'https://gw.alipayobjects.com/os/antfincdn/CA1dOm%2631B/openapi.json', - projectName: 'swagger', - }, - ], - mfsu: { - strategy: 'normal', - }, - outputPath: 'qingdao', - base: '/qingdao/', - publicPath: '/qingdao/', - esbuildMinifyIIFE: true, - requestRecord: {}, -}); +// https://umijs.org/config/ +import { defineConfig } from '@umijs/max'; +import { join } from 'path'; +import defaultSettings from './defaultSettings'; +import proxy from './proxy'; +import routes from './routes'; + +const { REACT_APP_ENV = 'dev' } = process.env; + +export default defineConfig({ + /** + * @name 开启 hash 模式 + * @description 让 build 之后的产物包含 hash 后缀。通常用于增量发布和避免浏览器加载缓存。 + * @doc https://umijs.org/docs/api/config#hash + */ + hash: true, + + /** + * @name 兼容性设置 + * @description 设置 ie11 不一定完美兼容,需要检查自己使用的所有依赖 + * @doc https://umijs.org/docs/api/config#targets + */ + // targets: { + // ie: 11, + // }, + /** + * @name 路由的配置,不在路由中引入的文件不会编译 + * @description 只支持 path,component,routes,redirect,wrappers,title 的配置 + * @doc https://umijs.org/docs/guides/routes + */ + // umi routes: https://umijs.org/docs/routing + routes, + /** + * @name 主题的配置 + * @description 虽然叫主题,但是其实只是 less 的变量设置 + * @doc antd的主题设置 https://ant.design/docs/react/customize-theme-cn + * @doc umi 的theme 配置 https://umijs.org/docs/api/config#theme + */ + theme: { + // 如果不想要 configProvide 动态设置主题需要把这个设置为 default + // 只有设置为 variable, 才能使用 configProvide 动态设置主色调 + 'root-entry-name': 'variable', + }, + /** + * @name moment 的国际化配置 + * @description 如果对国际化没有要求,打开之后能减少js的包大小 + * @doc https://umijs.org/docs/api/config#ignoremomentlocale + */ + ignoreMomentLocale: true, + /** + * @name 代理配置 + * @description 可以让你的本地服务器代理到你的服务器上,这样你就可以访问服务器的数据了 + * @see 要注意以下 代理只能在本地开发时使用,build 之后就无法使用了。 + * @doc 代理介绍 https://umijs.org/docs/guides/proxy + * @doc 代理配置 https://umijs.org/docs/api/config#proxy + */ + proxy: proxy[REACT_APP_ENV as keyof typeof proxy], + /** + * @name 快速热更新配置 + * @description 一个不错的热更新组件,更新时可以保留 state + */ + fastRefresh: true, + //============== 以下都是max的插件配置 =============== + /** + * @name 数据流插件 + * @@doc https://umijs.org/docs/max/data-flow + */ + model: {}, + /** + * 一个全局的初始数据流,可以用它在插件之间共享数据 + * @description 可以用来存放一些全局的数据,比如用户信息,或者一些全局的状态,全局初始状态在整个 Umi 项目的最开始创建。 + * @doc https://umijs.org/docs/max/data-flow#%E5%85%A8%E5%B1%80%E5%88%9D%E5%A7%8B%E7%8A%B6%E6%80%81 + */ + initialState: {}, + /** + * @name layout 插件 + * @doc https://umijs.org/docs/max/layout-menu + */ + title: '青岛智慧就业服务系统', + layout: { + locale: false, + ...defaultSettings, + }, + /** + * @name moment2dayjs 插件 + * @description 将项目中的 moment 替换为 dayjs + * @doc https://umijs.org/docs/max/moment2dayjs + */ + moment2dayjs: { + preset: 'antd', + plugins: ['duration'], + }, + /** + * @name 国际化插件 + * @doc https://umijs.org/docs/max/i18n + */ + locale: { + // default zh-CN + default: 'zh-CN', + antd: false, + // default true, when it is true, will use `navigator.language` overwrite default + baseNavigator: false, + }, + /** + * @name antd 插件 + * @description 内置了 babel import 插件 + * @doc https://umijs.org/docs/max/antd#antd + */ + antd: {}, + /** + * @name 网络请求配置 + * @description 它基于 axios 和 ahooks 的 useRequest 提供了一套统一的网络请求和错误处理方案。 + * @doc https://umijs.org/docs/max/request + */ + request: {}, + /** + * @name 权限插件 + * @description 基于 initialState 的权限插件,必须先打开 initialState + * @doc https://umijs.org/docs/max/access + */ + access: {}, + /** + * @name 中额外的 script + * @description 配置 中额外的 script + */ + headScripts: [ + // 解决首次加载时白屏的问题 + { src: '/qingdao/scripts/loading.js', async: true }, + ], + //================ pro 插件配置 ================= + presets: ['umi-presets-pro'], + /** + * @name openAPI 插件的配置 + * @description 基于 openapi 的规范生成serve 和mock,能减少很多样板代码 + * @doc https://pro.ant.design/zh-cn/docs/openapi/ + */ + openAPI: [ + { + requestLibPath: "import { request } from '@umijs/max'", + // 或者使用在线的版本 + // schemaPath: "https://gw.alipayobjects.com/os/antfincdn/M%24jrzTTYJN/oneapi.json" + schemaPath: join(__dirname, 'oneapi.json'), + mock: false, + }, + { + requestLibPath: "import { request } from '@umijs/max'", + schemaPath: 'https://gw.alipayobjects.com/os/antfincdn/CA1dOm%2631B/openapi.json', + projectName: 'swagger', + }, + ], + mfsu: { + strategy: 'normal', + }, + outputPath: 'qingdao', + base: '/qingdao/', + publicPath: '/qingdao/', + esbuildMinifyIIFE: true, + requestRecord: {}, +}); diff --git a/config/proxy.ts b/config/proxy.ts index ab24392..8d6782d 100644 --- a/config/proxy.ts +++ b/config/proxy.ts @@ -1,49 +1,49 @@ -/** - * @name 代理的配置 - * @see 在生产环境 代理是无法生效的,所以这里没有生产环境的配置 - * ------------------------------- - * The agent cannot take effect in the production environment - * so there is no configuration of the production environment - * For details, please see - * https://pro.ant.design/docs/deploy - * - * @doc https://umijs.org/docs/guides/proxy - */ -export default { - // 如果需要自定义本地开发服务器 请取消注释按需调整 - dev: { - // localhost:8000/api/** -> https://preview.pro.ant.design/api/** - '/api/': { - // 要代理的地址 - target: 'http://39.98.44.136:8080', - // 配置了这个可以从 http 代理到 https - // 依赖 origin 的功能可能需要这个,比如 cookie - changeOrigin: true, - pathRewrite: { '^/api': '' }, - }, - '/profile/avatar/': { - target: 'http://39.98.44.136:8080', - changeOrigin: true, - } - }, - - /** - * @name 详细的代理配置 - * @doc https://github.com/chimurai/http-proxy-middleware - */ - test: { - // localhost:8000/api/** -> https://preview.pro.ant.design/api/** - '/api/': { - target: 'https://proapi.azurewebsites.net', - changeOrigin: true, - pathRewrite: { '^': '' }, - }, - }, - pre: { - '/api/': { - target: 'your pre url', - changeOrigin: true, - pathRewrite: { '^': '' }, - }, - }, -}; +/** + * @name 代理的配置 + * @see 在生产环境 代理是无法生效的,所以这里没有生产环境的配置 + * ------------------------------- + * The agent cannot take effect in the production environment + * so there is no configuration of the production environment + * For details, please see + * https://pro.ant.design/docs/deploy + * + * @doc https://umijs.org/docs/guides/proxy + */ +export default { + // 如果需要自定义本地开发服务器 请取消注释按需调整 + dev: { + // localhost:8000/api/** -> https://preview.pro.ant.design/api/** + '/api/': { + // 要代理的地址 + target: 'https://qd.zhaopinzao8dian.com/api', + // 配置了这个可以从 http 代理到 https + // 依赖 origin 的功能可能需要这个,比如 cookie + changeOrigin: true, + pathRewrite: { '^/api': '' }, + }, + '/profile/avatar/': { + target: 'https://qd.zhaopinzao8dian.com/api', + changeOrigin: true, + } + }, + + /** + * @name 详细的代理配置 + * @doc https://github.com/chimurai/http-proxy-middleware + */ + test: { + // localhost:8000/api/** -> https://preview.pro.ant.design/api/** + '/api/': { + target: 'https://proapi.azurewebsites.net', + changeOrigin: true, + pathRewrite: { '^': '' }, + }, + }, + pre: { + '/api/': { + target: 'your pre url', + changeOrigin: true, + pathRewrite: { '^': '' }, + }, + }, +}; diff --git a/config/routes.ts b/config/routes.ts index b14a729..70645ef 100644 --- a/config/routes.ts +++ b/config/routes.ts @@ -1,119 +1,120 @@ -/** - * @name umi 的路由配置 - * @description 只支持 path,component,routes,redirect,wrappers,name,icon 的配置 - * @param path path 只支持两种占位符配置,第一种是动态参数 :id 的形式,第二种是 * 通配符,通配符只能出现路由字符串的最后。 - * @param component 配置 location 和 path 匹配后用于渲染的 React 组件路径。可以是绝对路径,也可以是相对路径,如果是相对路径,会从 src/pages 开始找起。 - * @param routes 配置子路由,通常在需要为多个路径增加 layout 组件时使用。 - * @param redirect 配置路由跳转 - * @param wrappers 配置路由组件的包装组件,通过包装组件可以为当前的路由组件组合进更多的功能。 比如,可以用于路由级别的权限校验 - * @param name 配置路由的标题,默认读取国际化文件 menu.ts 中 menu.xxxx 的值,如配置 name 为 login,则读取 menu.ts 中 menu.login 的取值作为标题 - * @param icon 配置路由的图标,取值参考 https://ant.design/components/icon-cn, 注意去除风格后缀和大小写,如想要配置图标为 则取值应为 stepBackward 或 StepBackward,如想要配置图标为 则取值应为 user 或者 User - * @doc https://umijs.org/docs/guides/routes - */ -export default [ - { - path: '/', - redirect: '/account/center', - }, - { - path: '*', - layout: false, - component: './404', - }, - { - path: '/user', - layout: false, - routes: [ - { - name: 'login', - path: '/user/login', - component: './User/Login', - }, - ], - }, - { - path: '/account', - routes: [ - { - name: 'acenter', - path: '/account/center', - component: './User/Center', - }, - { - name: 'asettings', - path: '/account/settings', - component: './User/Settings', - }, - ], - }, - { - name: 'area', - path: '/area', - routes: [ - { - name: '字典数据', - path: '/area/updata-router/index/:id', - component: './Area/Subway/UpLine', - }, - ], - }, - { - name: 'management', - path: '/management', - routes: [ - { - name: '字典数据', - path: '/management/see-matching/index/:id', - component: './Management/List/SeeMatching', - }, - ], - }, - { - name: 'system', - path: '/system', - routes: [ - { - name: '字典数据', - path: '/system/dict-data/index/:id', - component: './System/DictData', - }, - { - name: '字典数据', - path: '/system/admin-dict-data/index/:id', - component: './System/AdminDictData', - }, - { - name: '分配用户', - path: '/system/role-auth/user/:id', - component: './System/Role/authUser', - }, - ], - }, - { - name: 'monitor', - path: '/monitor', - routes: [ - { - name: '任务日志', - path: '/monitor/job-log/index/:id', - component: './Monitor/JobLog', - }, - ], - }, - { - name: 'tool', - path: '/tool', - routes: [ - { - name: '导入表', - path: '/tool/gen/import', - component: './Tool/Gen/import', - }, - { - name: '编辑表', - path: '/tool/gen/edit.tsx', - component: './Tool/Gen/edit.tsx', - }, - ], - }, -]; +/** + * @name umi 的路由配置 + * @description 只支持 path,component,routes,redirect,wrappers,name,icon 的配置 + * @param path path 只支持两种占位符配置,第一种是动态参数 :id 的形式,第二种是 * 通配符,通配符只能出现路由字符串的最后。 + * @param component 配置 location 和 path 匹配后用于渲染的 React 组件路径。可以是绝对路径,也可以是相对路径,如果是相对路径,会从 src/pages 开始找起。 + * @param routes 配置子路由,通常在需要为多个路径增加 layout 组件时使用。 + * @param redirect 配置路由跳转 + * @param wrappers 配置路由组件的包装组件,通过包装组件可以为当前的路由组件组合进更多的功能。 比如,可以用于路由级别的权限校验 + * @param name 配置路由的标题,默认读取国际化文件 menu.ts 中 menu.xxxx 的值,如配置 name 为 login,则读取 menu.ts 中 menu.login 的取值作为标题 + * @param icon 配置路由的图标,取值参考 https://ant.design/components/icon-cn, 注意去除风格后缀和大小写,如想要配置图标为 则取值应为 stepBackward 或 StepBackward,如想要配置图标为 则取值应为 user 或者 User + * @doc https://umijs.org/docs/guides/routes + */ +export default [ + { + path: '/', + redirect: '/account/center', + }, + { + path: '*', + layout: false, + component: './404', + }, + { + path: '/user', + layout: false, + routes: [ + { + name: 'login', + path: '/user/login', + component: './User/Login', + }, + ], + }, + { + path: '/account', + routes: [ + { + name: 'acenter', + path: '/account/center', + component: './User/Center', + }, + { + name: 'asettings', + path: '/account/settings', + component: './User/Settings', + }, + ], + }, + { + name: 'area', + path: '/area', + routes: [ + { + name: '字典数据', + path: '/area/updata-router/index/:id', + component: './Area/Subway/UpLine', + }, + ], + }, + { + name: 'management', + path: '/management', + routes: [ + { + name: '字典数据', + path: '/management/see-matching/index/:id', + component: './Management/List/SeeMatching', + }, + ], + }, + { + name: 'system', + path: '/system', + routes: [ + { + name: '字典数据', + path: '/system/dict-data/index/:id', + component: './System/DictData', + }, + { + name: '字典数据', + path: '/system/admin-dict-data/index/:id', + component: './System/AdminDictData', + }, + { + name: '分配用户', + path: '/system/role-auth/user/:id', + component: './System/Role/authUser', + }, + ], + }, + { + name: 'monitor', + path: '/monitor', + routes: [ + { + name: '任务日志', + path: '/monitor/job-log/index/:id', + component: './Monitor/JobLog', + }, + ], + }, + { + name: 'tool', + path: '/tool', + routes: [ + { + name: '导入表', + path: '/tool/gen/import', + component: './Tool/Gen/import', + }, + { + name: '编辑表', + path: '/tool/gen/edit.tsx', + component: './Tool/Gen/edit.tsx', + }, + ], + }, + +]; diff --git a/package.json b/package.json index d6e21ad..b7a876a 100644 --- a/package.json +++ b/package.json @@ -1,145 +1,147 @@ -{ - "name": "ant-design-pro", - "version": "6.0.0", - "private": true, - "description": "An out-of-box UI solution for enterprise applications", - "scripts": { - "dev": "npm run start:dev", - "build": "max build", - "deploy": "npm run build && npm run gh-pages", - "preview": "npm run build && max preview --port 8000", - "serve": "umi-serve", - "start": "cross-env UMI_ENV=dev max dev", - "start:dev": "cross-env REACT_APP_ENV=dev MOCK=none UMI_ENV=dev max dev", - "start:no-mock": "cross-env MOCK=none UMI_ENV=dev max dev", - "start:pre": "cross-env REACT_APP_ENV=pre UMI_ENV=dev max dev", - "start:test": "cross-env REACT_APP_ENV=test MOCK=none UMI_ENV=dev max dev", - "test": "jest", - "test:coverage": "npm run jest -- --coverage", - "test:update": "npm run jest -- -u", - "docker-hub:build": "docker build -f Dockerfile.hub -t ant-design-pro ./", - "docker-prod:build": "docker-compose -f ./docker/docker-compose.yml build", - "docker-prod:dev": "docker-compose -f ./docker/docker-compose.yml up", - "docker:build": "docker-compose -f ./docker/docker-compose.dev.yml build", - "docker:dev": "docker-compose -f ./docker/docker-compose.dev.yml up", - "docker:push": "npm run docker-hub:build && npm run docker:tag && docker push antdesign/ant-design-pro", - "docker:tag": "docker tag ant-design-pro antdesign/ant-design-pro", - "analyze": "cross-env ANALYZE=1 max build", - "gh-pages": "gh-pages -d dist", - "i18n-remove": "pro i18n-remove --locale=zh-CN --write", - "postinstall": "max setup", - "jest": "jest", - "lint": "npm run lint:js && npm run lint:prettier && npm run tsc", - "lint-staged": "lint-staged", - "lint-staged:js": "eslint --ext .js,.jsx,.ts,.tsx ", - "lint:fix": "eslint --fix --cache --ext .js,.jsx,.ts,.tsx --format=pretty ./src ", - "lint:js": "eslint --cache --ext .js,.jsx,.ts,.tsx --format=pretty ./src", - "lint:prettier": "prettier -c --write \"**/**.{js,jsx,tsx,ts,less,md,json}\" --end-of-line auto", - "openapi": "max openapi", - "prepare": "cd .. && husky install", - "prettier": "prettier -c --write \"**/**.{js,jsx,tsx,ts,less,md,json}\"", - "tsc": "tsc --noEmit", - "record": "cross-env NODE_ENV=development REACT_APP_ENV=test max record --scene=login" - }, - "lint-staged": { - "**/*.{js,jsx,ts,tsx}": "npm run lint-staged:js", - "**/*.{js,jsx,tsx,ts,less,md,json}": [ - "prettier --write" - ] - }, - "browserslist": [ - "> 1%", - "last 2 versions", - "not ie <= 10" - ], - "dependencies": { - "@amap/amap-jsapi-loader": "^1.0.1", - "@ant-design/icons": "^5.5.0", - "@ant-design/plots": "^2.3.2", - "@ant-design/pro-components": "^2.7.19", - "@ant-design/use-emotion-css": "1.0.4", - "@testing-library/dom": "^10.4.0", - "@umijs/route-utils": "^4.0.1", - "ant-design-pro": "file:", - "antd": "^5.21.1", - "antd-style": "^3.6.2", - "classnames": "^2.5.1", - "dayjs": "^1.11.13", - "echarts": "^5.6.0", - "fabric": "^6.4.0", - "highlight.js": "^11.10.0", - "lodash": "^4.17.21", - "moment": "^2.30.1", - "omit.js": "^2.0.2", - "query-string": "^9.1.0", - "rc-menu": "^9.15.0", - "rc-util": "^5.43.0", - "react": "^18.3.0", - "react-cropper": "^2.3.3", - "react-dev-inspector": "^2.0.1", - "react-dom": "^18.3.0", - "react-helmet-async": "^2.0.0", - "react-highlight": "^0.15.0" - }, - "devDependencies": { - "@ant-design/pro-cli": "^3.3.0", - "@testing-library/react": "^16.0.1", - "@types/classnames": "^2.3.1", - "@types/express": "^4.17.21", - "@types/history": "^4.7.11", - "@types/jest": "^29.5.12", - "@types/lodash": "^4.17.4", - "@types/react": "^18.3.0", - "@types/react-dom": "^18.3.0", - "@types/react-helmet": "^6.1.11", - "@umijs/fabric": "^2.14.1", - "@umijs/lint": "^4.2.9", - "@umijs/max": "^4.2.9", - "cross-env": "^7.0.3", - "eslint": "^9.11.0", - "express": "^4.21.0", - "gh-pages": "^6.1.0", - "husky": "^9.1.3", - "jest": "^29.7.0", - "jest-environment-jsdom": "^29.7.0", - "lint-staged": "^15.2.0", - "mockjs": "^1.1.0", - "prettier": "^3.3.0", - "swagger-ui-dist": "^5.17.14", - "ts-node": "^10.9.1", - "typescript": "^5.6.2", - "umi-presets-pro": "^2.0.0" - }, - "engines": { - "node": ">=12.0.0" - }, - "create-umi": { - "ignoreScript": [ - "docker*", - "functions*", - "site", - "generateMock" - ], - "ignoreDependencies": [ - "netlify*", - "serverless" - ], - "ignore": [ - ".dockerignore", - ".git", - ".github", - ".gitpod.yml", - "CODE_OF_CONDUCT.md", - "Dockerfile", - "Dockerfile.*", - "lambda", - "LICENSE", - "netlify.toml", - "README.*.md", - "azure-pipelines.yml", - "docker", - "CNAME", - "create-umi" - ] - } -} +{ + "name": "ant-design-pro", + "version": "6.0.0", + "private": true, + "description": "An out-of-box UI solution for enterprise applications", + "scripts": { + "dev": "npm run start:dev", + "build": "max build", + "deploy": "npm run build && npm run gh-pages", + "preview": "npm run build && max preview --port 8000", + "serve": "umi-serve", + "start": "cross-env UMI_ENV=dev max dev", + "start:dev": "cross-env REACT_APP_ENV=dev MOCK=none UMI_ENV=dev max dev", + "start:no-mock": "cross-env MOCK=none UMI_ENV=dev max dev", + "start:pre": "cross-env REACT_APP_ENV=pre UMI_ENV=dev max dev", + "start:test": "cross-env REACT_APP_ENV=test MOCK=none UMI_ENV=dev max dev", + "test": "jest", + "test:coverage": "npm run jest -- --coverage", + "test:update": "npm run jest -- -u", + "docker-hub:build": "docker build -f Dockerfile.hub -t ant-design-pro ./", + "docker-prod:build": "docker-compose -f ./docker/docker-compose.yml build", + "docker-prod:dev": "docker-compose -f ./docker/docker-compose.yml up", + "docker:build": "docker-compose -f ./docker/docker-compose.dev.yml build", + "docker:dev": "docker-compose -f ./docker/docker-compose.dev.yml up", + "docker:push": "npm run docker-hub:build && npm run docker:tag && docker push antdesign/ant-design-pro", + "docker:tag": "docker tag ant-design-pro antdesign/ant-design-pro", + "analyze": "cross-env ANALYZE=1 max build", + "gh-pages": "gh-pages -d dist", + "i18n-remove": "pro i18n-remove --locale=zh-CN --write", + "postinstall": "max setup", + "jest": "jest", + "lint": "npm run lint:js && npm run lint:prettier && npm run tsc", + "lint-staged": "lint-staged", + "lint-staged:js": "eslint --ext .js,.jsx,.ts,.tsx ", + "lint:fix": "eslint --fix --cache --ext .js,.jsx,.ts,.tsx --format=pretty ./src ", + "lint:js": "eslint --cache --ext .js,.jsx,.ts,.tsx --format=pretty ./src", + "lint:prettier": "prettier -c --write \"**/**.{js,jsx,tsx,ts,less,md,json}\" --end-of-line auto", + "openapi": "max openapi", + "prepare": "cd .. && husky install", + "prettier": "prettier -c --write \"**/**.{js,jsx,tsx,ts,less,md,json}\"", + "tsc": "tsc --noEmit", + "record": "cross-env NODE_ENV=development REACT_APP_ENV=test max record --scene=login" + }, + "lint-staged": { + "**/*.{js,jsx,ts,tsx}": "npm run lint-staged:js", + "**/*.{js,jsx,tsx,ts,less,md,json}": [ + "prettier --write" + ] + }, + "browserslist": [ + "> 1%", + "last 2 versions", + "not ie <= 10" + ], + "dependencies": { + "@amap/amap-jsapi-loader": "^1.0.1", + "@ant-design/charts": "^2.3.0", + "@ant-design/icons": "^5.5.0", + "@ant-design/maps": "^1.0.0", + "@ant-design/plots": "^2.3.2", + "@ant-design/pro-components": "^2.8.7", + "@ant-design/use-emotion-css": "1.0.4", + "@testing-library/dom": "^10.4.0", + "@umijs/route-utils": "^4.0.1", + "ant-design-pro": "file:", + "antd": "^5.21.1", + "antd-style": "^3.6.2", + "classnames": "^2.5.1", + "dayjs": "^1.11.13", + "echarts": "^5.6.0", + "fabric": "^6.4.0", + "highlight.js": "^11.10.0", + "lodash": "^4.17.21", + "moment": "^2.30.1", + "omit.js": "^2.0.2", + "query-string": "^9.1.0", + "rc-menu": "^9.15.0", + "rc-util": "^5.43.0", + "react": "^18.3.0", + "react-cropper": "^2.3.3", + "react-dev-inspector": "^2.0.1", + "react-dom": "^18.3.0", + "react-helmet-async": "^2.0.0", + "react-highlight": "^0.15.0" + }, + "devDependencies": { + "@ant-design/pro-cli": "^3.3.0", + "@testing-library/react": "^16.0.1", + "@types/classnames": "^2.3.1", + "@types/express": "^4.17.21", + "@types/history": "^4.7.11", + "@types/jest": "^29.5.12", + "@types/lodash": "^4.17.4", + "@types/react": "^18.3.0", + "@types/react-dom": "^18.3.0", + "@types/react-helmet": "^6.1.11", + "@umijs/fabric": "^2.14.1", + "@umijs/lint": "^4.2.9", + "@umijs/max": "^4.2.9", + "cross-env": "^7.0.3", + "eslint": "^9.11.0", + "express": "^4.21.0", + "gh-pages": "^6.1.0", + "husky": "^9.1.3", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", + "lint-staged": "^15.2.0", + "mockjs": "^1.1.0", + "prettier": "^3.3.0", + "swagger-ui-dist": "^5.17.14", + "ts-node": "^10.9.1", + "typescript": "^5.6.2", + "umi-presets-pro": "^2.0.0" + }, + "engines": { + "node": ">=12.0.0" + }, + "create-umi": { + "ignoreScript": [ + "docker*", + "functions*", + "site", + "generateMock" + ], + "ignoreDependencies": [ + "netlify*", + "serverless" + ], + "ignore": [ + ".dockerignore", + ".git", + ".github", + ".gitpod.yml", + "CODE_OF_CONDUCT.md", + "Dockerfile", + "Dockerfile.*", + "lambda", + "LICENSE", + "netlify.toml", + "README.*.md", + "azure-pipelines.yml", + "docker", + "CNAME", + "create-umi" + ] + } +} diff --git a/src/global.less b/src/global.less index 6d5ad58..81f10be 100644 --- a/src/global.less +++ b/src/global.less @@ -21,6 +21,22 @@ body, left: unset; } +.ant-table-row { + &-level-1 .ant-table-cell:first-child { + padding-left: 24px !important; + } + + &-level-2 .ant-table-cell:first-child { + padding-left: 48px !important; + } + + // 可根据需要添加更多层级 +} + +.ant-table-row-level-2 .ant-table-row-expand-icon { + display: none; +} + canvas { display: block; } @@ -54,3 +70,4 @@ ol { } } } + diff --git a/src/pages/Analysis/Industrytrend/components/chartcards.tsx b/src/pages/Analysis/Industrytrend/components/chartcards.tsx new file mode 100644 index 0000000..f28cd47 --- /dev/null +++ b/src/pages/Analysis/Industrytrend/components/chartcards.tsx @@ -0,0 +1,215 @@ +import React from 'react'; +import { Card, Select, Spin, Empty, Row, Col } from 'antd'; +import { Line, Bar, Pie, Heatmap } from '@ant-design/charts'; + +export const IndustryTrendCard = ({ + loading, + currentIndustryData, + config, + availableIndustries, + selectedIndustry, + onIndustryChange, +}) => ( + + {availableIndustries.map((industry: any) => ( + + ))} + + } + > + + {currentIndustryData.length > 0 ? ( + + ) : ( + + )} + + +); + +export const AreaAnalysisCard = ({ loading, areaData, config }) => ( + + {loading ? ( + + ) : areaData.length > 0 ? ( +
+ +
+ ) : ( + + )} +
+); + +export const SalaryTrendCard = ({ + loading, + currentSalaryData, + config, + availableSalaryRanges, + selectedSalaryRange, + onSalaryRangeChange, +}) => ( + + {availableSalaryRanges.map((range) => ( + + ))} + + } + > + + {currentSalaryData.length > 0 ? ( + + ) : ( + + )} + + +); + +export const WorkYearCard = ({ + loading, + workYearData, + config, + availableWorkYearRanges, + selectedWorkYearRange, + onWorkYearRangeChange, +}) => ( + + {availableWorkYearRanges.map((range: any) => ( + + ))} + + } + > + + {workYearData && workYearData.length > 0 ? ( +
+ +
+ ) : ( + + )} +
+
+); + +export const EducationCard = ({ + loading, + educationData, + config, + availableEducationLevels, + selectedEducationLevel, + onEducationLevelChange, +}) => ( + + + {availableEducationLevels.map((level) => ( + + ))} + + } + > + + {educationData.length > 0 ? ( + + ) : ( + + )} + + +); \ No newline at end of file diff --git a/src/pages/Analysis/Industrytrend/components/chartconfigs.tsx b/src/pages/Analysis/Industrytrend/components/chartconfigs.tsx new file mode 100644 index 0000000..e1922ec --- /dev/null +++ b/src/pages/Analysis/Industrytrend/components/chartconfigs.tsx @@ -0,0 +1,456 @@ +import { ChartConfig } from '@/types/analysis/industry'; +import dayjs from 'dayjs'; +import { formatDateForDisplay } from '../utils'; + +export const getHeatmapConfig = (areaData: any[], timeDimension: string) => { + const sortedData = [...areaData].sort((a, b) => { + if (timeDimension === '年') { + return parseInt(a.time) - parseInt(b.time); + } else if (timeDimension === '季度') { + const [yearA, quarterA] = a.time.split('-Q'); + const [yearB, quarterB] = b.time.split('-Q'); + return yearA === yearB + ? parseInt(quarterA) - parseInt(quarterB) + : parseInt(yearA) - parseInt(yearB); + } else { + return dayjs(a.time).valueOf() - dayjs(b.time).valueOf(); + } + }); + + return { + data: sortedData, + height: 240, + autoFit: true, + xField: 'name', + yField: 'time', + colorField: 'value', + shapeField: 'square', + sizeField: 'value', + xAxis: { + title: { + text: '区域', + style: { fontSize: 12 }, + }, + label: { + style: { + fontSize: 10, + fill: '#666', + }, + formatter: (text: string) => { + return text.length > 4 ? `${text.substring(0, 3)}...` : text; + }, + }, + }, + yAxis: { + title: { + text: '时间', + style: { fontSize: 12 }, + }, + label: { + formatter: (text: string) => { + if (timeDimension === '年') return `${text}年`; + if (timeDimension === '季度') return text.replace('-Q', '年Q'); + return text.replace('-', '年').replace('-', '月'); + }, + style: { + fontSize: 10, + fill: '#666', + }, + }, + }, + label: { + text: (d: { value: number }) => d.value.toString(), + position: 'inside', + style: { + fill: '#fff', + pointerEvents: 'none', + }, + }, + scale: { + size: { range: [14, 14] }, + color: { range: ['#dddddd', '#9ec8e0', '#5fa4cd', '#2e7ab6', '#114d90'] }, + }, + tooltip: { + title: (d: { name: any; time: any }) => `${d.name} - ${d.time}`, + field: 'value', + valueFormatter: (v: number) => v.toString(), + domStyles: { + 'g2-tooltip': { + padding: '8px 12px', + borderRadius: '4px', + }, + }, + }, + interactions: [{ type: 'element-active' }], + responsive: true, + }; +}; + +export const getIndustryChartConfig = (currentIndustryData: any[], type: string): ChartConfig => ({ + data: currentIndustryData, + height: 200, + xField: 'date', + yField: 'value', + seriesField: 'category', + xAxis: { + type: 'cat', + label: { + formatter: (text: string) => text, + }, + }, + yAxis: { + label: { + formatter: (val: string) => `${val}${type === '招聘增长率' ? '%' : ''}`, + }, + }, + point: { + size: 4, + shape: 'circle', + }, + animation: { + appear: { + animation: 'path-in', + duration: 1000, + }, + }, + smooth: true, + interactions: [ + { + type: 'tooltip', + cfg: { + render: (e, { title, items }) => { + const list = items.filter((item) => item.value); + return ( +
+

{title}

+ {list.map((item, index) => { + const { name, value, color } = item; + return ( +
+
+ + {name} +
+ + {value} + {type === '招聘增长率' ? '%' : ''} + +
+ ); + })} +
+ ); + }, + }, + }, + ], + legend: false, + tooltip: { + showTitle: undefined, + title: undefined, + customContent: undefined, + }, +}); + +export const getSalaryChartConfig = (currentSalaryData: any[]): ChartConfig => ({ + data: currentSalaryData, + height: 240, + xField: 'date', + yField: 'value', + seriesField: 'category', + xAxis: { + type: 'cat', + label: { + formatter: (text: string) => text, + }, + }, + yAxis: { + label: { + formatter: (val: string) => `${val}`, + }, + }, + point: { + size: 4, + shape: 'circle', + }, + animation: { + appear: { + animation: 'path-in', + duration: 1000, + }, + }, + smooth: true, + interactions: [ + { + type: 'tooltip', + cfg: { + render: (e, { title, items }) => { + const list = items.filter((item) => item.value); + return ( +
+

{title}

+ {list.map((item, index) => { + const { name, value, color } = item; + return ( +
+
+ + {name} +
+ {value} +
+ ); + })} +
+ ); + }, + }, + }, + ], + legend: false, + tooltip: { + showTitle: undefined, + title: undefined, + customContent: undefined, + }, +}); + +export const getWorkYearPieConfig = (workYearData: any[], selectedWorkYearRange: string) => { + const filteredData = workYearData + .filter((item) => item.category === selectedWorkYearRange) + .map((item) => ({ + ...item, + date: formatDateForDisplay(item.date, '月'), + value: item.value || 0, + type: `${formatDateForDisplay(item.date, '月')} ${item.category}`, + })); + + return { + data: filteredData, + angleField: 'value', + colorField: 'type', + radius: 0.2, + innerRadius: 0.5, + label: { + text: (d: { type: any; value: any }) => `${d.type}\n ${d.value}`, + position: 'spider', + }, + legend: false, + tooltip: { + showTitle: true, + title: '工作经验分布', + fields: ['type', 'value'], + formatter: (datum: { type: any; value: any }) => ({ + name: datum.type, + value: datum.value, + }), + }, + interactions: [{ type: 'element-active' }], + padding: 'auto', + autoFit: true, + }; +}; + +export const getEducationBarConfig = (educationData: any[], selectedEducationLevel: string) => { + const educationLevelOrder = [ + '不限', + '初中及以下', + '中专/中技', + '高中', + '大专', + '本科', + '硕士', + '博士', + 'MBA/EMBA', + '留学-学士', + '留学-硕士', + '留学-博士', + ]; + + const educationColorMap: Record = { + 不限: '#8884d8', + 初中及以下: '#82ca9d', + '中专/中技': '#ffc658', + 高中: '#ff8042', + 大专: '#0088FE', + 本科: '#00C49F', + 硕士: '#FFBB28', + 博士: '#FF8042', + 'MBA/EMBA': '#8884d8', + '留学-学士': '#82ca9d', + '留学-硕士': '#ffc658', + '留学-博士': '#ff8042', + }; + + const cleanEducationData = educationData + .filter((item) => item && item.category && item.date && !isNaN(item.value)) + .map((item) => ({ + ...item, + date: formatDateForDisplay(item.date, '月'), + category: item.category || '不限', + value: Number(item.value) || 0, + })); + + if (!selectedEducationLevel) { + const educationSummary: Record = {}; + + cleanEducationData.forEach((item) => { + if (!educationSummary[item.category]) { + educationSummary[item.category] = 0; + } + educationSummary[item.category] += item.value; + }); + + const barData = Object.entries(educationSummary) + .filter(([_, value]) => value > 0) + .map(([name, value]) => ({ + name, + value, + color: educationColorMap[name] || '#999', + })) + .sort((a, b) => educationLevelOrder.indexOf(a.name) - educationLevelOrder.indexOf(b.name)); + + return { + data: barData, + height: 200, + xField: 'value', + yField: 'name', + seriesField: 'name', + color: ({ name }: { name: string }) => educationColorMap[name] || '#999', + meta: { + name: { alias: '学历要求' }, + value: { alias: '岗位数量' }, + }, + xAxis: { + label: { + formatter: (val: string) => `${val}`, + }, + grid: { + line: { + style: { + stroke: '#f0f0f0', + lineDash: [4, 4], + }, + }, + }, + }, + yAxis: { + label: { + formatter: (text: string) => text, + }, + }, + barStyle: { + radius: [2, 2, 0, 0], + }, + tooltip: { + showTitle: true, + title: '学历要求分布', + fields: ['name', 'value'], + formatter: (datum: { name: any; value: any }) => ({ + name: datum.name, + value: datum.value, + }), + domStyles: { + 'g2-tooltip': { + background: 'rgba(255, 255, 255, 0.9)', + boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)', + borderRadius: '4px', + }, + }, + }, + interactions: [{ type: 'element-active' }], + legend: false, + animation: { + appear: { + animation: 'scale-in-y', + duration: 1000, + }, + }, + }; + } + + const timeData = cleanEducationData + .filter((item) => item.category === selectedEducationLevel) + .sort((a, b) => { + const dateA = dayjs(a.date, 'YYYY年MM月').valueOf(); + const dateB = dayjs(b.date, 'YYYY年MM月').valueOf(); + return dateA - dateB; + }); + + return { + data: timeData, + height: 200, + xField: 'date', + yField: 'value', + seriesField: 'category', + color: educationColorMap[selectedEducationLevel] || '#999', + meta: { + date: { + alias: '时间', + type: 'cat', + values: timeData + .map((item) => item.date) + .sort((a, b) => { + const dateA = dayjs(a, 'YYYY年MM月').valueOf(); + const dateB = dayjs(b, 'YYYY年MM月').valueOf(); + return dateA - dateB; + }), + }, + value: { alias: '岗位数量' }, + }, + barStyle: { + radius: [2, 2, 0, 0], + }, + tooltip: { + showTitle: true, + title: `${selectedEducationLevel}趋势`, + fields: ['date', 'value'], + formatter: (datum: { date: any; value: any }) => ({ name: datum.date, value: datum.value }), + domStyles: { + 'g2-tooltip': { + background: 'rgba(255, 255, 255, 0.9)', + boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)', + borderRadius: '4px', + }, + }, + }, + interactions: [{ type: 'element-active' }], + legend: false, + animation: { + appear: { + animation: 'scale-in-y', + duration: 1000, + }, + }, + xAxis: { + type: 'cat', + label: { + formatter: (text: string) => text, + }, + }, + }; +}; \ No newline at end of file diff --git a/src/pages/Analysis/Industrytrend/index.tsx b/src/pages/Analysis/Industrytrend/index.tsx new file mode 100644 index 0000000..0c563d0 --- /dev/null +++ b/src/pages/Analysis/Industrytrend/index.tsx @@ -0,0 +1,570 @@ +import React, { useEffect, useState, useMemo, useRef } from 'react'; +import { Card, Select, Button, Space, Spin, Empty, Row, Col, message, DatePicker } from 'antd'; +import { Line, Bar, Pie, Heatmap } from '@ant-design/charts'; +import { + getIndustryTrend, + getIndustryAreaTrend, + getSalaryTrend, + getWorkYearTrend, + getEducationTrend, +} from '@/services/analysis/industry'; +import dayjs from 'dayjs'; +import { useRequest } from '@umijs/max'; +import { + TimeDimension, + AnalysisType, + IndustryTrendState, + IndustryDataItem, + ChartConfig, +} from '@/types/analysis/industry'; +import { + formatQuarter, + formatDateForDisplay, + convertApiData, + convertSalaryData, + convertWorkYearData, + convertEducationData, +} from './utils'; +import { + getHeatmapConfig, + getIndustryChartConfig, + getSalaryChartConfig, + getWorkYearPieConfig, + getEducationBarConfig, +} from './components/chartconfigs'; +import { + IndustryTrendCard, + AreaAnalysisCard, + SalaryTrendCard, + WorkYearCard, + EducationCard, +} from './components/chartcards'; + +const { Option } = Select; +const { RangePicker } = DatePicker; + +const flattenAreaData = (apiResponse: any) => { + if (!apiResponse || typeof apiResponse !== 'object') { + return []; + } + + const flattenedData: { name: any; time: any; value: number }[] = []; + + for (const month in apiResponse) { + if (apiResponse.hasOwnProperty(month)) { + const areas = apiResponse[month]; + + areas.forEach((area: { name: any; time: any; data: string }) => { + flattenedData.push({ + name: area.name, + time: area.time, + value: parseInt(area.data) || 0, + }); + }); + } + } + + return flattenedData; +}; + +const IndustryTrendPage: React.FC = () => { + const [params, setParams] = useState({ + timeDimension: '月', + type: '岗位发布数量', + startTime: dayjs().subtract(5, 'month').format('YYYY-MM'), + endTime: dayjs().format('YYYY-MM'), + selectedIndustry: '', + selectedSalaryRange: '', + selectedWorkYearRange: '', + }); + + const [allData, setAllData] = useState([]); + const [areaData, setAreaData] = useState([]); + const [salaryData, setSalaryData] = useState([]); + const [availableIndustries, setAvailableIndustries] = useState([]); + const [availableSalaryRanges, setAvailableSalaryRanges] = useState([]); + const heatmapRef = useRef(null); + const containerRef = useRef(null); + const [workYearData, setWorkYearData] = useState([]); + const [availableWorkYearRanges, setAvailableWorkYearRanges] = useState([]); + const [educationData, setEducationData] = useState([]); + const [availableEducationLevels, setAvailableEducationLevels] = useState([]); + const [selectedEducationLevel, setSelectedEducationLevel] = useState(''); + + // 获取行业趋势数据 + const { loading: industryLoading, run: fetchIndustryData } = useRequest( + async () => { + let { startTime, endTime, timeDimension, type } = params; + + if (timeDimension === '季度') { + startTime = formatQuarter(startTime); + endTime = formatQuarter(endTime); + } + + return await getIndustryTrend({ + timeDimension, + type, + startTime, + endTime, + }); + }, + { + manual: true, + onSuccess: (data) => { + const formattedData = convertApiData(data); + setAllData(formattedData); + + const industries = Array.from(new Set(formattedData.map((item: any) => item.category))) + .filter(Boolean) + .sort(); + + setAvailableIndustries(industries); + + if (industries.length > 0 && !industries.includes(params.selectedIndustry)) { + setParams((p) => ({ ...p, selectedIndustry: industries[0] })); + } + }, + onError: (error) => { + message.error('行业数据加载失败'); + }, + }, + ); + + // 获取地区趋势数据 + const { loading: areaLoading, run: fetchAreaData } = useRequest( + async () => { + let { startTime, endTime, timeDimension, type } = params; + + if (timeDimension === '季度') { + startTime = formatQuarter(startTime); + endTime = formatQuarter(endTime); + } + + return await getIndustryAreaTrend({ + timeDimension, + type, + startTime, + endTime, + }); + }, + { + manual: true, + onSuccess: (res) => { + const formattedData = flattenAreaData(res); + setAreaData(formattedData); + }, + onError: (error) => { + message.error('地区数据加载失败'); + }, + }, + ); + + // 获取薪资趋势数据 + const { loading: salaryLoading, run: fetchSalaryData } = useRequest( + async () => { + const now = dayjs(); + let startTime = now.subtract(1, 'year').format('YYYY-MM'); + let endTime = now.format('YYYY-MM'); + const timeDimension = '月'; + + return await getSalaryTrend({ + timeDimension, + type: '岗位发布数量', + startTime, + endTime, + }); + }, + { + manual: true, + onSuccess: (data) => { + const formattedData = convertSalaryData(data); + setSalaryData(formattedData); + + const ranges = Array.from(new Set(formattedData.map((item: any) => item.category))) + .filter(Boolean) + .sort((a, b) => { + const extractNumber = (str: string) => parseInt(str.replace(/[^0-9]/g, '') || 0); + return extractNumber(a) - extractNumber(b); + }); + + setAvailableSalaryRanges(ranges); + + if (ranges.length > 0 && !ranges.includes(params.selectedSalaryRange)) { + setParams((p) => ({ ...p, selectedSalaryRange: ranges[0] })); + } + }, + onError: (error) => { + message.error('薪资数据加载失败'); + }, + }, + ); + + // 获取工作年限数据 + const { loading: workYearLoading, run: fetchWorkYearData } = useRequest( + async () => { + const now = dayjs(); + let startTime = now.subtract(1, 'year').format('YYYY-MM'); + let endTime = now.format('YYYY-MM'); + const timeDimension = '月'; + + return await getWorkYearTrend({ + timeDimension, + type: '岗位发布数量', + startTime, + endTime, + }); + }, + { + manual: true, + onSuccess: (data) => { + const formattedData = convertWorkYearData(data); + setWorkYearData(formattedData); + + const ranges = Array.from(new Set(formattedData.map((item: any) => item.category))) + .filter(Boolean) + .sort((a, b) => { + const order = ['应届', '1-3年', '3-5年', '5年以上']; + return order.indexOf(a) - order.indexOf(b); + }); + + setAvailableWorkYearRanges(ranges); + if (ranges.length > 0 && !ranges.includes(params.selectedWorkYearRange)) { + setParams((p) => ({ ...p, selectedWorkYearRange: ranges[0] })); + } + }, + onError: (error) => { + message.error('工作经验数据加载失败'); + }, + }, + ); + + // 获取学历数据 + const { loading: educationLoading, run: fetchEducationData } = useRequest( + async () => { + const now = dayjs(); + let startTime = now.subtract(1, 'year').format('YYYY-MM'); + let endTime = now.format('YYYY-MM'); + const timeDimension = '月'; + + return await getEducationTrend({ + timeDimension, + type: '岗位发布数量', + startTime, + endTime, + }); + }, + { + manual: true, + onSuccess: (data) => { + const formattedData = convertEducationData(data); + setEducationData(formattedData); + + const levels = Array.from(new Set(formattedData.map((item: any) => item.category))) + .filter(Boolean) + .sort((a, b) => { + const order = ['不限', '初中及以下', '中专/中技', '大专', '本科', '硕士', '博士']; + return order.indexOf(a) - order.indexOf(b); + }); + + setAvailableEducationLevels(levels); + if (levels.length > 0 && !levels.includes(selectedEducationLevel)) { + setSelectedEducationLevel(levels[0]); + } + }, + onError: (error) => { + message.error('学历数据加载失败'); + }, + }, + ); + + const currentWorkYearData = useMemo(() => { + if (!params.selectedWorkYearRange || workYearData.length === 0) return []; + + return workYearData + .filter((item) => item.category === params.selectedWorkYearRange) + .map((item) => ({ + ...item, + originalDate: item.date, + date: formatDateForDisplay(item.date, '月'), + })) + .sort((a, b) => dayjs(a.originalDate).valueOf() - dayjs(b.originalDate).valueOf()); + }, [workYearData, params.selectedWorkYearRange]); + + const currentIndustryData = useMemo(() => { + if (!params.selectedIndustry || allData.length === 0) return []; + + return allData + .filter((item) => item.category === params.selectedIndustry) + .map((item) => ({ + ...item, + originalDate: item.date, + date: formatDateForDisplay(item.date, params.timeDimension), + })) + .sort((a, b) => { + if (params.timeDimension === '季度') { + const [yearA, quarterA] = a.originalDate.split('-'); + const [yearB, quarterB] = b.originalDate.split('-'); + + const quarterToNumber = (q: string) => { + if (q.includes('第一')) return 1; + if (q.includes('第二')) return 2; + if (q.includes('第三')) return 3; + if (q.includes('第四')) return 4; + return 0; + }; + + return yearA === yearB + ? quarterToNumber(quarterA) - quarterToNumber(quarterB) + : parseInt(yearA) - parseInt(yearB); + } else if (params.timeDimension === '年') { + return parseInt(a.originalDate) - parseInt(b.originalDate); + } else { + return dayjs(a.originalDate).valueOf() - dayjs(b.originalDate).valueOf(); + } + }); + }, [allData, params.selectedIndustry, params.timeDimension]); + + const currentSalaryData = useMemo(() => { + if (!params.selectedSalaryRange || salaryData.length === 0) return []; + + return salaryData + .filter((item) => item.category === params.selectedSalaryRange) + .map((item) => ({ + ...item, + originalDate: item.date, + date: formatDateForDisplay(item.date, '月'), + })) + .sort((a, b) => dayjs(a.originalDate).valueOf() - dayjs(b.originalDate).valueOf()); + }, [salaryData, params.selectedSalaryRange]); + + const currentEducationData = useMemo(() => { + if (!selectedEducationLevel || educationData.length === 0) return []; + return educationData + .filter((item) => item.category === selectedEducationLevel) + .map((item) => ({ + ...item, + originalDate: item.date, + date: formatDateForDisplay(item.date, '月'), + })) + .sort((a, b) => dayjs(a.originalDate).valueOf() - dayjs(b.originalDate).valueOf()); + }, [educationData, selectedEducationLevel]); + + const heatmapConfig = useMemo( + () => getHeatmapConfig(areaData, params.timeDimension), + [areaData, params.timeDimension], + ); + + const industryChartConfig = useMemo( + () => getIndustryChartConfig(currentIndustryData, params.type), + [currentIndustryData, params.type], + ); + + const salaryChartConfig = useMemo( + () => getSalaryChartConfig(currentSalaryData), + [currentSalaryData], + ); + + const workYearPieConfig = useMemo( + () => getWorkYearPieConfig(workYearData, params.selectedWorkYearRange), + [workYearData, params.selectedWorkYearRange], + ); + + const educationBarConfig = useMemo( + () => getEducationBarConfig(educationData, selectedEducationLevel), + [educationData, selectedEducationLevel], + ); + + const handleTimeDimensionChange = (value: TimeDimension) => { + const now = dayjs(); + let newStartTime = ''; + + if (value === '月') { + newStartTime = now.subtract(5, 'month').format('YYYY-MM'); + } else if (value === '季度') { + newStartTime = now.subtract(6, 'quarter').format('YYYY-Q'); + } else { + newStartTime = now.subtract(5, 'year').format('YYYY'); + } + + setParams((p) => ({ + ...p, + timeDimension: value, + startTime: newStartTime, + endTime: + value === '月' + ? now.format('YYYY-MM') + : value === '季度' + ? now.format('YYYY-Q') + : now.format('YYYY'), + selectedIndustry: '', + })); + }; + + const handleDateRangeChange = (dates: any, dateStrings: [string, string]) => { + if (dates && dates[0] && dates[1]) { + setParams((p) => ({ + ...p, + startTime: dateStrings[0], + endTime: dateStrings[1], + })); + } + }; + + const disabledDate = (current: dayjs.Dayjs) => { + const now = dayjs(); + + if (params.timeDimension === '月') { + return current.isAfter(now.endOf('month')); + } else if (params.timeDimension === '季度') { + return current.isAfter(now.endOf('quarter')); + } else { + return current.isAfter(now.endOf('year')); + } + }; + + const getPickerValue = () => { + try { + return [ + dayjs( + params.startTime, + params.timeDimension === '年' + ? 'YYYY' + : params.timeDimension === '季度' + ? 'YYYY-Q' + : 'YYYY-MM', + ), + dayjs( + params.endTime, + params.timeDimension === '年' + ? 'YYYY' + : params.timeDimension === '季度' + ? 'YYYY-Q' + : 'YYYY-MM', + ), + ]; + } catch (e) { + return null; + } + }; + + useEffect(() => { + fetchIndustryData(); + fetchAreaData(); + fetchSalaryData(); + fetchWorkYearData(); + fetchEducationData(); + }, [params.timeDimension, params.startTime, params.endTime, params.type]); + + return ( +
+ +
+ + + + + + + + + +
+ + + {/* 行业趋势图表 - 全宽显示 */} + + setParams((p) => ({ ...p, selectedIndustry: value }))} + /> + + + {/* 区域分析和薪资趋势 - 中等屏幕下分成两列 */} + + + + + + setParams((p) => ({ ...p, selectedSalaryRange: value })) + } + /> + + + {/* 工作经验和学历要求 - 中等屏幕下分成两列 */} + + + setParams((p) => ({ ...p, selectedWorkYearRange: value })) + } + /> + + + + + +
+
+ ); +}; + +export default IndustryTrendPage; diff --git a/src/pages/Analysis/Industrytrend/utils.ts b/src/pages/Analysis/Industrytrend/utils.ts new file mode 100644 index 0000000..5d29e8f --- /dev/null +++ b/src/pages/Analysis/Industrytrend/utils.ts @@ -0,0 +1,232 @@ +import { TimeDimension, QuarterFormat, DateFormatter, QuarterFormatter, ApiDataConverter, IndustryDataItem, AreaDataItem } from '@/types/analysis/industry'; + +export const formatQuarter: QuarterFormatter = (dateStr: string): QuarterFormat => { + if (dateStr.includes('第')) return dateStr as QuarterFormat; + + const [year, quarterPart] = dateStr.includes('-Q') + ? dateStr.split('-Q') + : dateStr.split('-'); + + const quarterNum = quarterPart.replace('季度', ''); + const quarterMap: Record = { + '1': '第一季度', + '2': '第二季度', + '3': '第三季度', + '4': '第四季度' + }; + return `${year}-${quarterMap[quarterNum] || quarterPart}` as QuarterFormat; +}; + +export const formatDateForDisplay: DateFormatter = (dateStr: string, dimension: TimeDimension): string => { + try { + if (dimension === '年') return dateStr.split('-')[0]; + if (dimension === '季度') { + if (dateStr.includes('第')) { + const [year, quarter] = dateStr.split('-'); + return `${year}年${quarter}`; + } + const [year, quarterNum] = dateStr.split('-'); + const quarterMap: Record = { + '1': '第一季度', + '2': '第二季度', + '3': '第三季度', + '4': '第四季度', + 'Q1': '第一季度', + 'Q2': '第二季度', + 'Q3': '第三季度', + 'Q4': '第四季度' + }; + return `${year}年${quarterMap[quarterNum] || quarterNum}`; + } + const [year, month] = dateStr.split('-'); + return `${year}年${month}月`; + } catch (e) { + console.error('日期格式化错误:', e); + return dateStr; + } +}; + +export const convertApiData: ApiDataConverter = (apiData: any) => { + if (!apiData) return []; + try { + if (Array.isArray(apiData)) { + return apiData.map(item => ({ + date: item.time || item.date, + category: item.name || item.category || '未知行业', + value: Number(item.data || item.value) || 0 + })); + } + if (typeof apiData === 'object') { + const result: IndustryDataItem[] = []; + Object.entries(apiData).forEach(([date, items]) => { + if (Array.isArray(items)) { + items.forEach((item: any) => { + result.push({ + date, + category: item.name || item.category || '未知行业', + value: Number(item.data || item.value) || 0 + }); + }); + } + }); + return result; + } + return []; + } catch (error) { + console.error('数据转换错误:', error); + return []; + } +}; + +export const convertAreaApiData = (apiData: any): AreaDataItem[] => { + if (!apiData?.data) { + console.warn('convertAreaApiData: apiData.data 为空'); + return []; + } + + try { + const result: AreaDataItem[] = []; + + // 处理嵌套的月份数据 + if (typeof apiData.data === 'object' && !Array.isArray(apiData.data)) { + Object.entries(apiData.data).forEach(([time, items]) => { + if (Array.isArray(items)) { + items.forEach((item: any) => { + if (!item) return; + const uniqueName = items.filter((i: any) => i.name === item.name).length > 1 + ? `${item.name}_${index}` + : item.name; + result.push({ + name: item.name || '未知区域', + value: Number(item.data) || 0, + time: item.time || time, + x:uniqueName, + y:time, + category: item.name, + originalData: item + }); + }); + } + }); + return result; + } + if (Array.isArray(apiData.data)) { + return apiData.data.map((item: any) => ({ + name: item.name || '未知区域', + value: Number(item.data) || 0, + time: item.time || '未知时间', + x: item.name, + y: item.time, + category: item.name, + originalData: item + })); + } + return []; + } catch (error) { + console.error('数据转换错误:', error); + return []; + } +}; + +export const convertSalaryData: ApiDataConverter = (apiData: any) => { + if (!apiData) return []; + try { + if (Array.isArray(apiData)) { + return apiData.map(item => ({ + date: item.time || item.date, + category: item.name || item.category || '未知薪资区间', + value: Number(item.data || item.value) || 0 + })); + } + if (typeof apiData === 'object') { + const result: IndustryDataItem[] = []; + Object.entries(apiData).forEach(([date, items]) => { + if (Array.isArray(items)) { + items.forEach((item: any) => { + result.push({ + date, + category: item.name || item.category || '未知薪资区间', + value: Number(item.data || item.value) || 0 + }); + }); + } + }); + return result; + } + return []; + } catch (error) { + console.error('薪资数据转换错误:', error); + return []; + } +}; + +export const convertWorkYearData: ApiDataConverter = (apiData: any) => { + if (!apiData) return []; + try { + if (Array.isArray(apiData)) { + return apiData.map(item => ({ + date: item.time || item.date || '', + category: item.name || item.category || '未知经验要求', + value: Number(item.data || item.value || 0) // 确保数值不为null/undefined + })); + } + if (typeof apiData === 'object') { + const result: IndustryDataItem[] = []; + Object.entries(apiData).forEach(([date, items]) => { + if (Array.isArray(items)) { + items.forEach((item: any) => { + result.push({ + date: date || '', + category: item.name || item.category || '未知经验要求', + value: Number(item.data || item.value || 0) // 确保数值不为null/undefined + }); + }); + } + }); + return result; + } + return []; + } catch (error) { + console.error('工作经验数据转换错误:', error); + return []; + } +}; +export const convertEducationData: ApiDataConverter = (apiData: any) => { + if (!apiData) return []; + try { + const result: any[] = []; + + // 处理嵌套的月份数据 + if (typeof apiData === 'object' && !Array.isArray(apiData)) { + Object.entries(apiData).forEach(([date, items]) => { + if (Array.isArray(items)) { + items.forEach((item: any) => { + if (!item) return; // 跳过空项 + result.push({ + date: date || '', // 确保日期不为undefined + category: item.name || item.category || '不限', // 默认值 + value: Number(item.data || item.value || 0), // 确保数值有效 + originalData: item + }); + }); + } + }); + return result; + } + + // 处理数组格式的响应 + if (Array.isArray(apiData)) { + return apiData.map(item => ({ + date: item.time || item.date || '', + category: item.name || item.category || '不限', + value: Number(item.data || item.value || 0), + originalData: item + })); + } + + return []; + } catch (error) { + console.error('学历数据转换错误:', error); + return []; + } +}; \ No newline at end of file diff --git a/src/pages/Analysis/User/index.tsx b/src/pages/Analysis/User/index.tsx new file mode 100644 index 0000000..adddf9c --- /dev/null +++ b/src/pages/Analysis/User/index.tsx @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/pages/Application/ModelManagement/index.tsx b/src/pages/Application/ModelManagement/index.tsx new file mode 100644 index 0000000..defdd1c --- /dev/null +++ b/src/pages/Application/ModelManagement/index.tsx @@ -0,0 +1,303 @@ +import React, { Fragment } from 'react'; +import { Collapse, InputNumber, Select } from 'antd'; +import style from './modelStyle.less'; + +export default function ModelManagement() { + const text = ` + A dog is a type of domesticated animal. + Known for its loyalty and faithfulness, + it can be found as a welcome guest in many households across the world. +`; + + return ( + + + +
+
+
年龄匹配范围
+
+ +
+
+
+
工作经验范围
+
+ +
+
+
+
+ +
+
+
学历权重
+
+ +
+
+
+
期望薪资权重
+
+ +
+
+
+
工作经验权重
+
+ +
+
+
+
工作地区权重
+
+ +
+
+
+
+ +
+
+
浏览记录
+
+ +
+
+
+
申请记录
+
+ +
+
+
+
收藏记录
+
+ +
+
+
+
+ +
+
+
学历权重
+
+ +
+
+
+
期望薪资权重
+
+ +
+
+
+
工作经验权重
+
+ +
+
+
+
工作地区权重
+
+ +
+
+
+
相似度计算方式
+
+ +
+
+
+
索引粒度
+
+ +
+
+
+
地铁口附近
+
+ +
+
+
+
商圈附近
+
+ +
+
+
+
+
+
+ ); +} diff --git a/src/pages/Application/ModelManagement/modelStyle.less b/src/pages/Application/ModelManagement/modelStyle.less new file mode 100644 index 0000000..69239ab --- /dev/null +++ b/src/pages/Application/ModelManagement/modelStyle.less @@ -0,0 +1,19 @@ +.cards { + display: grid; + grid-template-columns: repeat(4, 1fr); + grid-row-gap: 30px; + + .card { + display: flex; + align-items: center; + margin-right: 80px; + + .label { + margin-right: 10px; + white-space: nowrap; + } + + .input { + } + } +} diff --git a/src/services/analysis/industry.ts b/src/services/analysis/industry.ts new file mode 100644 index 0000000..b9abfe8 --- /dev/null +++ b/src/services/analysis/industry.ts @@ -0,0 +1,52 @@ +import { request } from '@umijs/max'; + +// 行业 +export async function getIndustryTrend(params?: API.Analysis.IndustryParams) { + return request('/api/cms/statics/industry', { + method: 'GET', + params, + }); +} + +// 区域热力图 +export async function getIndustryAreaTrend(params: any) { + try { + const response = await request('/api/cms/statics/industryArea', { + method: 'GET', + params, + headers: { + 'Content-Type': 'application/json', + }, + }); + + console.log('接口原始响应:', response); // 调试日志 + return response; // 兼容不同后端响应格式 + } catch (error) { + console.error('接口请求异常:', error); + throw error; + } +} + +// 薪资 +export async function getSalaryTrend(params?: API.Analysis.IndustryParams) { + return request('/api/cms/statics/salary', { + method: 'GET', + params, + }); +} + +// 工作年限 +export async function getWorkYearTrend(params?: API.Analysis.IndustryParams) { + return request('/api/cms/statics/workYear', { + method: 'GET', + params, + }); +} + +// 学历趋势 +export async function getEducationTrend(params?: API.Analysis.IndustryParams) { + return request('/api/cms/statics/education', { + method: 'GET', + params, + }); +} diff --git a/src/services/session.ts b/src/services/session.ts index 8b4e0d3..b78c63b 100644 --- a/src/services/session.ts +++ b/src/services/session.ts @@ -1,159 +1,159 @@ -import { createIcon } from '@/utils/IconUtil'; -import { MenuDataItem } from '@ant-design/pro-components'; -import { request } from '@umijs/max'; -import React, { lazy } from 'react'; - - -let remoteMenu: any = null; - -export function getRemoteMenu() { - return remoteMenu; -} - -export function setRemoteMenu(data: any) { - remoteMenu = data; -} - - -function patchRouteItems(route: any, menu: any, parentPath: string) { - for (const menuItem of menu) { - if (menuItem.component === 'Layout' || menuItem.component === 'ParentView') { - if (menuItem.routes) { - let hasItem = false; - let newItem = null; - for (const routeChild of route.routes) { - if (routeChild.path === menuItem.path) { - hasItem = true; - newItem = routeChild; - } - } - if (!hasItem) { - newItem = { - path: menuItem.path, - routes: [], - children: [] - } - route.routes.push(newItem) - } - patchRouteItems(newItem, menuItem.routes, parentPath + menuItem.path + '/'); - } - } else { - const names: string[] = menuItem.component.split('/'); - let path = ''; - names.forEach(name => { - if (path.length > 0) { - path += '/'; - } - if (name !== 'index') { - path += name.at(0)?.toUpperCase() + name.substr(1); - } else { - path += name; - } - }) - if (!path.endsWith('.tsx')) { - path += '.tsx' - } - if (route.routes === undefined) { - route.routes = []; - } - if (route.children === undefined) { - route.children = []; - } - const newRoute = { - element: React.createElement(lazy(() => import('@/pages/' + path))), - path: parentPath + menuItem.path, - } - route.children.push(newRoute); - route.routes.push(newRoute); - } - } -} - -export function patchRouteWithRemoteMenus(routes: any) { - if (remoteMenu === null) { return; } - let proLayout = null; - for (const routeItem of routes) { - if (routeItem.id === 'ant-design-pro-layout') { - proLayout = routeItem; - break; - } - } - patchRouteItems(proLayout, remoteMenu, ''); -} - -/** 获取当前的用户 GET /api/getUserInfo */ -export async function getUserInfo(options?: Record) { - return request('/api/getInfo', { - method: 'GET', - ...(options || {}), - }); -} - -// 刷新方法 -export async function refreshToken() { - return request('/api/auth/refresh', { - method: 'post' - }) -} - -export async function getRouters(): Promise { - return request('/api/getRouters'); -} - -export function convertCompatRouters(childrens: API.RoutersMenuItem[]): any[] { - return childrens.map((item: API.RoutersMenuItem) => { - return { - path: item.path, - icon: createIcon(item.meta.icon), - // icon: item.meta.icon, - name: item.meta.title, - routes: item.children ? convertCompatRouters(item.children) : undefined, - hideChildrenInMenu: item.hidden, - hideInMenu: item.hidden, - component: item.component, - authority: item.perms, - }; - }); -} - -export async function getRoutersInfo(): Promise { - return getRouters().then((res) => { - if (res.code === 200) { - return convertCompatRouters(res.data); - } else { - return []; - } - }); -} - -export function getMatchMenuItem( - path: string, - menuData: MenuDataItem[] | undefined, -): MenuDataItem[] { - if (!menuData) return []; - let items: MenuDataItem[] = []; - menuData.forEach((item) => { - if (item.path) { - if (item.path === path) { - items.push(item); - return; - } - if (path.length >= item.path?.length) { - const exp = `${item.path}/*`; - if (path.match(exp)) { - if (item.routes) { - const subpath = path.substr(item.path.length + 1); - const subItem: MenuDataItem[] = getMatchMenuItem(subpath, item.routes); - items = items.concat(subItem); - } else { - const paths = path.split('/'); - if (paths.length >= 2 && paths[0] === item.path && paths[1] === 'index') { - items.push(item); - } - } - } - } - } - }); - return items; -} +import { createIcon } from '@/utils/IconUtil'; +import { MenuDataItem } from '@ant-design/pro-components'; +import { request } from '@umijs/max'; +import React, { lazy } from 'react'; + + +let remoteMenu: any = null; + +export function getRemoteMenu() { + return remoteMenu; +} + +export function setRemoteMenu(data: any) { + remoteMenu = data; +} + + +function patchRouteItems(route: any, menu: any, parentPath: string) { + for (const menuItem of menu) { + if (menuItem.component === 'Layout' || menuItem.component === 'ParentView') { + if (menuItem.routes) { + let hasItem = false; + let newItem = null; + for (const routeChild of route.routes) { + if (routeChild.path === menuItem.path) { + hasItem = true; + newItem = routeChild; + } + } + if (!hasItem) { + newItem = { + path: menuItem.path, + routes: [], + children: [] + } + route.routes.push(newItem) + } + patchRouteItems(newItem, menuItem.routes, parentPath + menuItem.path + '/'); + } + } else { + const names: string[] = menuItem.component.split('/'); + let path = ''; + names.forEach(name => { + if (path.length > 0) { + path += '/'; + } + if (name !== 'index') { + path += name.at(0)?.toUpperCase() + name.substr(1); + } else { + path += name; + } + }) + if (!path.endsWith('.tsx')) { + path += '.tsx' + } + if (route.routes === undefined) { + route.routes = []; + } + if (route.children === undefined) { + route.children = []; + } + const newRoute = { + element: React.createElement(lazy(() => import('@/pages/' + path))), + path: parentPath + menuItem.path, + } + route.children.push(newRoute); + route.routes.push(newRoute); + } + } +} + +export function patchRouteWithRemoteMenus(routes: any) { + if (remoteMenu === null) { return; } + let proLayout = null; + for (const routeItem of routes) { + if (routeItem.id === 'ant-design-pro-layout') { + proLayout = routeItem; + break; + } + } + patchRouteItems(proLayout, remoteMenu, ''); +} + +/** 获取当前的用户 GET /api/getUserInfo */ +export async function getUserInfo(options?: Record) { + return request('/api/getInfo', { + method: 'GET', + ...(options || {}), + }); +} + +// 刷新方法 +export async function refreshToken() { + return request('/api/auth/refresh', { + method: 'post' + }) +} + +export async function getRouters(): Promise { + return request('/api/getRouters'); +} + +export function convertCompatRouters(childrens: API.RoutersMenuItem[]): any[] { + return childrens.map((item: API.RoutersMenuItem) => { + return { + path: item.path, + icon: createIcon(item.meta.icon), + // icon: item.meta.icon, + name: item.meta.title, + routes: item.children ? convertCompatRouters(item.children) : undefined, + hideChildrenInMenu: item.hidden, + hideInMenu: item.hidden, + component: item.component, + authority: item.perms, + }; + }); +} + +export async function getRoutersInfo(): Promise { + return getRouters().then((res) => { + if (res.code === 200) { + return convertCompatRouters(res.data); + } else { + return []; + } + }); +} + +export function getMatchMenuItem( + path: string, + menuData: MenuDataItem[] | undefined, +): MenuDataItem[] { + if (!menuData) return []; + let items: MenuDataItem[] = []; + menuData.forEach((item) => { + if (item.path) { + if (item.path === path) { + items.push(item); + return; + } + if (path.length >= item.path?.length) { + const exp = `${item.path}/*`; + if (path.match(exp)) { + if (item.routes) { + const subpath = path.substr(item.path.length + 1); + const subItem: MenuDataItem[] = getMatchMenuItem(subpath, item.routes); + items = items.concat(subItem); + } else { + const paths = path.split('/'); + if (paths.length >= 2 && paths[0] === item.path && paths[1] === 'index') { + items.push(item); + } + } + } + } + } + }); + return items; +} diff --git a/src/types/analysis/industry.d.ts b/src/types/analysis/industry.d.ts new file mode 100644 index 0000000..9da12d0 --- /dev/null +++ b/src/types/analysis/industry.d.ts @@ -0,0 +1,103 @@ +import dayjs from 'dayjs'; +import { ReactNode } from 'react'; + +export type TimeDimension = '月' | '季度' | '年'; +export type AnalysisType = '岗位发布数量' | '招聘增长率'; +export type QuarterFormat = `${number}-${'Q1'|'Q2'|'Q3'|'Q4'|'第一季度'|'第二季度'|'第三季度'|'第四季度'}`; +export type AnalysisCategory = 'industry' | 'area' | 'salary'; +export type SalaryRange = '3k-5k' | '5k-8k' | '8k-10k' | '10k+'; +export type WorkYearRange = '应届' | '1-3年' | '3-5年' | '5年以上'; +export type EducationLevel = '大专' | '本科' | '硕士' | '博士' | '不限'; +export interface IndustryDataItem { + date: string; + category: string; + value: number; +} +export interface AreaDataItem { + name: string; + value: number; + time: string; +} +export interface IndustryTrendParams { + timeDimension: TimeDimension; + type: AnalysisType; + startTime: string; + endTime: string; + selectedIndustry: string; +} + +export interface IndustryTrendState extends IndustryTrendParams { + selectedWorkYearRange(): unknown; + selectedIndustry: string; +} + +export interface IndustryTrendApiResult { + data: IndustryDataItem[]; + code: number; + msg: string; +} + +export interface TooltipItem { + name: string; + value: number; + color: string; + data: { + date: string; + category: string; + value: number; + }; +} + +export interface ChartConfig { + data: IndustryDataItem[]; + height: number; + xField: string; + yField: string; + seriesField: string; + xAxis: { + type: string; + label: { + formatter: (text: string) => string; + }; + }; + yAxis: { + label: { + formatter: (val: string) => string; + }; + }; + tooltip: { + showTitle?: boolean; + title?: string | ((title: string, data?: TooltipItem[]) => string); + customContent?: (title: string, items: TooltipItem[]) => ReactNode; + }; + point: { + size: number; + shape: string; + }; + animation: { + appear: { + animation: string; + duration: number; + }; + }; + smooth: boolean; + interactions?: Array<{ + type: string; + cfg: { + render: (event: any, { title, items }: { title: string; items: TooltipItem[] }) => ReactNode; + }; + }>; + legend?: boolean; +} +export interface IndustryTrendState { + timeDimension: TimeDimension; + type: AnalysisType; + startTime: string; + endTime: string; + selectedIndustry: string; + selectedSalaryRange: string; + analysisCategory: AnalysisCategory; // 新增分析类别 +} +export type DateFormatter = (dateStr: string, dimension: TimeDimension) => string; +export type QuarterFormatter = (dateStr: string) => QuarterFormat; +export type ApiDataConverter = (apiData: any) => IndustryDataItem[]; \ No newline at end of file