Przeglądaj źródła

chore: :hammer: 完善项目工程化配置并统一 TypeScript 配置文件

Zimo 1 tydzień temu
rodzic
commit
43e2ceb435

+ 0 - 1
.prettierignore

@@ -3,7 +3,6 @@
 /dist*
 /public/*
 /docs/*
-/vite.config.ts
 /src/types/env.d.ts
 /src/types/auto-components.d.ts
 /src/types/auto-imports.d.ts

+ 0 - 18
.vscode/extensions.json

@@ -1,18 +0,0 @@
-{
-  "recommendations": [
-    "christian-kohler.path-intellisense",
-    "vscode-icons-team.vscode-icons",
-    "davidanson.vscode-markdownlint",
-    "dbaeumer.vscode-eslint",
-    "esbenp.prettier-vscode",
-    "mrmlnc.vscode-less",
-    "lokalise.i18n-ally",
-    "redhat.vscode-yaml",
-    "csstools.postcss",
-    "mikestead.dotenv",
-    "eamodio.gitlens",
-    "antfu.iconify",
-    "antfu.unocss",
-    "Vue.volar"
-  ]
-}

+ 25 - 149
.vscode/settings.json

@@ -1,99 +1,29 @@
 {
-  "typescript.tsdk": "./node_modules/typescript/lib",
-  "volar.tsPlugin": true,
-  "volar.tsPluginStatus": false,
+  "js/ts.tsdk.path": "./node_modules/typescript/lib",
   "npm.packageManager": "pnpm",
-  "editor.tabSize": 2,
-  "prettier.printWidth": 100, // 超过最大值换行
-  "editor.defaultFormatter": "esbenp.prettier-vscode",
-  "files.eol": "\n",
-  "search.exclude": {
-    "**/node_modules": true,
-    "**/*.log": true,
-    "**/*.log*": true,
-    "**/bower_components": true,
-    "**/dist": true,
-    "**/elehukouben": true,
-    "**/.git": true,
-    "**/.gitignore": true,
-    "**/.svn": true,
-    "**/.DS_Store": true,
-    "**/.idea": true,
-    "**/.vscode": false,
-    "**/yarn.lock": true,
-    "**/tmp": true,
-    "out": true,
-    "dist": true,
-    "node_modules": true,
-    "CHANGELOG.md": true,
-    "examples": true,
-    "res": true,
-    "screenshots": true,
-    "yarn-error.log": true,
-    "**/.yarn": true
-  },
-  "files.exclude": {
-    "**/.cache": true,
-    "**/.editorconfig": true,
-    "**/.eslintcache": true,
-    "**/bower_components": true,
-    "**/.idea": true,
-    "**/tmp": true,
-    "**/.git": true,
-    "**/.svn": true,
-    "**/.hg": true,
-    "**/CVS": true,
-    "**/.DS_Store": true
-  },
-  "files.watcherExclude": {
-    "**/.git/objects/**": true,
-    "**/.git/subtree-cache/**": true,
-    "**/.vscode/**": true,
-    "**/node_modules/**": true,
-    "**/tmp/**": true,
-    "**/bower_components/**": true,
-    "**/dist/**": true,
-    "**/yarn.lock": true
-  },
-  "stylelint.enable": true,
-  "stylelint.validate": ["css", "less", "postcss", "scss", "vue", "sass"],
-  "path-intellisense.mappings": {
-    "@/": "${workspaceRoot}/src"
-  },
-  "[javascriptreact]": {
-    "editor.defaultFormatter": "esbenp.prettier-vscode"
-  },
-  "[typescript]": {
-    "editor.defaultFormatter": "esbenp.prettier-vscode"
-  },
-  "[typescriptreact]": {
-    "editor.defaultFormatter": "esbenp.prettier-vscode"
-  },
-  "[html]": {
-    "editor.defaultFormatter": "esbenp.prettier-vscode"
-  },
-  "[css]": {
-    "editor.defaultFormatter": "esbenp.prettier-vscode"
-  },
-  "[less]": {
-    "editor.defaultFormatter": "esbenp.prettier-vscode"
-  },
-  "[scss]": {
-    "editor.defaultFormatter": "esbenp.prettier-vscode"
-  },
-  "[markdown]": {
+  "[javascript][typescript][javascriptreact][typescriptreact][html][json][jsonc][postcss][css][less][sass]": {
     "editor.defaultFormatter": "esbenp.prettier-vscode"
   },
   "editor.codeActionsOnSave": {
-    "source.fixAll.eslint": "explicit"
-  },
-  "editor.formatOnSave": true,
-  "[vue]": {
-    "editor.codeActionsOnSave": {
-      "source.fixAll.eslint": "explicit",
-      "source.fixAll.stylelint": "explicit"
-    }
-  },
+    "source.fixAll.eslint": "explicit",
+    "source.fixAll.stylelint": "explicit",
+    "source.removeUnusedImports": "explicit"
+  },
+  "eslint.useFlatConfig": true,
+  "eslint.validate": [
+    "javascript",
+    "javascriptreact",
+    "typescript",
+    "typescriptreact",
+    "html",
+    "markdown",
+    "json",
+    "jsonc",
+    "yaml",
+    "vue"
+  ],
+  "stylelint.validate": ["css", "postcss", "scss", "vue", "sass"],
+  "path-intellisense.mappings": { "@/": "${workspaceRoot}/src" },
   "i18n-ally.localesPaths": ["src/locales"],
   "i18n-ally.keystyle": "nested",
   "i18n-ally.sortKeys": true,
@@ -102,67 +32,13 @@
   "i18n-ally.sourceLanguage": "en",
   "i18n-ally.displayLanguage": "zh-CN",
   "i18n-ally.enabledFrameworks": ["vue", "react"],
-  "cSpell.words": [
-    "xingyu",
-    "yudao",
-    "unocss",
-    "browserslist",
-    "esnext",
-    "unplugin",
-    "qrcode",
-    "sider",
-    "pinia",
-    "sider",
-    "nprogress",
-    "INTLIFY",
-    "stylelint",
-    "esno",
-    "vitejs",
-    "sortablejs",
-    "codemirror",
-    "iconify",
-    "commitlint",
-    "videojs",
-    "echarts",
-    "wangeditor",
-    "cropperjs",
-    "logicflow",
-    "vueuse",
-    "zxcvbn",
-    "lintstagedrc",
-    "brotli",
-    "sider",
-    "pnpm"
-  ],
-  "vetur.format.scriptInitialIndent": true,
-  "vetur.format.styleInitialIndent": true,
-  "vetur.validation.script": false,
-  "MicroPython.executeButton": [
-    {
-      "text": "▶",
-      "tooltip": "运行",
-      "alignment": "left",
-      "command": "extension.executeFile",
-      "priority": 3.5
-    }
-  ],
-  "MicroPython.syncButton": [
-    {
-      "text": "$(sync)",
-      "tooltip": "同步",
-      "alignment": "left",
-      "command": "extension.execute",
-      "priority": 4
-    }
-  ],
-  // 控制相关文件嵌套展示
   "explorer.fileNesting.enabled": true,
   "explorer.fileNesting.expand": false,
   "explorer.fileNesting.patterns": {
     "*.ts": "$(capture).test.ts, $(capture).test.tsx",
     "*.tsx": "$(capture).test.ts, $(capture).test.tsx",
-    "*.env": "$(capture).env.*",
-    "package.json": "pnpm-lock.yaml,yarn.lock,LICENSE,README*,CHANGELOG*,CNAME,.gitattributes,.gitignore,prettier.config.js,stylelint.config.js,commitlint.config.js,.stylelintignore,.prettierignore,.gitpod.yml,.eslintrc.js,.eslintignore"
-  },
-  "terminal.integrated.scrollback": 10000
+    ".env": ".env.*",
+    "vite.config.ts": "eslint.config.ts,stylelint.config.ts,prettier.config.ts,commitlint.config.ts,postcss.config.ts,uno.config.ts,lint-staged.config.ts,tsconfig.json,web-types.json,index.html",
+    "package.json": "pnpm-lock.yaml,pnpm-workspace.yaml,LICENSE,README*,.gitattributes,.gitignore,.editorconfig,.gitpod.yml,.eslintignore,.prettierignore,.stylelintignore,.eslintrc-auto-import.json"
+  }
 }

+ 28 - 0
build/vite/css.ts

@@ -0,0 +1,28 @@
+import { dirname, relative } from 'node:path'
+import { normalizePath } from 'vite'
+
+const SCSS_VARIABLES_SUFFIX = '/src/styles/variables.scss'
+const SCSS_GLOBAL_MODULE_SUFFIX = '/src/styles/global.module.scss'
+
+function getScssUsePath(filename: string, variablesScssPath: string) {
+  const cleanFilename = filename.split('?')[0]
+  const relativePath = normalizePath(relative(dirname(cleanFilename), variablesScssPath))
+
+  return relativePath.startsWith('.') ? relativePath : `./${relativePath}`
+}
+
+export function createScssAdditionalData(variablesScssPath: string) {
+  return (source: string, filename: string) => {
+    const cleanFilename = filename.split('?')[0]
+    const normalizedFilename = normalizePath(cleanFilename)
+
+    if (
+      normalizedFilename.endsWith(SCSS_VARIABLES_SUFFIX) ||
+      normalizedFilename.endsWith(SCSS_GLOBAL_MODULE_SUFFIX)
+    ) {
+      return source
+    }
+
+    return `@use "${getScssUsePath(cleanFilename, variablesScssPath)}" as *;\n${source}`
+  }
+}

+ 19 - 0
build/vite/env.ts

@@ -0,0 +1,19 @@
+import { ConfigEnv, loadEnv } from 'vite'
+import { root } from './path'
+
+export function getEnv(
+  command: ConfigEnv['command'],
+  mode: ConfigEnv['mode']
+): { env: any; isBuild: boolean } {
+  const isBuild = command === 'build'
+
+  let env = {} as any
+
+  if (!isBuild) {
+    env = loadEnv(process.argv[3] === '--mode' ? process.argv[4] : process.argv[3], root)
+  } else {
+    env = loadEnv(mode, root)
+  }
+
+  return { env, isBuild }
+}

