From 7e8d7d34091e658dedcec2cc751979f04a135298 Mon Sep 17 00:00:00 2001 From: yy <3078169442@qq.com> Date: Mon, 26 May 2025 11:28:13 +0800 Subject: [PATCH 1/7] =?UTF-8?q?=E7=BB=9F=E8=AE=A1=E5=88=86=E6=9E=90?= =?UTF-8?q?=E6=96=B0=E5=A2=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/config.ts | 318 ++++++++++----------- config/routes.ts | 239 ++++++++-------- package.json | 291 +++++++++---------- src/global.less | 18 +- src/pages/Analysis/Industrytrend/index.tsx | 289 +++++++++++++++++++ src/pages/Analysis/User/index.tsx | 0 src/services/analysis/industry.ts | 8 + src/services/session.ts | 318 ++++++++++----------- src/types/analysis/industry.d.ts | 20 ++ 9 files changed, 905 insertions(+), 596 deletions(-) create mode 100644 src/pages/Analysis/Industrytrend/index.tsx create mode 100644 src/pages/Analysis/User/index.tsx create mode 100644 src/services/analysis/industry.ts create mode 100644 src/types/analysis/industry.d.ts 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/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..e0b9123 100644 --- a/package.json +++ b/package.json @@ -1,145 +1,146 @@ -{ - "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/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 6b69bce..8d30145 100644 --- a/src/global.less +++ b/src/global.less @@ -20,24 +20,14 @@ body, .ant-pro-sider.ant-layout-sider.ant-pro-sider-fixed { left: unset; } -.ant-table-cell .ant-table-row-expand-icon { - vertical-align: middle; - margin-right: 8px; -} .ant-table-row { - &-level-0 .ant-table-cell:first-child { - padding-left: 16px !important; - } &-level-1 .ant-table-cell:first-child { - padding-left: 40px !important; + padding-left: 24px !important; } &-level-2 .ant-table-cell:first-child { - padding-left: 64px !important; + padding-left: 48px !important; } -} -.ant-table-row .ant-table-cell:first-child { - display: flex; - align-items: center; + // 可根据需要添加更多层级 } .ant-table-row-level-2 .ant-table-row-expand-icon { display: none; @@ -74,4 +64,4 @@ ol { } } } -} +} \ 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..1d793b7 --- /dev/null +++ b/src/pages/Analysis/Industrytrend/index.tsx @@ -0,0 +1,289 @@ +import React, { useEffect, useState, useMemo } from 'react'; +import { Card, Select, Button, Space, Spin, Empty, Row, Col, message } from 'antd'; +import { Line } from '@ant-design/charts'; +import { getIndustryTrend } from '@/services/analysis/industry'; +import dayjs from 'dayjs'; +import { useRequest } from '@umijs/max'; + +const { Option } = Select; +type IndustryDataItem = { + date: string; + category: string; + value: number; +}; +const IndustryTrendPage: React.FC = () => { + const [params, setParams] = useState({ + timeDimension: '月' as '月' | '季度' | '年', + type: '岗位发布数量' as '岗位发布数量' | '招聘增长率', + startTime: dayjs().subtract(6, 'month').format('YYYY-MM'), + endTime: dayjs().format('YYYY-MM'), + selectedIndustry: '' + }); + const [allData, setAllData] = useState([]); + const [prevSelectedIndustry, setPrevSelectedIndustry] = useState(''); + const { loading, run } = useRequest( + () => getIndustryTrend({ + ...params, + startTime: formatTimeParam(params.startTime, params.timeDimension), + endTime: formatTimeParam(params.endTime, params.timeDimension) + }), + { + manual: true, + onError: (error) => message.error('数据加载失败: ' + error.message), + onSuccess: (responseData) => { + const formattedData = convertApiData(responseData); + setAllData(formattedData); + const industryToSelect = prevSelectedIndustry || + (formattedData.length > 0 ? formattedData[0].category : ''); + setParams(prev => ({ ...prev, selectedIndustry: industryToSelect })); + } + } + ); + const convertApiData = (apiData: any): IndustryDataItem[] => { + if (!apiData) return []; + + try { + const result: IndustryDataItem[] = []; + if (typeof apiData === 'object' && !Array.isArray(apiData)) { + Object.entries(apiData).forEach(([month, monthData]) => { + if (Array.isArray(monthData)) { + (monthData as any[]).forEach(item => { + const convertedItem = { + date: month, + category: item.name || item.category, + value: Number(item.data || item.value) || 0 + }; + console.log('转换后的数据项:', convertedItem); + result.push(convertedItem); + }); + } + }); + return result; + } + if (Array.isArray(apiData)) { + return apiData.map(item => { + const convertedItem = { + date: item.time || item.date, + category: item.name || item.category, + value: Number(item.data || item.value) || 0 + }; + console.log('转换后的数据项:', convertedItem); + return convertedItem; + }); + } + + return []; + } catch (error) { + console.error('数据转换错误:', error); + return []; + } + }; + const industries = useMemo(() => { + if (allData.length === 0) return []; + const categories = new Set(); + allData.forEach(item => categories.add(item.category)); + return Array.from(categories).sort(); + }, [allData]); + const formatTimeParam = (dateStr: string, dimension: string) => { + const date = dayjs(dateStr); + switch (dimension) { + case '季度': return `${date.year()}-Q${Math.ceil((date.month() + 1) / 3)}`; + case '年': return `${date.year()}`; + default: return date.format('YYYY-MM'); + } + }; + const handleIndustryChange = (value: string) => { + setPrevSelectedIndustry(value); + setParams(p => ({ ...p, selectedIndustry: value })); + }; + + const currentIndustryData = useMemo(() => { + if (!params.selectedIndustry) return []; + + const filteredData = allData + .filter(item => item.category === params.selectedIndustry) + .map(item => ({ + date: item.date, + category: item.category, + value: item.value ?? 0, + })) + .sort((a, b) => dayjs(a.date).valueOf() - dayjs(b.date).valueOf()); + console.log("当前行业数据 (过滤后):", filteredData); + return filteredData; + }, [allData, params.selectedIndustry]); + console.log("最终图表数据:", currentIndustryData); + const chartConfig = { + data: currentIndustryData, + height: 180, + xField: 'date', + yField: 'value', + seriesField: 'category', + xAxis: { + type: 'cat', + tickCount: 5, + label: { + formatter: (text: string) => { + if (params.timeDimension === '年') return text.split('-')[0]; + if (params.timeDimension === '季度') return text.split('-')[1]; + return text.split('-')[1]; + }, + }, + }, + yAxis: { + label: { + formatter: (val: string) => `${val}${params.type === '招聘增长率' ? '%' : '个'}`, + }, + }, + tooltip: false, + point: { + size: 4, + shape: 'circle', + }, + animation: { + appear: { + animation: 'path-in', + duration: 1000, + }, + }, + }; + + + useEffect(() => { + run(); + }, [params.timeDimension, params.startTime, params.endTime]); + + return ( +
+ +
+ + + + + + + + + +
+ + + + + {currentIndustryData.length > 0 ? ( + + ) : ( + + )} + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ ); +}; + +export default IndustryTrendPage; \ 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..e69de29 diff --git a/src/services/analysis/industry.ts b/src/services/analysis/industry.ts new file mode 100644 index 0000000..8f10341 --- /dev/null +++ b/src/services/analysis/industry.ts @@ -0,0 +1,8 @@ +import { request } from '@umijs/max'; + +export async function getIndustryTrend(params?: API.Analysis.IndustryParams) { + return request('/api/cms/statics/industry', { + method: 'GET', + params + }); +} \ No newline at end of file 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..dcb266e --- /dev/null +++ b/src/types/analysis/industry.d.ts @@ -0,0 +1,20 @@ +declare namespace API.Analysis { + export interface IndustryResult { + data: TrendItem[]; + code: number; + msg: string; + } + + export interface TrendItem { + date: string; + category: string; + value: number; + } + + export interface IndustryParams { + timeDimension: '月' | '季度' | '年'; + type: '岗位发布数量' | '招聘增长率'; + startTime: string; + endTime: string; + } + } \ No newline at end of file From fa577431b8c828422ed2b6df051721ed972d3060 Mon Sep 17 00:00:00 2001 From: yy <3078169442@qq.com> Date: Mon, 26 May 2025 12:19:08 +0800 Subject: [PATCH 2/7] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=97=A5=E6=9C=9F?= =?UTF-8?q?=E9=80=89=E6=8B=A9=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/Analysis/Industrytrend/index.tsx | 320 ++++++++++++--------- 1 file changed, 179 insertions(+), 141 deletions(-) diff --git a/src/pages/Analysis/Industrytrend/index.tsx b/src/pages/Analysis/Industrytrend/index.tsx index 1d793b7..28da5fe 100644 --- a/src/pages/Analysis/Industrytrend/index.tsx +++ b/src/pages/Analysis/Industrytrend/index.tsx @@ -1,76 +1,122 @@ import React, { useEffect, useState, useMemo } from 'react'; -import { Card, Select, Button, Space, Spin, Empty, Row, Col, message } from 'antd'; +import { Card, Select, Button, Space, Spin, Empty, Row, Col, message, DatePicker } from 'antd'; import { Line } from '@ant-design/charts'; import { getIndustryTrend } from '@/services/analysis/industry'; import dayjs from 'dayjs'; import { useRequest } from '@umijs/max'; const { Option } = Select; +const { RangePicker } = DatePicker; + type IndustryDataItem = { date: string; category: string; value: number; }; +const formatDateForDisplay = (dateStr: string, dimension: string): string => { + try { + if (dimension === '年') { + return dateStr.split('-')[0]; + } + if (dimension === '季度') { + const [year, quarter] = dateStr.split('-Q'); + return `${year}年Q${quarter}`; + } + // 默认按月显示 + const [year, month] = dateStr.split('-'); + return `${year}年${month}月`; + } catch (e) { + console.error('日期格式化错误:', e); + return dateStr; + } +}; + const IndustryTrendPage: React.FC = () => { const [params, setParams] = useState({ timeDimension: '月' as '月' | '季度' | '年', type: '岗位发布数量' as '岗位发布数量' | '招聘增长率', - startTime: dayjs().subtract(6, 'month').format('YYYY-MM'), + startTime: dayjs().subtract(5, 'month').format('YYYY-MM'), endTime: dayjs().format('YYYY-MM'), - selectedIndustry: '' + selectedIndustry: '' }); + const [allData, setAllData] = useState([]); - const [prevSelectedIndustry, setPrevSelectedIndustry] = useState(''); - const { loading, run } = useRequest( - () => getIndustryTrend({ - ...params, - startTime: formatTimeParam(params.startTime, params.timeDimension), - endTime: formatTimeParam(params.endTime, params.timeDimension) - }), + const [availableIndustries, setAvailableIndustries] = useState([]); + const { loading, run: fetchData } = useRequest( + async () => { + try { + const resp = await getIndustryTrend({ + timeDimension: params.timeDimension, + type: params.type, + startTime: formatTimeParam(params.startTime, params.timeDimension), + endTime: formatTimeParam(params.endTime, params.timeDimension) + }); + return resp; + } catch (error) { + message.error('数据加载失败'); + throw error; + } + }, { manual: true, - onError: (error) => message.error('数据加载失败: ' + error.message), - onSuccess: (responseData) => { - const formattedData = convertApiData(responseData); + onSuccess: (data) => { + const formattedData = convertApiData(data); setAllData(formattedData); - const industryToSelect = prevSelectedIndustry || - (formattedData.length > 0 ? formattedData[0].category : ''); - setParams(prev => ({ ...prev, selectedIndustry: industryToSelect })); + const industries = Array.from( + new Set(formattedData.map(item => item.category)) + ).filter(Boolean).sort(); + + setAvailableIndustries(industries); + if (industries.length > 0 && !industries.includes(params.selectedIndustry)) { + setParams(p => ({ ...p, selectedIndustry: industries[0] })); + } } } ); + + const formatTimeParam = (dateStr: string, dimension: string): string => { + try { + const date = dayjs(dateStr); + switch (dimension) { + case '季度': + return `${date.year()}-Q${Math.ceil((date.month() + 1) / 3)}`; + case '年': + return `${date.year()}`; + default: + return date.format('YYYY-MM'); + } + } catch (e) { + console.error('日期参数格式化错误:', e); + return dateStr; + } + }; + const convertApiData = (apiData: any): IndustryDataItem[] => { if (!apiData) return []; try { - const result: IndustryDataItem[] = []; - if (typeof apiData === 'object' && !Array.isArray(apiData)) { - Object.entries(apiData).forEach(([month, monthData]) => { - if (Array.isArray(monthData)) { - (monthData as any[]).forEach(item => { - const convertedItem = { - date: month, - category: item.name || item.category, + 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 - }; - console.log('转换后的数据项:', convertedItem); - result.push(convertedItem); + }); }); } }); return result; } - if (Array.isArray(apiData)) { - return apiData.map(item => { - const convertedItem = { - date: item.time || item.date, - category: item.name || item.category, - value: Number(item.data || item.value) || 0 - }; - console.log('转换后的数据项:', convertedItem); - return convertedItem; - }); - } return []; } catch (error) { @@ -78,40 +124,65 @@ const IndustryTrendPage: React.FC = () => { return []; } }; - const industries = useMemo(() => { - if (allData.length === 0) return []; - const categories = new Set(); - allData.forEach(item => categories.add(item.category)); - return Array.from(categories).sort(); - }, [allData]); - const formatTimeParam = (dateStr: string, dimension: string) => { - const date = dayjs(dateStr); - switch (dimension) { - case '季度': return `${date.year()}-Q${Math.ceil((date.month() + 1) / 3)}`; - case '年': return `${date.year()}`; - default: return date.format('YYYY-MM'); + const handleTimeDimensionChange = (value: '月' | '季度' | '年') => { + const now = dayjs(); + let newStartTime = ''; + switch (value) { + case '月': + newStartTime = now.subtract(6, 'month').format('YYYY-MM'); + break; + case '季度': + newStartTime = now.subtract(6, 'quarter').format('YYYY-Q'); + break; + case '年': + default: + 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 handleIndustryChange = (value: string) => { - setPrevSelectedIndustry(value); - setParams(p => ({ ...p, selectedIndustry: value })); - }; - const currentIndustryData = useMemo(() => { - if (!params.selectedIndustry) return []; - - const filteredData = allData + if (!params.selectedIndustry || allData.length === 0) return []; + + return allData .filter(item => item.category === params.selectedIndustry) .map(item => ({ - date: item.date, - category: item.category, - value: item.value ?? 0, + ...item, + date: formatDateForDisplay(item.date, params.timeDimension) })) .sort((a, b) => dayjs(a.date).valueOf() - dayjs(b.date).valueOf()); - console.log("当前行业数据 (过滤后):", filteredData); - return filteredData; - }, [allData, params.selectedIndustry]); - console.log("最终图表数据:", currentIndustryData); + }, [allData, params.selectedIndustry, params.timeDimension]); + 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) { + console.error('日期解析错误:', e); + return null; + } + }; + + // 图表配置 const chartConfig = { data: currentIndustryData, height: 180, @@ -120,13 +191,8 @@ const IndustryTrendPage: React.FC = () => { seriesField: 'category', xAxis: { type: 'cat', - tickCount: 5, label: { - formatter: (text: string) => { - if (params.timeDimension === '年') return text.split('-')[0]; - if (params.timeDimension === '季度') return text.split('-')[1]; - return text.split('-')[1]; - }, + formatter: (text: string) => text, }, }, yAxis: { @@ -145,36 +211,35 @@ const IndustryTrendPage: React.FC = () => { duration: 1000, }, }, + smooth: true, }; - + // 初始化加载数据 useEffect(() => { - run(); - }, [params.timeDimension, params.startTime, params.endTime]); + fetchData(); + }, [params.timeDimension, params.startTime, params.endTime, params.type]); return (
- -
+ +
+ + { ); }; - - export default IndustryTrendPage; \ No newline at end of file diff --git a/src/types/analysis/industry.d.ts b/src/types/analysis/industry.d.ts index 2a26174..422f599 100644 --- a/src/types/analysis/industry.d.ts +++ b/src/types/analysis/industry.d.ts @@ -1,4 +1,5 @@ import dayjs from 'dayjs'; +import { ReactNode } from 'react'; export type TimeDimension = '月' | '季度' | '年'; export type AnalysisType = '岗位发布数量' | '招聘增长率'; @@ -27,6 +28,17 @@ export interface IndustryTrendApiResult { 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; @@ -44,7 +56,11 @@ export interface ChartConfig { formatter: (val: string) => string; }; }; - tooltip: false; + tooltip: { + showTitle?: boolean; + title?: string | ((title: string, data?: TooltipItem[]) => string); + customContent?: (title: string, items: TooltipItem[]) => ReactNode; + }; point: { size: number; shape: string; @@ -56,6 +72,13 @@ export interface ChartConfig { }; }; smooth: boolean; + interactions?: Array<{ + type: string; + cfg: { + render: (event: any, { title, items }: { title: string; items: TooltipItem[] }) => ReactNode; + }; + }>; + legend?: boolean; } export type DateFormatter = (dateStr: string, dimension: TimeDimension) => string; From 4136170101358ba1783dbfb8f2ee2d653f205943 Mon Sep 17 00:00:00 2001 From: yy <3078169442@qq.com> Date: Fri, 30 May 2025 14:26:32 +0800 Subject: [PATCH 5/7] =?UTF-8?q?=E5=90=8E=E7=AB=AF=E6=9F=A5=E7=9C=8B?= =?UTF-8?q?=E6=95=88=E6=9E=9C=EF=BC=8C=E4=B8=8D=E7=94=A8=E6=8B=89=E5=8F=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/proxy.ts | 98 ++-- package.json | 1 + src/app.tsx | 542 ++++++++--------- src/pages/Analysis/Industrytrend/index.tsx | 641 +++++++++++++++------ src/pages/Analysis/Industrytrend/utils.ts | 85 ++- src/services/analysis/industry.ts | 24 + src/types/analysis/industry.d.ts | 19 +- 7 files changed, 911 insertions(+), 499 deletions(-) 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/package.json b/package.json index e0b9123..b7a876a 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "@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", diff --git a/src/app.tsx b/src/app.tsx index 900d7e9..ab7c2f4 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -1,271 +1,271 @@ -import { AvatarDropdown, AvatarName, Footer, SelectLang } from '@/components'; -import type { Settings as LayoutSettings } from '@ant-design/pro-components'; -import { SettingDrawer } from '@ant-design/pro-components'; -import type { RunTimeLayoutConfig } from '@umijs/max'; -import { history } from '@umijs/max'; -import defaultSettings from '../config/defaultSettings'; -import { errorConfig } from './requestErrorConfig'; -import { clearSessionToken, getAccessToken, getRefreshToken, getTokenExpireTime } from './access'; -import { - getRemoteMenu, - getRoutersInfo, - getUserInfo, - patchRouteWithRemoteMenus, - setRemoteMenu, -} from './services/session'; -import { PageEnum } from './enums/pagesEnums'; -import { stringify } from 'querystring'; -import { message } from 'antd'; - -const isDev = process.env.NODE_ENV === 'development'; -const loginOut = async () => { - clearSessionToken(); - setRemoteMenu(null); - const { search, pathname } = window.location; - const urlParams = new URL(window.location.href).searchParams; - /** 此方法会跳转到 redirect 参数所在的位置 */ - const redirect = urlParams.get('redirect'); - // Note: There may be security issues, please note - console.log('redirect', window.location.pathname, redirect); - if (window.location.pathname !== '/qingdao/user/login' && !redirect) { - history.replace({ - pathname: '/user/login', - search: stringify({ - redirect: pathname.replace('/qingdao', '') + search, - }), - }); - } -}; - -/** - * @see https://umijs.org/zh-CN/plugins/plugin-initial-state - * */ -export async function getInitialState(): Promise<{ - settings?: Partial; - currentUser?: API.CurrentUser; - loading?: boolean; - fetchUserInfo?: () => Promise; -}> { - const fetchUserInfo = async () => { - try { - const response = await getUserInfo({ - skipErrorHandler: true, - }); - if (response.user.avatar === '') { - response.user.avatar = - 'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png'; - } - return { - ...response.user, - permissions: response.permissions, - roles: response.roles, - } as API.CurrentUser; - } catch (error) { - console.log(error); - history.push(PageEnum.LOGIN); - } - return undefined; - }; - // 如果不是登录页面,执行 - const { location } = history; - if (location.pathname !== PageEnum.LOGIN) { - const currentUser = await fetchUserInfo(); - return { - fetchUserInfo, - currentUser, - settings: defaultSettings as Partial, - }; - } - return { - fetchUserInfo, - settings: defaultSettings as Partial, - }; -} - -// ProLayout 支持的api https://procomponents.ant.design/components/layout -export const layout: RunTimeLayoutConfig = ({ initialState, setInitialState }) => { - return { - // actionsRender: () => [, ], - actionsRender: () => [], - avatarProps: { - src: initialState?.currentUser?.avatar, - title: , - render: (_, avatarChildren) => { - return {avatarChildren}; - }, - }, - waterMarkProps: { - // content: initialState?.currentUser?.nickName, - }, - // actionRef: layoutActionRef, - menu: { - locale: false, - // // 每当 initialState?.currentUser?.userid 发生修改时重新执行 request - params: { - userId: initialState?.currentUser?.userId, - }, - request: async () => { - if (!initialState?.currentUser?.userId) { - return []; - } - return getRemoteMenu(); - }, - }, - footerRender: () =>