diff --git a/.commitlintrc.cjs b/.commitlintrc.cjs index 812e548..26d9f14 100644 --- a/.commitlintrc.cjs +++ b/.commitlintrc.cjs @@ -1,77 +1,132 @@ -// .commitlintrc.js +// commitlint.config.js +const fs = require('node:fs') +const path = require('node:path') +const { execSync } = require('node:child_process') + +const scopes = fs + .readdirSync(path.resolve(__dirname, 'src'), { withFileTypes: true }) + .filter(dirent => dirent.isDirectory()) + .map(dirent => dirent.name.replace(/s$/, '')) + +// precomputed scope +const scopeComplete = execSync('git status --porcelain || true') + .toString() + .trim() + .split('\n') + .find(r => ~r.indexOf('M src')) + ?.replace(/(\/)/g, '%%') + ?.match(/src%%((\w|-)*)/)?.[1] + ?.replace(/s$/, '') + /** @type {import('cz-git').UserConfig} */ module.exports = { + ignores: [commit => commit.includes('init')], extends: ['@commitlint/config-conventional'], rules: { - // @see: https://commitlint.js.org/#/reference-rules + 'body-leading-blank': [2, 'always'], + 'footer-leading-blank': [1, 'always'], + 'header-max-length': [2, 'always', 108], + 'subject-empty': [2, 'never'], + 'type-empty': [2, 'never'], + 'subject-case': [0], 'type-enum': [ 2, 'always', - ['build', 'chore', 'ci', 'docs', 'feat', 'fix', 'perf', 'refactor', 'revert', 'style', 'test'] - ] + [ + 'feat', + 'fix', + 'perf', + 'style', + 'docs', + 'test', + 'refactor', + 'build', + 'ci', + 'chore', + 'revert', + 'wip', + 'workflow', + 'types', + 'release', + ], + ], }, prompt: { - alias: { fd: 'docs: fix typos' }, + /** @use `yarn commit :f` */ + alias: { + f: 'docs: fix typos', + r: 'docs: update README', + s: 'style: update code format', + b: 'build: bump dependencies', + c: 'chore: update config', + }, + customScopesAlign: !scopeComplete ? 'top' : 'bottom', + defaultScope: scopeComplete, + scopes: [...scopes, 'mock'], + allowEmptyIssuePrefixs: true, + allowCustomIssuePrefixs: true, messages: { - type: "选择你要提交的类型 | Select the type of change that you're committing:", - scope: '选择一个提交范围(可选)| Denote the SCOPE of this change (optional):', - customScope: '请输入自定义的提交范围 | Denote the SCOPE of this change:', - subject: '填写简短精炼的变更描述 | Write a SHORT, IMPERATIVE tense description of the change:\n', - body: '填写更加详细的变更描述(可选)。使用 "|" 换行 | Provide a LONGER description of the change (optional). Use "|" to break new line:\n', - breaking: - '列举非兼容性重大的变更(可选)。使用 "|" 换行 | List any BREAKING CHANGES (optional). Use "|" to break new line:\n', - footerPrefixesSelect: - '选择关联issue前缀(可选)| Select the ISSUES type of changeList by this change (optional):', - customFooterPrefix: '输入自定义issue前缀 | Input ISSUES prefix:', - footer: '列举关联issue (可选) 例如: #31, #I3244 | List any ISSUES by this change. E.g.: #31, #34:\n', - confirmCommit: '是否提交或修改commit ? | Are you sure you want to proceed with the commit above?' + type: '选择你要提交的类型 :', + scope: '选择一个提交范围(可选):', + customScope: '请输入自定义的提交范围 :', + subject: '填写简短精炼的变更描述 :\n', + body: '填写更加详细的变更描述(可选)。使用 "|" 换行 :\n', + breaking: '列举非兼容性重大的变更(可选)。使用 "|" 换行 :\n', + footerPrefixsSelect: '选择关联issue前缀(可选):', + customFooterPrefixs: '输入自定义issue前缀 :', + footer: '列举关联issue (可选) 例如: #31, #I3244 :\n', + confirmCommit: '是否提交或修改commit ?', }, types: [ - { value: 'feat', name: 'feat: 新增功能 | A new feature' }, - { value: 'fix', name: 'fix: 修复缺陷 | A bug fix' }, + { 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' + name: 'docs: 📝 文档更新 | Documentation only changes', + emoji: ':memo:', }, { value: 'style', - name: 'style: 代码格式 | Changes that do not affect the meaning of the code' + 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' + 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' + name: 'perf: ⚡️ 性能提升 | A code change that improves performance', + emoji: ':zap:', }, { value: 'test', - name: 'test: 测试相关 | Adding missing tests or correcting existing tests' + 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' + 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' + name: 'ci: 🎡 持续集成 | Changes to our CI configuration files and scripts', + emoji: ':ferris_wheel:', }, - { value: 'revert', name: 'revert: 回退代码 | Revert to a commit' }, + { value: 'revert', name: 'revert: 🔨 回退代码 | Revert to a commit', emoji: ':hammer:' }, { value: 'chore', - name: 'chore: 其他修改 | Other changes that do not modify src or test files' - } + name: 'chore: ⏪️ 其他修改 | Other changes that do not modify src or test files', + emoji: ':rewind:', + }, ], - useEmoji: false, + useEmoji: true, emojiAlign: 'center', - useAI: false, - aiNumber: 1, themeColorCode: '', - scopes: [], allowCustomScopes: true, allowEmptyScopes: true, - customScopesAlign: 'bottom', customScopesAlias: 'custom', emptyScopesAlias: 'empty', upperCaseSubject: false, @@ -80,21 +135,21 @@ module.exports = { breaklineNumber: 100, breaklineChar: '|', skipQuestions: [], - issuePrefixes: [ + issuePrefixs: [ // 如果使用 gitee 作为开发管理 { value: 'link', name: 'link: 链接 ISSUES 进行中' }, - { value: 'closed', name: 'closed: 标记 ISSUES 已完成' } + { value: 'closed', name: 'closed: 标记 ISSUES 已完成' }, ], - customIssuePrefixAlign: 'top', - emptyIssuePrefixAlias: 'skip', - customIssuePrefixAlias: 'custom', - allowCustomIssuePrefix: true, - allowEmptyIssuePrefix: true, + customIssuePrefixsAlign: 'top', + emptyIssuePrefixsAlias: 'skip', + customIssuePrefixsAlias: 'custom', confirmColorize: true, + maxHeaderLength: Number.POSITIVE_INFINITY, + maxSubjectLength: Number.POSITIVE_INFINITY, + minSubjectLength: 0, scopeOverrides: undefined, defaultBody: '', defaultIssues: '', - defaultScope: '', - defaultSubject: '' - } -}; + defaultSubject: '', + }, +} \ No newline at end of file diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..661d01e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,43 @@ +# 官网是这么介绍 EditorConfig 的: +# EditorConfig帮助开发人员在不同的编辑器和IDE之间定义和维护一致的编码样式。 +# EditorConfig 项目由用于定义编码样式的文件格式和一组文本编辑器插件组成,这些插件使编辑器能够读取文件格式并遵循定义的样式。 +# EditorConfig 文件易于阅读,并且与版本控制系统配合使用。 +# 不同的开发人员,不同的编辑器,有不同的编码风格,而 EditorConfig 就是用来协同团队开发人员之间的代码的风格及样式规范化的一个工具, +# 而.editorconfig正是它的默认配置文件 + +#EditorConfig 的匹配规则是从上往下,即先定义的规则优先级比后定义的优先级要高。 + +# 告诉 EditorConfig 插件,这是根文件,不用继续往上查找 +root = true + +# 匹配全部文件 +[*] + +# 设置字符集 +charset=utf-8 + +# 结尾换行符,可选"lf"、"cr"、"crlf" +end_of_line=LF + +# 在文件结尾插入新行 +insert_final_newline=true + +# 缩进风格,可选"space"、"tab" +indent_style=space + +# 缩进的空格数 +indent_size=2 + +max_line_length = 100 + +# 匹配 yml 和 yaml、json 结尾的文件 +[*.{yml,yaml,json}] +indent_style = space +indent_size = 2 + +[*.md] +# 删除一行中的前后空格 +trim_trailing_whitespace = false + +[Makefile] +indent_style = tab diff --git a/.eslintrc-auto-import.json b/.eslintrc-auto-import.json index af1083b..ff73334 100644 --- a/.eslintrc-auto-import.json +++ b/.eslintrc-auto-import.json @@ -71,6 +71,8 @@ "watch": true, "watchEffect": true, "watchPostEffect": true, - "watchSyncEffect": true + "watchSyncEffect": true, + "ElMessage": true, + "ElMessageBox": true } } diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index 477ae12..0000000 --- a/.prettierignore +++ /dev/null @@ -1,9 +0,0 @@ -/dist/* -.local -/node_modules/** - -**/*.svg -**/*.sh - -/public/* -stats.html diff --git a/.stylelintignore b/.stylelintignore deleted file mode 100644 index b7eb686..0000000 --- a/.stylelintignore +++ /dev/null @@ -1,4 +0,0 @@ -/dist/* -/public/* -public/* -stats.html diff --git a/.stylelintrc.cjs b/.stylelintrc.cjs deleted file mode 100644 index 4095486..0000000 --- a/.stylelintrc.cjs +++ /dev/null @@ -1,40 +0,0 @@ -// @see: https://stylelint.io - -module.exports = { - root: true, - // 继承某些已有的规则 - extends: [ - "stylelint-config-standard", // 配置 stylelint 拓展插件 - "stylelint-config-html/vue", // 配置 vue 中 template 样式格式化 - "stylelint-config-standard-scss", // 配置 stylelint scss 插件 - "stylelint-config-recommended-vue/scss", // 配置 vue 中 scss 样式格式化 - "stylelint-config-recess-order" // 配置 stylelint css 属性书写顺序插件, - ], - overrides: [ - // 扫描 .vue/html 文件中的 + diff --git a/src/assets/icons/svg/ctrl+k.svg b/src/assets/icons/svg/ctrl+k.svg new file mode 100644 index 0000000..a937dbb --- /dev/null +++ b/src/assets/icons/svg/ctrl+k.svg @@ -0,0 +1 @@ + diff --git a/src/assets/images/logo.png b/src/assets/images/logo.png new file mode 100644 index 0000000..0cf7439 Binary files /dev/null and b/src/assets/images/logo.png differ diff --git a/src/components/IconSelect/index.vue b/src/components/IconSelect/index.vue index f3c9d35..007ba9d 100644 --- a/src/components/IconSelect/index.vue +++ b/src/components/IconSelect/index.vue @@ -1,8 +1,10 @@ @@ -51,16 +52,12 @@ function selectedIcon(name: string) { :label="classify.classifyName" >
-
+
- {{ - item - }} + + {{ item }} +
@@ -78,6 +75,7 @@ function selectedIcon(name: string) { display: grid; grid-template-columns: repeat(auto-fill, minmax(40px, 1fr)); } + .icon-item { cursor: pointer; padding: 0px 4px; @@ -87,13 +85,16 @@ function selectedIcon(name: string) { text-align: center; font-size: 18px; } + .icon-item:hover { box-shadow: 1px 1px 10px 0 #a1a1a1; } + .el-tab-pane { height: 200px; overflow: auto; } + .icon_name { display: none; } @@ -104,10 +105,13 @@ function selectedIcon(name: string) { .icon-body { padding: 10px; } + .icon_name { display: block; } + overflow: hidden; + .grid-container { margin-top: 12px; position: relative; @@ -115,7 +119,11 @@ function selectedIcon(name: string) { grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); border-left: 1px solid #eee; border-top: 1px solid #eee; + overflow-y: auto; + overflow-x: hidden; + height: 500px; } + .icon-item { padding: 16px 0; margin: 0 !important; @@ -136,14 +144,17 @@ function selectedIcon(name: string) { .disabled { pointer-events: none; } + .grid { border-top: 1px solid #eee; } } + .icons-container span { font-size: 12px !important; color: #99a9bf; } + .icons-container svg { font-size: 24px !important; color: #606266; diff --git a/src/components/Popover/index.vue b/src/components/Popover/index.vue new file mode 100644 index 0000000..d402c2b --- /dev/null +++ b/src/components/Popover/index.vue @@ -0,0 +1,407 @@ + + + + + diff --git a/src/config/design.ts b/src/config/design.ts index c81af2d..ea87a65 100644 --- a/src/config/design.ts +++ b/src/config/design.ts @@ -1,5 +1,12 @@ export type LayoutType = 'vertical'; +// 仿豆包折叠逻辑 +export type CollapseType = + | 'alwaysCollapsed' // 始终折叠 + | 'followSystem' // 跟随系统视口宽度 + | 'alwaysExpanded' // 始终打开 + | 'narrowExpandWideCollapse'; // 系统视口 宽小则张,宽大则收 + export interface DesignConfigState { // 系统主题 darkMode: 'light' | 'dark' | 'inverted'; @@ -13,12 +20,10 @@ export interface DesignConfigState { pageAnimateType: string; // 布局模式 (纵向:vertical | ... | 自己定义) layout: LayoutType; - // 是否折叠菜单-视口宽度自动决定 + // 折叠类型 + collapseType: CollapseType; + // 是否折叠菜单 isCollapse: boolean; - // 是否折叠菜单-用户意愿点击决定 - isCollapseManual: boolean; - // 最终是否折叠菜单,动态根据上述两种折叠条件决定 - isCollapseFinal: boolean; } export const themeColorList: string[] = [ @@ -56,12 +61,10 @@ const design: DesignConfigState = { pageAnimateType: 'zoom-fade', // 布局模式 (纵向:vertical | ... | 自己定义) layout: 'vertical', - // 是否折叠菜单-视口宽度自动决定 + // 折叠类型 + collapseType: 'followSystem', + // 是否折叠菜单 isCollapse: false, - // 是否折叠菜单-用户手动控制决定 - isCollapseManual: false, - // 最终是否折叠菜单,动态根据上述两种折叠条件决定 - isCollapseFinal: false, }; export default design; diff --git a/src/config/index.ts b/src/config/index.ts index db08e0b..e787f06 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -8,5 +8,11 @@ export const LOGIN_URL: string = '/login'; // 默认主题颜色 export const DEFAULT_THEME_COLOR: string = '#2992FF'; +// 折叠阈值 +export const COLLAPSE_THRESHOLD: number = 600; + +// 左侧菜单宽度 +export const SIDE_BAR_WIDTH: number = 280; + // 路由白名单地址[本地存在的路由 staticRouter.ts 中] export const ROUTER_WHITE_LIST: string[] = ['/500']; diff --git a/src/hooks/useCollapseToggle.ts b/src/hooks/useCollapseToggle.ts new file mode 100644 index 0000000..ff7636f --- /dev/null +++ b/src/hooks/useCollapseToggle.ts @@ -0,0 +1,61 @@ +import { COLLAPSE_THRESHOLD } from '@/config/index'; +import { useWindowWidthObserver } from '@/hooks/useWindowWidthObserver'; +import { useDesignStore } from '@/store/modules/design'; + +/** + * 侧边栏折叠切换逻辑组合式函数 方便多个页面调用 + * @param threshold 折叠阈值(可选,默认使用全局配置) + */ +export function useCollapseToggle(threshold: number = COLLAPSE_THRESHOLD) { + const designStore = useDesignStore(); + // 获取当前视口宽度是否大于阈值,但不做响应式处理,传入一个空函数执行 + const { isAboveThreshold } = useWindowWidthObserver(threshold, () => {}); + + /** 核心折叠切换方法 */ + const changeCollapse = () => { + // 切换最终折叠状态 + designStore.setCollapseFinal(!designStore.isCollapse); + + if (isAboveThreshold.value) { + // 宽屏逻辑 + if (designStore.isCollapse) { + designStore.setCollapseType('alwaysCollapsed'); + } + else { + designStore.setCollapseType( + designStore.collapseType === 'narrowExpandWideCollapse' + ? 'alwaysExpanded' + : 'followSystem', + ); + } + } + else { + // 窄屏逻辑 + if (designStore.isCollapse) { + designStore.setCollapseType('followSystem'); + } + else { + designStore.setCollapseType( + designStore.collapseType === 'alwaysCollapsed' + ? 'narrowExpandWideCollapse' + : 'alwaysExpanded', + ); + } + } + }; + + return { + changeCollapse, + }; +} + +// 使用示例与特性说明 +// +// diff --git a/src/hooks/useWindowWidthObserver.ts b/src/hooks/useWindowWidthObserver.ts index 028324e..77429ba 100644 --- a/src/hooks/useWindowWidthObserver.ts +++ b/src/hooks/useWindowWidthObserver.ts @@ -1,15 +1,17 @@ import type { MaybeRef } from 'vue'; import { onBeforeUnmount, ref, unref, watch } from 'vue'; +import { COLLAPSE_THRESHOLD, SIDE_BAR_WIDTH } from '@/config/index'; import { useDesignStore } from '@/store/modules/design'; /** + * 这里逻辑是研究豆包的折叠逻辑后,设计的折叠方法 * 基于ResizeObserver的窗口宽度监听hooks(高性能实时监控) * @param threshold 宽度阈值(默认600px,支持响应式) * @param onChange 自定义回调(传入则覆盖默认逻辑,参数:当前视口宽度是否超过阈值) * @returns {object} 包含卸载监听的方法及当前状态 */ export function useWindowWidthObserver( - threshold: MaybeRef = 600, + threshold: MaybeRef = COLLAPSE_THRESHOLD, onChange?: (isAboveThreshold: boolean) => void, ) { const designStore = useDesignStore(); @@ -20,15 +22,40 @@ export function useWindowWidthObserver( // 默认逻辑:修改全局折叠状态 const updateCollapseState = (isAbove: boolean) => { - if (!isAbove) { - // 小于阈值时 且为展开状态时候 - // 跟随用户当前的意愿 - designStore.setCollapseFinal(designStore.isCollapseManual); - // 如果是开,则执行展开动画表示用户意愿 + // 判断当前的折叠状态 + switch (designStore.collapseType) { + case 'alwaysCollapsed': + designStore.setCollapseFinal(true); + break; + case 'followSystem': + designStore.setCollapseFinal(!isAbove); + designStore.setCollapseFinal(!isAbove); + break; + case 'alwaysExpanded': + designStore.setCollapseFinal(false); + if (isAbove) { + // 大于的时候执行关闭动画 + console.log('执行关闭动画'); + } + else { + // 小于的时候执行打开动画 + console.log('小于的时候执行打开动画'); + } + break; + case 'narrowExpandWideCollapse': + designStore.setCollapseFinal(isAbove); + designStore.setCollapseFinal(isAbove); } - else if (!designStore.isCollapseFinal && isAbove) { - // 大于阈值时 且为收起状态时 - designStore.setCollapseFinal(true); + console.log('最终的折叠状态:', designStore.isCollapse); + + if (!designStore.isCollapse) { + document.documentElement.style.setProperty( + `--sidebar-left-container-default-width`, + `${SIDE_BAR_WIDTH}px`, + ); + } + else { + document.documentElement.style.setProperty(`--sidebar-left-container-default-width`, ``); } }; diff --git a/src/layouts/LayoutVertical/index.vue b/src/layouts/LayoutVertical/index.vue index 6e75556..f2c6287 100644 --- a/src/layouts/LayoutVertical/index.vue +++ b/src/layouts/LayoutVertical/index.vue @@ -4,22 +4,11 @@ import { useWindowWidthObserver } from '@/hooks/useWindowWidthObserver'; import Aside from '@/layouts/components/Aside/index.vue'; import Header from '@/layouts/components/Header/index.vue'; import Main from '@/layouts/components/Main/index.vue'; - import { useDesignStore } from '@/store/modules/design'; const designStore = useDesignStore(); -// 动态绑定左侧菜单animate动画 -const menuAnimate = computed(() => designStore.pageAnimateType); -const menuCollapseFinal = computed(() => designStore.isCollapseFinal); - -console.log('menuAnimate===>', menuAnimate); -console.log('menuCollapseFinal===>', menuCollapseFinal); - -watch([menuCollapseFinal, menuAnimate], (newValue, oldValue) => { - console.log('newValue', newValue); - console.log('oldValue', oldValue); -}); +console.log('每次加载全局的折叠状态', designStore.collapseType); /** 监听窗口大小变化,折叠侧边栏 */ useWindowWidthObserver(); @@ -27,40 +16,46 @@ useWindowWidthObserver(); diff --git a/src/layouts/components/Aside/index.vue b/src/layouts/components/Aside/index.vue index 198a822..c02be63 100644 --- a/src/layouts/components/Aside/index.vue +++ b/src/layouts/components/Aside/index.vue @@ -1,15 +1,155 @@ - - + + diff --git a/src/layouts/components/Header/components/Avatar.vue b/src/layouts/components/Header/components/Avatar.vue new file mode 100644 index 0000000..4606554 --- /dev/null +++ b/src/layouts/components/Header/components/Avatar.vue @@ -0,0 +1,121 @@ + + + + + + diff --git a/src/layouts/components/Header/components/Collapse.vue b/src/layouts/components/Header/components/Collapse.vue index 04a7c74..a17e373 100644 --- a/src/layouts/components/Header/components/Collapse.vue +++ b/src/layouts/components/Header/components/Collapse.vue @@ -1,34 +1,35 @@ + - + diff --git a/src/layouts/components/Header/components/CreateChat.vue b/src/layouts/components/Header/components/CreateChat.vue new file mode 100644 index 0000000..53798c6 --- /dev/null +++ b/src/layouts/components/Header/components/CreateChat.vue @@ -0,0 +1,15 @@ + + + + + + diff --git a/src/layouts/components/Header/components/LoginBtn.vue b/src/layouts/components/Header/components/LoginBtn.vue new file mode 100644 index 0000000..018f119 --- /dev/null +++ b/src/layouts/components/Header/components/LoginBtn.vue @@ -0,0 +1,17 @@ + + + + + + diff --git a/src/layouts/components/Header/components/TitleEditing.vue b/src/layouts/components/Header/components/TitleEditing.vue new file mode 100644 index 0000000..46bde23 --- /dev/null +++ b/src/layouts/components/Header/components/TitleEditing.vue @@ -0,0 +1,54 @@ + + + + + + diff --git a/src/layouts/components/Header/index.vue b/src/layouts/components/Header/index.vue index 412b50b..b67c670 100644 --- a/src/layouts/components/Header/index.vue +++ b/src/layouts/components/Header/index.vue @@ -1,10 +1,70 @@ - + diff --git a/src/pages/chat/home/index.vue b/src/pages/chat/home/index.vue new file mode 100644 index 0000000..c516cd3 --- /dev/null +++ b/src/pages/chat/home/index.vue @@ -0,0 +1,83 @@ + + + + + + diff --git a/src/pages/chat/index.vue b/src/pages/chat/index.vue index e513df1..925805c 100644 --- a/src/pages/chat/index.vue +++ b/src/pages/chat/index.vue @@ -3,10 +3,8 @@ import { BubbleList, Sender } from 'vue-element-plus-x'; import { useRoute, useRouter } from 'vue-router'; import { createSession } from '@/api'; import { send } from '@/api/chat'; -import IconSelect from '@/components/IconSelect/index.vue'; import { ModelEnum } from '@/constants/enums'; import { useUserStore } from '@/store'; - import { useChatStore } from '@/store/modules/chat'; const route = useRoute(); @@ -85,9 +83,7 @@ async function handleSend() {