+ 14 - 0
build/vite/path.ts

@@ -0,0 +1,14 @@
+import { dirname, relative, resolve } from 'path'
+import { normalizePath } from 'vite'
+
+export const root = process.cwd()
+
+export function pathResolve(dir: string) {
+  return resolve(root, '.', dir)
+}
+
+export function getRelativeScssUsePath(filename: string, targetPath: string) {
+  const cleanFilename = filename.split('?')[0]
+  const relativePath = normalizePath(relative(dirname(cleanFilename), targetPath))
+  return relativePath.startsWith('.') ? relativePath : `./${relativePath}`
+}

+ 99 - 0
commitlint.config.ts

@@ -0,0 +1,99 @@
+import { defineConfig } from 'cz-git'
+
+export default defineConfig({
+  prompt: {
+    messages: {
+      type: '选择你要提交的类型 :',
+      scope: '选择一个提交范围(可选):',
+      customScope: '请输入自定义的提交范围 :',
+      subject: '填写简短精炼的变更描述 :\n',
+      body: '填写更加详细的变更描述(可选)。使用 "|" 换行 :\n',
+      breaking: '列举非兼容性重大的变更(可选)。使用 "|" 换行 :\n',
+      footerPrefixesSelect: '选择关联issue前缀(可选):',
+      customFooterPrefix: '输入自定义issue前缀 :',
+      footer: '列举关联issue (可选) 例如: #31, #I3244 :\n',
+      confirmCommit: '是否提交或修改commit ?'
+    },
+    types: [
+      { value: 'feat', name: 'feat:     ✨  新增功能 | A new feature', emoji: ':sparkles:' },
+      { value: 'fix', name: 'fix:      🐛  修复缺陷 | A bug fix', emoji: ':bug:' },
+      {
+        value: 'docs',
+        name: 'docs:     📝  文档更新 | Documentation only changes',
+        emoji: ':memo:'
+      },
+      {
+        value: 'style',
+        name: 'style:    💄  代码格式 | Changes that do not affect the meaning of the code',
+        emoji: ':lipstick:'
+      },
+      {
+        value: 'refactor',
+        name: 'refactor: ♻️   代码重构 | A code change that neither fixes a bug nor adds a feature',
+        emoji: ':recycle:'
+      },
+      {
+        value: 'perf',
+        name: 'perf:     ⚡️  性能提升 | A code change that improves performance',
+        emoji: ':zap:'
+      },
+      {
+        value: 'test',
+        name: 'test:     ✅  测试相关 | Adding missing tests or correcting existing tests',
+        emoji: ':white_check_mark:'
+      },
+      {
+        value: 'build',
+        name: 'build:    📦️  构建相关 | Changes that affect the build system or external dependencies',
+        emoji: ':package:'
+      },
+      {
+        value: 'ci',
+        name: 'ci:       🎡  持续集成 | Changes to our CI configuration files and scripts',
+        emoji: ':ferris_wheel:'
+      },
+      { value: 'revert', name: 'revert:   ⏪️  回退代码 | Revert to a commit', emoji: ':rewind:' },
+      {
+        value: 'chore',
+        name: 'chore:    🔨  其他修改 | Other changes that do not modify src or test files',
+        emoji: ':hammer:'
+      }
+    ],
+    useEmoji: true,
+    emojiAlign: 'center',
+    useAI: false,
+    aiNumber: 3,
+    aiModel: 'gpt-5.6-luna',
+    aiQuestionCB: ({ maxSubjectLength, diff }) =>
+      `用完整句子为以下 Git diff 代码写一个有见解并简洁的 Git 中文提交消息,不加任何前缀,并且内容不能超过 ${maxSubjectLength} 个字符: \`\`\`diff\n${diff}\n\`\`\``,
+    themeColorCode: '',
+    scopes: [],
+    allowCustomScopes: true,
+    allowEmptyScopes: true,
+    customScopesAlign: 'bottom',
+    customScopesAlias: 'custom | 以上都不是?我要自定义',
+    emptyScopesAlias: 'empty | 跳过',
+    upperCaseSubject: null,
+    markBreakingChangeMode: true,
+    allowBreakingChanges: ['feat', 'fix'],
+    breaklineNumber: 100,
+    breaklineChar: '|',
+    skipQuestions: [],
+    issuePrefixes: [
+      // 如果使用 gitee 作为开发管理
+      { value: 'link', name: 'link:     链接 ISSUES 进行中' },
+      { value: 'closed', name: 'closed:   标记 ISSUES 已完成' }
+    ],
+    customIssuePrefixAlign: 'top',
+    emptyIssuePrefixAlias: 'skip | 跳过',
+    customIssuePrefixAlias: 'custom | 自定义前缀',
+    allowCustomIssuePrefix: true,
+    allowEmptyIssuePrefix: true,
+    confirmColorize: true,
+    scopeOverrides: undefined,
+    defaultBody: '',
+    defaultIssues: '',
+    defaultScope: '',
+    defaultSubject: ''
+  }
+})

+ 0 - 117
eslint.config.mjs

@@ -1,117 +0,0 @@
-import pluginVue from 'eslint-plugin-vue'
-import tseslint from 'typescript-eslint'
-import unocss from '@unocss/eslint-config/flat'
-import autoImportGlobals from './.eslintrc-auto-import.json' with { type: 'json' }
-
-export default tseslint.config(
-  // Global ignores (replaces .eslintignore)
-  {
-    ignores: [
-      'build/',
-      'config/',
-      'dist/',
-      'dist*/',
-      '*.js',
-      '*.mjs',
-      '!eslint.config.mjs',
-      'test/unit/coverage/',
-      'node_modules/',
-      'src/main.ts',
-      'src/types/auto-components.d.ts',
-      'src/components/Tinyflow/ui/**'
-    ]
-  },
-
-  // Base TypeScript config
-  ...tseslint.configs.recommended,
-
-  // Vue recommended config
-  ...pluginVue.configs['flat/recommended'],
-
-  // UnoCSS config
-  unocss,
-
-  // Vue files use vue-eslint-parser with TypeScript parser
-  {
-    files: ['**/*.vue'],
-    languageOptions: {
-      parserOptions: {
-        parser: tseslint.parser
-      }
-    }
-  },
-
-  // Main rules config
-  {
-    languageOptions: {
-      ecmaVersion: 2020,
-      sourceType: 'module',
-      globals: {
-        ...autoImportGlobals.globals
-      },
-      parserOptions: {
-        ecmaFeatures: {
-          jsx: true
-        }
-      }
-    },
-    rules: {
-      // Vue rules
-      'vue/no-reserved-component-names': 'off',
-      'vue/custom-event-name-casing': 'off',
-      'vue/attributes-order': 'off',
-      'vue/one-component-per-file': 'off',
-      'vue/html-closing-bracket-newline': 'off',
-      'vue/html-indent': 'off', // Vue 模板缩进交给 Prettier,避免格式化规则互相拉扯。
-      'vue/max-attributes-per-line': 'off',
-      'vue/multiline-html-element-content-newline': 'off',
-      'vue/singleline-html-element-content-newline': 'off',
-      'vue/attribute-hyphenation': 'off',
-      'vue/require-default-prop': 'off',
-      'vue/require-explicit-emits': 'off',
-      'vue/require-toggle-inside-transition': 'off',
-      'vue/html-self-closing': [
-        'error',
-        {
-          html: {
-            void: 'always',
-            normal: 'never',
-            component: 'always'
-          },
-          svg: 'always',
-          math: 'always'
-        }
-      ],
-      'vue/multi-word-component-names': 'off',
-      'vue/no-v-html': 'off',
-
-      // TypeScript rules
-      '@typescript-eslint/ban-ts-comment': 'off',
-      '@typescript-eslint/explicit-function-return-type': 'off',
-      '@typescript-eslint/no-explicit-any': 'off',
-      '@typescript-eslint/no-empty-function': 'off',
-      '@typescript-eslint/no-non-null-assertion': 'off',
-      '@typescript-eslint/explicit-module-boundary-types': 'off',
-      '@typescript-eslint/no-unused-vars': 'off',
-      '@typescript-eslint/no-require-imports': 'off',
-      '@typescript-eslint/no-unused-expressions': 'off',
-      '@typescript-eslint/no-unsafe-function-type': 'off',
-      '@typescript-eslint/no-wrapper-object-types': 'off',
-      '@typescript-eslint/no-this-alias': 'off',
-      '@typescript-eslint/no-empty-object-type': 'off',
-      'vue/no-ref-as-operand': 'off',
-      'vue/no-mutating-props': 'off',
-      'vue/no-side-effects-in-computed-properties': 'off',
-      'no-use-before-define': 'off',
-      '@typescript-eslint/no-use-before-define': 'off',
-      'no-unused-vars': 'off',
-      'space-before-function-paren': 'off',
-
-      // UnoCSS rules - 芋艿:禁用 unocss 顺序提示
-      '@unocss/order': 'off',
-      '@unocss/order-attributify': 'off',
-      'unocss/order': 'off',
-      'unocss/order-attributify': 'off'
-    }
-  }
-)

+ 61 - 0
eslint.config.ts

