feat: 布局,样式统一,集成依赖

This commit is contained in:
何嘉悦 2025-05-19 06:58:57 +08:00
parent b5cddb0041
commit c163da8e8a
37 changed files with 1429 additions and 300 deletions

View File

@ -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
View 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

View File

@ -71,6 +71,8 @@
"watch": true,
"watchEffect": true,
"watchPostEffect": true,
"watchSyncEffect": true
"watchSyncEffect": true,
"ElMessage": true,
"ElMessageBox": true
}
}

View File

@ -1,9 +0,0 @@
/dist/*
.local
/node_modules/**
**/*.svg
**/*.sh
/public/*
stats.html

View File

@ -1,4 +0,0 @@
/dist/*
/public/*
public/*
stats.html

View File

@ -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"]
};

View File

@ -1,3 +1,17 @@
{
"recommendations": ["Vue.volar"]
//
"recommendations": [
// vue
"Vue.volar",
// EditorConfig scss
"EditorConfig.EditorConfig",
// unocss
"antfu.unocss",
// eslint
"dbaeumer.vscode-eslint"
]
}
//
//
// vscode

View File

@ -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"]
]
}

View File

@ -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

View File

@ -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",

View File

@ -4,4 +4,4 @@
<router-view />
</template>
<style scoped></style>
<style scoped lang="scss"></style>

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -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;

View 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/bottomYoffsetYXoffsetX
'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/rightXoffsetXYoffsetY
'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>

View File

@ -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;

View File

@ -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'];

View 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>

View File

@ -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`, ``);
}
};

View File

@ -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>

View File

@ -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>

View 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>

View File

@ -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>

View 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>

View 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>

View 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>

View File

@ -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>

View 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>

View File

@ -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 }}

View File

@ -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,
});

View File

@ -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
View 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);
}
}

View File

@ -1,3 +1,4 @@
@use './btn-style.scss';
@use 'reset-css';
@use './element-plus.scss';

9
src/styles/var.scss Normal file
View File

@ -0,0 +1,9 @@
:root {
/* 头部高度 */
--header-container-default-heigth: 56px;
/* 侧边栏背景色 */
--sidebar-background-color: #f3f4f6;
}

View File

@ -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
View File

@ -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']

View File

@ -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 *;',
},
},
},
};
});