feat: ✨ 布局,样式统一,集成依赖
This commit is contained in:
parent
b5cddb0041
commit
c163da8e8a
@ -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: '',
|
||||
},
|
||||
}
|
||||
43
.editorconfig
Normal file
43
.editorconfig
Normal file
@ -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
|
||||
@ -71,6 +71,8 @@
|
||||
"watch": true,
|
||||
"watchEffect": true,
|
||||
"watchPostEffect": true,
|
||||
"watchSyncEffect": true
|
||||
"watchSyncEffect": true,
|
||||
"ElMessage": true,
|
||||
"ElMessageBox": true
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,9 +0,0 @@
|
||||
/dist/*
|
||||
.local
|
||||
/node_modules/**
|
||||
|
||||
**/*.svg
|
||||
**/*.sh
|
||||
|
||||
/public/*
|
||||
stats.html
|
||||
@ -1,4 +0,0 @@
|
||||
/dist/*
|
||||
/public/*
|
||||
public/*
|
||||
stats.html
|
||||
@ -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 文件中的 <style> 标签内的样式
|
||||
{
|
||||
files: ["**/*.{vue,html}"],
|
||||
customSyntax: "postcss-html"
|
||||
}
|
||||
],
|
||||
rules: {
|
||||
"function-url-quotes": "always", // URL 的引号 "always(必须加上引号)"|"never(没有引号)"
|
||||
"color-hex-length": "long", // 指定 16 进制颜色的简写或扩写 "short(16进制简写)"|"long(16进制扩写)"
|
||||
"rule-empty-line-before": "never", // 要求或禁止在规则之前的空行 "always(规则之前必须始终有一个空行)"|"never(规则前绝不能有空行)"|"always-multi-line(多行规则之前必须始终有一个空行)"|"never-multi-line(多行规则之前绝不能有空行)"
|
||||
"font-family-no-missing-generic-family-keyword": null, // 禁止在字体族名称列表中缺少通用字体族关键字
|
||||
"scss/at-import-partial-extension": null, // 解决不能使用 @import 引入 scss 文件
|
||||
"property-no-unknown": null, // 禁止未知的属性
|
||||
"no-empty-source": null, // 禁止空源码
|
||||
"selector-class-pattern": null, // 强制选择器类名的格式
|
||||
"value-no-vendor-prefix": null, // 关闭 vendor-prefix (为了解决多行省略 -webkit-box)
|
||||
"no-descending-specificity": null, // 不允许较低特异性的选择器出现在覆盖较高特异性的选择器
|
||||
"value-keyword-case": null, // 解决在 scss 中使用 v-bind 大写单词报错
|
||||
"selector-pseudo-class-no-unknown": [
|
||||
true,
|
||||
{
|
||||
ignorePseudoClasses: ["global", "v-deep", "deep"]
|
||||
}
|
||||
]
|
||||
},
|
||||
ignoreFiles: ["**/*.js", "**/*.jsx", "**/*.tsx", "**/*.ts"]
|
||||
};
|
||||
16
.vscode/extensions.json
vendored
16
.vscode/extensions.json
vendored
@ -1,3 +1,17 @@
|
||||
{
|
||||
"recommendations": ["Vue.volar"]
|
||||
// 扩展推荐
|
||||
"recommendations": [
|
||||
// 推荐扩展 vue 官方推荐
|
||||
"Vue.volar",
|
||||
// 推荐扩展 EditorConfig 格式化 scss 换行问题
|
||||
"EditorConfig.EditorConfig",
|
||||
// 推荐扩展 unocss 官方推荐
|
||||
"antfu.unocss",
|
||||
// 推荐扩展 eslint 官方推荐
|
||||
"dbaeumer.vscode-eslint"
|
||||
]
|
||||
}
|
||||
|
||||
// 最好都安装启用一下,依赖都是最新版本的
|
||||
// 规范开发
|
||||
// 配置文件都配置好了 启用拓展后 vscode 自动保存 将修复不规范的代码
|
||||
|
||||
8
.vscode/settings.json
vendored
8
.vscode/settings.json
vendored
@ -5,6 +5,8 @@
|
||||
"prettier.enable": false,
|
||||
// 关闭默认格式化
|
||||
"editor.formatOnSave": false,
|
||||
// 开启 stylint
|
||||
// "stylelint.enable": true,
|
||||
|
||||
// 保存自动修复
|
||||
"editor.codeActionsOnSave": {
|
||||
@ -16,6 +18,9 @@
|
||||
"source.fixAll.stylelint": "explicit"
|
||||
},
|
||||
|
||||
// 开启 stylelint
|
||||
// "stylelint.validate": ["scss", "vue"], // 必须包含"vue"以识别Vue文件中的<style>
|
||||
|
||||
// 静默样式规则自动修复
|
||||
"eslint.rules.customizations": [
|
||||
{ "rule": "style/*", "severity": "off", "fixable": true },
|
||||
@ -52,6 +57,5 @@
|
||||
"scss",
|
||||
"pcss",
|
||||
"postcss"
|
||||
],
|
||||
"cSpell.words": ["aeac", "esno", "radash", "unocss", "unplugin"]
|
||||
]
|
||||
}
|
||||
|
||||
2
LICENSE
2
LICENSE
@ -1,6 +1,6 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 何嘉悦
|
||||
Copyright (c) 2025 HeJiaYue520
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
||||
@ -12,6 +12,7 @@
|
||||
"fix": "eslint . --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.1",
|
||||
"@jsonlee_12138/enum": "^1.0.4",
|
||||
"@vueuse/core": "^13.2.0",
|
||||
"element-plus": "^2.9.10",
|
||||
@ -26,19 +27,18 @@
|
||||
"vue-router": "4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@antfu/eslint-config": "^4.13.0",
|
||||
"@antfu/eslint-config": "^4.13.1",
|
||||
"@changesets/cli": "^2.29.4",
|
||||
"@commitlint/config-conventional": "^19.8.1",
|
||||
"@vitejs/plugin-vue": "^5.2.4",
|
||||
"@vue/tsconfig": "^0.7.0",
|
||||
"commitlint": "^19.8.1",
|
||||
"cz-git": "^1.11.1",
|
||||
"eslint": "^9.26.0",
|
||||
"eslint": "^9.27.0",
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^16.0.0",
|
||||
"prettier": "^3.5.3",
|
||||
"sass-embedded": "^1.88.0",
|
||||
"stylelint": "^16.19.1",
|
||||
"sass-embedded": "^1.89.0",
|
||||
"typescript": "~5.8.3",
|
||||
"unocss": "66.1.2",
|
||||
"unplugin-auto-import": "^19.2.0",
|
||||
|
||||
@ -4,4 +4,4 @@
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped lang="scss"></style>
|
||||
|
||||
1
src/assets/icons/svg/ctrl+k.svg
Normal file
1
src/assets/icons/svg/ctrl+k.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="37" height="14" fill="none" viewBox="0 0 37 14" class="short-cut-lh6ko_"><rect width="22.3" height="12.3" x="0.35" y="0.85" stroke="currentColor" stroke-width="0.7" rx="1.65"></rect><path fill="currentColor" d="M6.97 10.666c-1.913 0-3.11-1.416-3.11-3.682v-.01c0-2.27 1.192-3.686 3.106-3.686 1.484 0 2.642.933 2.852 2.285l-.005.01h-.884l-.005-.01C8.69 4.67 7.938 4.1 6.966 4.1c-1.353 0-2.202 1.113-2.202 2.876v.01c0 1.762.85 2.87 2.207 2.87.981 0 1.728-.502 1.948-1.313l.01-.01h.889v.01c-.235 1.289-1.348 2.124-2.847 2.124m5.885-.127c-1.084 0-1.538-.4-1.538-1.406V5.939h-.83v-.703h.83V3.874h.879v1.362h1.152v.703h-1.152v2.979c0 .62.215.87.761.87.152 0 .235-.006.391-.02v.722c-.166.03-.327.05-.493.05m1.563-.039V5.236h.85v.782h.077c.2-.552.694-.874 1.407-.874.16 0 .341.02.424.034v.825a3 3 0 0 0-.522-.049c-.81 0-1.387.513-1.387 1.284V10.5zm3.657 0V3.146h.85V10.5z"></path><rect width="12.3" height="12.3" x="24.35" y="0.85" stroke="currentColor" stroke-width="0.7" rx="1.65"></rect><path fill="currentColor" d="M28.103 10.5V3.454h.878v3.423h.079l3.085-3.423h1.104l-2.817 3.042 3.076 4.004H32.37l-2.544-3.394-.845.933V10.5z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
BIN
src/assets/images/logo.png
Normal file
BIN
src/assets/images/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
@ -1,8 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import { useClipboard } from '@vueuse/core';
|
||||
import SvgIcon from '@/components/SvgIcon/index.vue';
|
||||
import icons from './requireIcons';
|
||||
|
||||
const emits = defineEmits(['selected']);
|
||||
const { copy } = useClipboard();
|
||||
|
||||
const name = ref('');
|
||||
const iconList = ref(icons);
|
||||
@ -12,9 +14,7 @@ function filterIcons() {
|
||||
if (name.value) {
|
||||
let index = 0;
|
||||
iconList.value.forEach((icons) => {
|
||||
iconList.value[index].iconList = icons.iconList.filter(item =>
|
||||
item.includes(name.value),
|
||||
);
|
||||
iconList.value[index].iconList = icons.iconList.filter(item => item.includes(name.value));
|
||||
index++;
|
||||
});
|
||||
}
|
||||
@ -22,6 +22,7 @@ function filterIcons() {
|
||||
|
||||
function selectedIcon(name: string) {
|
||||
emits('selected', name);
|
||||
copy(name);
|
||||
document.body.click();
|
||||
}
|
||||
</script>
|
||||
@ -51,16 +52,12 @@ function selectedIcon(name: string) {
|
||||
:label="classify.classifyName"
|
||||
>
|
||||
<div class="grid-container">
|
||||
<div
|
||||
v-for="item of classify.iconList"
|
||||
:key="item"
|
||||
@click="selectedIcon(item)"
|
||||
>
|
||||
<div v-for="item of classify.iconList" :key="item" @click="selectedIcon(item)">
|
||||
<div class="icon-item flex-center flex-col gap-3px">
|
||||
<SvgIcon :name="item" />
|
||||
<span class="icon_name text-overflow max-w-80px">{{
|
||||
item
|
||||
}}</span>
|
||||
<span class="icon_name text-overflow max-w-80px">
|
||||
{{ item }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -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;
|
||||
|
||||
407
src/components/Popover/index.vue
Normal file
407
src/components/Popover/index.vue
Normal file
@ -0,0 +1,407 @@
|
||||
<script setup lang="ts">
|
||||
import type { CSSProperties } from 'vue';
|
||||
import { onClickOutside } from '@vueuse/core';
|
||||
|
||||
type PopoverPosition =
|
||||
| 'top'
|
||||
| 'top-start'
|
||||
| 'top-end'
|
||||
| 'bottom'
|
||||
| 'bottom-start'
|
||||
| 'bottom-end'
|
||||
| 'left'
|
||||
| 'left-start'
|
||||
| 'left-end'
|
||||
| 'right'
|
||||
| 'right-start'
|
||||
| 'right-end';
|
||||
|
||||
type Offset = [number, number];
|
||||
const props = withDefaults(defineProps<PopoverProps>(), {
|
||||
position: 'bottom',
|
||||
offset: () => [8, 8],
|
||||
boundary: 'viewport',
|
||||
closeOnContentClick: false,
|
||||
closeOnTriggerClick: false,
|
||||
triggerStyle: () => ({}),
|
||||
popoverStyle: () => ({}),
|
||||
popoverClass: '',
|
||||
});
|
||||
const emits = defineEmits<{
|
||||
(e: 'show'): void;
|
||||
(e: 'hide'): void;
|
||||
(e: 'positionChange', pos: PopoverPosition): void;
|
||||
}>();
|
||||
|
||||
const VIEWPORT_PADDING = 16;
|
||||
|
||||
interface PopoverProps {
|
||||
position?: PopoverPosition;
|
||||
offset?: Offset;
|
||||
triggerStyle?: CSSProperties;
|
||||
popoverStyle?: CSSProperties;
|
||||
popoverClass?: string;
|
||||
boundary?: 'viewport' | HTMLElement;
|
||||
closeOnContentClick?: boolean;
|
||||
closeOnTriggerClick?: boolean;
|
||||
}
|
||||
|
||||
const triggerRef = ref<HTMLElement | null>(null);
|
||||
const popoverRef = ref<HTMLElement | null>(null);
|
||||
const showPoperContent = ref(false);
|
||||
const currentPosition = ref<PopoverPosition>(props.position);
|
||||
|
||||
// 新增:动画前钩子触发位置计算
|
||||
function beforeEnter() {
|
||||
updatePosition();
|
||||
}
|
||||
|
||||
function getBoundaryRect() {
|
||||
if (props.boundary === 'viewport') {
|
||||
return {
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: window.innerWidth,
|
||||
bottom: window.innerHeight,
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
x: 0,
|
||||
y: 0,
|
||||
toJSON: () => ({}),
|
||||
};
|
||||
}
|
||||
return (props.boundary as HTMLElement).getBoundingClientRect();
|
||||
}
|
||||
|
||||
function calculatePosition(
|
||||
triggerRect: DOMRect,
|
||||
popoverRect: DOMRect,
|
||||
position: PopoverPosition,
|
||||
): { top: number; left: number; origin: string } {
|
||||
const [offsetX, offsetY] = props.offset!; // 解构X/Y两个方向的偏移量
|
||||
const { width: tWidth, height: tHeight } = triggerRect;
|
||||
const { width: pWidth, height: pHeight } = popoverRect;
|
||||
|
||||
const positionMap: Record<PopoverPosition, { top: number; left: number; origin: string }> = {
|
||||
// 上下方向位置(top/bottom系列):Y轴用offsetY,X轴用offsetX
|
||||
'top': {
|
||||
top: triggerRect.top - pHeight - offsetY, // Y轴:触发元素顶部到弹框底部的距离 - Y偏移
|
||||
left: triggerRect.left + tWidth / 2 - pWidth / 2 + offsetX, // X轴:水平居中 + X偏移
|
||||
origin: 'bottom center',
|
||||
},
|
||||
'top-start': {
|
||||
top: triggerRect.top - pHeight - offsetY,
|
||||
left: triggerRect.left + offsetX, // X轴:触发元素左侧 + X偏移
|
||||
origin: 'bottom left',
|
||||
},
|
||||
'top-end': {
|
||||
top: triggerRect.top - pHeight - offsetY,
|
||||
left: triggerRect.left + tWidth - pWidth + offsetX, // X轴:触发元素右侧 - 弹框宽度 + X偏移
|
||||
origin: 'bottom right',
|
||||
},
|
||||
'bottom': {
|
||||
top: triggerRect.bottom + offsetY, // Y轴:触发元素底部 + Y偏移
|
||||
left: triggerRect.left + tWidth / 2 - pWidth / 2 + offsetX, // X轴:水平居中 + X偏移
|
||||
origin: 'top center',
|
||||
},
|
||||
'bottom-start': {
|
||||
top: triggerRect.bottom + offsetY,
|
||||
left: triggerRect.left + offsetX, // X轴:触发元素左侧 + X偏移
|
||||
origin: 'top left',
|
||||
},
|
||||
'bottom-end': {
|
||||
top: triggerRect.bottom + offsetY,
|
||||
left: triggerRect.left + tWidth - pWidth + offsetX, // X轴:触发元素右侧 - 弹框宽度 + X偏移
|
||||
origin: 'top right',
|
||||
},
|
||||
|
||||
// 左右方向位置(left/right系列):X轴用offsetX,Y轴用offsetY
|
||||
'left': {
|
||||
top: triggerRect.top + tHeight / 2 - pHeight / 2 + offsetY, // Y轴:垂直居中 + Y偏移
|
||||
left: triggerRect.left - pWidth - offsetX, // X轴:触发元素左侧 - 弹框宽度 - X偏移
|
||||
origin: 'right center',
|
||||
},
|
||||
'left-start': {
|
||||
top: triggerRect.top + offsetY, // Y轴:触发元素顶部 + Y偏移
|
||||
left: triggerRect.left - pWidth - offsetX,
|
||||
origin: 'right top',
|
||||
},
|
||||
'left-end': {
|
||||
top: triggerRect.top + tHeight - pHeight + offsetY, // Y轴:触发元素底部 - 弹框高度 + Y偏移
|
||||
left: triggerRect.left - pWidth - offsetX,
|
||||
origin: 'right bottom',
|
||||
},
|
||||
'right': {
|
||||
top: triggerRect.top + tHeight / 2 - pHeight / 2 + offsetY, // Y轴:垂直居中 + Y偏移
|
||||
left: triggerRect.right + offsetX, // X轴:触发元素右侧 + X偏移
|
||||
origin: 'left center',
|
||||
},
|
||||
'right-start': {
|
||||
top: triggerRect.top + offsetY, // Y轴:触发元素顶部 + Y偏移
|
||||
left: triggerRect.right + offsetX,
|
||||
origin: 'left top',
|
||||
},
|
||||
'right-end': {
|
||||
top: triggerRect.top + tHeight - pHeight + offsetY, // Y轴:触发元素底部 - 弹框高度 + Y偏移
|
||||
left: triggerRect.right + offsetX,
|
||||
origin: 'left bottom',
|
||||
},
|
||||
};
|
||||
|
||||
return positionMap[position];
|
||||
}
|
||||
|
||||
function adjustPosition(
|
||||
triggerRect: DOMRect,
|
||||
popoverRect: DOMRect,
|
||||
boundaryRect: DOMRect,
|
||||
): PopoverPosition {
|
||||
const allPositions: PopoverPosition[] = [
|
||||
'top',
|
||||
'top-start',
|
||||
'top-end',
|
||||
'bottom',
|
||||
'bottom-start',
|
||||
'bottom-end',
|
||||
'left',
|
||||
'left-start',
|
||||
'left-end',
|
||||
'right',
|
||||
'right-start',
|
||||
'right-end',
|
||||
];
|
||||
|
||||
const candidatePositions = [props.position, ...allPositions.filter(p => p !== props.position)];
|
||||
|
||||
for (const pos of candidatePositions) {
|
||||
const { top, left } = calculatePosition(triggerRect, popoverRect, pos);
|
||||
if (
|
||||
top >= boundaryRect.top + VIEWPORT_PADDING
|
||||
&& left >= boundaryRect.left + VIEWPORT_PADDING
|
||||
&& top + popoverRect.height <= boundaryRect.bottom - VIEWPORT_PADDING
|
||||
&& left + popoverRect.width <= boundaryRect.right - VIEWPORT_PADDING
|
||||
) {
|
||||
return pos;
|
||||
}
|
||||
}
|
||||
|
||||
return props.position;
|
||||
}
|
||||
|
||||
function updatePosition() {
|
||||
if (!triggerRef.value || !popoverRef.value)
|
||||
return;
|
||||
|
||||
// 强制获取最新尺寸(避免缓存)
|
||||
const triggerRect = triggerRef.value.getBoundingClientRect();
|
||||
const popoverRect = popoverRef.value.getBoundingClientRect();
|
||||
const boundaryRect = getBoundaryRect();
|
||||
|
||||
const adjustedPos = adjustPosition(triggerRect, popoverRect, boundaryRect);
|
||||
currentPosition.value = adjustedPos;
|
||||
emits('positionChange', adjustedPos);
|
||||
|
||||
const { top, left, origin } = calculatePosition(triggerRect, popoverRect, adjustedPos);
|
||||
popoverRef.value.style.top = `${top}px`;
|
||||
popoverRef.value.style.left = `${left}px`;
|
||||
popoverRef.value.style.transformOrigin = origin;
|
||||
}
|
||||
|
||||
watch(
|
||||
[() => showPoperContent.value, () => props.position],
|
||||
async ([newShow]) => {
|
||||
if (newShow) {
|
||||
// 等待弹框元素渲染完成
|
||||
await nextTick();
|
||||
updatePosition(); // 直接调用,无需 requestAnimationFrame
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
window.addEventListener('resize', updatePosition);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', updatePosition);
|
||||
});
|
||||
|
||||
function handleTriggerClick() {
|
||||
if (showPoperContent.value) {
|
||||
if (props.closeOnTriggerClick)
|
||||
hidePopover();
|
||||
}
|
||||
else {
|
||||
showPoperContent.value = true;
|
||||
nextTick(() => emits('show'));
|
||||
}
|
||||
}
|
||||
|
||||
function handleContentClick(e: MouseEvent) {
|
||||
if (props.closeOnContentClick)
|
||||
hidePopover();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
function hidePopover() {
|
||||
showPoperContent.value = false;
|
||||
emits('hide');
|
||||
}
|
||||
|
||||
onClickOutside(popoverRef, () => !props.closeOnTriggerClick && hidePopover(), {
|
||||
ignore: [triggerRef],
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
show: () => (showPoperContent.value = true),
|
||||
hide: hidePopover,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="triggerRef"
|
||||
:style="props.triggerStyle"
|
||||
role="button"
|
||||
aria-haspopup="true"
|
||||
:aria-expanded="showPoperContent"
|
||||
@click.stop="handleTriggerClick"
|
||||
>
|
||||
<slot name="trigger" />
|
||||
</div>
|
||||
|
||||
<Teleport to="body">
|
||||
<Transition name="popover-fade" @before-enter="beforeEnter">
|
||||
<div
|
||||
v-if="showPoperContent"
|
||||
ref="popoverRef"
|
||||
class="popover-content"
|
||||
:style="props.popoverStyle"
|
||||
:class="[props.popoverClass]"
|
||||
role="dialog"
|
||||
aria-modal="false"
|
||||
:data-popper-placement="currentPosition"
|
||||
@click="handleContentClick"
|
||||
>
|
||||
<slot name="header" />
|
||||
<slot />
|
||||
<slot name="footer" />
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
/* 动画样式保持不变 */
|
||||
.popover-fade-enter-active,
|
||||
.popover-fade-leave-active {
|
||||
transition:
|
||||
opacity 0.2s ease,
|
||||
transform 0.2s ease;
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
|
||||
.popover-fade-enter-from,
|
||||
.popover-fade-leave-to {
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.popover-fade-enter-to,
|
||||
.popover-fade-leave-from {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
|
||||
.popover-content {
|
||||
position: fixed;
|
||||
min-width: 120px;
|
||||
background: #fff;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
|
||||
padding: 12px 16px;
|
||||
z-index: 1000;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border: 6px solid transparent;
|
||||
}
|
||||
|
||||
[data-popper-placement^="top"]::before {
|
||||
top: 100%;
|
||||
border-top-color: #e5e7eb;
|
||||
}
|
||||
|
||||
[data-popper-placement="top"]::before {
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
[data-popper-placement="top-start"]::before {
|
||||
left: 16px;
|
||||
}
|
||||
|
||||
[data-popper-placement="top-end"]::before {
|
||||
right: 16px;
|
||||
}
|
||||
|
||||
[data-popper-placement^="bottom"]::before {
|
||||
bottom: 100%;
|
||||
border-bottom-color: #e5e7eb;
|
||||
}
|
||||
|
||||
[data-popper-placement="bottom"]::before {
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
[data-popper-placement="bottom-start"]::before {
|
||||
left: 16px;
|
||||
}
|
||||
|
||||
[data-popper-placement="bottom-end"]::before {
|
||||
right: 16px;
|
||||
}
|
||||
|
||||
[data-popper-placement^="left"]::before {
|
||||
left: 100%;
|
||||
border-left-color: #e5e7eb;
|
||||
}
|
||||
|
||||
[data-popper-placement="left"]::before {
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
[data-popper-placement="left-start"]::before {
|
||||
top: 16px;
|
||||
}
|
||||
|
||||
[data-popper-placement="left-end"]::before {
|
||||
bottom: 16px;
|
||||
}
|
||||
|
||||
[data-popper-placement^="right"]::before {
|
||||
right: 100%;
|
||||
border-right-color: #e5e7eb;
|
||||
}
|
||||
|
||||
[data-popper-placement="right"]::before {
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
[data-popper-placement="right-start"]::before {
|
||||
top: 16px;
|
||||
}
|
||||
|
||||
[data-popper-placement="right-end"]::before {
|
||||
bottom: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -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;
|
||||
|
||||
@ -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'];
|
||||
|
||||
61
src/hooks/useCollapseToggle.ts
Normal file
61
src/hooks/useCollapseToggle.ts
Normal file
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
// 使用示例与特性说明
|
||||
// <script setup lang="ts">
|
||||
// import { useCollapseToggle } from '@/composables/useCollapseToggle';
|
||||
// import { COLLAPSE_THRESHOLD } from '@/config/index'; (可传,不传入全局配置走)
|
||||
// const { changeCollapseIcon } = useCollapseToggle(designStore, COLLAPSE_THRESHOLD);
|
||||
// </script>
|
||||
// <template>
|
||||
// <!-- 其他页面的按钮 -->
|
||||
// <button @click="changeCollapseIcon">切换侧边栏</button>
|
||||
// </template>
|
||||
@ -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<number> = 600,
|
||||
threshold: MaybeRef<number> = 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`, ``);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -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();
|
||||
|
||||
<template>
|
||||
<el-container class="layout-container">
|
||||
<el-aside
|
||||
v-if="menuCollapseFinal"
|
||||
class="layout-aside transition-all"
|
||||
:class="menuAnimate"
|
||||
>
|
||||
<el-scrollbar class="layout-scrollbar">
|
||||
<Aside />
|
||||
</el-scrollbar>
|
||||
</el-aside>
|
||||
<el-container>
|
||||
<el-header class="layout-header">
|
||||
<Header />
|
||||
</el-header>
|
||||
<!-- 路由页面 -->
|
||||
<Main />
|
||||
<el-header class="layout-header">
|
||||
<Header />
|
||||
</el-header>
|
||||
<el-container class="layout-container-main">
|
||||
<Transition :class="designStore.pageAnimateType">
|
||||
<Aside v-if="!designStore.isCollapse" class="layout-aside transition-all" />
|
||||
</Transition>
|
||||
<el-main class="layout-main">
|
||||
<!-- 路由页面 -->
|
||||
<Main />
|
||||
</el-main>
|
||||
</el-container>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.layout-container {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
.layout-aside {
|
||||
// z-index: $layout-aside-z-index; // 左侧菜单层级
|
||||
// padding-right: $aside-menu-padding-right; // 左侧布局右边距[用于悬浮和选择更明显]
|
||||
// padding-left: $aside-menu-padding-left; // 左侧布局左边距[用于悬浮和选择更明显]
|
||||
background-color: var(--el-menu-bg-color);
|
||||
border-right: none;
|
||||
// box-shadow: $aside-menu-box-shadow; // 左侧布局右边框阴影
|
||||
}
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.layout-header {
|
||||
// height: $aside-header-height;
|
||||
background-color: var(--el-header-bg-color);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.layout-aside {
|
||||
overflow: hidden;
|
||||
width: var(--sidebar-left-container-default-width, 0px);
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.layout-main {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.layout-container-main {
|
||||
margin-left: var(--sidebar-left-container-default-width, 0px);
|
||||
}
|
||||
}
|
||||
|
||||
@ -68,8 +63,8 @@ useWindowWidthObserver();
|
||||
.el-menu {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.layout-scrollbar {
|
||||
width: 100%;
|
||||
// height: calc(100vh - $aside-header-height);
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,15 +1,155 @@
|
||||
<!-- Aside -->
|
||||
<script setup lang="ts"></script>
|
||||
<!-- Aside 侧边栏 -->
|
||||
<script setup lang="ts">
|
||||
import logo from '@/assets/images/logo.png';
|
||||
import Collapse from '@/layouts/components/Header/components/Collapse.vue';
|
||||
|
||||
/* 创建会话 开始 */
|
||||
function handleCreatChat() {
|
||||
console.log('创建新会话');
|
||||
}
|
||||
/* 创建会话 结束 */
|
||||
|
||||
/* 会话组件 开始 */
|
||||
const active = ref();
|
||||
const conversationsList = ref([]);
|
||||
|
||||
function handleChange() {
|
||||
console.log('点击了会话');
|
||||
}
|
||||
/* 会话组件 结束 */
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="aside-container">
|
||||
侧边栏
|
||||
<div class="aside-wrapper">
|
||||
<div class="aside-header">
|
||||
<div class="flex items-center gap-8px hover:cursor-pointer" @click="handleCreatChat">
|
||||
<el-image :src="logo" alt="logo" fit="cover" class="logo-img" />
|
||||
<span class="logo-text max-w-150px text-overflow">Elemennt-Plus-X</span>
|
||||
</div>
|
||||
<Collapse class="ml-auto" />
|
||||
</div>
|
||||
|
||||
<div class="creat-chat-btn-wrapper">
|
||||
<div class="creat-chat-btn" @click="handleCreatChat">
|
||||
<el-icon class="add-icon">
|
||||
<Plus />
|
||||
</el-icon>
|
||||
<span class="creat-chat-text">新对话</span>
|
||||
<SvgIcon name="ctrl+k" size="37" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="aside-body">
|
||||
<div v-if="conversationsList.length > 0" class="flex h-full">
|
||||
<Conversations
|
||||
:active="active"
|
||||
:items="conversationsList"
|
||||
row-key="id"
|
||||
label-key="sessionTitle"
|
||||
@change="handleChange"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<el-empty v-else class="h-full flex-center" description="暂无对话记录" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.aside-container {
|
||||
background-color: aqua;
|
||||
height: 100vh;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
box-sizing: border-box;
|
||||
background-color: var(--sidebar-background-color);
|
||||
border-right: 1px solid rgba(0, 0, 0, 0.08);
|
||||
|
||||
.aside-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
.aside-header {
|
||||
height: 36px;
|
||||
margin: 10px 12px 0px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.logo-img {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
background-color: #fff;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
padding: 4px;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
font-size: 16px;
|
||||
color: rgba(0, 0, 0, 0.85);
|
||||
font-weight: 700;
|
||||
transform: skewX(-2deg);
|
||||
}
|
||||
}
|
||||
|
||||
.creat-chat-btn-wrapper {
|
||||
padding: 0 12px;
|
||||
|
||||
.creat-chat-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 6px;
|
||||
margin-top: 16px;
|
||||
margin-bottom: 6px;
|
||||
color: #0057ff;
|
||||
background-color: rgba(0, 87, 255, 0.06);
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(0, 102, 255, 0.15);
|
||||
cursor: pointer;
|
||||
gap: 6px;
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(0, 87, 255, 0.12);
|
||||
}
|
||||
|
||||
.creat-chat-text {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.add-icon {
|
||||
font-size: 16px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.svg-icon {
|
||||
margin-left: auto;
|
||||
height: 24px;
|
||||
color: rgba(0, 87, 255, 0.3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.aside-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
121
src/layouts/components/Header/components/Avatar.vue
Normal file
121
src/layouts/components/Header/components/Avatar.vue
Normal file
@ -0,0 +1,121 @@
|
||||
<!-- 头像 -->
|
||||
<script setup lang="ts">
|
||||
import Popover from '@/components/Popover/index.vue';
|
||||
import SvgIcon from '@/components/SvgIcon/index.vue';
|
||||
|
||||
const src = ref('https://avatars.githubusercontent.com/u/76239030');
|
||||
|
||||
/* 弹出面板 开始 */
|
||||
const popoverStyle = ref({
|
||||
width: '200px',
|
||||
padding: '4px',
|
||||
height: 'fit-content',
|
||||
});
|
||||
const popoverRef = ref();
|
||||
|
||||
// 弹出面板内容
|
||||
const popoverList = ref([
|
||||
{
|
||||
key: '1',
|
||||
title: '收藏夹',
|
||||
icon: 'book-mark-fill',
|
||||
},
|
||||
{
|
||||
key: '2',
|
||||
title: '设置',
|
||||
icon: 'settings-4-fill',
|
||||
},
|
||||
{
|
||||
key: '3',
|
||||
divider: true,
|
||||
},
|
||||
{
|
||||
key: '4',
|
||||
title: '退出登录',
|
||||
icon: 'logout-box-r-line',
|
||||
},
|
||||
]);
|
||||
|
||||
// 点击
|
||||
function handleClick(item: any) {
|
||||
switch (item.key) {
|
||||
case '1':
|
||||
console.log('点击了收藏夹');
|
||||
break;
|
||||
case '2':
|
||||
console.log('点击了设置');
|
||||
break;
|
||||
case '4':
|
||||
popoverRef.value.hide();
|
||||
ElMessageBox.confirm('退出登录不会丢失任何数据,你仍可以登录此账号。', '确认退出登录?', {
|
||||
confirmButtonText: '确认退出',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
confirmButtonClass: 'el-button--danger',
|
||||
cancelButtonClass: 'el-button--info',
|
||||
roundButton: true,
|
||||
})
|
||||
.then(() => {
|
||||
// 在这里执行退出方法
|
||||
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: '退出成功',
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
// ElMessage({
|
||||
// type: 'info',
|
||||
// message: '取消',
|
||||
// });
|
||||
});
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* 弹出面板 结束 */
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="avatar-container">
|
||||
<Popover
|
||||
ref="popoverRef"
|
||||
position="bottom-end"
|
||||
:offset="[-10, 8]"
|
||||
:trigger-style="{ cursor: 'pointer' }"
|
||||
popover-class="popover-content"
|
||||
:popover-style="popoverStyle"
|
||||
>
|
||||
<!-- 触发元素插槽 -->
|
||||
<template #trigger>
|
||||
<el-avatar :src="src" :size="28" fit="fit" shape="circle" />
|
||||
</template>
|
||||
|
||||
<div class="popover-content-box">
|
||||
<div v-for="item in popoverList" :key="item.key" class="popover-content-box-items h-full">
|
||||
<div
|
||||
v-if="!item.divider"
|
||||
class="popover-content-box-item flex items-center h-full gap-8px p-8px pl-10px pr-12px rounded-lg hover:cursor-pointer hover:bg-[rgba(0,0,0,.04)]"
|
||||
@click="handleClick(item)"
|
||||
>
|
||||
<SvgIcon :name="item.icon!" size="16" class-name="flex-none" />
|
||||
<div class="popover-content-box-item-text font-size-14px text-overflow max-h-120px">
|
||||
{{ item.title }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="item.divider" class="divder h-1px bg-gray-200 my-4px" />
|
||||
</div>
|
||||
</div>
|
||||
</Popover>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.popover-content {
|
||||
width: 520px;
|
||||
height: 520px;
|
||||
}
|
||||
</style>
|
||||
@ -1,34 +1,35 @@
|
||||
<!-- 侧边栏折叠按钮 -->
|
||||
<script setup lang="ts">
|
||||
import SvgIcon from '@/components/SvgIcon/index.vue';
|
||||
import { SIDE_BAR_WIDTH } from '@/config/index';
|
||||
import { useCollapseToggle } from '@/hooks/useCollapseToggle';
|
||||
import { useDesignStore } from '@/store/modules/design';
|
||||
|
||||
const { changeCollapse } = useCollapseToggle();
|
||||
const designStore = useDesignStore();
|
||||
|
||||
/** 切换图标 */
|
||||
function changeCollapseIcon() {
|
||||
// 定义图标切换变量(true-折叠,false-展开)
|
||||
// 保存当前用户 手动 意图,并跟新最终的展开状态
|
||||
const isFinal = designStore.setCollapseManual(!designStore.isCollapseFinal);
|
||||
designStore.setCollapseFinal(isFinal);
|
||||
function handleChangeCollapse() {
|
||||
changeCollapse();
|
||||
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`, ``);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="hover:bg-[rgba(46,50,56,.05)] hover:c-[rgba(0,0,0,0.8)] hover:cursor-pointer p-2px rounded-md flex-center inline-block c-[rgba(0,0,0,0.6)] active:bg-[rgba(46,50,56,.08)] transition-all transition-delay-10"
|
||||
@click="changeCollapseIcon"
|
||||
>
|
||||
<SvgIcon
|
||||
v-if="designStore.isCollapseFinal"
|
||||
name="ms-left-panel-close-outline"
|
||||
size="24"
|
||||
/>
|
||||
<SvgIcon
|
||||
v-if="!designStore.isCollapseFinal"
|
||||
name="ms-left-panel-open-outline"
|
||||
size="24"
|
||||
/>
|
||||
<div class="collapse-container btn-icon-btn" @click="handleChangeCollapse">
|
||||
<SvgIcon v-if="!designStore.isCollapse" name="ms-left-panel-close-outline" size="24" />
|
||||
<SvgIcon v-if="designStore.isCollapse" name="ms-left-panel-open-outline" size="24" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
<style lang="scss" scoped>
|
||||
// .collapse-container {
|
||||
// }
|
||||
</style>
|
||||
|
||||
15
src/layouts/components/Header/components/CreateChat.vue
Normal file
15
src/layouts/components/Header/components/CreateChat.vue
Normal file
@ -0,0 +1,15 @@
|
||||
<!-- 添加新会话按钮 -->
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="create-chat-container flex-center flex-none p-6px pl-8px pr-8px c-#0057ff b-#0057ff b-rounded-12px border-1px hover:bg-#0057ff hover:c-#fff hover:b-#fff hover:cursor-pointer border-solid"
|
||||
>
|
||||
<el-icon size="12" class="flex-center flex-none w-14px h-14px">
|
||||
<Plus />
|
||||
</el-icon>
|
||||
<span class="ml-4px font-size-14px font-700">新对话</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
17
src/layouts/components/Header/components/LoginBtn.vue
Normal file
17
src/layouts/components/Header/components/LoginBtn.vue
Normal file
@ -0,0 +1,17 @@
|
||||
<!-- LoginBtn 登录按钮 -->
|
||||
<script setup lang="ts">
|
||||
function handleClickLogin() {
|
||||
console.log('handleClickLogin');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="login-btn bg-#191c1f c-#fff font-size-14px rounded-8px flex-center text-overflow p-10px pl-12px pr-12px min-w-49px h-16px cursor-pointer hover:bg-#232629"
|
||||
@click="handleClickLogin"
|
||||
>
|
||||
登录
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
54
src/layouts/components/Header/components/TitleEditing.vue
Normal file
54
src/layouts/components/Header/components/TitleEditing.vue
Normal file
@ -0,0 +1,54 @@
|
||||
<!-- 标题编辑 -->
|
||||
<script setup lang="ts">
|
||||
import SvgIcon from '@/components/SvgIcon/index.vue';
|
||||
|
||||
function handleClickTitle() {
|
||||
ElMessageBox.prompt('', '编辑对话名称', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
inputErrorMessage: '请输入对话名称',
|
||||
inputValidator: (value) => {
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
})
|
||||
.then(({ value }) => {
|
||||
ElMessage({
|
||||
type: 'success',
|
||||
message: `修改成功:${value}`,
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
// ElMessage({
|
||||
// type: 'info',
|
||||
// message: '取消修改',
|
||||
// });
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="title-editing-container hover:bg-[rgba(0,0,0,.04)] cursor-pointer rounded-md p-4px flex items-center"
|
||||
@click="handleClickTitle"
|
||||
>
|
||||
<span class="font-size-14px text-overflow max-w-320px">标题编辑标题编辑标题编辑标题编辑标题编辑标题编辑标题编辑标题编辑</span>
|
||||
<SvgIcon name="draft-line" size="14" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.title-editing-container {
|
||||
&:hover {
|
||||
.svg-icon {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.svg-icon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,10 +1,70 @@
|
||||
<!-- Header 头部 -->
|
||||
<script setup lang="ts">
|
||||
import { SIDE_BAR_WIDTH } from '@/config/index';
|
||||
import { useUserStore } from '@/store';
|
||||
import Avatar from './components/Avatar.vue';
|
||||
import Collapse from './components/Collapse.vue';
|
||||
import CreateChat from './components/CreateChat.vue';
|
||||
import LoginBtn from './components/LoginBtn.vue';
|
||||
import TitleEditing from './components/TitleEditing.vue';
|
||||
|
||||
const userStore = useUserStore();
|
||||
console.log('userStore', userStore.token);
|
||||
|
||||
onMounted(() => {
|
||||
document.documentElement.style.setProperty(
|
||||
`--sidebar-left-container-default-width`,
|
||||
`${SIDE_BAR_WIDTH}px`,
|
||||
);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>头部 <Collapse /></div>
|
||||
<div class="header-container">
|
||||
<div class="header-box relative z-10 top-0 left-0 right-0">
|
||||
<div class="absolute left-0 right-0 top-0 bottom-0 flex items-center flex-row">
|
||||
<!-- 左边 -->
|
||||
<div class="left-box flex h-full items-center pl-20px gap-12px flex-shrink-0 flex-row">
|
||||
<Collapse />
|
||||
<CreateChat />
|
||||
</div>
|
||||
|
||||
<!-- 中间 -->
|
||||
<div class="middle-box flex h-full items-center gap-12px flex-1 pl-12px">
|
||||
<div class="w-0.5px h-30px bg-[rgba(217,217,217)]" />
|
||||
<TitleEditing />
|
||||
</div>
|
||||
|
||||
<!-- 右边 -->
|
||||
<div class="right-box flex h-full items-center pr-20px flex-shrink-0 mr-auto flex-row">
|
||||
<!-- <Avatar v-if="userStore.token" /> -->
|
||||
<!-- <LoginBtn v-else /> -->
|
||||
<Avatar />
|
||||
<LoginBtn />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
<style scoped lang="scss">
|
||||
.header-container {
|
||||
width: 100%;
|
||||
height: fit-content;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
.header-box {
|
||||
height: var(--header-container-default-heigth);
|
||||
width: 100%;
|
||||
width: calc(
|
||||
100% - var(--sidebar-left-container-default-width, 0px) - var(
|
||||
--sidebar-right-container-default-width,
|
||||
0px
|
||||
)
|
||||
);
|
||||
margin: 0 var(--sidebar-right-container-default-width, 0px) 0
|
||||
var(--sidebar-left-container-default-width, 0px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
83
src/pages/chat/home/index.vue
Normal file
83
src/pages/chat/home/index.vue
Normal file
@ -0,0 +1,83 @@
|
||||
<!-- 对话首页 -->
|
||||
<script setup lang="ts">
|
||||
const senderValue = ref('');
|
||||
const isSelect = ref(false);
|
||||
function handleSend() {
|
||||
console.log(senderValue.value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="chat-home-container">
|
||||
<div class="chat-home-wrap">
|
||||
<div class="chat-home-welecome">
|
||||
<div
|
||||
class="welecome-text min-h-45px text-center mt-0px mr-auto ml-auto mb-32px font-size-32px text-align-center font-600"
|
||||
>
|
||||
早上好,Hjy。
|
||||
</div>
|
||||
</div>
|
||||
<Sender
|
||||
v-model="senderValue"
|
||||
class="chat-home-sender"
|
||||
:auto-size="{
|
||||
maxRows: 9,
|
||||
minRows: 3,
|
||||
}"
|
||||
variant="updown"
|
||||
clearable
|
||||
allow-speech
|
||||
@submit="handleSend"
|
||||
>
|
||||
<template #prefix>
|
||||
<div class="flex items-center gap-8px">
|
||||
<div
|
||||
class="flex items-center gap-4px px-12px py-8px rounded-15px cursor-pointer font-size-12px border-1px border-gray border-solid hover:bg-[rgba(0,0,0,.04)]"
|
||||
>
|
||||
<el-icon>
|
||||
<Paperclip />
|
||||
</el-icon>
|
||||
</div>
|
||||
|
||||
<div
|
||||
:class="{ isSelect }"
|
||||
class="flex items-center gap-4px px-10px py-8px rounded-15px cursor-pointer font-size-12px border-1px border-gray border-solid hover:bg-[rgba(0,0,0,.04)]"
|
||||
@click="isSelect = !isSelect"
|
||||
>
|
||||
<el-icon>
|
||||
<ElementPlus />
|
||||
</el-icon>
|
||||
<span>深度思考</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Sender>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.chat-home-container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: calc(100vh - var(--header-container-default-heigth));
|
||||
|
||||
.chat-home-wrap {
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
min-height: 450px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
// background-color: antiquewhite;
|
||||
|
||||
.chat-home-sender {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -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() {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<IconSelect />
|
||||
|
||||
<div class="chat-container">
|
||||
<BubbleList :list="chatList">
|
||||
<template #content="{ item }">
|
||||
{{ item.content }}
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
import { createRouter, createWebHashHistory } from 'vue-router';
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
import { jwtGuard } from './permissions';
|
||||
|
||||
const routes: Readonly<RouteRecordRaw>[] = [
|
||||
{
|
||||
path: '/',
|
||||
redirect: '/chat',
|
||||
component: () => import('@/layouts/index.vue'),
|
||||
children: [
|
||||
{
|
||||
@ -14,9 +15,14 @@ const routes: Readonly<RouteRecordRaw>[] = [
|
||||
},
|
||||
{
|
||||
path: ':id',
|
||||
name: 'chat',
|
||||
name: '/chat',
|
||||
component: () => import('@/pages/chat/index.vue'),
|
||||
},
|
||||
{
|
||||
path: '/chat/home',
|
||||
name: 'chatHome',
|
||||
component: () => import('@/pages/chat/home/index.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@ -27,7 +33,7 @@ const routes: Readonly<RouteRecordRaw>[] = [
|
||||
] as const;
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
history: createWebHistory(),
|
||||
routes,
|
||||
});
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { LayoutType } from '@/config/design';
|
||||
import type { CollapseType, LayoutType } from '@/config/design';
|
||||
import { defineStore } from 'pinia';
|
||||
import designSetting from '@/config/design';
|
||||
|
||||
@ -9,9 +9,8 @@ const {
|
||||
isPageAnimate,
|
||||
pageAnimateType: rePageAnimateType,
|
||||
layout: reLayout,
|
||||
isCollapse: reIsCollapse,
|
||||
isCollapseManual: reIsCollapseManual,
|
||||
isCollapseFinal: reIsCollapseFinal,
|
||||
collapseType: reCollapseType,
|
||||
isCollapse: reisCollapse,
|
||||
} = designSetting;
|
||||
|
||||
export const useDesignStore = defineStore(
|
||||
@ -35,25 +34,17 @@ export const useDesignStore = defineStore(
|
||||
// layout.value = layoutType;
|
||||
// };
|
||||
|
||||
// 更据视口宽度和阈值-是否展开左侧菜单
|
||||
const isCollapse = ref<boolean>(reIsCollapse);
|
||||
const setCollapse = (collapse: boolean) => {
|
||||
isCollapse.value = collapse;
|
||||
};
|
||||
|
||||
// 是否手动点击展开左侧菜单
|
||||
const isCollapseManual = ref<boolean>(reIsCollapseManual);
|
||||
const setCollapseManual = (collapseManual: boolean): boolean => {
|
||||
isCollapseManual.value = collapseManual;
|
||||
return collapseManual;
|
||||
// 折叠状态
|
||||
const collapseType = ref<CollapseType>(reCollapseType);
|
||||
const setCollapseType = (type: CollapseType) => {
|
||||
collapseType.value = type;
|
||||
};
|
||||
|
||||
// 最终是否展开左侧菜单
|
||||
const isCollapseFinal = ref<boolean>(reIsCollapseFinal);
|
||||
const isCollapse = ref<boolean>(reisCollapse);
|
||||
|
||||
const setCollapseFinal = (collapseFinal: boolean) => {
|
||||
console.log('最终的折叠状态', collapseFinal);
|
||||
isCollapseFinal.value = collapseFinal;
|
||||
isCollapse.value = collapseFinal;
|
||||
};
|
||||
|
||||
return {
|
||||
@ -65,11 +56,9 @@ export const useDesignStore = defineStore(
|
||||
pageAnimateType,
|
||||
setPageAnimateType,
|
||||
layout,
|
||||
collapseType,
|
||||
setCollapseType,
|
||||
isCollapse,
|
||||
setCollapse,
|
||||
isCollapseManual,
|
||||
setCollapseManual,
|
||||
isCollapseFinal,
|
||||
setCollapseFinal,
|
||||
};
|
||||
},
|
||||
|
||||
35
src/styles/btn-style.scss
Normal file
35
src/styles/btn-style.scss
Normal file
@ -0,0 +1,35 @@
|
||||
// 公共的图表按钮样式
|
||||
.btn-icon-btn {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
display: inline-block;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 30%;
|
||||
padding: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
// 鼠标移入
|
||||
&:hover {
|
||||
background-color: rgb(0, 0, 0, .05);
|
||||
|
||||
.svg-icon {
|
||||
color: rgb(0, 0, 0, .6);
|
||||
}
|
||||
}
|
||||
|
||||
// 鼠标按下
|
||||
&:active {
|
||||
background-color: rgb(0, 0, 0, .08);
|
||||
|
||||
.svg-icon {
|
||||
color: rgb(0, 0, 0, .7);
|
||||
}
|
||||
}
|
||||
|
||||
// 图标
|
||||
.svg-icon {
|
||||
user-select: none;
|
||||
color: rgb(0, 0, 0, .5);
|
||||
}
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
@use './btn-style.scss';
|
||||
@use 'reset-css';
|
||||
@use './element-plus.scss';
|
||||
|
||||
|
||||
9
src/styles/var.scss
Normal file
9
src/styles/var.scss
Normal file
@ -0,0 +1,9 @@
|
||||
:root {
|
||||
/* 头部高度 */
|
||||
--header-container-default-heigth: 56px;
|
||||
|
||||
/* 侧边栏背景色 */
|
||||
--sidebar-background-color: #f3f4f6;
|
||||
|
||||
|
||||
}
|
||||
135
types/auto-imports.d.ts
vendored
135
types/auto-imports.d.ts
vendored
@ -6,66 +6,85 @@
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
declare global {
|
||||
const EffectScope: typeof import('vue')['EffectScope']
|
||||
const computed: typeof import('vue')['computed']
|
||||
const createApp: typeof import('vue')['createApp']
|
||||
const customRef: typeof import('vue')['customRef']
|
||||
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
|
||||
const defineComponent: typeof import('vue')['defineComponent']
|
||||
const effectScope: typeof import('vue')['effectScope']
|
||||
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
|
||||
const getCurrentScope: typeof import('vue')['getCurrentScope']
|
||||
const h: typeof import('vue')['h']
|
||||
const inject: typeof import('vue')['inject']
|
||||
const isProxy: typeof import('vue')['isProxy']
|
||||
const isReactive: typeof import('vue')['isReactive']
|
||||
const isReadonly: typeof import('vue')['isReadonly']
|
||||
const isRef: typeof import('vue')['isRef']
|
||||
const markRaw: typeof import('vue')['markRaw']
|
||||
const nextTick: typeof import('vue')['nextTick']
|
||||
const onActivated: typeof import('vue')['onActivated']
|
||||
const onBeforeMount: typeof import('vue')['onBeforeMount']
|
||||
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
|
||||
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
|
||||
const onDeactivated: typeof import('vue')['onDeactivated']
|
||||
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
|
||||
const onMounted: typeof import('vue')['onMounted']
|
||||
const onRenderTracked: typeof import('vue')['onRenderTracked']
|
||||
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
|
||||
const onScopeDispose: typeof import('vue')['onScopeDispose']
|
||||
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
|
||||
const onUnmounted: typeof import('vue')['onUnmounted']
|
||||
const onUpdated: typeof import('vue')['onUpdated']
|
||||
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
|
||||
const provide: typeof import('vue')['provide']
|
||||
const reactive: typeof import('vue')['reactive']
|
||||
const readonly: typeof import('vue')['readonly']
|
||||
const ref: typeof import('vue')['ref']
|
||||
const resolveComponent: typeof import('vue')['resolveComponent']
|
||||
const shallowReactive: typeof import('vue')['shallowReactive']
|
||||
const shallowReadonly: typeof import('vue')['shallowReadonly']
|
||||
const shallowRef: typeof import('vue')['shallowRef']
|
||||
const toRaw: typeof import('vue')['toRaw']
|
||||
const toRef: typeof import('vue')['toRef']
|
||||
const toRefs: typeof import('vue')['toRefs']
|
||||
const toValue: typeof import('vue')['toValue']
|
||||
const triggerRef: typeof import('vue')['triggerRef']
|
||||
const unref: typeof import('vue')['unref']
|
||||
const useAttrs: typeof import('vue')['useAttrs']
|
||||
const useCssModule: typeof import('vue')['useCssModule']
|
||||
const useCssVars: typeof import('vue')['useCssVars']
|
||||
const useId: typeof import('vue')['useId']
|
||||
const useModel: typeof import('vue')['useModel']
|
||||
const useSlots: typeof import('vue')['useSlots']
|
||||
const useTemplateRef: typeof import('vue')['useTemplateRef']
|
||||
const watch: typeof import('vue')['watch']
|
||||
const watchEffect: typeof import('vue')['watchEffect']
|
||||
const watchPostEffect: typeof import('vue')['watchPostEffect']
|
||||
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
|
||||
const EffectScope: (typeof import("vue"))["EffectScope"];
|
||||
const ElMessage: (typeof import("element-plus/es"))["ElMessage"];
|
||||
const ElMessageBox: (typeof import("element-plus/es"))["ElMessageBox"];
|
||||
const computed: (typeof import("vue"))["computed"];
|
||||
const createApp: (typeof import("vue"))["createApp"];
|
||||
const customRef: (typeof import("vue"))["customRef"];
|
||||
const defineAsyncComponent: (typeof import("vue"))["defineAsyncComponent"];
|
||||
const defineComponent: (typeof import("vue"))["defineComponent"];
|
||||
const effectScope: (typeof import("vue"))["effectScope"];
|
||||
const getCurrentInstance: (typeof import("vue"))["getCurrentInstance"];
|
||||
const getCurrentScope: (typeof import("vue"))["getCurrentScope"];
|
||||
const h: (typeof import("vue"))["h"];
|
||||
const inject: (typeof import("vue"))["inject"];
|
||||
const isProxy: (typeof import("vue"))["isProxy"];
|
||||
const isReactive: (typeof import("vue"))["isReactive"];
|
||||
const isReadonly: (typeof import("vue"))["isReadonly"];
|
||||
const isRef: (typeof import("vue"))["isRef"];
|
||||
const markRaw: (typeof import("vue"))["markRaw"];
|
||||
const nextTick: (typeof import("vue"))["nextTick"];
|
||||
const onActivated: (typeof import("vue"))["onActivated"];
|
||||
const onBeforeMount: (typeof import("vue"))["onBeforeMount"];
|
||||
const onBeforeUnmount: (typeof import("vue"))["onBeforeUnmount"];
|
||||
const onBeforeUpdate: (typeof import("vue"))["onBeforeUpdate"];
|
||||
const onDeactivated: (typeof import("vue"))["onDeactivated"];
|
||||
const onErrorCaptured: (typeof import("vue"))["onErrorCaptured"];
|
||||
const onMounted: (typeof import("vue"))["onMounted"];
|
||||
const onRenderTracked: (typeof import("vue"))["onRenderTracked"];
|
||||
const onRenderTriggered: (typeof import("vue"))["onRenderTriggered"];
|
||||
const onScopeDispose: (typeof import("vue"))["onScopeDispose"];
|
||||
const onServerPrefetch: (typeof import("vue"))["onServerPrefetch"];
|
||||
const onUnmounted: (typeof import("vue"))["onUnmounted"];
|
||||
const onUpdated: (typeof import("vue"))["onUpdated"];
|
||||
const onWatcherCleanup: (typeof import("vue"))["onWatcherCleanup"];
|
||||
const provide: (typeof import("vue"))["provide"];
|
||||
const reactive: (typeof import("vue"))["reactive"];
|
||||
const readonly: (typeof import("vue"))["readonly"];
|
||||
const ref: (typeof import("vue"))["ref"];
|
||||
const resolveComponent: (typeof import("vue"))["resolveComponent"];
|
||||
const shallowReactive: (typeof import("vue"))["shallowReactive"];
|
||||
const shallowReadonly: (typeof import("vue"))["shallowReadonly"];
|
||||
const shallowRef: (typeof import("vue"))["shallowRef"];
|
||||
const toRaw: (typeof import("vue"))["toRaw"];
|
||||
const toRef: (typeof import("vue"))["toRef"];
|
||||
const toRefs: (typeof import("vue"))["toRefs"];
|
||||
const toValue: (typeof import("vue"))["toValue"];
|
||||
const triggerRef: (typeof import("vue"))["triggerRef"];
|
||||
const unref: (typeof import("vue"))["unref"];
|
||||
const useAttrs: (typeof import("vue"))["useAttrs"];
|
||||
const useCssModule: (typeof import("vue"))["useCssModule"];
|
||||
const useCssVars: (typeof import("vue"))["useCssVars"];
|
||||
const useId: (typeof import("vue"))["useId"];
|
||||
const useModel: (typeof import("vue"))["useModel"];
|
||||
const useSlots: (typeof import("vue"))["useSlots"];
|
||||
const useTemplateRef: (typeof import("vue"))["useTemplateRef"];
|
||||
const watch: (typeof import("vue"))["watch"];
|
||||
const watchEffect: (typeof import("vue"))["watchEffect"];
|
||||
const watchPostEffect: (typeof import("vue"))["watchPostEffect"];
|
||||
const watchSyncEffect: (typeof import("vue"))["watchSyncEffect"];
|
||||
}
|
||||
// for type re-export
|
||||
declare global {
|
||||
// @ts-ignore
|
||||
export type { Component, Slot, Slots, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
|
||||
import('vue')
|
||||
export type {
|
||||
Component,
|
||||
Slot,
|
||||
Slots,
|
||||
ComponentPublicInstance,
|
||||
ComputedRef,
|
||||
DirectiveBinding,
|
||||
ExtractDefaultPropTypes,
|
||||
ExtractPropTypes,
|
||||
ExtractPublicPropTypes,
|
||||
InjectionKey,
|
||||
PropType,
|
||||
Ref,
|
||||
MaybeRef,
|
||||
MaybeRefOrGetter,
|
||||
VNode,
|
||||
WritableComputedRef,
|
||||
} from "vue";
|
||||
import("vue");
|
||||
}
|
||||
|
||||
11
types/components.d.ts
vendored
11
types/components.d.ts
vendored
@ -3,21 +3,20 @@
|
||||
// Generated by unplugin-vue-components
|
||||
// Read more: https://github.com/vuejs/core/pull/3399
|
||||
// biome-ignore lint: disable
|
||||
export {}
|
||||
export {};
|
||||
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
ElAside: typeof import('element-plus/es')['ElAside']
|
||||
ElAvatar: typeof import('element-plus/es')['ElAvatar']
|
||||
ElContainer: typeof import('element-plus/es')['ElContainer']
|
||||
ElEmpty: typeof import('element-plus/es')['ElEmpty']
|
||||
ElHeader: typeof import('element-plus/es')['ElHeader']
|
||||
ElIcon: typeof import('element-plus/es')['ElIcon']
|
||||
ElInput: typeof import('element-plus/es')['ElInput']
|
||||
ElImage: typeof import('element-plus/es')['ElImage']
|
||||
ElMain: typeof import('element-plus/es')['ElMain']
|
||||
ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
|
||||
ElTabPane: typeof import('element-plus/es')['ElTabPane']
|
||||
ElTabs: typeof import('element-plus/es')['ElTabs']
|
||||
IconSelect: typeof import('./../src/components/IconSelect/index.vue')['default']
|
||||
Popover: typeof import('./../src/components/Popover/index.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
SvgIcon: typeof import('./../src/components/SvgIcon/index.vue')['default']
|
||||
|
||||
@ -36,5 +36,13 @@ export default defineConfig(({ mode, command }) => {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
},
|
||||
},
|
||||
css: {
|
||||
// css全局变量使用,@/styles/variable.scss文件
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
additionalData: '@use "@/styles/var.scss" as *;',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user