@@ -0,0 +1,61 @@
+import antfu from '@antfu/eslint-config'
+import prettierConfig from 'eslint-config-prettier'
+
+export default antfu(
+  {
+    ignores: [
+      'build/',
+      'config/',
+      'dist/',
+      'dist*/',
+      '*.js',
+      '*.mjs',
+      '!eslint.config.mjs',
+      'test/unit/coverage/',
+      'node_modules/',
+      'src/main.ts',
+      'src/types/auto-components.d.ts',
+      'src/components/Tinyflow/ui/**',
+    ],
+    stylistic: false,
+    unocss: true,
+  },
+  {
+    rules: {
+      'curly': ['error', 'multi'],
+      'no-dupe-else-if': 'error',
+      'no-empty-static-block': 'error',
+      'no-nonoctal-decimal-escape': 'error',
+      'no-shadow': 'off',
+      'no-unsafe-optional-chaining': 'error',
+      'ts/no-shadow': [
+        'warn',
+        {
+          allow: [],
+          builtinGlobals: false,
+          hoist: 'functions',
+          ignoreOnInitialization: false,
+          ignoreTypeValueShadow: true,
+          ignoreFunctionTypeParameterNameValueShadow: true,
+        },
+      ],
+      'no-unused-vars': 'off',
+      'unused-imports/no-unused-imports': 'warn',
+      'unused-imports/no-unused-vars': [
+        'warn',
+        {
+          vars: 'all',
+          varsIgnorePattern: '^_',
+          args: 'after-used',
+          argsIgnorePattern: '^_',
+          caughtErrors: 'none',
+          destructuredArrayIgnorePattern: '^_',
+          ignoreRestSiblings: true,
+        },
+      ],
+      'perfectionist/sort-imports': ['error', { type: 'line-length' }],
+      'vue/no-unused-refs': 'off',
+    },
+  },
+  prettierConfig
+)

+ 0 - 12
lint-staged.config.mjs

@@ -1,12 +0,0 @@
-export default {
-  'src/**/*.{js,ts,tsx,vue}': [
-    'eslint --fix --cache --cache-location node_modules/.cache/eslint/',
-    'prettier --write --loglevel warn --cache --cache-location node_modules/.cache/prettier/.prettier-cache'
-  ],
-  'src/**/*.{css,less,scss,postcss,vue}': [
-    'stylelint --fix --cache --cache-location node_modules/.cache/stylelint/'
-  ],
-  'src/**/*.{json,html,md}': [
-    'prettier --write --loglevel warn --cache --cache-location node_modules/.cache/prettier/.prettier-cache'
-  ]
-}

+ 14 - 0
lint-staged.config.ts

@@ -0,0 +1,14 @@
+import type { Configuration } from 'lint-staged'
+
+const eslint = 'eslint --fix --cache --cache-location node_modules/.cache/eslint/'
+const stylelint = 'stylelint --fix --cache --cache-location node_modules/.cache/stylelint/'
+const prettier =
+  'prettier --write --ignore-unknown --log-level warn --cache --cache-location node_modules/.cache/prettier/.prettier-cache'
+
+// Vue 文件按顺序修复,避免多个任务同时写入同一个文件。
+export default {
+  '*.vue': [eslint, stylelint, prettier],
+  '*.{js,jsx,mjs,cjs,ts,tsx,mts,cts}': [eslint, prettier],
+  '*.{css,less,scss,postcss}': [stylelint, prettier],
+  '*.{json,jsonc,yaml,yml,md,html}': prettier,
+} satisfies Configuration

+ 34 - 15
package.json

@@ -1,9 +1,24 @@
 {
   "name": "yudao-ui-admin-vue3",
+  "type": "module",
   "version": "2026.08-snapshot",
+  "private": false,
   "description": "基于vue3、vite4、element-plus、typesScript",
   "author": "xingyu",
-  "private": false,
+  "license": "MIT",
+  "homepage": "https://gitee.com/yudaocode/yudao-ui-admin-vue3",
+  "repository": {
+    "type": "git",
+    "url": "git+https://gitee.com/yudaocode/yudao-ui-admin-vue3"
+  },
+  "bugs": {
+    "url": "https://gitee.com/yudaocode/yudao-ui-admin-vue3/issues"
+  },
+  "web-types": "./web-types.json",
+  "engines": {
+    "node": ">= 20.19.0",
+    "pnpm": ">=8.6.0"
+  },
   "scripts": {
     "i": "pnpm install",
     "dev": "vite --mode env.local",
@@ -26,7 +41,9 @@
     "lint:format:check": "prettier --check --cache --cache-location node_modules/.cache/prettier/.prettier-cache \"src/**/*.{js,ts,json,tsx,css,less,scss,vue,html,md}\"",
     "lint:style": "stylelint --fix \"./src/**/*.{vue,less,postcss,css,scss}\" --cache --cache-location node_modules/.cache/stylelint/",
     "lint:style:check": "stylelint \"./src/**/*.{vue,less,postcss,css,scss}\" --cache --cache-location node_modules/.cache/stylelint/",
-    "lint:lint-staged": "lint-staged"
+    "lint:lint-staged": "lint-staged",
+    "commit": "cz",
+    "prepare": "simple-git-hooks"
   },
   "dependencies": {
     "@element-plus/icons-vue": "2.3.2",
@@ -91,6 +108,7 @@
     "xml-js": "^1.6.11"
   },
   "devDependencies": {
+    "@antfu/eslint-config": "^9.5.1",
     "@commitlint/cli": "^20.5.3",
     "@commitlint/config-conventional": "^20.5.3",
     "@iconify/json": "^2.2.470",
@@ -108,26 +126,33 @@
     "autoprefixer": "^10.5.0",
     "bpmn-js": "^18.16.1",
     "bpmn-js-properties-panel": "5.54.0",
+    "commitizen": "^4.3.2",
     "consola": "^3.4.2",
+    "cz-git": "^1.14.0",
     "eslint": "^10.3.0",
+    "eslint-config-prettier": "^10.1.8",
     "eslint-plugin-vue": "^10.9.1",
+    "jiti": "^2.6.1",
     "lint-staged": "^16.4.0",
     "postcss": "^8.5.14",
     "postcss-html": "^1.8.1",
+    "postcss-less": "^6.0.0",
     "postcss-scss": "^4.0.9",
     "prettier": "^3.8.3",
-    "prettier-eslint": "^16.4.2",
     "rimraf": "^6.1.3",
     "rollup": "^4.60.3",
     "sass": "^1.99.0",
+    "simple-git-hooks": "^2.14.0",
     "stylelint": "^17.11.0",
     "stylelint-config-html": "^1.1.0",
+    "stylelint-config-recess-order": "^7.8.0",
     "stylelint-config-recommended": "^18.0.0",
     "stylelint-config-standard": "^40.0.0",
     "stylelint-order": "^8.1.1",
     "typescript": "6.0.3",
     "typescript-eslint": "^8.59.2",
     "unocss": "^66.6.8",
+    "unocss-preset-animations": "^1.3.0",
     "unplugin-auto-import": "^21.0.0",
     "unplugin-element-plus": "^0.11.2",
     "unplugin-vue-components": "^32.0.0",
@@ -137,18 +162,12 @@
     "vue-eslint-parser": "^10.4.0",
     "vue-tsc": "^3.2.8"
   },
-  "license": "MIT",
-  "repository": {
-    "type": "git",
-    "url": "git+https://gitee.com/yudaocode/yudao-ui-admin-vue3"
-  },
-  "bugs": {
-    "url": "https://gitee.com/yudaocode/yudao-ui-admin-vue3/issues"
+  "config": {
+    "commitizen": {
+      "path": "node_modules/cz-git"
+    }
   },
-  "homepage": "https://gitee.com/yudaocode/yudao-ui-admin-vue3",
-  "web-types": "./web-types.json",
-  "engines": {
-    "node": ">= 20.19.0",
-    "pnpm": ">=8.6.0"
+  "simple-git-hooks": {
+    "pre-commit": "pnpm exec lint-staged"
   }
 }

Plik diff jest za duży
+ 588 - 190
pnpm-lock.yaml


+ 1 - 0
pnpm-workspace.yaml

@@ -3,3 +3,4 @@ allowBuilds:
   core-js-pure: true
   es5-ext: true
   esbuild: true
+  simple-git-hooks: false

+ 0 - 5
postcss.config.js

@@ -1,5 +0,0 @@
-module.exports = {
-  plugins: {
-    autoprefixer: {}
-  }
-}

+ 1 - 0
postcss.config.ts

@@ -0,0 +1 @@
+export default { plugins: { autoprefixer: {} } }

+ 0 - 22
prettier.config.js

@@ -1,22 +0,0 @@
-module.exports = {
-  printWidth: 100, // 每行代码长度(默认80)
-  tabWidth: 2, // 每个tab相当于多少个空格(默认2)ab进行缩进(默认false)
-  useTabs: false, // 是否使用tab
-  semi: false, // 声明结尾使用分号(默认true)
-  vueIndentScriptAndStyle: false,
-  singleQuote: true, // 使用单引号(默认false)
-  quoteProps: 'as-needed',
-  bracketSpacing: true, // 对象字面量的大括号间使用空格(默认true)
-  trailingComma: 'none', // 多行使用拖尾逗号(默认none)
-  jsxSingleQuote: false,
-  // 箭头函数参数括号 默认avoid 可选 avoid| always
-  // avoid 能省略括号的时候就省略 例如x => x
-  // always 总是有括号
-  arrowParens: 'always',
-  insertPragma: false,
-  requirePragma: false,
-  proseWrap: 'never',
-  htmlWhitespaceSensitivity: 'strict',
-  endOfLine: 'auto',
-  rangeStart: 0
-}

+ 27 - 0
prettier.config.ts

@@ -0,0 +1,27 @@
+import type { Config } from 'prettier'
+
+const config: Config = {
+  experimentalTernaries: false,
+  experimentalOperatorPosition: 'start',
+  printWidth: 100,
+  tabWidth: 2,
+  useTabs: false,
+  semi: false,
+  singleQuote: true,
+  quoteProps: 'consistent',
+  jsxSingleQuote: true,
+  trailingComma: 'es5',
+  bracketSpacing: true,
+  objectWrap: 'collapse',
+  bracketSameLine: true,
+  arrowParens: 'avoid',
+  insertPragma: false,
+  proseWrap: 'never',
+  htmlWhitespaceSensitivity: 'strict',
+  vueIndentScriptAndStyle: true,
+  endOfLine: 'auto',
+  embeddedLanguageFormatting: 'auto',
+  singleAttributePerLine: false
+}
+
+export default config

+ 165 - 180
src/views/ai/image/index/components/ImageList.vue

@@ -1,9 +1,164 @@
+<script setup lang="ts">
+  import type { LoadingOptionsResolved } from 'element-plus'
+
+  import { ElLoading } from 'element-plus'
+
+  import type { ImageMidjourneyActionVO, ImageMidjourneyButtonsVO, ImageVO } from '@/api/ai/image'
+
+  import download from '@/utils/download'
+  import { ImageApi } from '@/api/ai/image'
+  import { AiImageStatusEnum } from '@/views/ai/utils/constants'
+
+  import ImageCard from './ImageCard.vue'
+  import ImageDetail from './ImageDetail.vue'
+
+  // 暴露组件方法
+
+  const emits = defineEmits(['onRegeneration'])
+  const message = useMessage() // 消息弹窗
+  const router = useRouter() // 路由
+
+  // 图片分页相关的参数
+  const queryParams = reactive({ pageNo: 1, pageSize: 10 })
+  const pageTotal = ref<number>(0) // page size
+  const imageList = ref<ImageVO[]>([]) // image 列表
+  const imageListLoadingInstance = ref<any>() // image 列表是否正在加载中
+  const imageListRef = ref<any>() // ref
+  // 图片轮询相关的参数(正在生成中的)
+  const inProgressImageMap = ref<any>({}) // 监听的 image 映射,一般是生成中(需要轮询),key 为 image 编号,value 为 image
+  const inProgressTimer = ref<any>() // 生成中的 image 定时器,轮询生成进展
+  // 图片详情相关的参数
+  const isShowImageDetail = ref<boolean>(false) // 图片详情是否展示
+  const showImageDetailId = ref<number>(0) // 图片详情的图片编号
+
+  /** 处理查看绘图作品 */
+  const handleViewPublic = () => {
+    router.push({ name: 'AiImageSquare' })
+  }
+
+  /** 查看图片的详情  */
+  const handleDetailOpen = async () => {
+    isShowImageDetail.value = true
+  }
+
+  /** 关闭图片的详情  */
+  const handleDetailClose = async () => {
+    isShowImageDetail.value = false
+  }
+
+  /** 获得 image 图片列表 */
+  const getImageList = async () => {
+    try {
+      // 1. 加载图片列表
+      imageListLoadingInstance.value = ElLoading.service({
+        target: imageListRef.value,
+        text: '加载中...',
+      } as LoadingOptionsResolved)
+      const { list, total } = await ImageApi.getImagePageMy(queryParams)
+      imageList.value = list
+      pageTotal.value = total
+
+      // 2. 计算需要轮询的图片
+      const newWatImages = {}
+      imageList.value.forEach(item => {
+        if (item.status === AiImageStatusEnum.IN_PROGRESS) {
+          newWatImages[item.id] = item
+        }
+      })
+      inProgressImageMap.value = newWatImages
+    } finally {
+      // 关闭正在“加载中”的 Loading
+      if (imageListLoadingInstance.value) {
+        imageListLoadingInstance.value.close()
+        imageListLoadingInstance.value = null
+      }
+    }
+  }
+
+  /** 轮询生成中的 image 列表 */
+  const refreshWatchImages = async () => {
+    const imageIds = Object.keys(inProgressImageMap.value).map(Number)
+    if (imageIds.length === 0) {
+      return
+    }
+    const list = (await ImageApi.getImageListMyByIds(imageIds)) as ImageVO[]
+    const newWatchImages = {}
+    list.forEach(image => {
+      if (image.status === AiImageStatusEnum.IN_PROGRESS) {
+        newWatchImages[image.id] = image
+      } else {
+        const index = imageList.value.findIndex(oldImage => image.id === oldImage.id)
+        if (index >= 0) {
+          // 更新 imageList
+          imageList.value[index] = image
+        }
+      }
+    })
+    inProgressImageMap.value = newWatchImages
+  }
+
+  /** 图片的点击事件 */
+  const handleImageButtonClick = async (type: string, imageDetail: ImageVO) => {
+    // 详情
+    if (type === 'more') {
+      showImageDetailId.value = imageDetail.id
+      await handleDetailOpen()
+      return
+    }
+    // 删除
+    if (type === 'delete') {
+      await message.confirm(`是否删除照片?`)
+      await ImageApi.deleteImageMy(imageDetail.id)
+      await getImageList()
+      message.success('删除成功!')
+      return
+    }
+    // 下载
+    if (type === 'download') {
+      download.image({ url: imageDetail.picUrl })
+      return
+    }
+    // 重新生成
+    if (type === 'regeneration') {
+      emits('onRegeneration', imageDetail)
+    }
+  }
+
+  /** 处理 Midjourney 按钮点击事件  */
+  const handleImageMidjourneyButtonClick = async (
+    button: ImageMidjourneyButtonsVO,
+    imageDetail: ImageVO
+  ) => {
+    // 1. 构建 params 参数
+    const data = { id: imageDetail.id, customId: button.customId } as ImageMidjourneyActionVO
+    // 2. 发送 action
+    await ImageApi.midjourneyAction(data)
+    // 3. 刷新列表
+    await getImageList()
+  }
+
+  defineExpose({ getImageList }) /** 组件挂在的时候 */
+  onMounted(async () => {
+    // 获取 image 列表
+    await getImageList()
+    // 自动刷新 image 列表
+    inProgressTimer.value = setInterval(async () => {
+      await refreshWatchImages()
+    }, 1000 * 3)
+  })
+
+  /** 组件取消挂在的时候 */
+  onUnmounted(async () => {
+    if (inProgressTimer.value) {
+      clearInterval(inProgressTimer.value)
+    }
+  })
+</script>
 <template>
   <el-card
-    class="wh-full"
+    class="size-full"
     :body-style="{ margin: 0, padding: 0, height: '100%', position: 'relative' }"
-    shadow="never"
-  >
+    shadow="never">
     <template #header>
       绘画任务
       <!-- TODO @fan:看看,怎么优化下这个样子哈。 -->
@@ -11,198 +166,28 @@
     </template>
     <!-- 图片列表 -->
     <div
-      class="relative flex flex-row flex-wrap content-start h-full overflow-auto p-5 pb-[140px] box-border [&>div]:mr-5 [&>div]:mb-5"
       ref="imageListRef"
-    >
+      class="p-5 pb-[140px] flex flex-row flex-wrap h-full content-start box-border relative overflow-auto [&>div]:mb-5 [&>div]:mr-5">
       <ImageCard
         v-for="image in imageList"
         :key="image.id"
         :detail="image"
         @on-btn-click="handleImageButtonClick"
-        @on-mj-btn-click="handleImageMidjourneyButtonClick"
-      />
+        @on-mj-btn-click="handleImageMidjourneyButtonClick" />
     </div>
     <div
-      class="absolute bottom-[60px] h-[50px] leading-[90px] w-full z-[999] bg-white flex flex-row justify-center items-center"
-    >
+      class="leading-[90px] bg-white flex flex-row h-[50px] w-full items-center bottom-[60px] justify-center absolute z-[999]">
       <Pagination
-        :total="pageTotal"
         v-model:page="queryParams.pageNo"
         v-model:limit="queryParams.pageSize"
-        @pagination="getImageList"
-      />
+        :total="pageTotal"
+        @pagination="getImageList" />
     </div>
   </el-card>
 
   <!-- 图片详情 -->
   <ImageDetail
-    :show="isShowImageDetail"
     :id="showImageDetailId"
-    @handle-drawer-close="handleDetailClose"
-  />
+    :show="isShowImageDetail"
+    @handle-drawer-close="handleDetailClose" />
 </template>
-<script setup lang="ts">
-import {
-  ImageApi,
-  ImageVO,
-  ImageMidjourneyActionVO,
-  ImageMidjourneyButtonsVO
-} from '@/api/ai/image'
-import ImageDetail from './ImageDetail.vue'
-import ImageCard from './ImageCard.vue'
-import { ElLoading, LoadingOptionsResolved } from 'element-plus'
-import { AiImageStatusEnum } from '@/views/ai/utils/constants'
-import download from '@/utils/download'
-
-const message = useMessage() // 消息弹窗
-const router = useRouter() // 路由
-
-// 图片分页相关的参数
-const queryParams = reactive({
-  pageNo: 1,
-  pageSize: 10
-})
-const pageTotal = ref<number>(0) // page size
-const imageList = ref<ImageVO[]>([]) // image 列表
-const imageListLoadingInstance = ref<any>() // image 列表是否正在加载中
-const imageListRef = ref<any>() // ref
-// 图片轮询相关的参数(正在生成中的)
-const inProgressImageMap = ref<{}>({}) // 监听的 image 映射,一般是生成中(需要轮询),key 为 image 编号,value 为 image
-const inProgressTimer = ref<any>() // 生成中的 image 定时器,轮询生成进展
-// 图片详情相关的参数
-const isShowImageDetail = ref<boolean>(false) // 图片详情是否展示
-const showImageDetailId = ref<number>(0) // 图片详情的图片编号
-
-/** 处理查看绘图作品 */
-const handleViewPublic = () => {
-  router.push({
-    name: 'AiImageSquare'
-  })
-}
-
-/** 查看图片的详情  */
-const handleDetailOpen = async () => {
-  isShowImageDetail.value = true
-}
-
-/** 关闭图片的详情  */
-const handleDetailClose = async () => {
-  isShowImageDetail.value = false
-}
-
-/** 获得 image 图片列表 */
-const getImageList = async () => {
-  try {
-    // 1. 加载图片列表
-    imageListLoadingInstance.value = ElLoading.service({
-      target: imageListRef.value,
-      text: '加载中...'
-    } as LoadingOptionsResolved)
-    const { list, total } = await ImageApi.getImagePageMy(queryParams)
-    imageList.value = list
-    pageTotal.value = total
-
-    // 2. 计算需要轮询的图片
-    const newWatImages = {}
-    imageList.value.forEach((item) => {
-      if (item.status === AiImageStatusEnum.IN_PROGRESS) {
-        newWatImages[item.id] = item
-      }
-    })
-    inProgressImageMap.value = newWatImages
-  } finally {
-    // 关闭正在“加载中”的 Loading
-    if (imageListLoadingInstance.value) {
-      imageListLoadingInstance.value.close()
-      imageListLoadingInstance.value = null
-    }
-  }
-}
-
-/** 轮询生成中的 image 列表 */
-const refreshWatchImages = async () => {
-  const imageIds = Object.keys(inProgressImageMap.value).map(Number)
-  if (imageIds.length == 0) {
-    return
-  }
-  const list = (await ImageApi.getImageListMyByIds(imageIds)) as ImageVO[]
-  const newWatchImages = {}
-  list.forEach((image) => {
-    if (image.status === AiImageStatusEnum.IN_PROGRESS) {
-      newWatchImages[image.id] = image
-    } else {
-      const index = imageList.value.findIndex((oldImage) => image.id === oldImage.id)
-      if (index >= 0) {
-        // 更新 imageList
-        imageList.value[index] = image
-      }
-    }
-  })
-  inProgressImageMap.value = newWatchImages
-}
-
-/** 图片的点击事件 */
-const handleImageButtonClick = async (type: string, imageDetail: ImageVO) => {
-  // 详情
-  if (type === 'more') {
-    showImageDetailId.value = imageDetail.id
-    await handleDetailOpen()
-    return
-  }
-  // 删除
-  if (type === 'delete') {
-    await message.confirm(`是否删除照片?`)
-    await ImageApi.deleteImageMy(imageDetail.id)
-    await getImageList()
-    message.success('删除成功!')
-    return
-  }
-  // 下载
-  if (type === 'download') {
-    download.image({ url: imageDetail.picUrl })
-    return
-  }
-  // 重新生成
-  if (type === 'regeneration') {
-    emits('onRegeneration', imageDetail)
-    return
-  }
-}
-
-/** 处理 Midjourney 按钮点击事件  */
-const handleImageMidjourneyButtonClick = async (
-  button: ImageMidjourneyButtonsVO,
-  imageDetail: ImageVO
-) => {
-  // 1. 构建 params 参数
-  const data = {
-    id: imageDetail.id,
-    customId: button.customId
-  } as ImageMidjourneyActionVO
-  // 2. 发送 action
-  await ImageApi.midjourneyAction(data)
-  // 3. 刷新列表
-  await getImageList()
-}
-
-defineExpose({ getImageList }) // 暴露组件方法
-
-const emits = defineEmits(['onRegeneration'])
-
-/** 组件挂在的时候 */
-onMounted(async () => {
-  // 获取 image 列表
-  await getImageList()
-  // 自动刷新 image 列表
-  inProgressTimer.value = setInterval(async () => {
-    await refreshWatchImages()
-  }, 1000 * 3)
-})
-
-/** 组件取消挂在的时候 */
-onUnmounted(async () => {
-  if (inProgressTimer.value) {
-    clearInterval(inProgressTimer.value)
-  }
-})
-</script>

+ 68 - 83
src/views/ai/image/index/index.vue

@@ -1,113 +1,98 @@
 <!-- image -->
+<script setup lang="ts">
+  import type { ImageVO } from '@/api/ai/image'
+  import type { ModelVO } from '@/api/ai/model/model'
+
+  import { ModelApi } from '@/api/ai/model/model'
+  import { AiModelTypeEnum, AiPlatformEnum } from '@/views/ai/utils/constants'
+
+  import Dall3 from './components/dall3/index.vue'
+  import ImageList from './components/ImageList.vue'
+  import Common from './components/common/index.vue'
+  import Midjourney from './components/midjourney/index.vue'
+  import StableDiffusion from './components/stableDiffusion/index.vue'
+
+  const imageListRef = ref<any>() // image 列表 ref
+  const dall3Ref = ref<any>() // dall3(openai) ref
+  const midjourneyRef = ref<any>() // midjourney ref
+  const stableDiffusionRef = ref<any>() // stable diffusion ref
+
+  // 定义属性
+  const selectPlatform = ref('common') // 选中的平台
+  const platformOptions = [
+    { label: '通用', value: 'common' },
+    { label: 'DALL3 绘画', value: AiPlatformEnum.OPENAI },
+    { label: 'MJ 绘画', value: AiPlatformEnum.MIDJOURNEY },
+    { label: 'SD 绘图', value: AiPlatformEnum.STABLE_DIFFUSION },
+  ]
+
+  const models = ref<ModelVO[]>([]) // 模型列表
+
+  /** 绘画 start  */
+  const handleDrawStart = async (_platform: string) => {}
+
+  /** 绘画 complete */
+  const handleDrawComplete = async (_platform: string) => {
+    await imageListRef.value.getImageList()
+  }
+
+  /** 重新生成:将画图详情填充到对应平台 */
+  const handleRegeneration = async (image: ImageVO) => {
+    // 切换平台
+    selectPlatform.value = image.platform
+    // 根据不同平台填充 image
+    await nextTick()
+    if (image.platform === AiPlatformEnum.MIDJOURNEY) {
+      midjourneyRef.value.settingValues(image)
+    } else if (image.platform === AiPlatformEnum.OPENAI) {
+      dall3Ref.value.settingValues(image)
+    } else if (image.platform === AiPlatformEnum.STABLE_DIFFUSION) {
+      stableDiffusionRef.value.settingValues(image)
+    }
+    // TODO @fan:貌似 other 重新设置不行?
+  }
+
+  /** 组件挂载的时候 */
+  onMounted(async () => {
+    // 获取模型列表
+    models.value = await ModelApi.getModelSimpleList(AiModelTypeEnum.IMAGE)
+  })
+</script>
+
 <template>
-  <div class="absolute inset-0 flex flex-row wh-full">
-    <div class="flex flex-col p-5 w-[390px]">
+  <div class="flex flex-row size-full inset-0 absolute">
+    <div class="p-5 flex flex-col w-[390px]">
       <div class="mb-[30px]">
         <el-segmented
           v-model="selectPlatform"
           :options="platformOptions"
-          class="w-[350px] !bg-[#ececec] [--el-border-radius-base:16px] [--el-segmented-item-selected-color:#fff]"
-        />
+          class="w-[350px] [--el-border-radius-base:16px] [--el-segmented-item-selected-color:#fff] !bg-[#ececec]" />
       </div>
       <div class="h-full overflow-y-auto">
         <Common
           v-if="selectPlatform === 'common'"
           ref="commonRef"
           :models="models"
-          @on-draw-complete="handleDrawComplete"
-        />
+          @on-draw-complete="handleDrawComplete" />
         <Dall3
           v-if="selectPlatform === AiPlatformEnum.OPENAI"
           ref="dall3Ref"
           :models="models"
           @on-draw-start="handleDrawStart"
-          @on-draw-complete="handleDrawComplete"
-        />
+          @on-draw-complete="handleDrawComplete" />
         <Midjourney
           v-if="selectPlatform === AiPlatformEnum.MIDJOURNEY"
           ref="midjourneyRef"
-          :models="models"
-        />
+          :models="models" />
         <StableDiffusion
           v-if="selectPlatform === AiPlatformEnum.STABLE_DIFFUSION"
           ref="stableDiffusionRef"
           :models="models"
-          @on-draw-complete="handleDrawComplete"
-        />
+          @on-draw-complete="handleDrawComplete" />
       </div>
     </div>
-    <div class="flex-1 bg-white">
+    <div class="bg-white flex-1">
       <ImageList ref="imageListRef" @on-regeneration="handleRegeneration" />
     </div>
   </div>
 </template>
-
-<script setup lang="ts">
-import ImageList from './components/ImageList.vue'
-import { AiPlatformEnum } from '@/views/ai/utils/constants'
-import { ImageVO } from '@/api/ai/image'
-import Dall3 from './components/dall3/index.vue'
-import Midjourney from './components/midjourney/index.vue'
-import StableDiffusion from './components/stableDiffusion/index.vue'
-import Common from './components/common/index.vue'
-import { ModelApi, ModelVO } from '@/api/ai/model/model'
-import { AiModelTypeEnum } from '@/views/ai/utils/constants'
-
-const imageListRef = ref<any>() // image 列表 ref
-const dall3Ref = ref<any>() // dall3(openai) ref
-const midjourneyRef = ref<any>() // midjourney ref
-const stableDiffusionRef = ref<any>() // stable diffusion ref
-
-// 定义属性
-const selectPlatform = ref('common') // 选中的平台
-const platformOptions = [
-  {
-    label: '通用',
-    value: 'common'
-  },
-  {
-    label: 'DALL3 绘画',
-    value: AiPlatformEnum.OPENAI
-  },
-  {
-    label: 'MJ 绘画',
-    value: AiPlatformEnum.MIDJOURNEY
-  },
-  {
-    label: 'SD 绘图',
-    value: AiPlatformEnum.STABLE_DIFFUSION
-  }
-]
-
-const models = ref<ModelVO[]>([]) // 模型列表
-
-/** 绘画 start  */
-const handleDrawStart = async (_platform: string) => {}
-
-/** 绘画 complete */
-const handleDrawComplete = async (_platform: string) => {
-  await imageListRef.value.getImageList()
-}
-
-/** 重新生成:将画图详情填充到对应平台 */
-const handleRegeneration = async (image: ImageVO) => {
-  // 切换平台
-  selectPlatform.value = image.platform
-  // 根据不同平台填充 image
-  await nextTick()
-  if (image.platform === AiPlatformEnum.MIDJOURNEY) {
-    midjourneyRef.value.settingValues(image)
-  } else if (image.platform === AiPlatformEnum.OPENAI) {
-    dall3Ref.value.settingValues(image)
-  } else if (image.platform === AiPlatformEnum.STABLE_DIFFUSION) {
-    stableDiffusionRef.value.settingValues(image)
-  }
-  // TODO @fan:貌似 other 重新设置不行?
-}
-
-/** 组件挂载的时候 */
-onMounted(async () => {
-  // 获取模型列表
-  models.value = await ModelApi.getModelSimpleList(AiModelTypeEnum.IMAGE)
-})
-</script>

+ 135 - 133
src/views/ai/mindmap/index/components/Right.vue

@@ -1,7 +1,110 @@
+<script lang="ts" setup>
+  import markdownit from 'markdown-it'
+  import { Markmap } from 'markmap-view'
+  import { Transformer } from 'markmap-lib'
+  import { Toolbar } from 'markmap-toolbar'
+
+  import download from '@/utils/download'
+
+  // 消息弹窗
+
+  const props = defineProps<{
+    generatedContent: string // 生成结果
+    isEnd: boolean // 是否结束
+    isGenerating: boolean // 是否正在生成
+    isStart: boolean // 开始状态,开始时需要清除 html
+  }>()
+  const md = markdownit()
+  const message = useMessage()
+  const contentRef = ref<HTMLDivElement>() // 右侧出来 header 以下的区域
+  const mdContainerRef = ref<HTMLDivElement>() // markdown 的容器,用来滚动到底下的
+  const mindMapRef = ref<HTMLDivElement>() // 思维导图的容器
+  const svgRef = ref<SVGElement>() // 思维导图的渲染 svg
+  const toolBarRef = ref<HTMLDivElement>() // 思维导图右下角的工具栏,缩放等
+  const html = ref('') // 生成过程中的文本
+  const contentAreaHeight = ref(0) // 生成区域的高度,出去 header 部分
+  let markMap: Markmap | null = null
+  const transformer = new Transformer()
+
+  /** 处理内容 */
+  const processContent = (text: string) => {
+    const arr: string[] = []
+    const lines = text.split('\n')
+    for (let line of lines) {
+      if (line.includes('```')) {
+        continue
+      }
+      line = line.replace(/([*_~`>])|(\d+\.)\s/g, '')
+      arr.push(line)
+    }
+    return arr.join('\n')
+  }
+
+  /** 更新思维导图的展示 */
+  const update = () => {
+    try {
+      const { root } = transformer.transform(processContent(props.generatedContent))
+      markMap?.setData(root)
+      markMap?.fit()
+    } catch (e) {
+      console.error(e)
+    }
+  }
+
+  onMounted(() => {
+    contentAreaHeight.value = contentRef.value?.clientHeight || 0 // 获取区域高度
+    /** 初始化思维导图 **/
+    try {
+      markMap = Markmap.create(svgRef.value!)
+      const { el } = Toolbar.create(markMap)
+      toolBarRef.value?.append(el)
+      nextTick(update)
+    } catch (e) {
+      message.error('思维导图初始化失败')
+    }
+  })
+
+  watch(props, ({ generatedContent, isGenerating, isEnd, isStart }) => {
+    // 开始生成的时候清空一下 markdown 的内容
+    if (isStart) {
+      html.value = ''
+    }
+    // 生成内容的时候使用 markdown 来渲染
+    if (isGenerating) {
+      html.value = md.render(generatedContent)
+    }
+    // 生成结束时更新思维导图
+    if (isEnd) {
+      update()
+    }
+  })
+
+  /** 下载图片:download SVG to png file */
+  const downloadImage = () => {
+    const svgElement = mindMapRef.value
+    // 将 SVG 渲染到图片对象
+    const serializer = new XMLSerializer()
+    const source = `<?xml version="1.0" standalone="no"?>\r\n${serializer.serializeToString(svgRef.value!)}`
+    const base64Url = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(source)}`
+    download.image({
+      url: base64Url,
+      canvasWidth: svgElement?.offsetWidth,
+      canvasHeight: svgElement?.offsetHeight,
+      drawWithImageSize: false,
+    })
+  }
+
+  defineExpose({
+    scrollBottom() {
+      mdContainerRef.value?.scrollTo(0, mdContainerRef.value?.scrollHeight)
+    },
+  })
+</script>
+
 <template>
-  <el-card class="my-card h-full flex-grow">
+  <el-card class="my-card flex-grow h-full">
     <template #header>
-      <h3 class="m-0 px-7 shrink-0 flex items-center justify-between">
+      <h3 class="m-0 px-7 flex shrink-0 items-center justify-between">
         <span>思维导图预览</span>
         <!-- 展示在右上角 -->
         <el-button v-show="isEnd" size="small" type="primary" @click="downloadImage">
@@ -15,154 +118,53 @@
 
     <div ref="contentRef" class="hide-scroll-bar h-full box-border">
       <!--展示 markdown 的容器,最终生成的是 html 字符串,直接用 v-html 嵌入-->
-      <div v-if="isGenerating" ref="mdContainerRef" class="wh-full overflow-y-auto">
+      <div v-if="isGenerating" ref="mdContainerRef" class="size-full overflow-y-auto">
         <div class="flex flex-col items-center justify-center" v-html="html"></div>
       </div>
 
-      <div ref="mindMapRef" class="wh-full">
+      <div ref="mindMapRef" class="size-full">
         <svg ref="svgRef" :style="{ height: `${contentAreaHeight}px` }" class="w-full" />
-        <div ref="toolBarRef" class="absolute bottom-[10px] right-5"></div>
+        <div ref="toolBarRef" class="bottom-[10px] right-5 absolute"></div>
       </div>
     </div>
   </el-card>
 </template>
+<style lang="scss" scoped>
+  .hide-scroll-bar {
+    -ms-overflow-style: none;
+    scrollbar-width: none;
 
-<script lang="ts" setup>
-import { Markmap } from 'markmap-view'
-import { Transformer } from 'markmap-lib'
-import { Toolbar } from 'markmap-toolbar'
-import markdownit from 'markdown-it'
-import download from '@/utils/download'
-
-const md = markdownit()
-const message = useMessage() // 消息弹窗
-
-const props = defineProps<{
-  generatedContent: string // 生成结果
-  isEnd: boolean // 是否结束
-  isGenerating: boolean // 是否正在生成
-  isStart: boolean // 开始状态,开始时需要清除 html
-}>()
-const contentRef = ref<HTMLDivElement>() // 右侧出来 header 以下的区域
-const mdContainerRef = ref<HTMLDivElement>() // markdown 的容器,用来滚动到底下的
-const mindMapRef = ref<HTMLDivElement>() // 思维导图的容器
-const svgRef = ref<SVGElement>() // 思维导图的渲染 svg
-const toolBarRef = ref<HTMLDivElement>() // 思维导图右下角的工具栏,缩放等
-const html = ref('') // 生成过程中的文本
-const contentAreaHeight = ref(0) // 生成区域的高度,出去 header 部分
-let markMap: Markmap | null = null
-const transformer = new Transformer()
-
-onMounted(() => {
-  contentAreaHeight.value = contentRef.value?.clientHeight || 0 // 获取区域高度
-  /** 初始化思维导图 **/
-  try {
-    markMap = Markmap.create(svgRef.value!)
-    const { el } = Toolbar.create(markMap)
-    toolBarRef.value?.append(el)
-    nextTick(update)
-  } catch (e) {
-    message.error('思维导图初始化失败')
+    &::-webkit-scrollbar {
+      width: 0;
+      height: 0;
+    }
   }
-})
 
-watch(props, ({ generatedContent, isGenerating, isEnd, isStart }) => {
-  // 开始生成的时候清空一下 markdown 的内容
-  if (isStart) {
-    html.value = ''
-  }
-  // 生成内容的时候使用 markdown 来渲染
-  if (isGenerating) {
-    html.value = md.render(generatedContent)
-  }
-  // 生成结束时更新思维导图
-  if (isEnd) {
-    update()
-  }
-})
-
-/** 更新思维导图的展示 */
-const update = () => {
-  try {
-    const { root } = transformer.transform(processContent(props.generatedContent))
-    markMap?.setData(root)
-    markMap?.fit()
-  } catch (e) {
-    console.error(e)
-  }
-}
-
-/** 处理内容 */
-const processContent = (text: string) => {
-  const arr: string[] = []
-  const lines = text.split('\n')
-  for (let line of lines) {
-    if (line.indexOf('```') !== -1) {
-      continue
+  .my-card {
+    display: flex;
+    flex-direction: column;
+
+    :deep(.el-card__body) {
+      @extend .hide-scroll-bar;
+
+      padding: 0;
+      overflow-y: auto;
+      box-sizing: border-box;
+      flex-grow: 1;
     }
-    line = line.replace(/([*_~`>])|(\d+\.)\s/g, '')
-    arr.push(line)
   }
-  return arr.join('\n')
-}
-
-/** 下载图片:download SVG to png file */
-const downloadImage = () => {
-  const svgElement = mindMapRef.value
-  // 将 SVG 渲染到图片对象
-  const serializer = new XMLSerializer()
-  const source = `<?xml version="1.0" standalone="no"?>\r\n${serializer.serializeToString(svgRef.value!)}`
-  const base64Url = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(source)}`
-  download.image({
-    url: base64Url,
-    canvasWidth: svgElement?.offsetWidth,
-    canvasHeight: svgElement?.offsetHeight,
-    drawWithImageSize: false
-  })
-}
 
-defineExpose({
-  scrollBottom() {
-    mdContainerRef.value?.scrollTo(0, mdContainerRef.value?.scrollHeight)
+  // markmap的tool样式覆盖
+  :deep(.markmap) {
+    width: 100%;
   }
-})
-</script>
-<style lang="scss" scoped>
-.hide-scroll-bar {
-  -ms-overflow-style: none;
-  scrollbar-width: none;
 
-  &::-webkit-scrollbar {
-    width: 0;
-    height: 0;
+  :deep(.mm-toolbar-brand) {
+    display: none;
   }
-}
-
-.my-card {
-  display: flex;
-  flex-direction: column;
 
-  :deep(.el-card__body) {
-    @extend .hide-scroll-bar;
-
-    padding: 0;
-    overflow-y: auto;
-    box-sizing: border-box;
-    flex-grow: 1;
+  :deep(.mm-toolbar) {
+    display: flex;
+    flex-direction: row;
   }
-}
-
-// markmap的tool样式覆盖
-:deep(.markmap) {
-  width: 100%;
-}
-
-:deep(.mm-toolbar-brand) {
-  display: none;
-}
-
-:deep(.mm-toolbar) {
-  display: flex;
-  flex-direction: row;
-}
 </style>

+ 24 - 70
stylelint.config.js → stylelint.config.ts

@@ -1,62 +1,34 @@
-module.exports = {
-  root: true,
+import type { Config } from 'stylelint'
+
+export default {
   plugins: ['stylelint-order'],
   customSyntax: 'postcss-html',
   extends: ['stylelint-config-standard'],
   rules: {
-    'selector-pseudo-class-no-unknown': [
-      true,
-      {
-        ignorePseudoClasses: ['global', 'deep']
-      }
-    ],
+    'selector-pseudo-class-no-unknown': [true, { ignorePseudoClasses: ['global', 'deep'] }],
     'at-rule-no-unknown': [
       true,
-      {
-        ignoreAtRules: ['function', 'if', 'each', 'include', 'mixin', 'extend', 'use']
-      }
+      { ignoreAtRules: ['function', 'if', 'each', 'include', 'mixin', 'extend', 'use'] },
     ],
     'media-query-no-invalid': null,
-    'function-no-unknown': null,
+    'function-no-unknown': true,
     'no-empty-source': null,
     'named-grid-areas-no-invalid': null,
-    // 'unicode-bom': 'never',
     'no-descending-specificity': null,
     'font-family-no-missing-generic-family-keyword': null,
-    // 'declaration-colon-space-after': 'always-single-line',
-    // 'declaration-colon-space-before': 'never',
-    // 'declaration-block-trailing-semicolon': null,
-    'rule-empty-line-before': [
-      'always',
-      {
-        ignore: ['after-comment', 'first-nested']
-      }
-    ],
-    'unit-no-unknown': [
-      true,
-      {
-        ignoreUnits: ['rpx']
-      }
-    ],
+    'rule-empty-line-before': ['always', { ignore: ['after-comment', 'first-nested'] }],
+    'unit-no-unknown': [true, { ignoreUnits: ['rpx'] }],
     'order/order': [
       [
         'dollar-variables',
         'custom-properties',
         'at-rules',
         'declarations',
-        {
-          type: 'at-rule',
-          name: 'supports'
-        },
-        {
-          type: 'at-rule',
-          name: 'media'
-        },
-        'rules'
+        { type: 'at-rule', name: 'supports' },
+        { type: 'at-rule', name: 'media' },
+        'rules',
       ],
-      {
-        severity: 'warning'
-      }
+      { severity: 'warning' },
     ],
     // Specify the alphabetical order of the attributes in the declaration block
     'order/properties-order': [
@@ -205,53 +177,35 @@ module.exports = {
       'page',
       'set-link-source',
       'unicode-bidi',
-      'speak'
-    ]
+      'speak',
+    ],
   },
   ignoreFiles: ['**/*.js', '**/*.jsx', '**/*.tsx', '**/*.ts'],
   overrides: [
     {
       files: ['*.vue', '**/*.vue', '*.html', '**/*.html'],
-      extends: ['stylelint-config-recommended', 'stylelint-config-html'],
+      extends: ['stylelint-config-html'],
       rules: {
         'keyframes-name-pattern': null,
         'selector-class-pattern': null,
         'no-duplicate-selectors': null,
-        'selector-pseudo-class-no-unknown': [
-          true,
-          {
-            ignorePseudoClasses: ['deep', 'global']
-          }
-        ],
+        'selector-pseudo-class-no-unknown': [true, { ignorePseudoClasses: ['deep', 'global'] }],
         'selector-pseudo-element-no-unknown': [
           true,
-          {
-            ignorePseudoElements: ['v-deep', 'v-global', 'v-slotted']
-          }
-        ],
-        // Vue SFC 样式里会使用 @use、@include 等 SCSS at-rule。
-        'at-rule-no-unknown': [
-          true,
-          {
-            ignoreAtRules: ['function', 'if', 'each', 'include', 'mixin', 'extend', 'use']
-          }
+          { ignorePseudoElements: ['v-deep', 'v-global', 'v-slotted'] },
         ],
         // TagsView 使用的 WebKit 私有 mask 语法有效,但 Stylelint 不能完整识别。
         'declaration-property-value-no-unknown': [
           true,
-          {
-            ignoreProperties: {
-              '-webkit-mask-box-image': [/.*/]
-            }
-          }
-        ]
-      }
+          { ignoreProperties: { '-webkit-mask-box-image': [/.*/] } },
+        ],
+      },
     },
     // 独立 SCSS 文件不会被全局 postcss-html 语法完整检查。
     // 先只让这个 IM 选择弹窗公共 partial 使用 SCSS parser,暂不放大全量历史 SCSS 问题。
     {
       files: ['src/views/im/home/components/picker/picker-dialog.scss'],
-      customSyntax: 'postcss-scss'
-    }
-  ]
-}
+      customSyntax: 'postcss-scss',
+    },
+  ],
+} satisfies Config

+ 18 - 19
tsconfig.json

@@ -1,29 +1,18 @@
 {
   "compilerOptions": {
     "target": "esnext",
+    "jsx": "preserve",
+    "lib": ["esnext", "dom"],
     "useDefineForClassFields": true,
+    "experimentalDecorators": true,
+    "baseUrl": "./",
+    "rootDir": "./",
     "module": "esnext",
     "moduleResolution": "node",
     "ignoreDeprecations": "6.0",
-    "strict": true,
-    "jsx": "preserve",
-    "sourceMap": true,
+    "paths": { "@/*": ["src/*"] },
     "resolveJsonModule": true,
-    "esModuleInterop": true,
-    "lib": ["esnext", "dom"],
-    "baseUrl": "./",
-    "allowJs": true,
-    "forceConsistentCasingInFileNames": true,
-    "allowSyntheticDefaultImports": true,
-    "strictFunctionTypes": false,
-    "noUnusedLocals": true,
-    "noUnusedParameters": true,
-    "experimentalDecorators": true,
-    "noImplicitAny": false,
-    "skipLibCheck": true,
-    "paths": {
-      "@/*": ["src/*"]
-    },
+    "typeRoots": ["./node_modules/@types/", "./types"],
     "types": [
       // "@intlify/unplugin-vue-i18n/types",
       "vite/client"
@@ -31,8 +20,18 @@
       // "@types/qrcode",
       // "vite-plugin-svg-icons/client"
     ],
+    "allowJs": true,
+    "strict": true,
+    "strictFunctionTypes": false,
+    "noImplicitAny": false,
+    "noUnusedLocals": true,
+    "noUnusedParameters": true,
     "outDir": "target", // 请保留这个属性,防止tsconfig.json文件报错
-    "typeRoots": ["./node_modules/@types/", "./types"]
+    "sourceMap": true,
+    "allowSyntheticDefaultImports": true,
+    "esModuleInterop": true,
+    "forceConsistentCasingInFileNames": true,
+    "skipLibCheck": true
   },
   "include": [
     "src",

+ 135 - 96
uno.config.ts

@@ -1,107 +1,146 @@
-import { defineConfig, toEscapedSelector as e, presetUno } from 'unocss'
-// import transformerVariantGroup from '@unocss/transformer-variant-group'
+import type { Rule, RuleContext } from 'unocss'
 
-export default defineConfig({
-  // ...UnoCSS options
-  rules: [
-    [
-      /^custom-hover$/,
-      ([], { rawSelector }) => {
-        const selector = e(rawSelector)
-        return `
-${selector} {
-  display: flex;
-  height: 100%;
-  padding: 0 10px;
-  cursor: pointer;
-  align-items: center;
-  transition: background var(--transition-time-02);
-}
-/* you can have multiple rules */
-${selector}:hover {
-  background-color: var(--top-header-hover-color);
+import { presetAnimations } from 'unocss-preset-animations'
+import {
+  defineConfig,
+  presetAttributify,
+  presetTypography,
+  presetWind4,
+  toEscapedSelector,
+  transformerCompileClass,
+  transformerDirectives
+} from 'unocss'
+type SelectorRuleFactory = (selector: string) => string
+
+function css(strings: TemplateStringsArray, ...values: string[]): string {
+  const source = String.raw({ raw: strings }, ...values)
+  const lines = source.split('\n')
+  const indentation = Math.min(
+    ...lines.filter((line) => line.trim()).map((line) => line.match(/^\s*/)?.[0].length ?? 0)
+  )
+
+  return lines
+    .map((line) => line.slice(indentation))
+    .join('\n')
+    .trim()
 }
-.dark ${selector}:hover {
-  background-color: var(--el-bg-color-overlay);
+
+function createSelectorRule(pattern: RegExp, factory: SelectorRuleFactory): Rule {
+  return [
+    pattern,
+    (_match: RegExpMatchArray, { rawSelector }: Readonly<RuleContext>) =>
+      factory(toEscapedSelector(rawSelector))
+  ]
 }
-`
+
+const unoRules: Rule[] = [
+  createSelectorRule(
+    /^custom-hover$/,
+    (selector) => css`
+      ${selector} {
+        display: flex;
+        height: 100%;
+        padding: 0 10px;
+        cursor: pointer;
+        align-items: center;
+        transition: background var(--transition-time-02);
       }
-    ],
-    [
-      /^layout-border__left$/,
-      ([], { rawSelector }) => {
-        const selector = e(rawSelector)
-        return `
-${selector}:before {
-  content: "";
-  position: absolute;
-  top: 0;
-  left: 0;
-  height: 100%;
-  background-color: var(--el-border-color);
-  z-index: 3;
-}
-`
+
+      ${selector}:hover {
+        background-color: var(--top-header-hover-color);
       }
-    ],
-    [
-      /^layout-border__right$/,
-      ([], { rawSelector }) => {
-        const selector = e(rawSelector)
-        return `
-${selector}:after {
-  content: "";
-  position: absolute;
-  top: 0;
-  right: 0;
-  width: 1px;
-  height: 100%;
-  background-color: var(--el-border-color);
-  z-index: 3;
-}
-`
+
+      .dark ${selector}:hover {
+        background-color: var(--el-bg-color-overlay);
       }
-    ],
-    [
-      /^layout-border__top$/,
-      ([], { rawSelector }) => {
-        const selector = e(rawSelector)
-        return `
-${selector}:before {
-  content: "";
-  position: absolute;
-  top: 0;
-  left: 0;
-  width: 100%;
-  height: 1px;
-  background-color: var(--el-border-color);
-  z-index: 3;
-}
-`
+    `
+  ),
+  createSelectorRule(
+    /^layout-border__left$/,
+    (selector) => css`
+      ${selector}:before {
+        content: '';
+        position: absolute;
+        top: 0;
+        left: 0;
+        height: 100%;
+        background-color: var(--el-border-color);
+        z-index: 3;
+      }
+    `
+  ),
+  createSelectorRule(
+    /^layout-border__right$/,
+    (selector) => css`
+      ${selector}:after {
+        content: '';
+        position: absolute;
+        top: 0;
+        right: 0;
+        width: 1px;
+        height: 100%;
+        background-color: var(--el-border-color);
+        z-index: 3;
+      }
+    `
+  ),
+  createSelectorRule(
+    /^layout-border__top$/,
+    (selector) => css`
+      ${selector}:before {
+        content: '';
+        position: absolute;
+        top: 0;
+        left: 0;
+        width: 100%;
+        height: 1px;
+        background-color: var(--el-border-color);
+        z-index: 3;
+      }
+    `
+  ),
+  createSelectorRule(
+    /^layout-border__bottom$/,
+    (selector) => css`
+      ${selector}:after {
+        content: '';
+        position: absolute;
+        bottom: 0;
+        left: 0;
+        width: 100%;
+        height: 1px;
+        background-color: var(--el-border-color);
+        z-index: 3;
       }
-    ],
+    `
+  )
+]
+
+export default defineConfig({
+  rules: unoRules,
+  presets: [presetWind4(), presetAnimations(), presetAttributify(), presetTypography()],
+  transformers: [transformerDirectives(), transformerCompileClass()],
+  shortcuts: [
     [
-      /^layout-border__bottom$/,
-      ([], { rawSelector }) => {
-        const selector = e(rawSelector)
-        return `
-${selector}:after {
-  content: "";
-  position: absolute;
-  bottom: 0;
-  left: 0;
-  width: 100%;
-  height: 1px;
-  background-color: var(--el-border-color);
-  z-index: 3;
-}
-`
+      /^flex-?(inline)?-?(col)?-(start|end|center|baseline|stretch)-?(start|end|center|between|around|evenly|left|right)?$/,
+      ([, inline, col, items, justify]) => {
+        const cls = [inline ? 'inline-flex' : 'flex']
+
+        if (col) {
+          cls.push('flex-col')
+        }
+
+        if (items === 'center' && !justify) {
+          cls.push('items-center')
+          cls.push('justify-center')
+        } else {
+          cls.push(`items-${items}`)
+          if (justify) {
+            cls.push(`justify-${justify}`)
+          }
+        }
+        return cls.join(' ')
       }
     ]
-  ],
-  presets: [presetUno({ dark: 'class', attributify: false })],
-  // transformers: [transformerVariantGroup()],
-  shortcuts: {
-    'wh-full': 'w-full h-full'
-  }
+  ]
 })

+ 57 - 102
vite.config.ts

@@ -1,108 +1,63 @@
-import {dirname, relative, resolve} from 'path'
-import type {ConfigEnv, UserConfig} from 'vite'
-import {loadEnv, normalizePath} from 'vite'
-import {createVitePlugins} from './build/vite'
-import {exclude, include} from "./build/vite/optimize"
-// 当前执行node命令时文件夹的地址(工作目录)
-const root = process.cwd()
+import type { ConfigEnv, UserConfig } from 'vite'
 
-// 路径查找
-function pathResolve(dir: string) {
-    return resolve(root, '.', dir)
-}
-
-function getRelativeScssUsePath(filename: string, targetPath: string) {
-    const cleanFilename = filename.split('?')[0]
-    const relativePath = normalizePath(relative(dirname(cleanFilename), targetPath))
-    return relativePath.startsWith('.') ? relativePath : `./${relativePath}`
-}
+import { getEnv } from './build/vite/env'
+import { createVitePlugins } from './build/vite'
+import { pathResolve, root } from './build/vite/path'
+import { exclude, include } from './build/vite/optimize'
+import { createScssAdditionalData } from './build/vite/css'
 
 // https://vitejs.dev/config/
-export default ({command, mode}: ConfigEnv): UserConfig => {
-    let env = {} as any
-    const isBuild = command === 'build'
-    if (!isBuild) {
-        env = loadEnv((process.argv[3] === '--mode' ? process.argv[4] : process.argv[3]), root)
-    } else {
-        env = loadEnv(mode, root)
-    }
-    const variablesScssPath = pathResolve('src/styles/variables.scss')
-    return {
-        base: env.VITE_BASE_PATH,
-        root: root,
-        // 服务端渲染
-        server: {
-            port: env.VITE_PORT, // 端口号
-            host: "0.0.0.0",
-            open: env.VITE_OPEN === 'true',
-            // 本地跨域代理. 目前注释的原因:暂时没有用途,server 端已经支持跨域
-            // proxy: {
-            //   ['/admin-api']: {
-            //     target: env.VITE_BASE_URL,
-            //     ws: false,
-            //     changeOrigin: true,
-            //     rewrite: (path) => path.replace(new RegExp(`^/admin-api`), ''),
-            //   },
-            // },
-        },
-        // 项目使用的vite插件。 单独提取到build/vite/plugin中管理
-        plugins: createVitePlugins(isBuild, env),
-        css: {
-            lightningcss: {
-                // Preserve legacy star-hack declarations by stripping invalid syntax during minification.
-                errorRecovery: true
-            },
-            preprocessorOptions: {
-                scss: {
-                    additionalData: (source: string, filename: string) => {
-                        const normalizedFilename = normalizePath(filename)
-                        // Windows 下更容易触发重复注入:定义或显式转导变量的文件,不能再次注入同一个
-                        // `@use ... as *`,否则 Sass 会报 duplicate global variables。
-                        if (
-                            normalizedFilename.endsWith('/src/styles/variables.scss') ||
-                            normalizedFilename.endsWith('/src/styles/global.module.scss')
-                        ) {
-                            return source
-                        }
-                        return `@use "${getRelativeScssUsePath(filename, variablesScssPath)}" as *;\n${source}`
-                    },
-                    api: 'modern-compiler'
-                }
+export default ({ command, mode }: ConfigEnv): UserConfig => {
+  const { env, isBuild } = getEnv(command, mode)
+  const variablesScssPath = pathResolve('src/styles/variables.scss')
+  return {
+    base: env.VITE_BASE_PATH,
+    root,
+    server: {
+      port: env.VITE_PORT,
+      host: '0.0.0.0',
+      open: env.VITE_OPEN === 'true'
+    },
+    plugins: createVitePlugins(isBuild, env),
+    css: {
+      lightningcss: {
+        errorRecovery: true
+      },
+      preprocessorOptions: {
+        scss: {
+          additionalData: createScssAdditionalData(variablesScssPath)
+        }
+      }
+    },
+    resolve: {
+      alias: {
+        '@': pathResolve('src')
+      }
+    },
+    build: {
+      chunkSizeWarningLimit: 2000,
+      minify: 'oxc',
+      outDir: env.VITE_OUT_DIR || 'dist',
+      reportCompressedSize: false,
+      sourcemap: env.VITE_SOURCEMAP === 'true' ? 'inline' : false,
+      rollupOptions: {
+        output: {
+          minify: {
+            compress: {
+              dropDebugger: env.VITE_DROP_DEBUGGER === 'true',
+              dropConsole: env.VITE_DROP_CONSOLE === 'true'
             }
-        },
-        resolve: {
-            extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.scss', '.css'],
-            alias: [
-                {
-                    find: /\@\//,
-                    replacement: `${pathResolve('src')}/`
-                }
+          },
+          codeSplitting: {
+            groups: [
+              { name: 'echarts', test: /node_modules[\\/]echarts[\\/]/ }, // 将 echarts 单独打包,参考 https://gitee.com/yudaocode/yudao-ui-admin-vue3/issues/IAB1SX 讨论
+              { name: 'form-create', test: /node_modules[\\/]@form-create[\\/]element-ui[\\/]/ }, // 参考 https://github.com/yudaocode/yudao-ui-admin-vue3/issues/148 讨论
+              { name: 'form-designer', test: /node_modules[\\/]@form-create[\\/]designer[\\/]/ }
             ]
-        },
-        build: {
-            chunkSizeWarningLimit: 2000,
-            minify: 'oxc',
-            outDir: env.VITE_OUT_DIR || 'dist',
-            reportCompressedSize: false,
-            sourcemap: env.VITE_SOURCEMAP === 'true' ? 'inline' : false,
-            rollupOptions: {
-                output: {
-                    minify: {
-                        compress: {
-                            dropDebugger: env.VITE_DROP_DEBUGGER === 'true',
-                            dropConsole: env.VITE_DROP_CONSOLE === 'true'
-                        }
-                    },
-                    codeSplitting: {
-                        groups: [
-                            { name: 'echarts', test: /node_modules[\\/]echarts[\\/]/ }, // 将 echarts 单独打包,参考 https://gitee.com/yudaocode/yudao-ui-admin-vue3/issues/IAB1SX 讨论
-                            { name: 'form-create', test: /node_modules[\\/]@form-create[\\/]element-ui[\\/]/ }, // 参考 https://github.com/yudaocode/yudao-ui-admin-vue3/issues/148 讨论
-                            { name: 'form-designer', test: /node_modules[\\/]@form-create[\\/]designer[\\/]/ }
-                        ]
-                    }
-                },
-            },
-        },
-        optimizeDeps: {include, exclude}
-    }
+          }
+        }
+      }
+    },
+    optimizeDeps: { include, exclude }
+  }
 }

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików