mirror of
https://gitee.com/HuLaSpark/HuLa.git
synced 2024-12-01 19:28:07 +08:00
commit
2a873c1c6a
12
.env.dev
12
.env.dev
@ -1,12 +0,0 @@
|
||||
# 后端服务地址
|
||||
VITE_SERVICE_URL="http://localhost:9190"
|
||||
# websocket服务地址
|
||||
VITE_WEBSOCKET_URL="ws://localhost:8090"
|
||||
# 项目标题
|
||||
VITE_APP_TITLE="HuLa—IM"
|
||||
# 标签后缀
|
||||
VITE_TITLE_SUFFIX=" | HuLa"
|
||||
# 项目备案号
|
||||
VITE_APP_ICP="桂ICP备2021000000号"
|
||||
# 项目名称
|
||||
VITE_APP_NAME="HuLa-IM-Tauri"
|
@ -4,9 +4,7 @@ VITE_SERVICE_URL="http://127.0.0.1:9190"
|
||||
VITE_WEBSOCKET_URL="ws://127.0.0.1:8090"
|
||||
# 项目标题
|
||||
VITE_APP_TITLE="HuLa—IM"
|
||||
# 标签后缀
|
||||
VITE_TITLE_SUFFIX=" | HuLa"
|
||||
# 项目备案号
|
||||
VITE_APP_ICP="桂ICP备2021000000号"
|
||||
# 项目名称
|
||||
VITE_APP_NAME="HuLa-IM-Tauri"
|
||||
# gitee token
|
||||
VITE_GITEE_TOKEN="0312a213a6b6882beb96f487e75661a6"
|
18
.github/workflows/release.yml
vendored
18
.github/workflows/release.yml
vendored
@ -20,17 +20,16 @@ jobs:
|
||||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: 8
|
||||
run_install: true
|
||||
|
||||
# 安装 Node.js
|
||||
- name: Use Node.js
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'pnpm'
|
||||
node-version: '20'
|
||||
|
||||
# 安装 pnpm
|
||||
- name: Install pnpm
|
||||
run: npm install -g pnpm@9.2.0
|
||||
|
||||
# 安装 Rust
|
||||
- name: install Rust stable
|
||||
@ -38,6 +37,9 @@ jobs:
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build Vite + Tauri
|
||||
run: pnpm build
|
||||
|
||||
@ -50,4 +52,4 @@ jobs:
|
||||
releaseName: 'v__VERSION__'
|
||||
releaseBody: 'See the assets to download and install this version.'
|
||||
releaseDraft: true
|
||||
prerelease: false
|
||||
prerelease: false
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -22,3 +22,6 @@ dist-ssr
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
src-tauri/target
|
||||
.env.production
|
||||
|
133
CHANGELOG.md
133
CHANGELOG.md
@ -1,109 +1,32 @@
|
||||
## 1.5.0 (2024-04-21)
|
||||
# [1.6.0](https://github.com/nongyehong/HuLa-IM-Tauri/compare/v1.5.0...v1.6.0) (2024-07-03)
|
||||
|
||||
* :bug: fix(custom): 修复使用n-virtual-list组件未阻止滚动问题 ([cd9acf9](https://github.com/nongyehong/HuLa-IM-Tauri/commit/cd9acf9))
|
||||
* :bug: fix(setting): 修复独立窗口无法接收响应式数据 ([c4729d4](https://github.com/nongyehong/HuLa-IM-Tauri/commit/c4729d4))
|
||||
* :bug: fix(system): 修复艾特框滚动和选中问题 ([76d3c9f](https://github.com/nongyehong/HuLa-IM-Tauri/commit/76d3c9f))
|
||||
* :bug: fix(system): 修复回复功能的一些问题 ([e1eb827](https://github.com/nongyehong/HuLa-IM-Tauri/commit/e1eb827))
|
||||
* :bug: fix(system): 修复系统托盘图标切换问题 ([998ab14](https://github.com/nongyehong/HuLa-IM-Tauri/commit/998ab14))
|
||||
* :bug: fix(system): 修复已知问题 ([0b7878e](https://github.com/nongyehong/HuLa-IM-Tauri/commit/0b7878e))
|
||||
* :pencil2: docs(style): 变更提交文本内容 ([7ae0c4b](https://github.com/nongyehong/HuLa-IM-Tauri/commit/7ae0c4b))
|
||||
* :pencil2: docs(style): 更新md文档项目展示内容 ([afa4675](https://github.com/nongyehong/HuLa-IM-Tauri/commit/afa4675))
|
||||
* :sparkles: feat(custom): 新增表情功能 ([baae17e](https://github.com/nongyehong/HuLa-IM-Tauri/commit/baae17e))
|
||||
* :sparkles: feat(setting): 新增发送信息快捷键 ([eba6395](https://github.com/nongyehong/HuLa-IM-Tauri/commit/eba6395))
|
||||
* :sparkles: feat(style): 新增系统托盘无状态图标切换 ([0289f35](https://github.com/nongyehong/HuLa-IM-Tauri/commit/0289f35))
|
||||
* :sparkles: feat(system): 初步实现回复功能 ([e8164e0](https://github.com/nongyehong/HuLa-IM-Tauri/commit/e8164e0))
|
||||
* :tada: release(custom): 发布v1.5.0版本 ([93770c1](https://github.com/nongyehong/HuLa-IM-Tauri/commit/93770c1))
|
||||
* :zap: perf(setting): 优化系统托盘图标在不同窗口下显示不同功能 ([b10d5c0](https://github.com/nongyehong/HuLa-IM-Tauri/commit/b10d5c0))
|
||||
* :zap: perf(style): 提取复用hooks ([8aeaf76](https://github.com/nongyehong/HuLa-IM-Tauri/commit/8aeaf76))
|
||||
* :zap: perf(system): 优化艾特框功能 ([62d86fa](https://github.com/nongyehong/HuLa-IM-Tauri/commit/62d86fa))
|
||||
* :zap: perf(system): 优化回复功能的实现 ([c5109d7](https://github.com/nongyehong/HuLa-IM-Tauri/commit/c5109d7))
|
||||
* :zap: perf(system): 优化聊天框中常用功能 ([42d2453](https://github.com/nongyehong/HuLa-IM-Tauri/commit/42d2453))
|
||||
* :zap: perf(system): 优化输入框功能 ([844c251](https://github.com/nongyehong/HuLa-IM-Tauri/commit/844c251))
|
||||
|
||||
|
||||
|
||||
## 1.4.0 (2024-04-01)
|
||||
|
||||
* :bug: fix(system): 使用pinia-shared-state来解决标签页之间store的共享问题 ([54cc2a6](https://github.com/nongyehong/HuLa-IM-Tauri/commit/54cc2a6))
|
||||
* :bug: fix(system): 修复已知的问题 ([4b557e2](https://github.com/nongyehong/HuLa-IM-Tauri/commit/4b557e2))
|
||||
* :medal: version(system): . add LICENSE. ([a3f4318](https://github.com/nongyehong/HuLa-IM-Tauri/commit/a3f4318))
|
||||
* :medal: version(system): 发布1.4.0版本 ([665237d](https://github.com/nongyehong/HuLa-IM-Tauri/commit/665237d))
|
||||
* :sparkles: feat(custom): 新增自动登录功能 ([c2fb6d7](https://github.com/nongyehong/HuLa-IM-Tauri/commit/c2fb6d7))
|
||||
* :sparkles: feat(setting): 新增关闭窗口提示 ([183a2e7](https://github.com/nongyehong/HuLa-IM-Tauri/commit/183a2e7))
|
||||
* :sparkles: feat(style): 新增个人信息框 ([9fbb6f3](https://github.com/nongyehong/HuLa-IM-Tauri/commit/9fbb6f3))
|
||||
* :sparkles: feat(style): 新增用户信息框跟随图片背景颜色功能 ([2feb9c0](https://github.com/nongyehong/HuLa-IM-Tauri/commit/2feb9c0))
|
||||
* :sparkles: feat(system): 新增群聊侧边栏 ([4f81f50](https://github.com/nongyehong/HuLa-IM-Tauri/commit/4f81f50))
|
||||
* :sparkles: feat(system): 新增消息独立窗口功能 ([60939bf](https://github.com/nongyehong/HuLa-IM-Tauri/commit/60939bf))
|
||||
* :sparkles: feat(system): 新增在线状态 ([ed7be38](https://github.com/nongyehong/HuLa-IM-Tauri/commit/ed7be38))
|
||||
* :sparkles: feat(system): 新增ait功能 ([9a94c17](https://github.com/nongyehong/HuLa-IM-Tauri/commit/9a94c17))
|
||||
* :zap: perf(setting): 使用命名空间简化ts类型导入 ([bd96330](https://github.com/nongyehong/HuLa-IM-Tauri/commit/bd96330))
|
||||
* :zap: perf(setting): 优化配置 ([d139a36](https://github.com/nongyehong/HuLa-IM-Tauri/commit/d139a36))
|
||||
* :zap: perf(style): 完善主题的配置,优化信息列表功能 ([70fb55b](https://github.com/nongyehong/HuLa-IM-Tauri/commit/70fb55b))
|
||||
* :zap: perf(system): 优化艾特功能 ([e7cb15a](https://github.com/nongyehong/HuLa-IM-Tauri/commit/e7cb15a))
|
||||
* :zap: perf(system): 优化屏幕共享功能 ([5d6c9f3](https://github.com/nongyehong/HuLa-IM-Tauri/commit/5d6c9f3))
|
||||
* :zap: perf(system): 优化文件类型消息 ([2eae033](https://github.com/nongyehong/HuLa-IM-Tauri/commit/2eae033))
|
||||
* :zap: perf(system): 优化系统托盘图标闪烁 ([5614750](https://github.com/nongyehong/HuLa-IM-Tauri/commit/5614750))
|
||||
* :zap: perf(system): 优化系统托盘右键菜单 ([c1563ef](https://github.com/nongyehong/HuLa-IM-Tauri/commit/c1563ef))
|
||||
|
||||
|
||||
|
||||
## 1.3.0-beta (2024-03-12)
|
||||
|
||||
* :bug: fix(custom): 修改页面样式没有加载问题,修复打包后静态文件路径问题 ([63ce821](https://github.com/nongyehong/HuLa-IM-Tauri/commit/63ce821))
|
||||
* :pencil2: docs(style): 更新README文档 ([a4c1712](https://github.com/nongyehong/HuLa-IM-Tauri/commit/a4c1712))
|
||||
* :zap: perf(system): 使用原生event进行窗口通信 ([8adefce](https://github.com/nongyehong/HuLa-IM-Tauri/commit/8adefce))
|
||||
|
||||
|
||||
|
||||
## <small>1.2.9-alpha (2024-03-08)</small>
|
||||
|
||||
* :bug: fix(custom): 回退vite版本5.0.12导致启动不了端口服务的问题 ([ba99bb2](https://github.com/nongyehong/HuLa-IM-Tauri/commit/ba99bb2))
|
||||
* :bug: fix(custom): 修复跨标签页没有判断类型的问题 ([e4acbd5](https://github.com/nongyehong/HuLa-IM-Tauri/commit/e4acbd5))
|
||||
* :bug: fix(custom): 修复新建窗口的问题,修复窗口背景的问题 ([f1010e4](https://github.com/nongyehong/HuLa-IM-Tauri/commit/f1010e4))
|
||||
* :bug: fix(custom): 修复已知的问题 ([600e224](https://github.com/nongyehong/HuLa-IM-Tauri/commit/600e224))
|
||||
* :bug: fix(custom): 修复已知问题 ([648995e](https://github.com/nongyehong/HuLa-IM-Tauri/commit/648995e))
|
||||
* :bug: fix(custom): 修复因为配置的问题导致的代码格式化 ([ed4d825](https://github.com/nongyehong/HuLa-IM-Tauri/commit/ed4d825))
|
||||
* :lipstick: style(custom): 使用.editorconfig和.gitattributes来约束行分隔符和编辑器设置 ([72808d8](https://github.com/nongyehong/HuLa-IM-Tauri/commit/72808d8))
|
||||
* :new: version(custom): 发布v1.0.0-alpha版本 ([d2ac653](https://github.com/nongyehong/HuLa-IM-Tauri/commit/d2ac653))
|
||||
* :sparkles: feat(custom): 新增登录页面样式 ([8f0d27e](https://github.com/nongyehong/HuLa-IM-Tauri/commit/8f0d27e))
|
||||
* :sparkles: feat(custom): 新增动态页面 ([196d2d1](https://github.com/nongyehong/HuLa-IM-Tauri/commit/196d2d1))
|
||||
* :sparkles: feat(custom): 新增好友和群聊详情页 ([b11593d](https://github.com/nongyehong/HuLa-IM-Tauri/commit/b11593d))
|
||||
* :sparkles: feat(custom): 新增好友聊天页面设置侧边栏 ([6a1688b](https://github.com/nongyehong/HuLa-IM-Tauri/commit/6a1688b))
|
||||
* :sparkles: feat(custom): 新增护眼主题,隐藏窗口默认顶部菜单栏,优化rust打包配置 ([295bde5](https://github.com/nongyehong/HuLa-IM-Tauri/commit/295bde5))
|
||||
* :sparkles: feat(custom): 新增聊天页面的总体样式 ([5e21671](https://github.com/nongyehong/HuLa-IM-Tauri/commit/5e21671))
|
||||
* :sparkles: feat(custom): 新增聊天页面页脚显示功能 ([ebd7a41](https://github.com/nongyehong/HuLa-IM-Tauri/commit/ebd7a41))
|
||||
* :sparkles: feat(custom): 新增输入框功能 ([009c3b0](https://github.com/nongyehong/HuLa-IM-Tauri/commit/009c3b0))
|
||||
* :sparkles: feat(custom): 新增拖拽排序、消息移除功能 ([393a689](https://github.com/nongyehong/HuLa-IM-Tauri/commit/393a689))
|
||||
* :sparkles: feat(custom): 新增消息气泡功能(第一版) ([2f2bd45](https://github.com/nongyehong/HuLa-IM-Tauri/commit/2f2bd45))
|
||||
* :sparkles: feat(custom): 新增右键菜单功能,修复隐藏顶部菜单栏后无法拖拽的问题 ([1b6e719](https://github.com/nongyehong/HuLa-IM-Tauri/commit/1b6e719))
|
||||
* :sparkles: feat(custom): 新增元素平滑上升的vue指令 ([e973799](https://github.com/nongyehong/HuLa-IM-Tauri/commit/e973799))
|
||||
* :sparkles: feat(custom): 新增主题切换功能,修复一些已知的问题 ([b6f668f](https://github.com/nongyehong/HuLa-IM-Tauri/commit/b6f668f))
|
||||
* :sparkles: feat(custom): 新增unocss及其插件 ([2af6767](https://github.com/nongyehong/HuLa-IM-Tauri/commit/2af6767))
|
||||
* :sunflower: update(custom): 更新侧边栏功能显示 ([acc0194](https://github.com/nongyehong/HuLa-IM-Tauri/commit/acc0194))
|
||||
* :sunflower: update(custom): 更新登录界面样式 ([2394aa2](https://github.com/nongyehong/HuLa-IM-Tauri/commit/2394aa2))
|
||||
* :sunflower: update(custom): 更新登录页面的样式,新增二维码登录页面 ([3fbc6b0](https://github.com/nongyehong/HuLa-IM-Tauri/commit/3fbc6b0))
|
||||
* :sunflower: update(custom): 更新登录页面和多窗口的样式 ([be6bc2d](https://github.com/nongyehong/HuLa-IM-Tauri/commit/be6bc2d))
|
||||
* :sunflower: update(custom): 更新登录页面样式,更新主页的列表样式 ([ad69ada](https://github.com/nongyehong/HuLa-IM-Tauri/commit/ad69ada))
|
||||
* :sunflower: update(custom): 更新好友和群聊详情页面 ([16007a2](https://github.com/nongyehong/HuLa-IM-Tauri/commit/16007a2))
|
||||
* :sunflower: update(custom): 更新聊天框页面的图标样式 ([60bc859](https://github.com/nongyehong/HuLa-IM-Tauri/commit/60bc859))
|
||||
* :sunflower: update(custom): 更新气泡动画 ([e8dc441](https://github.com/nongyehong/HuLa-IM-Tauri/commit/e8dc441))
|
||||
* :sunflower: update(custom): 更新图标,新增系统托盘功能 ([3f1ea39](https://github.com/nongyehong/HuLa-IM-Tauri/commit/3f1ea39))
|
||||
* :sunflower: update(custom): 更新图标风格和主题颜色 ([db566cd](https://github.com/nongyehong/HuLa-IM-Tauri/commit/db566cd))
|
||||
* :sunflower: update(custom): 更新项目主要依赖版本 ([2a609ff](https://github.com/nongyehong/HuLa-IM-Tauri/commit/2a609ff))
|
||||
* :sunflower: update(custom): 更新悬浮按钮样式,修复已知的部分问题 ([1f01b7b](https://github.com/nongyehong/HuLa-IM-Tauri/commit/1f01b7b))
|
||||
* :sunflower: update(custom): 更新依赖版本 ([5c26e4e](https://github.com/nongyehong/HuLa-IM-Tauri/commit/5c26e4e))
|
||||
* :sunflower: update(custom): 更新用户列表样式 ([80ff121](https://github.com/nongyehong/HuLa-IM-Tauri/commit/80ff121))
|
||||
* :sunflower: update(custom): 更新右键菜单到信息和用户列表中 ([487767c](https://github.com/nongyehong/HuLa-IM-Tauri/commit/487767c))
|
||||
* :sunflower: update(custom): 更新右键菜单位置抉择 ([070d1e6](https://github.com/nongyehong/HuLa-IM-Tauri/commit/070d1e6))
|
||||
* :sunflower: update(custom): 更新右键菜单样式,新增聊天输入框右键菜单 ([4224cbd](https://github.com/nongyehong/HuLa-IM-Tauri/commit/4224cbd))
|
||||
* :sunflower: update(custom): 更新主页的收缩功能 ([6f58e78](https://github.com/nongyehong/HuLa-IM-Tauri/commit/6f58e78))
|
||||
* :sunflower: update(custom): 更新主页的样式 ([bf94200](https://github.com/nongyehong/HuLa-IM-Tauri/commit/bf94200))
|
||||
* :sunflower: update(custom): 更新主页的样式 ([6a39060](https://github.com/nongyehong/HuLa-IM-Tauri/commit/6a39060))
|
||||
* :sunflower: update(custom): 更新主页消息列表样式 ([910a6c4](https://github.com/nongyehong/HuLa-IM-Tauri/commit/910a6c4))
|
||||
* :sunflower: update(custom): 更新cz-customizable配置 ([43145e0](https://github.com/nongyehong/HuLa-IM-Tauri/commit/43145e0))
|
||||
* :sunflower: update(custom): 升级vue、vue-tsc、TS、vite版本 ([27e7bd0](https://github.com/nongyehong/HuLa-IM-Tauri/commit/27e7bd0))
|
||||
* init 初始化仓库 ([85a59e8](https://github.com/nongyehong/HuLa-IM-Tauri/commit/85a59e8))
|
||||
* init 初始化项目配置 ([0b48de8](https://github.com/nongyehong/HuLa-IM-Tauri/commit/0b48de8))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **components:** :bug: 修复回复功能缺陷 ([af50422](https://github.com/nongyehong/HuLa-IM-Tauri/commit/af5042261bc598a68b94db780a332ab38d5a577c))
|
||||
* **hook:** :bug: 修复回复功能不显示问题 ([9d0fee7](https://github.com/nongyehong/HuLa-IM-Tauri/commit/9d0fee7e5eb0919846d526b1f5a331d3a47f68d8))
|
||||
* **rust:** :bug: 修复mac系统背景玻璃拟态导致的问题 (#IA5AO8) ([89a7605](https://github.com/nongyehong/HuLa-IM-Tauri/commit/89a7605055d3ab7de83491e1745773458237d7d3)), closes [#IA5AO8](https://github.com/nongyehong/HuLa-IM-Tauri/issues/IA5AO8)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* :sparkles: 发布v1.6.0版本 ([71a1dd9](https://github.com/nongyehong/HuLa-IM-Tauri/commit/71a1dd93833d4c9534945f28fe636115ef59e862))
|
||||
* **component:** :sparkles: 新增GPT欢迎页面,完善设置页面 ([9b771e0](https://github.com/nongyehong/HuLa-IM-Tauri/commit/9b771e02ec31af1238f9662e839df6197f501376))
|
||||
* **component:** :sparkles: 新增GPT页面设置功能 ([4c85b4a](https://github.com/nongyehong/HuLa-IM-Tauri/commit/4c85b4afccdafe83aa0fcbd53e94ef5fc63a7a70))
|
||||
* **component:** :sparkles: 新增GPT组件 ([7260840](https://github.com/nongyehong/HuLa-IM-Tauri/commit/7260840f4b50bcbb4dad8645a84ade8280de4036))
|
||||
* **components:** :sparkles: 实现群聊回复表情功能 ([1fb3530](https://github.com/nongyehong/HuLa-IM-Tauri/commit/1fb3530cbdceef702430b272b99d3e99277c52d0))
|
||||
* **components:** :sparkles: 完善右键功能的显示资料 ([cf4820b](https://github.com/nongyehong/HuLa-IM-Tauri/commit/cf4820bffbdee50fc1e7b44c72b51cd2c4d80091))
|
||||
* **style:** :sparkles: 新增项目版本信息打印 ([e17cb7c](https://github.com/nongyehong/HuLa-IM-Tauri/commit/e17cb7c24a233417ab34a1de3b04cbdc32ebc2e0))
|
||||
* **view:** :sparkles: 新增GPT首页推荐功能样式 ([e927a95](https://github.com/nongyehong/HuLa-IM-Tauri/commit/e927a95fa4f95da7299459941b00d2f633217bca))
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **components:** :zap: 优化表情回应 ([94d2cb1](https://github.com/nongyehong/HuLa-IM-Tauri/commit/94d2cb1fec8db8901ffc85cdf8680919c58abf11))
|
||||
* **components:** :zap: 优化群聊回复表情功能 ([0c4615d](https://github.com/nongyehong/HuLa-IM-Tauri/commit/0c4615d4135fb3f740cb88f8f38502c9fc90bc5d))
|
||||
* **services:** :zap: 优化请求接口以及消息提示 ([0355f97](https://github.com/nongyehong/HuLa-IM-Tauri/commit/0355f976b854d96e613160d2bf6cc7e5605ea0ac))
|
||||
* **system:** :zap: 对接后端服务 ([ea4b82b](https://github.com/nongyehong/HuLa-IM-Tauri/commit/ea4b82be25a058a198716cebcf8becfcf252819c))
|
||||
* **views:** :zap: 优化页面收缩功能 ([31f7e17](https://github.com/nongyehong/HuLa-IM-Tauri/commit/31f7e1732cbe571e3f53564c57a339812b2c1a5b))
|
||||
|
||||
|
||||
|
||||
|
@ -49,6 +49,10 @@ HuLa is an instant messaging system developed with Tauri, Vite 5, Vue 3, and Typ
|
||||
|
||||
![img_4.png](preview/img_4.png)
|
||||
|
||||
![img_5.png](preview/img_5.png)
|
||||
|
||||
![img_6.png](preview/img_6.png)
|
||||
|
||||
HuLa adopts a modular architecture design, with the front end built using Vue 3 for the user interface, enhanced by TypeScript for better code readability and maintainability. On the backend, we use the Tauri framework for packaging and distributing the application, leveraging its native integration with the operating system to offer users more functionality and higher performance.
|
||||
|
||||
## Installation and Running
|
||||
@ -73,4 +77,4 @@ pnpm run tauri:build
|
||||
```
|
||||
|
||||
## SubmissionSpecification
|
||||
use **pnpm run commit** to invoke the _git commit_ interaction and follow the prompts to complete the input and selection of information
|
||||
use **pnpm run commit** to invoke the _git commit_ interaction and follow the prompts to complete the input and selection of information
|
||||
|
@ -49,6 +49,10 @@ HuLa 是一个基于 Tauri、Vite 5、Vue 3 和 TypeScript 构建的即时通讯
|
||||
|
||||
![img_4.png](preview/img_4.png)
|
||||
|
||||
![img_5.png](preview/img_5.png)
|
||||
|
||||
![img_6.png](preview/img_6.png)
|
||||
|
||||
## 安装与运行
|
||||
|
||||
```bash
|
||||
@ -71,4 +75,4 @@ pnpm run tauri:build
|
||||
```
|
||||
|
||||
## 提交规范
|
||||
执行 **pnpm run commit** 唤起 _git commit_ 交互,根据提示完成信息的输入和选择
|
||||
执行 **pnpm run commit** 唤起 _git commit_ 交互,根据提示完成信息的输入和选择
|
||||
|
@ -7,7 +7,7 @@
|
||||
<title>HuLa</title>
|
||||
|
||||
<!--引入iconpark图标库-->
|
||||
<script defer src="https://lf1-cdn-tos.bytegoofy.com/obj/iconpark/svg_30895_97.8e01ce86874358e3d6e87829c9ed23c6.js"></script>
|
||||
<script defer src="https://lf1-cdn-tos.bytegoofy.com/obj/iconpark/svg_30895_113.b759c39620e6fa93d145499865d57e99.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
54
package.json
54
package.json
@ -2,7 +2,7 @@
|
||||
"name": "hula-im-tauri",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"version": "v1.5.0",
|
||||
"version": "v1.6.0",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=18.12.0",
|
||||
@ -24,12 +24,12 @@
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite --mode dev",
|
||||
"prod": "vite --mode prod",
|
||||
"build": "vue-tsc --noEmit && vite build --mode pord",
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"preview": "vite preview",
|
||||
"tauri:dev": "tauri dev",
|
||||
"tauri:build": "tauri build",
|
||||
"tauri:build->debug": "tauri build --debug",
|
||||
"tauri:icon": "tauri icon public/logo.png",
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
"commit": "git add . && git-cz",
|
||||
@ -38,10 +38,10 @@
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^1.5.6",
|
||||
"axios": "^1.6.8",
|
||||
"@tauri-apps/api": "^1.6.0",
|
||||
"axios": "^1.7.2",
|
||||
"colorthief": "^2.4.0",
|
||||
"dayjs": "^1.11.10",
|
||||
"dayjs": "^1.11.11",
|
||||
"grapheme-splitter": "^1.0.4",
|
||||
"lodash-es": "^4.17.21",
|
||||
"mitt": "^3.0.1",
|
||||
@ -50,47 +50,47 @@
|
||||
"pinia-plugin-persistedstate": "^3.2.1",
|
||||
"pinia-shared-state": "^0.5.1",
|
||||
"vue": "^3.4.31",
|
||||
"vue-draggable-plus": "^0.4.0",
|
||||
"vue-router": "^4.3.0"
|
||||
"vue-draggable-plus": "^0.4.1",
|
||||
"vue-router": "^4.4.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/eslint-parser": "^7.23.3",
|
||||
"@commitlint/cli": "^19.2.2",
|
||||
"@babel/eslint-parser": "^7.24.7",
|
||||
"@commitlint/cli": "^19.3.0",
|
||||
"@commitlint/config-conventional": "^19.2.2",
|
||||
"@rollup/plugin-terser": "^0.4.4",
|
||||
"@tauri-apps/cli": "1.5.14",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/node": "^20.10.4",
|
||||
"@types/node": "^20.14.9",
|
||||
"@typescript-eslint/eslint-plugin": "7.1.0",
|
||||
"@typescript-eslint/parser": "^7.1.0",
|
||||
"@typescript-eslint/parser": "^7.15.0",
|
||||
"@unocss/preset-uno": "^0.61.0",
|
||||
"@unocss/reset": "^0.61.0",
|
||||
"@unocss/transformer-directives": "^0.61.0",
|
||||
"@unocss/transformer-variant-group": "^0.61.0",
|
||||
"@unocss/vite": "^0.61.0",
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"@vitejs/plugin-vue": "^5.0.5",
|
||||
"@vitejs/plugin-vue-jsx": "^4.0.0",
|
||||
"@vueuse/core": "^10.8.0",
|
||||
"@vueuse/core": "^10.11.0",
|
||||
"commitizen": "^4.3.0",
|
||||
"conventional-changelog": "^6.0.0",
|
||||
"conventional-changelog-cli": "^5.0.0",
|
||||
"cz-git": "^1.9.1",
|
||||
"eslint": "^8.56.0",
|
||||
"cz-git": "^1.9.3",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-import": "^2.29.1",
|
||||
"eslint-plugin-prettier": "^5.0.1",
|
||||
"eslint-plugin-vue": "^9.19.2",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-vue": "^9.27.0",
|
||||
"husky": "^9.0.11",
|
||||
"lint-staged": "^15.2.0",
|
||||
"oxlint": "^0.2.4",
|
||||
"prettier": "^3.2.5",
|
||||
"sass": "^1.71.1",
|
||||
"sass-loader": "^14.1.1",
|
||||
"typescript": "^5.4.5",
|
||||
"unplugin-auto-import": "^0.17.2",
|
||||
"lint-staged": "^15.2.7",
|
||||
"oxlint": "^0.2.18",
|
||||
"prettier": "^3.3.2",
|
||||
"sass": "^1.77.6",
|
||||
"sass-loader": "^14.2.1",
|
||||
"typescript": "^5.5.3",
|
||||
"unplugin-auto-import": "^0.17.6",
|
||||
"unplugin-vue-components": "^0.26.0",
|
||||
"vite": "5.3.2",
|
||||
"vue-tsc": "^2.0.6"
|
||||
"vue-tsc": "^2.0.24"
|
||||
},
|
||||
"config": {
|
||||
"commitizen": {
|
||||
|
2107
pnpm-lock.yaml
2107
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
BIN
preview/img_5.png
Normal file
BIN
preview/img_5.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 477 KiB |
BIN
preview/img_6.png
Normal file
BIN
preview/img_6.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 289 KiB |
2
src-tauri/Cargo.lock
generated
2
src-tauri/Cargo.lock
generated
@ -1543,7 +1543,7 @@ checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||
|
||||
[[package]]
|
||||
name = "hula"
|
||||
version = "1.5.0"
|
||||
version = "1.6.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "hula"
|
||||
version = "1.5.0"
|
||||
version = "1.6.0"
|
||||
description = "hula"
|
||||
authors = ["nongyehong"]
|
||||
license = ""
|
||||
|
@ -2,13 +2,13 @@
|
||||
"build": {
|
||||
"beforeDevCommand": "pnpm dev",
|
||||
"beforeBuildCommand": "pnpm build",
|
||||
"devPath": "http://localhost:6130",
|
||||
"devPath": "http://127.0.0.1:6130",
|
||||
"distDir": "../dist",
|
||||
"withGlobalTauri": true
|
||||
},
|
||||
"package": {
|
||||
"productName": "HuLa",
|
||||
"version": "1.5.0"
|
||||
"version": "1.6.0"
|
||||
},
|
||||
"tauri": {
|
||||
"updater": {
|
||||
|
23
src/App.vue
23
src/App.vue
@ -1,8 +1,11 @@
|
||||
<template>
|
||||
<NaiveProvider :message-max="3" :notific-max="3">
|
||||
<div id="app">
|
||||
<div v-if="!isLock" id="app">
|
||||
<router-view />
|
||||
</div>
|
||||
|
||||
<!-- 锁屏页面 -->
|
||||
<LockScreen v-else />
|
||||
</NaiveProvider>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
@ -10,10 +13,17 @@ import { setting } from '@/stores/setting.ts'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { StoresEnum, ThemeEnum } from '@/enums'
|
||||
import { onlineStatus } from '@/stores/onlineStatus.ts'
|
||||
import LockScreen from '@/views/LockScreen.vue'
|
||||
import router from '@/router'
|
||||
|
||||
const settingStore = setting()
|
||||
const OLStatusStore = onlineStatus()
|
||||
const { themes } = storeToRefs(settingStore)
|
||||
const { themes, lockScreen, page } = storeToRefs(settingStore)
|
||||
/** 不需要锁屏的页面 */
|
||||
const LockExclusion = new Set(['/login', '/tray', '/qrCode', '/about', '/onlineStatus'])
|
||||
const isLock = computed(() => {
|
||||
return !LockExclusion.has(router.currentRoute.value.path) && lockScreen.value.enable
|
||||
})
|
||||
|
||||
/** 禁止图片以及输入框的拖拽 */
|
||||
const preventDrag = (e: MouseEvent) => {
|
||||
@ -24,6 +34,15 @@ const preventDrag = (e: MouseEvent) => {
|
||||
}
|
||||
}
|
||||
|
||||
/** 控制阴影 */
|
||||
watch(
|
||||
() => page.value.shadow,
|
||||
(val) => {
|
||||
document.documentElement.style.setProperty('--shadow-enabled', val ? '0' : '1')
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
// initWebSocket()
|
||||
// /**! 使用msi或者其他安装包安装后才会显示应用的名字和图标 */
|
||||
|
1
src/assets/img/loading-bright.svg
Normal file
1
src/assets/img/loading-bright.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24"><circle cx="12" cy="2" r="0" fill="#ffffff"><animate attributeName="r" begin="0" calcMode="spline" dur="1s" keySplines="0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8" repeatCount="indefinite" values="0;2;0;0"/></circle><circle cx="12" cy="2" r="0" fill="#ffffff" transform="rotate(45 12 12)"><animate attributeName="r" begin="0.125s" calcMode="spline" dur="1s" keySplines="0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8" repeatCount="indefinite" values="0;2;0;0"/></circle><circle cx="12" cy="2" r="0" fill="#ffffff" transform="rotate(90 12 12)"><animate attributeName="r" begin="0.25s" calcMode="spline" dur="1s" keySplines="0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8" repeatCount="indefinite" values="0;2;0;0"/></circle><circle cx="12" cy="2" r="0" fill="#ffffff" transform="rotate(135 12 12)"><animate attributeName="r" begin="0.375s" calcMode="spline" dur="1s" keySplines="0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8" repeatCount="indefinite" values="0;2;0;0"/></circle><circle cx="12" cy="2" r="0" fill="#ffffff" transform="rotate(180 12 12)"><animate attributeName="r" begin="0.5s" calcMode="spline" dur="1s" keySplines="0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8" repeatCount="indefinite" values="0;2;0;0"/></circle><circle cx="12" cy="2" r="0" fill="#ffffff" transform="rotate(225 12 12)"><animate attributeName="r" begin="0.625s" calcMode="spline" dur="1s" keySplines="0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8" repeatCount="indefinite" values="0;2;0;0"/></circle><circle cx="12" cy="2" r="0" fill="#ffffff" transform="rotate(270 12 12)"><animate attributeName="r" begin="0.75s" calcMode="spline" dur="1s" keySplines="0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8" repeatCount="indefinite" values="0;2;0;0"/></circle><circle cx="12" cy="2" r="0" fill="#ffffff" transform="rotate(315 12 12)"><animate attributeName="r" begin="0.875s" calcMode="spline" dur="1s" keySplines="0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8;0.2 0.2 0.4 0.8" repeatCount="indefinite" values="0;2;0;0"/></circle></svg>
|
After Width: | Height: | Size: 2.0 KiB |
BIN
src/assets/img/lock_bg.jpg
Normal file
BIN
src/assets/img/lock_bg.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 219 KiB |
@ -84,7 +84,7 @@ import { CacheUserItem, MockItem } from '@/services/types.ts'
|
||||
import { emit, listen } from '@tauri-apps/api/event'
|
||||
import { setting } from '@/stores/setting.ts'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { sendOptions } from '@/views/home-window/more/settings/config.ts'
|
||||
import { sendOptions } from '@/views/homeWindow/more/settings/config.ts'
|
||||
import { useMsgInput } from '@/hooks/useMsgInput.ts'
|
||||
import { useCommon } from '@/hooks/useCommon.ts'
|
||||
import { onKeyStroke } from '@vueuse/core'
|
||||
|
@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<!-- 底部栏 -->
|
||||
<main
|
||||
class="size-full relative z-10 bg-[--right-bg-color] color-[--icon-color]"
|
||||
style="box-shadow: 0 -4px 4px var(--box-shadow-color)">
|
||||
style="box-shadow: var(--shadow-enabled) -4px 4px var(--box-shadow-color)"
|
||||
class="size-full relative z-10 bg-[--right-bg-color] border-t-(1px solid [--line-color]) color-[--icon-color]">
|
||||
<!-- 输入框顶部选项栏 -->
|
||||
<n-flex align="center" justify="space-between" class="p-[10px_22px_5px] select-none">
|
||||
<n-flex align="center" :size="0" class="input-options">
|
||||
|
@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<!-- 顶部操作栏和显示用户名 -->
|
||||
<main
|
||||
style="box-shadow: 0 4px 4px var(--box-shadow-color)"
|
||||
class="relative z-30 flex-y-center justify-between p-[8px_20px_12px] select-none">
|
||||
style="box-shadow: var(--shadow-enabled) 4px 4px var(--box-shadow-color)"
|
||||
class="relative z-30 flex-y-center border-b-(1px solid [--line-color]) justify-between p-[6px_20px_12px] select-none">
|
||||
<n-flex align="center">
|
||||
<span class="color-[--text-color]">{{ activeItem.name }}</span>
|
||||
<svg v-if="activeItem.hot_Flag === IsAllUserEnum.Yes" class="size-20px color-#13987f select-none outline-none">
|
||||
|
@ -15,7 +15,8 @@
|
||||
style="max-height: calc(100vh - 260px)"
|
||||
:class="{ 'right-1px': activeItem.type === RoomTypeEnum.SINGLE }"
|
||||
class="relative h-100vh"
|
||||
:ignore-item-resize="true"
|
||||
ignore-item-resize
|
||||
item-resizable
|
||||
:padding-top="10"
|
||||
:item-size="itemSize"
|
||||
:items="chatMessageList">
|
||||
@ -30,7 +31,9 @@
|
||||
{ 'active-reply': activeReply === item.message.id }
|
||||
]">
|
||||
<!-- 信息间隔时间 -->
|
||||
<span class="text-(12px #909090) select-none bg-[--time-color] p-4px rounded-6px" v-if="item.timeBlock">
|
||||
<span
|
||||
class="text-(12px #909090) select-none bg-[--time-color] p-4px rounded-6px custom-shadow"
|
||||
v-if="item.timeBlock">
|
||||
{{ item.timeBlock }}
|
||||
</span>
|
||||
<!-- 消息为撤回消息 -->
|
||||
@ -48,7 +51,7 @@
|
||||
</template>
|
||||
</div>
|
||||
<!-- 好友或者群聊的信息 -->
|
||||
<article
|
||||
<div
|
||||
v-else
|
||||
class="flex flex-col w-full"
|
||||
:class="[{ 'items-end': item.fromUser.uid === userUid }, chatStore.isGroup ? 'gap-18px' : 'gap-2px']">
|
||||
@ -236,7 +239,7 @@
|
||||
:size="6"
|
||||
v-if="item.message.body.reply"
|
||||
@click="jumpToReplyMsg(item.message.body.reply.id)"
|
||||
class="reply-bubble relative w-fit">
|
||||
class="reply-bubble relative w-fit custom-shadow">
|
||||
<svg class="size-14px"><use href="#to-top"></use></svg>
|
||||
<span>{{ `${item.message.body.reply.username}:` }}</span>
|
||||
<!-- 当回复消息为图片时渲染 -->
|
||||
@ -273,7 +276,7 @@
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</n-flex>
|
||||
</template>
|
||||
</n-virtual-list>
|
||||
|
@ -1,6 +1,21 @@
|
||||
<template>
|
||||
<!--! 这里最好不要使用n-flex,滚动高度会有问题 -->
|
||||
<main v-if="isGroup" style="border-left: 1px solid var(--line-color)" class="item-box shadow-md">
|
||||
<main
|
||||
v-if="isGroup"
|
||||
:class="
|
||||
isCollapsed ? 'w-180px border-l-(1px solid [--line-color]) p-[12px_0_12px_6px] custom-shadow' : 'w-0 pr-1px'
|
||||
"
|
||||
class="item-box">
|
||||
<!-- 收缩按钮 -->
|
||||
<div
|
||||
@click.stop="isCollapsed = !isCollapsed"
|
||||
style="border-radius: 18px 0 0 18px"
|
||||
class="contraction transition-all duration-600 ease-in-out absolute top-35% left--14px cursor-pointer opacity-0 bg-#c8c8c833 h-60px w-14px">
|
||||
<svg :class="isCollapsed ? 'rotate-180' : 'rotate-0'" class="size-16px color-#909090 absolute top-38%">
|
||||
<use href="#left-arrow"></use>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<n-flex v-if="!isSearch" align="center" justify="space-between" class="pr-8px pl-8px h-42px">
|
||||
<span class="text-14px">群聊成员 {{ userList.length }}</span>
|
||||
<svg @click="handleSearch" class="size-14px"><use href="#search"></use></svg>
|
||||
@ -103,6 +118,7 @@ const searchRef = ref('')
|
||||
/** 手动触发Popover显示 */
|
||||
const infoPopover = ref(false)
|
||||
const inputInstRef = ref<InputInst | null>(null)
|
||||
const isCollapsed = ref(true)
|
||||
const { optionsList, report, selectKey } = useChatMain()
|
||||
const { handlePopoverUpdate } = usePopover(selectKey, 'image-chat-sidebar')
|
||||
|
||||
|
@ -8,7 +8,7 @@
|
||||
<!-- 输入框和操作列表 -->
|
||||
<ChatFooter class="flex-1" />
|
||||
</n-flex>
|
||||
<ChatSidebar :active-item="activeItemRef" />
|
||||
<ChatSidebar />
|
||||
</n-flex>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
|
@ -43,33 +43,37 @@ export enum EventEnum {
|
||||
/** 独立窗口 */
|
||||
ALONE = 'alone',
|
||||
/** 共享屏幕 */
|
||||
SHARE_SCREEN = 'shareScreen'
|
||||
SHARE_SCREEN = 'shareScreen',
|
||||
/** 锁屏 */
|
||||
LOCK_SCREEN = 'lockScreen'
|
||||
}
|
||||
|
||||
/** Mitt兄弟组件通信 */
|
||||
export enum MittEnum {
|
||||
/** 更新消息数量 */
|
||||
UPDATE_MSG_TOTAL = 'updateMsgTotal',
|
||||
UPDATE_MSG_TOTAL,
|
||||
/** 显示消息框 */
|
||||
MSG_BOX_SHOW = 'msgBoxShow',
|
||||
MSG_BOX_SHOW,
|
||||
/** 发送消息 */
|
||||
SEND_MESSAGE = 'sendMessage',
|
||||
SEND_MESSAGE,
|
||||
/** 跳到发送信息 */
|
||||
TO_SEND_MSG = 'toSendMsg',
|
||||
TO_SEND_MSG,
|
||||
/** 缩小窗口 */
|
||||
SHRINK_WINDOW = 'shrinkWindow',
|
||||
SHRINK_WINDOW,
|
||||
/** 详情页面显示 */
|
||||
DETAILS_SHOW = 'detailsShow',
|
||||
DETAILS_SHOW,
|
||||
/** 消息列表被清空或者暂无消息 */
|
||||
NOT_MSG = 'notMsg',
|
||||
NOT_MSG,
|
||||
/** 回复消息 */
|
||||
REPLY_MEG = 'replyMeg',
|
||||
REPLY_MEG,
|
||||
/** 手动触发InfoPopover */
|
||||
INFO_POPOVER = 'infoPopover',
|
||||
INFO_POPOVER,
|
||||
/** 打开个人信息编辑窗口 */
|
||||
OPEN_EDIT_INFO = 'openEditInfo',
|
||||
OPEN_EDIT_INFO,
|
||||
/** 关闭个人信息浮窗 */
|
||||
CLOSE_INFO_SHOW = 'closeInfoShow'
|
||||
CLOSE_INFO_SHOW,
|
||||
/** 左边菜单弹窗 */
|
||||
LEFT_MODAL_SHOW
|
||||
}
|
||||
|
||||
/** 主题类型 */
|
||||
@ -228,3 +232,11 @@ export enum WsResEnum {
|
||||
/** ws连接错误 */
|
||||
WS_ERROR = 'wsError'
|
||||
}
|
||||
|
||||
/** 左边菜单弹出框类型 */
|
||||
export enum ModalEnum {
|
||||
/** 锁屏弹窗 */
|
||||
LOCK_SCREEN,
|
||||
/** 检查更新弹窗 */
|
||||
CHECK_UPDATE
|
||||
}
|
||||
|
@ -74,7 +74,11 @@ export const useChatMain = (activeItem?: SessionItem) => {
|
||||
label: '撤回',
|
||||
icon: 'corner-down-left',
|
||||
click: async (item: MessageType) => {
|
||||
await apis.recallMsg({ roomId: 1, msgId: item.message.id })
|
||||
const res = await apis.recallMsg({ roomId: 1, msgId: item.message.id })
|
||||
if (res.errMsg) {
|
||||
window.$message.error(res.errMsg)
|
||||
return
|
||||
}
|
||||
chatStore.updateRecallStatus({ msgId: item.message.id })
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,9 @@
|
||||
import { emit } from '@tauri-apps/api/event'
|
||||
import { invoke } from '@tauri-apps/api/tauri'
|
||||
import axios from 'axios'
|
||||
import { delay } from 'lodash-es'
|
||||
import { EventEnum } from '@/enums'
|
||||
import { useWindow } from '@/hooks/useWindow.ts'
|
||||
|
||||
export const useLogin = () => {
|
||||
/**
|
||||
@ -12,7 +16,32 @@ export const useLogin = () => {
|
||||
})
|
||||
}
|
||||
|
||||
/** 登出账号 */
|
||||
|
||||
const logout = async () => {
|
||||
const { createWebviewWindow } = useWindow()
|
||||
localStorage.removeItem('USER_INFO')
|
||||
localStorage.removeItem('TOKEN')
|
||||
// 清空axios请求头
|
||||
const instance = axios.create()
|
||||
instance.defaults.headers.common.Authorization = ''
|
||||
// todo 退出账号 需要关闭其他的全部窗口
|
||||
await createWebviewWindow('登录', 'login', 320, 448, 'home', true, false, 320, 448).then(() => {
|
||||
/** 给一点延迟,不然创建登录窗口后还没有来得及设置阴影和圆角效果 */
|
||||
delay(async () => {
|
||||
/** 如果图标在闪烁则先暂停闪烁 */
|
||||
await invoke('tray_blink', { isRun: false }).catch((error) => {
|
||||
console.error('暂停闪烁失败:', error)
|
||||
})
|
||||
/** 通知全部打开的窗口然后关闭 */
|
||||
await emit(EventEnum.LOGOUT)
|
||||
await emit('logout_success')
|
||||
}, 300)
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
setLoginState
|
||||
setLoginState,
|
||||
logout
|
||||
}
|
||||
}
|
||||
|
@ -24,8 +24,8 @@ export const useMsgInput = (messageInputDom: Ref) => {
|
||||
const chatKey = ref(chat.value.sendKey)
|
||||
const msgInput = ref('')
|
||||
const ait = ref(false)
|
||||
/** 临时消息id */
|
||||
const tempMessageId = ref(0)
|
||||
// /** 临时消息id */
|
||||
// const tempMessageId = ref(0)
|
||||
/** 艾特后的关键字的key */
|
||||
const aitKey = ref('')
|
||||
/** 是否正在输入拼音 */
|
||||
@ -184,8 +184,8 @@ export const useMsgInput = (messageInputDom: Ref) => {
|
||||
})
|
||||
}
|
||||
// 判断文本信息是否超过限制
|
||||
if (msg.type === MsgEnum.TEXT && msg.content.length > 2000) {
|
||||
window.$message.info('消息内容超过限制2000,请删减内容')
|
||||
if (msg.type === MsgEnum.TEXT && msg.content.length > 500) {
|
||||
window.$message.info('消息内容超过限制500,请分段发送')
|
||||
return
|
||||
}
|
||||
// TODO 当输入的类型是混合类型如输入文本加上图片的类型需要处理 (nyh -> 2024-02-28 06:32:13)
|
||||
@ -199,16 +199,13 @@ export const useMsgInput = (messageInputDom: Ref) => {
|
||||
msgType: msg.type,
|
||||
body: { content: msg.content, replyMsgId: msg.reply !== 0 ? msg.reply : undefined }
|
||||
})
|
||||
.then((res) => {
|
||||
.then(async (res) => {
|
||||
if (res.data.message.type === MsgEnum.TEXT) {
|
||||
chatStore.pushMsg(res.data)
|
||||
// 发完消息就要刷新会话列表,
|
||||
// FIXME 如果当前会话已经置顶了,可以不用刷新
|
||||
chatStore.updateSessionLastActiveTime(globalStore.currentSession.roomId)
|
||||
} else {
|
||||
// 更新上传状态下的消息
|
||||
chatStore.updateMsg(tempMessageId.value, res.data)
|
||||
await chatStore.pushMsg(res.data)
|
||||
}
|
||||
// 发完消息就要刷新会话列表,
|
||||
// FIXME 如果当前会话已经置顶了,可以不用刷新
|
||||
chatStore.updateSessionLastActiveTime(globalStore.currentSession.roomId)
|
||||
})
|
||||
msgInput.value = ''
|
||||
messageInputDom.value.innerHTML = ''
|
||||
|
220
src/hooks/useUpload.ts
Normal file
220
src/hooks/useUpload.ts
Normal file
@ -0,0 +1,220 @@
|
||||
import { ref } from 'vue'
|
||||
import { createEventHook } from '@vueuse/core'
|
||||
import apis from '@/services/apis'
|
||||
|
||||
/** 文件信息类型 */
|
||||
export type FileInfoType = {
|
||||
name: string
|
||||
type: string
|
||||
size: number
|
||||
suffix: string
|
||||
width?: number
|
||||
height?: number
|
||||
downloadUrl?: string
|
||||
second?: number
|
||||
thumbWidth?: number
|
||||
thumbHeight?: number
|
||||
thumbUrl?: string
|
||||
}
|
||||
|
||||
const Max = 100 // 单位M
|
||||
const MAX_FILE_SIZE = Max * 1024 * 1024 // 最大上传限制
|
||||
|
||||
/**
|
||||
* 文件上传Hook
|
||||
*/
|
||||
export const useUpload = () => {
|
||||
const isUploading = ref(false) // 是否正在上传
|
||||
const progress = ref(0) // 进度
|
||||
const fileInfo = ref<FileInfoType | null>(null) // 文件信息
|
||||
|
||||
const { on: onChange, trigger } = createEventHook()
|
||||
const onStart = createEventHook()
|
||||
|
||||
/**
|
||||
* 上传文件
|
||||
* @param url 上传链接
|
||||
* @param file 文件
|
||||
* @param inner 是否内部调用
|
||||
*/
|
||||
const upload = async (url: string, file: File, inner?: boolean) => {
|
||||
isUploading.value = true
|
||||
|
||||
const xhr = new XMLHttpRequest()
|
||||
xhr.open('PUT', url, true)
|
||||
xhr.setRequestHeader('Content-Type', file.type)
|
||||
xhr.upload.onprogress = function (e) {
|
||||
if (!inner) {
|
||||
progress.value = Math.round((e.loaded / e.total) * 100)
|
||||
}
|
||||
}
|
||||
xhr.onload = function () {
|
||||
isUploading.value = false
|
||||
if (inner) return
|
||||
if (xhr.status === 200) {
|
||||
trigger('success')
|
||||
} else {
|
||||
trigger('fail')
|
||||
}
|
||||
}
|
||||
xhr.send(file)
|
||||
}
|
||||
/**
|
||||
* 获取视频第一帧
|
||||
*/
|
||||
const getVideoCover = (file: File) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const video = document.createElement('video')
|
||||
const tempUrl = URL.createObjectURL(file)
|
||||
video.src = tempUrl
|
||||
video.crossOrigin = 'anonymous' // 视频跨域
|
||||
video.currentTime = 2 // 第2帧
|
||||
video.oncanplay = () => {
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = video.videoWidth
|
||||
canvas.height = video.videoHeight
|
||||
canvas.getContext('2d')?.drawImage(video, 0, 0, canvas.width, canvas.height)
|
||||
|
||||
// 将canvas转为图片file
|
||||
canvas.toBlob((blob) => {
|
||||
if (!blob) return
|
||||
// 时间戳生成唯一文件名
|
||||
const name = Date.now() + 'thumb.jpg'
|
||||
const thumbFile = new File([blob], name, { type: 'image/jpeg' })
|
||||
// 转成File对象 并上传
|
||||
apis.getUploadUrl({ fileName: name, scene: '1' }).then(async (res) => {
|
||||
if (res.data.uploadUrl && res.data.downloadUrl) {
|
||||
await upload(res.data.uploadUrl, thumbFile, true)
|
||||
// 等待上传完成
|
||||
const timer = setInterval(() => {
|
||||
if (!isUploading.value) {
|
||||
clearInterval(timer)
|
||||
resolve({
|
||||
thumbWidth: canvas.width,
|
||||
thumbHeight: canvas.height,
|
||||
thumbUrl: res.data.downloadUrl,
|
||||
thumbSize: thumbFile.size,
|
||||
tempUrl
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
video.onerror = function () {
|
||||
URL.revokeObjectURL(tempUrl) // 释放临时URL资源
|
||||
reject({ width: 0, height: 0, url: null })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取图片宽高
|
||||
*/
|
||||
const getImgWH = (file: File) => {
|
||||
const img = new Image()
|
||||
const tempUrl = URL.createObjectURL(file)
|
||||
img.src = tempUrl
|
||||
return new Promise((resolve, reject) => {
|
||||
img.onload = function () {
|
||||
resolve({ width: img.width, height: img.height, tempUrl })
|
||||
}
|
||||
img.onerror = function () {
|
||||
URL.revokeObjectURL(tempUrl) // 释放临时URL资源
|
||||
reject({ width: 0, height: 0, url: null })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取音频时长
|
||||
*/
|
||||
const getAudioDuration = (file: File) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const audio = new Audio()
|
||||
const tempUrl = URL.createObjectURL(file)
|
||||
audio.src = tempUrl
|
||||
// 计算音频的时长
|
||||
const countAudioTime = async () => {
|
||||
while (isNaN(audio.duration) || audio.duration === Infinity) {
|
||||
// 防止浏览器卡死
|
||||
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||
// 随机进度条位置
|
||||
audio.currentTime = 100000 * Math.random()
|
||||
}
|
||||
// 取整
|
||||
const second = Math.round(audio.duration || 0)
|
||||
resolve({ second, tempUrl })
|
||||
}
|
||||
countAudioTime()
|
||||
audio.onerror = function () {
|
||||
reject({ second: 0, tempUrl })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析文件
|
||||
* @param file 文件
|
||||
* @param addParams 参数
|
||||
* @returns 文件大小、文件类型、文件名、文件后缀...
|
||||
*/
|
||||
const parseFile = async (file: File, addParams: Record<string, any> = {}) => {
|
||||
const { name, size, type } = file
|
||||
const suffix = name.split('.').pop()?.trim().toLowerCase() || ''
|
||||
const baseInfo = { name, size, type, suffix, ...addParams }
|
||||
|
||||
if (type.includes('image')) {
|
||||
const { width, height, tempUrl } = (await getImgWH(file)) as any
|
||||
return { ...baseInfo, width, height, tempUrl }
|
||||
}
|
||||
|
||||
if (type.includes('audio')) {
|
||||
const { second, tempUrl } = (await getAudioDuration(file)) as any
|
||||
return { second, tempUrl, ...baseInfo }
|
||||
}
|
||||
// 如果是视频
|
||||
if (type.includes('video')) {
|
||||
const { thumbWidth, thumbHeight, tempUrl, thumbTempUrl, thumbUrl, thumbSize } = (await getVideoCover(file)) as any
|
||||
return { ...baseInfo, thumbWidth, thumbHeight, tempUrl, thumbTempUrl, thumbUrl, thumbSize }
|
||||
}
|
||||
|
||||
return baseInfo
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传文件
|
||||
* @param file 文件
|
||||
* @param addParams 额外参数
|
||||
*/
|
||||
const uploadFile = async (file: File, addParams?: Record<string, any>) => {
|
||||
if (isUploading.value || !file) return
|
||||
const info = await parseFile(file, addParams)
|
||||
|
||||
// 限制文件大小
|
||||
if (info.size > MAX_FILE_SIZE) {
|
||||
window.$message.error(`文件大小不能超过 ${Max}MB`)
|
||||
return
|
||||
}
|
||||
|
||||
const { downloadUrl, uploadUrl } = (await apis.getUploadUrl({ fileName: info.name, scene: '1' })).data
|
||||
|
||||
if (uploadUrl && downloadUrl) {
|
||||
fileInfo.value = { ...info, downloadUrl }
|
||||
await onStart.trigger(fileInfo)
|
||||
await upload(uploadUrl, file)
|
||||
} else {
|
||||
await trigger('fail')
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
fileInfo,
|
||||
isUploading,
|
||||
progress,
|
||||
onStart: onStart.on,
|
||||
onChange,
|
||||
uploadFile
|
||||
}
|
||||
}
|
@ -2,6 +2,10 @@ import { WebviewWindow } from '@tauri-apps/api/window'
|
||||
import { EventEnum } from '@/enums'
|
||||
import { emit } from '@tauri-apps/api/event'
|
||||
|
||||
/**
|
||||
* 监听窗口状态变化
|
||||
* @param label 窗口标签
|
||||
*/
|
||||
export const useWindowState = (label: string) => {
|
||||
const win = WebviewWindow.getByLabel(label)
|
||||
|
||||
|
@ -1,28 +1,35 @@
|
||||
<template>
|
||||
<main
|
||||
data-tauri-drag-region
|
||||
id="center"
|
||||
class="resizable select-none flex flex-col shadow-inner"
|
||||
class="resizable select-none flex flex-col border-r-(1px solid [--line-color])"
|
||||
:style="{ width: `${initWidth}px` }">
|
||||
<div class="resize-handle" @mousedown="initDrag"></div>
|
||||
<!-- 分隔条 -->
|
||||
<div v-if="!shrinkStatus" class="resize-handle transition-all duration-600 ease-in-out" @mousedown="initDrag">
|
||||
<div :class="{ 'opacity-100': isDragging }" class="transition-all duration-600 ease-in-out opacity-0 drag-icon">
|
||||
<div style="border-radius: 8px 0 0 8px" class="bg-#c8c8c833 h-60px w-14px absolute top-40% right-0 drag-icon">
|
||||
<svg class="size-16px absolute top-1/2 right--2px transform -translate-y-1/2 color-#909090">
|
||||
<use href="#sliding"></use>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ActionBar
|
||||
class="absolute right-0"
|
||||
class="absolute right-0 w-full"
|
||||
v-if="shrinkStatus"
|
||||
:shrink-status="!shrinkStatus"
|
||||
:max-w="false"
|
||||
:current-label="appWindow.label" />
|
||||
|
||||
<!-- <div class="resize-handle" @mousedown="initDrag"></div>-->
|
||||
|
||||
<!-- 顶部搜索栏 -->
|
||||
<header
|
||||
style="box-shadow: 0 2px 4px var(--box-shadow-color)"
|
||||
class="mt-30px w-full h-38px flex flex-col items-center">
|
||||
style="box-shadow: var(--shadow-enabled) 4px 4px var(--box-shadow-color)"
|
||||
class="mt-30px w-full h-40px flex flex-col items-center border-b-(1px solid [--line-color])">
|
||||
<div class="flex-center gap-5px w-full pr-16px pl-16px box-border">
|
||||
<n-input
|
||||
id="search"
|
||||
@focus="() => router.push('/searchDetails')"
|
||||
class="rounded-6px w-full"
|
||||
class="rounded-6px w-full relative"
|
||||
style="background: var(--search-bg-color)"
|
||||
:maxlength="20"
|
||||
clearable
|
||||
@ -32,16 +39,28 @@
|
||||
<svg class="w-12px h-12px"><use href="#search"></use></svg>
|
||||
</template>
|
||||
</n-input>
|
||||
<n-button size="small" secondary style="padding: 0 5px">
|
||||
<n-button @click="addPanels.show = !addPanels.show" size="small" secondary style="padding: 0 5px">
|
||||
<template #icon>
|
||||
<svg class="w-24px h-24px"><use href="#plus"></use></svg>
|
||||
</template>
|
||||
</n-button>
|
||||
|
||||
<!-- 添加面板 -->
|
||||
<div v-if="addPanels.show" class="add-item">
|
||||
<div class="menu-list">
|
||||
<div v-for="(item, index) in addPanels.list" :key="index">
|
||||
<div class="menu-item" @click="() => item.click()">
|
||||
<svg><use :href="`#${item.icon}`"></use></svg>
|
||||
{{ item.label }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 列表 -->
|
||||
<div id="centerList">
|
||||
<div id="centerList" class="h-full">
|
||||
<router-view />
|
||||
</div>
|
||||
</main>
|
||||
@ -66,10 +85,31 @@ const { width } = useWindowSize()
|
||||
const isDrag = ref(true)
|
||||
/** 当前消息 */
|
||||
const currentMsg = ref()
|
||||
/** 添加面板是否显示 */
|
||||
const addPanels = ref({
|
||||
show: false,
|
||||
list: [
|
||||
{
|
||||
label: '发起群聊',
|
||||
icon: 'launch',
|
||||
click: () => {
|
||||
console.log('发起群聊')
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '加好友/群',
|
||||
icon: 'people-plus',
|
||||
click: () => {
|
||||
console.log('加好友/群')
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const startX = ref()
|
||||
const startWidth = ref()
|
||||
const shrinkStatus = ref(false)
|
||||
const isDragging = ref(false)
|
||||
|
||||
watchEffect(() => {
|
||||
if (width.value >= 310 && width.value < 800) {
|
||||
@ -96,6 +136,9 @@ const closeMenu = (event: Event) => {
|
||||
if (!e.matches('#search, #search *, #centerList *, #centerList') && route === '/searchDetails') {
|
||||
router.go(-1)
|
||||
}
|
||||
if (!e.matches('.add-item')) {
|
||||
addPanels.value.show = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 定义一个函数,在鼠标拖动时调用 */
|
||||
@ -120,6 +163,7 @@ const initDrag = (e: MouseEvent) => {
|
||||
if (!isDrag.value) return
|
||||
startX.value = e.clientX
|
||||
startWidth.value = initWidth.value
|
||||
isDragging.value = true
|
||||
document.addEventListener('mousemove', doDrag, false)
|
||||
document.addEventListener('mouseup', stopDrag, false)
|
||||
}
|
||||
@ -127,6 +171,12 @@ const initDrag = (e: MouseEvent) => {
|
||||
const stopDrag = () => {
|
||||
document.removeEventListener('mousemove', doDrag, false)
|
||||
document.removeEventListener('mouseup', stopDrag, false)
|
||||
isDragging.value = false
|
||||
setTimeout(() => {
|
||||
// 移除 hover 样式
|
||||
const resizeHandle = document.querySelector('.resize-handle') as HTMLElement
|
||||
resizeHandle.classList.remove('hover')
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
|
@ -10,8 +10,20 @@
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 1px;
|
||||
width: 6px;
|
||||
cursor: ew-resize;
|
||||
z-index: 9999;
|
||||
background-color: var(--split-color);
|
||||
}
|
||||
background: transparent;
|
||||
&:hover {
|
||||
.drag-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.add-item {
|
||||
@include menu-item-style(absolute);
|
||||
top: 60px;
|
||||
right: 30px;
|
||||
@include menu-list();
|
||||
}
|
||||
|
@ -25,7 +25,7 @@
|
||||
<!-- 该选项有提示时展示 -->
|
||||
<n-popover style="padding: 12px" v-else-if="item.tip" trigger="manual" v-model:show="tipShow" placement="right">
|
||||
<template #trigger>
|
||||
<n-badge :max="99" :value="item.badge" :dot="dotShow">
|
||||
<n-badge :max="99" :value="item.badge" dot :show="dotShow">
|
||||
<svg class="size-22px" @click="handleTipShow">
|
||||
<use
|
||||
:href="`#${activeUrl === item.url || openWindowsList.has(item.url) ? item.iconAction : item.icon}`"></use>
|
||||
@ -112,7 +112,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { itemsBottom, itemsTop, moreList } from '../config.ts'
|
||||
import { itemsBottom, itemsTop, moreList } from '../config.tsx'
|
||||
import { leftHook } from '../hook.ts'
|
||||
|
||||
const dotShow = ref(false)
|
||||
|
@ -1,11 +1,10 @@
|
||||
import { useWindow } from '@/hooks/useWindow.ts'
|
||||
import { emit } from '@tauri-apps/api/event'
|
||||
import { EventEnum } from '@/enums'
|
||||
import { delay } from 'lodash-es'
|
||||
import { invoke } from '@tauri-apps/api/tauri'
|
||||
import axios from 'axios'
|
||||
import { MittEnum, ModalEnum } from '@/enums'
|
||||
import Mitt from '@/utils/Bus.ts'
|
||||
import { useLogin } from '@/hooks/useLogin.ts'
|
||||
|
||||
const { createWebviewWindow } = useWindow()
|
||||
const { logout } = useLogin()
|
||||
/**
|
||||
* 上半部分操作栏配置
|
||||
* @param url 路由地址
|
||||
@ -109,8 +108,14 @@ const moreList = ref<OPT.L.MoreList[]>([
|
||||
label: '检查更新',
|
||||
icon: 'arrow-circle-up',
|
||||
click: () => {
|
||||
// todo 检查更新
|
||||
console.log('检查更新')
|
||||
Mitt.emit(MittEnum.LEFT_MODAL_SHOW, ModalEnum.CHECK_UPDATE)
|
||||
}
|
||||
},
|
||||
{
|
||||
label: '锁定屏幕',
|
||||
icon: 'lock',
|
||||
click: () => {
|
||||
Mitt.emit(MittEnum.LEFT_MODAL_SHOW, ModalEnum.LOCK_SCREEN)
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -133,24 +138,7 @@ const moreList = ref<OPT.L.MoreList[]>([
|
||||
label: '退出账号',
|
||||
icon: 'power',
|
||||
click: async () => {
|
||||
localStorage.removeItem('USER_INFO')
|
||||
localStorage.removeItem('TOKEN')
|
||||
// 清空axios请求头
|
||||
const instance = axios.create()
|
||||
instance.defaults.headers.common.Authorization = ''
|
||||
// todo 退出账号 需要关闭其他的全部窗口
|
||||
await createWebviewWindow('登录', 'login', 320, 448, 'home', true, false, 320, 448).then(() => {
|
||||
/** 给一点延迟,不然创建登录窗口后还没有来得及设置阴影和圆角效果 */
|
||||
delay(async () => {
|
||||
/** 如果图标在闪烁则先暂停闪烁 */
|
||||
await invoke('tray_blink', { isRun: false }).catch((error) => {
|
||||
console.error('暂停闪烁失败:', error)
|
||||
})
|
||||
/** 通知全部打开的窗口然后关闭 */
|
||||
await emit(EventEnum.LOGOUT)
|
||||
await emit('logout_success')
|
||||
}, 300)
|
||||
})
|
||||
await logout()
|
||||
}
|
||||
}
|
||||
])
|
@ -4,7 +4,7 @@ import { useUserStore } from '@/stores/user.ts'
|
||||
import { useCachedStore } from '@/stores/cached.ts'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { onlineStatus } from '@/stores/onlineStatus.ts'
|
||||
import { itemsTop } from '@/layout/left/config.ts'
|
||||
import { itemsTop } from '@/layout/left/config.tsx'
|
||||
import { EventEnum, IsYetEnum, MittEnum, MsgEnum, ThemeEnum } from '@/enums'
|
||||
import { BadgeType, UserInfoType } from '@/services/types.ts'
|
||||
import { useChatStore } from '@/stores/chat.ts'
|
||||
@ -82,6 +82,8 @@ export const leftHook = () => {
|
||||
return sessionList.value.reduce((total, item) => total + item.unreadCount, 0)
|
||||
})
|
||||
|
||||
/* =================================== 方法 =============================================== */
|
||||
|
||||
/** 跟随系统主题模式切换主题 */
|
||||
const followOS = () => {
|
||||
themeColor.value = prefers.matches ? 'rgba(63,63,63, 0.2)' : 'rgba(241,241,241, 0.2)'
|
||||
@ -134,10 +136,17 @@ export const leftHook = () => {
|
||||
})
|
||||
}
|
||||
|
||||
/** 佩戴卸下徽章 */
|
||||
/** 佩戴徽章 */
|
||||
const toggleWarningBadge = async (badge: BadgeType) => {
|
||||
if (!badge?.id) return
|
||||
await apis.setUserBadge(badge.id)
|
||||
const res = await apis.setUserBadge(badge.id)
|
||||
if (res.success) {
|
||||
window.$message.success('佩戴成功')
|
||||
/** 获取用户信息 */
|
||||
apis.getUserInfo().then((res) => {
|
||||
editInfo.value.content = res.data
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/** 计算字符长度 */
|
||||
|
@ -6,12 +6,35 @@
|
||||
<ActionList />
|
||||
<!-- 编辑资料弹窗 -->
|
||||
<InfoPopover />
|
||||
|
||||
<!-- 弹出框 -->
|
||||
<component :is="componentMap" />
|
||||
</main>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
<script lang="tsx" setup>
|
||||
import LeftAvatar from './components/LeftAvatar.vue'
|
||||
import ActionList from './components/ActionList.vue'
|
||||
import InfoPopover from './components/InfoPopover.vue'
|
||||
import Mitt from '@/utils/Bus.ts'
|
||||
import { lock, LockScreen, CheckUpdate } from './model.tsx'
|
||||
import { DefineComponent, DefineSetupFnComponent } from 'vue'
|
||||
import { MittEnum, ModalEnum } from '@/enums'
|
||||
|
||||
const componentMap = shallowRef<DefineComponent>()
|
||||
/** 弹窗组件内容映射 */
|
||||
const componentMapping: Record<number, DefineSetupFnComponent<DefineComponent>> = {
|
||||
[ModalEnum.LOCK_SCREEN]: LockScreen,
|
||||
[ModalEnum.CHECK_UPDATE]: CheckUpdate
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
Mitt.on(MittEnum.LEFT_MODAL_SHOW, (event) => {
|
||||
componentMap.value = componentMapping[event as ModalEnum] as DefineComponent
|
||||
nextTick(() => {
|
||||
lock.value.modalShow = true
|
||||
})
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
248
src/layout/left/model.tsx
Normal file
248
src/layout/left/model.tsx
Normal file
@ -0,0 +1,248 @@
|
||||
import {
|
||||
NAvatar,
|
||||
NButton,
|
||||
NFlex,
|
||||
NFormItem,
|
||||
NInput,
|
||||
NModal,
|
||||
NForm,
|
||||
NTimelineItem,
|
||||
NTimeline,
|
||||
NScrollbar,
|
||||
NSkeleton,
|
||||
NIcon
|
||||
} from 'naive-ui'
|
||||
import { FormInst } from 'naive-ui'
|
||||
import { setting } from '@/stores/setting.ts'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { emit } from '@tauri-apps/api/event'
|
||||
import { EventEnum } from '@/enums'
|
||||
import pkg from '~/package.json'
|
||||
import { handRelativeTime } from '@/utils/Day.ts'
|
||||
|
||||
const formRef = ref<FormInst | null>()
|
||||
const formValue = ref({
|
||||
lockPassword: ''
|
||||
})
|
||||
export const lock = ref({
|
||||
modalShow: false,
|
||||
loading: false,
|
||||
rules: {
|
||||
lockPassword: {
|
||||
required: true,
|
||||
message: '请输入锁屏密码',
|
||||
trigger: ['input']
|
||||
}
|
||||
},
|
||||
async handleLock() {
|
||||
const settingStore = setting()
|
||||
const { lockScreen } = storeToRefs(settingStore)
|
||||
formRef.value?.validate((errors) => {
|
||||
if (errors) return
|
||||
lock.value.loading = true
|
||||
lockScreen.value.password = formValue.value.lockPassword
|
||||
lockScreen.value.enable = true
|
||||
setTimeout(async () => {
|
||||
/** 发送锁屏事件,当打开的窗口接受到后会自动锁屏 */
|
||||
await emit(EventEnum.LOCK_SCREEN)
|
||||
lock.value.loading = false
|
||||
lock.value.modalShow = false
|
||||
formValue.value.lockPassword = ''
|
||||
}, 1000)
|
||||
})
|
||||
formRef.value?.restoreValidation()
|
||||
}
|
||||
})
|
||||
|
||||
/*============================================ model =====================================================*/
|
||||
/** 锁屏弹窗 */
|
||||
export const LockScreen = defineComponent(() => {
|
||||
const settingStore = setting()
|
||||
const { login } = storeToRefs(settingStore)
|
||||
return () => (
|
||||
<NModal v-model:show={lock.value.modalShow} maskClosable={false} class="w-350px border-rd-8px">
|
||||
<div class="bg-[--bg-popover] w-360px h-full p-6px box-border flex flex-col">
|
||||
<svg onClick={() => (lock.value.modalShow = false)} class="w-12px h-12px ml-a cursor-pointer select-none">
|
||||
<use href="#close"></use>
|
||||
</svg>
|
||||
<div class="flex flex-col gap-10px p-10px select-none">
|
||||
<NFlex vertical justify="center" align="center" size={20}>
|
||||
<span class="text-(14px center)">锁定屏幕</span>
|
||||
|
||||
<NAvatar bordered round size={80} src={login.value.accountInfo.avatar} />
|
||||
|
||||
<p class="text-(14px center [--text-color]) truncate w-200px">{login.value.accountInfo.name}</p>
|
||||
</NFlex>
|
||||
<NForm ref={formRef} model={formValue.value} rules={lock.value.rules}>
|
||||
<NFormItem label-placement="left" label="锁屏密码" path={'lockPassword'} class="w-full">
|
||||
<NInput
|
||||
show-password-on="click"
|
||||
v-model:value={formValue.value.lockPassword}
|
||||
class="border-(1px solid #ccc)"
|
||||
size="small"
|
||||
type="password"
|
||||
placeholder="请输入锁屏密码"
|
||||
/>
|
||||
</NFormItem>
|
||||
</NForm>
|
||||
|
||||
<NButton loading={lock.value.loading} onClick={lock.value.handleLock} class="w-full" color="#13987f">
|
||||
确定
|
||||
</NButton>
|
||||
</div>
|
||||
</div>
|
||||
</NModal>
|
||||
)
|
||||
})
|
||||
|
||||
/** 检查更新弹窗 */
|
||||
export const CheckUpdate = defineComponent(() => {
|
||||
const url = `https://gitee.com/api/v5/repos/nongyehong/HuLa-IM-Tauri/releases/tags/${pkg.version}?access_token=${import.meta.env.VITE_GITEE_TOKEN}`
|
||||
/** 项目提交日志记录 */
|
||||
const commitLog = ref<{ message: string; icon: string }[]>([])
|
||||
const loading = ref(false)
|
||||
const checkLoading = ref(false)
|
||||
/** 版本更新日期 */
|
||||
const versionTime = ref('')
|
||||
|
||||
const commitTypeMap: { [key: string]: string } = {
|
||||
feat: 'feat',
|
||||
fix: 'fix',
|
||||
docs: 'docs',
|
||||
style: 'style',
|
||||
refactor: 'refactor',
|
||||
perf: 'perf',
|
||||
test: 'test',
|
||||
build: 'build',
|
||||
ci: 'ci',
|
||||
revert: 'revert',
|
||||
chore: 'chore'
|
||||
}
|
||||
|
||||
const mapCommitType = (commitMessage: string) => {
|
||||
for (const type in commitTypeMap) {
|
||||
if (new RegExp(`^${type}`, 'i').test(commitMessage)) {
|
||||
return commitTypeMap[type]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 记录检测更新的版本 */
|
||||
let lastVersion: string | null = null
|
||||
|
||||
const checkUpdate = () => {
|
||||
const url = `https://gitee.com/api/v5/repos/nongyehong/HuLa-IM-Tauri/tags?access_token=${import.meta.env.VITE_GITEE_TOKEN}&sort=name&direction=desc&page=1&per_page=1`
|
||||
if (lastVersion && lastVersion === pkg.version) {
|
||||
window.$message.success('当前已是最新版本')
|
||||
return
|
||||
}
|
||||
checkLoading.value = true
|
||||
fetch(url).then((res) => {
|
||||
res
|
||||
.json()
|
||||
.then(async (data) => {
|
||||
if (data[0].name === pkg.version) {
|
||||
checkLoading.value = false
|
||||
window.$message.success('当前已是最新版本')
|
||||
lastVersion = pkg.version
|
||||
} else {
|
||||
// TODO 获取最新版本的提交日志,并且更换按钮文字为下载最新版本 (nyh -> 2024-07-11 22:20:33)
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
checkLoading.value = false
|
||||
window.$message.error('请检查配置,配置好token后再试')
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const init = () => {
|
||||
loading.value = true
|
||||
fetch(url).then((res) => {
|
||||
if (!res.ok) {
|
||||
commitLog.value = [{ message: '获取更新日志失败,请配置token后再试', icon: 'cloudError' }]
|
||||
loading.value = false
|
||||
return
|
||||
}
|
||||
res.json().then(async (data) => {
|
||||
versionTime.value = data.created_at
|
||||
await nextTick(() => {
|
||||
// 使用正则表达式提取 * 号后面的内容
|
||||
const regex = /\* (.+)/g
|
||||
let match
|
||||
const logs = []
|
||||
while ((match = regex.exec(data.body)) !== null) {
|
||||
logs.push(match[1])
|
||||
}
|
||||
commitLog.value = logs.map((commit) => {
|
||||
// 获取最后一个 : 号的位置
|
||||
const lastColonIndex = commit.lastIndexOf(':')
|
||||
// 截取最后一个 : 号后的内容
|
||||
const message = lastColonIndex !== -1 ? commit.substring(lastColonIndex + 1).trim() : commit
|
||||
return {
|
||||
message: message,
|
||||
icon: mapCommitType(commit)!
|
||||
}
|
||||
})
|
||||
loading.value = false
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
init()
|
||||
})
|
||||
return () => (
|
||||
<NModal v-model:show={lock.value.modalShow} maskClosable={false} class="w-350px border-rd-8px">
|
||||
<div class="bg-[--bg-popover] w-500px h-full p-6px box-border flex flex-col">
|
||||
<svg onClick={() => (lock.value.modalShow = false)} class="w-12px h-12px ml-a cursor-pointer select-none">
|
||||
<use href="#close"></use>
|
||||
</svg>
|
||||
{loading.value ? (
|
||||
<NFlex vertical justify={'center'} size={10}>
|
||||
<NSkeleton text repeat={1} class="rounded-8px h-30px w-120px" />
|
||||
<NSkeleton text repeat={1} class="rounded-8px h-300px" />
|
||||
<NSkeleton text repeat={1} class="rounded-8px w-80px h-30px m-[0_0_0_auto]" />
|
||||
</NFlex>
|
||||
) : (
|
||||
<NFlex size={10} vertical justify={'center'} class="p-14px box-border select-none">
|
||||
<NFlex justify={'space-between'} align={'center'}>
|
||||
<NFlex align={'center'} size={10}>
|
||||
<p>当前版本:</p>
|
||||
<p class="text-(24px #909090) font-bold">{pkg.version}</p>
|
||||
</NFlex>
|
||||
<NFlex align={'center'} size={10}>
|
||||
<p class="text-(12px #909090)">版本发布日期:</p>
|
||||
<p class="text-(12px #13987f)">{handRelativeTime(versionTime.value)}</p>
|
||||
</NFlex>
|
||||
</NFlex>
|
||||
<p class="text-(14px #909090)">版本更新日志</p>
|
||||
<NScrollbar class="max-h-460px p-[0_10px] box-border">
|
||||
<NTimeline class="p-[0_6px] box-border">
|
||||
{commitLog.value.map((log, index) => (
|
||||
<NTimelineItem key={index} content={log.message}>
|
||||
{{
|
||||
icon: () => (
|
||||
<NIcon size={20}>
|
||||
<svg>
|
||||
<use href={`#${log.icon}`}></use>
|
||||
</svg>
|
||||
</NIcon>
|
||||
)
|
||||
}}
|
||||
</NTimelineItem>
|
||||
))}
|
||||
</NTimeline>
|
||||
</NScrollbar>
|
||||
<NFlex justify={'end'}>
|
||||
<NButton loading={checkLoading.value} onClick={checkUpdate} secondary type="tertiary">
|
||||
检查更新
|
||||
</NButton>
|
||||
</NFlex>
|
||||
</NFlex>
|
||||
)}
|
||||
</div>
|
||||
</NModal>
|
||||
)
|
||||
})
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<main class="flex-1 bg-[--right-bg-color] h-full w-100vw min-w-600px shadow-inner">
|
||||
<main class="flex-1 bg-[--right-bg-color] h-full w-100vw min-w-600px">
|
||||
<ActionBar :current-label="appWindow.label" />
|
||||
<!-- 需要判断当前路由是否是信息详情界面 -->
|
||||
<ChatBox :active-item="activeItem" v-if="msgBoxShow && isChat && activeItem !== -1" />
|
||||
|
@ -47,3 +47,13 @@ export const dynamicList = Array.from({ length: 10 }, (_, i) => {
|
||||
isAuth: i % 2 === 0
|
||||
}
|
||||
})
|
||||
|
||||
/** 动态评论 */
|
||||
export const dynamicCommentList = Array.from({ length: 50 }, (_, i) => {
|
||||
return {
|
||||
id: i,
|
||||
avatar: `${avatars}?${i}`,
|
||||
user: `泰勒斯威夫特${i}`,
|
||||
content: '点赞了你的动态'
|
||||
}
|
||||
})
|
||||
|
@ -7,12 +7,12 @@ const routes: Array<RouteRecordRaw> = [
|
||||
{
|
||||
path: '/login',
|
||||
name: 'login',
|
||||
component: () => import('@/views/login-window/Login.vue')
|
||||
component: () => import('@/views/loginWindow/Login.vue')
|
||||
},
|
||||
{
|
||||
path: '/qrCode',
|
||||
name: 'qrCode',
|
||||
component: () => import('@/views/login-window/QRCode.vue')
|
||||
component: () => import('@/views/loginWindow/QRCode.vue')
|
||||
},
|
||||
{
|
||||
path: '/tray',
|
||||
@ -27,7 +27,7 @@ const routes: Array<RouteRecordRaw> = [
|
||||
{
|
||||
path: '/message',
|
||||
name: 'message',
|
||||
component: () => import('@/views/home-window/message/index.vue')
|
||||
component: () => import('@/views/homeWindow/message/index.vue')
|
||||
},
|
||||
{
|
||||
path: '/friendsList',
|
||||
@ -37,29 +37,29 @@ const routes: Array<RouteRecordRaw> = [
|
||||
{
|
||||
path: '/searchDetails',
|
||||
name: 'searchDetails',
|
||||
component: () => import('@/views/home-window/SearchDetails.vue')
|
||||
component: () => import('@/views/homeWindow/SearchDetails.vue')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/robot',
|
||||
name: 'robot',
|
||||
component: () => import('@/views/home-window/robot/index.vue'),
|
||||
component: () => import('@/views/homeWindow/robot/index.vue'),
|
||||
children: [
|
||||
{
|
||||
path: '/welcome',
|
||||
name: 'welcome',
|
||||
component: () => import('@/views/home-window/robot/views/Welcome.vue')
|
||||
component: () => import('@/views/homeWindow/robot/views/Welcome.vue')
|
||||
},
|
||||
{
|
||||
path: '/chat',
|
||||
name: 'chat',
|
||||
component: () => import('@/views/home-window/robot/views/Chat.vue')
|
||||
component: () => import('@/views/homeWindow/robot/views/Chat.vue')
|
||||
},
|
||||
{
|
||||
path: '/chatSettings',
|
||||
name: 'chatSettings',
|
||||
component: () => import('@/views/home-window/robot/views/chatSettings/index.vue')
|
||||
component: () => import('@/views/homeWindow/robot/views/chatSettings/index.vue')
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -81,37 +81,32 @@ const routes: Array<RouteRecordRaw> = [
|
||||
{
|
||||
path: '/about',
|
||||
name: 'about',
|
||||
component: () => import('@/views/home-window/more/About.vue')
|
||||
component: () => import('@/views/homeWindow/more/About.vue')
|
||||
},
|
||||
{
|
||||
path: '/alone',
|
||||
name: 'alone',
|
||||
component: () => import('@/views/home-window/message/Alone.vue')
|
||||
component: () => import('@/views/homeWindow/message/Alone.vue')
|
||||
},
|
||||
{
|
||||
path: '/sharedScreen',
|
||||
name: 'sharedScreen',
|
||||
component: () => import('@/views/home-window/SharedScreen.vue')
|
||||
component: () => import('@/views/homeWindow/SharedScreen.vue')
|
||||
},
|
||||
{
|
||||
path: '/settings',
|
||||
name: 'settings',
|
||||
component: () => import('@/views/home-window/more/settings/index.vue'),
|
||||
component: () => import('@/views/homeWindow/more/settings/index.vue'),
|
||||
children: [
|
||||
{
|
||||
path: '/general',
|
||||
name: 'general',
|
||||
component: () => import('@/views/home-window/more/settings/General.vue')
|
||||
},
|
||||
{
|
||||
path: '/remind',
|
||||
name: 'remind',
|
||||
component: () => import('@/views/home-window/more/settings/Remind.vue')
|
||||
component: () => import('@/views/homeWindow/more/settings/General.vue')
|
||||
},
|
||||
{
|
||||
path: '/loginSetting',
|
||||
name: 'loginSetting',
|
||||
component: () => import('@/views/home-window/more/settings/LoginSetting.vue')
|
||||
component: () => import('@/views/homeWindow/more/settings/LoginSetting.vue')
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
import Dynamic from '@/views/home-window/Dynamic.vue'
|
||||
import Mail from '@/views/home-window/Mail.vue'
|
||||
import OnlineStatus from '@/views/home-window/onlineStatus/index.vue'
|
||||
import Dynamic from '@/views/homeWindow/Dynamic.vue'
|
||||
import Mail from '@/views/homeWindow/Mail.vue'
|
||||
import OnlineStatus from '@/views/homeWindow/onlineStatus/index.vue'
|
||||
import Tray from '@/views/Tray.vue'
|
||||
import Layout from '@/layout/index.vue'
|
||||
import FriendsList from '@/views/home-window/FriendsList.vue'
|
||||
import FriendsList from '@/views/homeWindow/FriendsList.vue'
|
||||
|
||||
export { Dynamic, Mail, OnlineStatus, Tray, Layout, FriendsList }
|
||||
|
@ -49,8 +49,7 @@ export const createAxios = (config?: AxiosRequestConfig): AxiosInstance => {
|
||||
return Promise.reject(
|
||||
window.$message.create('当前为测试环境,请注意辨别', {
|
||||
type: 'warning',
|
||||
closable: true,
|
||||
duration: 0
|
||||
closable: true
|
||||
})
|
||||
)
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { URLEnum } from '@/enums'
|
||||
|
||||
const { PROD, VITE_SERVICE_URL } = import.meta.env
|
||||
// 本地配置到 .env.dev 里面修改。生产配置在 .env.prod 里面
|
||||
// 本地配置到 .env.development 里面修改。生产配置在 .env.production 里面
|
||||
const prefix = PROD ? VITE_SERVICE_URL : ''
|
||||
|
||||
export default {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { StoresEnum } from '@/enums'
|
||||
import { statusItem } from '@/views/home-window/onlineStatus/config.ts'
|
||||
import { statusItem } from '@/views/homeWindow/onlineStatus/config.ts'
|
||||
import Colorthief from 'colorthief'
|
||||
|
||||
const colorthief = new Colorthief()
|
||||
|
@ -15,6 +15,10 @@ export const setting = defineStore(StoresEnum.SETTING, {
|
||||
},
|
||||
/** 是否启用ESC关闭窗口 */
|
||||
escClose: true,
|
||||
lockScreen: {
|
||||
enable: false,
|
||||
password: ''
|
||||
},
|
||||
/** 系统托盘 */
|
||||
tips: {
|
||||
type: CloseBxEnum.HIDE,
|
||||
@ -41,6 +45,10 @@ export const setting = defineStore(StoresEnum.SETTING, {
|
||||
sendKey: 'Enter',
|
||||
/** 是否双击打开独立会话窗口 */
|
||||
isDouble: true
|
||||
},
|
||||
/** 界面设置 */
|
||||
page: {
|
||||
shadow: false
|
||||
}
|
||||
}),
|
||||
actions: {
|
||||
|
@ -14,7 +14,7 @@
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
@apply border-(solid 1px [--line-color]) shadow-md;
|
||||
@apply border-(solid 1px [--line-color]) custom-shadow;
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
@ -51,7 +51,7 @@
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
@apply flex flex-col absolute top-44px right-0 bg-[--bg-chat-drawer] p-22px box-border w-320px h-100vh shadow-[0_14px_14px_rgba(0,0,0,0.35)];
|
||||
@apply flex flex-col absolute top-42px right-0 bg-[--bg-chat-drawer] p-22px box-border w-320px h-100vh shadow-[0_14px_14px_rgba(0,0,0,0.35)];
|
||||
}
|
||||
|
||||
/**! 使用vue内置transition做过渡效果 */
|
||||
|
@ -1,6 +1,6 @@
|
||||
/** 气泡样式 */
|
||||
@mixin bubble {
|
||||
@apply w-fit max-w-35vw min-h-1em p-[8px_12px] text-15px line-height-22px bg-[--bg-bubble] rounded-[2px_18px_18px] shadow-md;
|
||||
@apply w-fit max-w-35vw min-h-1em p-[8px_12px] text-15px line-height-22px bg-[--bg-bubble] rounded-[2px_18px_18px] custom-shadow;
|
||||
word-break: break-all; /** 强制连续文本换行 */
|
||||
&.active {
|
||||
background-color: var(--bg-bubble-active);
|
||||
@ -24,6 +24,7 @@
|
||||
}
|
||||
/**! 气泡动画 */
|
||||
.bubble-animation {
|
||||
@apply transform-gpu;
|
||||
animation: bubble-twinkle 0.4s ease-out forwards;
|
||||
}
|
||||
.photo-wall {
|
||||
@ -75,7 +76,7 @@
|
||||
}
|
||||
/** emoji回复气泡的样式 */
|
||||
.emoji-reply-bubble {
|
||||
@apply relative rounded-50px p-[4px_8px] cursor-pointer select-none bg-#13987F66 text-14px w-fit border-(1px solid #13987F) shadow-md;
|
||||
@apply relative rounded-50px p-[4px_8px] cursor-pointer select-none bg-#13987F66 text-14px w-fit border-(1px solid #13987F) custom-shadow;
|
||||
}
|
||||
/** 跳转到回复内容时候显示的样式 */
|
||||
.active-reply {
|
||||
|
@ -1,5 +1,5 @@
|
||||
.item-box {
|
||||
@apply flex flex-col w-180px h-100vh p-[12px_0_12px_6px] box-border select-none text-[--text-color];
|
||||
@apply relative flex flex-col h-100vh box-border select-none text-[--text-color];
|
||||
.item {
|
||||
height: 42px;
|
||||
padding: 0 4px;
|
||||
@ -11,6 +11,11 @@
|
||||
background-color: var(--bg-group-hover);
|
||||
}
|
||||
}
|
||||
&:hover {
|
||||
.contraction {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
/**! 修改naive-ui虚拟列表滚动条的宽度 */
|
||||
:deep(
|
||||
@ -18,4 +23,4 @@
|
||||
.n-scrollbar + .n-scrollbar-rail.n-scrollbar-rail--vertical
|
||||
) {
|
||||
width: 6px;
|
||||
}
|
||||
}
|
||||
|
@ -59,8 +59,6 @@
|
||||
--reply-color: #909090;
|
||||
--reply-hover: #505050;
|
||||
--bg-reply-img-count: #e3e3e3;
|
||||
// 主页面面板分割线样式
|
||||
--split-color: #f1f1f1;
|
||||
// 编辑资料背景颜色
|
||||
--bg-edit: #f0f0f0;
|
||||
// 聊天框时间戳样式
|
||||
@ -71,6 +69,9 @@
|
||||
--chat-right-bg: #f1f1f1;
|
||||
--chat-text-color: #505050;
|
||||
--chat-hover-color: #e3e3e3;
|
||||
|
||||
// 是否启用阴影
|
||||
--shadow-enabled: 1;
|
||||
}
|
||||
|
||||
html[data-theme='dark'] {
|
||||
@ -133,8 +134,6 @@ html[data-theme='dark'] {
|
||||
--reply-color: #e3e3e3;
|
||||
--reply-hover: #b1b1b1;
|
||||
--bg-reply-img-count: #505050;
|
||||
// 主页面面板分割线样式
|
||||
--split-color: #3b3b3b;
|
||||
// 编辑资料背景颜色
|
||||
--bg-edit: #262626;
|
||||
// 聊天框时间戳样式
|
||||
|
107
src/typings/components.d.ts
vendored
107
src/typings/components.d.ts
vendored
@ -7,58 +7,59 @@ export {}
|
||||
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
ActionBar: (typeof import('./../components/windows/ActionBar.vue'))['default']
|
||||
ChatBox: (typeof import('./../components/rightBox/chatBox/index.vue'))['default']
|
||||
ChatFooter: (typeof import('./../components/rightBox/chatBox/ChatFooter.vue'))['default']
|
||||
ChatHeader: (typeof import('./../components/rightBox/chatBox/ChatHeader.vue'))['default']
|
||||
ChatMain: (typeof import('./../components/rightBox/chatBox/ChatMain.vue'))['default']
|
||||
ChatSidebar: (typeof import('./../components/rightBox/chatBox/ChatSidebar.vue'))['default']
|
||||
ContextMenu: (typeof import('./../components/common/ContextMenu.vue'))['default']
|
||||
Details: (typeof import('./../components/rightBox/Details.vue'))['default']
|
||||
Emoji: (typeof import('./../components/rightBox/emoji/index.vue'))['default']
|
||||
Image: (typeof import('./../components/rightBox/renderMessage/Image.vue'))['default']
|
||||
InfoPopover: (typeof import('./../components/common/InfoPopover.vue'))['default']
|
||||
MsgInput: (typeof import('./../components/rightBox/MsgInput.vue'))['default']
|
||||
NaiveProvider: (typeof import('./../components/common/NaiveProvider.vue'))['default']
|
||||
NAlert: (typeof import('naive-ui'))['NAlert']
|
||||
NAvatar: (typeof import('naive-ui'))['NAvatar']
|
||||
NAvatarGroup: (typeof import('naive-ui'))['NAvatarGroup']
|
||||
NBadge: (typeof import('naive-ui'))['NBadge']
|
||||
NButton: (typeof import('naive-ui'))['NButton']
|
||||
NButtonGroup: (typeof import('naive-ui'))['NButtonGroup']
|
||||
NCheckbox: (typeof import('naive-ui'))['NCheckbox']
|
||||
NCollapse: (typeof import('naive-ui'))['NCollapse']
|
||||
NCollapseItem: (typeof import('naive-ui'))['NCollapseItem']
|
||||
NConfigProvider: (typeof import('naive-ui'))['NConfigProvider']
|
||||
NDialogProvider: (typeof import('naive-ui'))['NDialogProvider']
|
||||
NDropdown: (typeof import('naive-ui'))['NDropdown']
|
||||
NEllipsis: (typeof import('naive-ui'))['NEllipsis']
|
||||
NFlex: (typeof import('naive-ui'))['NFlex']
|
||||
NIcon: (typeof import('naive-ui'))['NIcon']
|
||||
NIconWrapper: (typeof import('naive-ui'))['NIconWrapper']
|
||||
NImage: (typeof import('naive-ui'))['NImage']
|
||||
NImageGroup: (typeof import('naive-ui'))['NImageGroup']
|
||||
NInput: (typeof import('naive-ui'))['NInput']
|
||||
NLoadingBarProvider: (typeof import('naive-ui'))['NLoadingBarProvider']
|
||||
NMessageProvider: (typeof import('naive-ui'))['NMessageProvider']
|
||||
NModal: (typeof import('naive-ui'))['NModal']
|
||||
NModalProvider: (typeof import('naive-ui'))['NModalProvider']
|
||||
NNotificationProvider: (typeof import('naive-ui'))['NNotificationProvider']
|
||||
NPopover: (typeof import('naive-ui'))['NPopover']
|
||||
NPopselect: (typeof import('naive-ui'))['NPopselect']
|
||||
NQrCode: (typeof import('naive-ui'))['NQrCode']
|
||||
NRadio: (typeof import('naive-ui'))['NRadio']
|
||||
NScrollbar: (typeof import('naive-ui'))['NScrollbar']
|
||||
NSelect: (typeof import('naive-ui'))['NSelect']
|
||||
NSkeleton: (typeof import('naive-ui'))['NSkeleton']
|
||||
NSwitch: (typeof import('naive-ui'))['NSwitch']
|
||||
NTabPane: (typeof import('naive-ui'))['NTabPane']
|
||||
NTabs: (typeof import('naive-ui'))['NTabs']
|
||||
NTooltip: (typeof import('naive-ui'))['NTooltip']
|
||||
NVirtualList: (typeof import('naive-ui'))['NVirtualList']
|
||||
RenderMessage: (typeof import('./../components/rightBox/renderMessage/index.vue'))['default']
|
||||
RouterLink: (typeof import('vue-router'))['RouterLink']
|
||||
RouterView: (typeof import('vue-router'))['RouterView']
|
||||
Text: (typeof import('./../components/rightBox/renderMessage/Text.vue'))['default']
|
||||
ActionBar: typeof import('./../components/windows/ActionBar.vue')['default']
|
||||
ChatBox: typeof import('./../components/rightBox/chatBox/index.vue')['default']
|
||||
ChatFooter: typeof import('./../components/rightBox/chatBox/ChatFooter.vue')['default']
|
||||
ChatHeader: typeof import('./../components/rightBox/chatBox/ChatHeader.vue')['default']
|
||||
ChatMain: typeof import('./../components/rightBox/chatBox/ChatMain.vue')['default']
|
||||
ChatSidebar: typeof import('./../components/rightBox/chatBox/ChatSidebar.vue')['default']
|
||||
ContextMenu: typeof import('./../components/common/ContextMenu.vue')['default']
|
||||
Details: typeof import('./../components/rightBox/Details.vue')['default']
|
||||
Emoji: typeof import('./../components/rightBox/emoji/index.vue')['default']
|
||||
Image: typeof import('./../components/rightBox/renderMessage/Image.vue')['default']
|
||||
InfoPopover: typeof import('./../components/common/InfoPopover.vue')['default']
|
||||
MsgInput: typeof import('./../components/rightBox/MsgInput.vue')['default']
|
||||
NaiveProvider: typeof import('./../components/common/NaiveProvider.vue')['default']
|
||||
NAlert: typeof import('naive-ui')['NAlert']
|
||||
NAvatar: typeof import('naive-ui')['NAvatar']
|
||||
NAvatarGroup: typeof import('naive-ui')['NAvatarGroup']
|
||||
NBadge: typeof import('naive-ui')['NBadge']
|
||||
NButton: typeof import('naive-ui')['NButton']
|
||||
NButtonGroup: typeof import('naive-ui')['NButtonGroup']
|
||||
NCheckbox: typeof import('naive-ui')['NCheckbox']
|
||||
NCode: (typeof import('naive-ui'))['NCode']
|
||||
NCollapse: typeof import('naive-ui')['NCollapse']
|
||||
NCollapseItem: typeof import('naive-ui')['NCollapseItem']
|
||||
NConfigProvider: typeof import('naive-ui')['NConfigProvider']
|
||||
NDialogProvider: typeof import('naive-ui')['NDialogProvider']
|
||||
NDropdown: typeof import('naive-ui')['NDropdown']
|
||||
NEllipsis: typeof import('naive-ui')['NEllipsis']
|
||||
NFlex: typeof import('naive-ui')['NFlex']
|
||||
NIcon: typeof import('naive-ui')['NIcon']
|
||||
NIconWrapper: typeof import('naive-ui')['NIconWrapper']
|
||||
NImage: typeof import('naive-ui')['NImage']
|
||||
NImageGroup: typeof import('naive-ui')['NImageGroup']
|
||||
NInput: typeof import('naive-ui')['NInput']
|
||||
NLoadingBarProvider: typeof import('naive-ui')['NLoadingBarProvider']
|
||||
NMessageProvider: typeof import('naive-ui')['NMessageProvider']
|
||||
NModal: typeof import('naive-ui')['NModal']
|
||||
NModalProvider: typeof import('naive-ui')['NModalProvider']
|
||||
NNotificationProvider: typeof import('naive-ui')['NNotificationProvider']
|
||||
NPopover: typeof import('naive-ui')['NPopover']
|
||||
NPopselect: typeof import('naive-ui')['NPopselect']
|
||||
NQrCode: typeof import('naive-ui')['NQrCode']
|
||||
NRadio: typeof import('naive-ui')['NRadio']
|
||||
NScrollbar: typeof import('naive-ui')['NScrollbar']
|
||||
NSelect: typeof import('naive-ui')['NSelect']
|
||||
NSkeleton: typeof import('naive-ui')['NSkeleton']
|
||||
NSwitch: typeof import('naive-ui')['NSwitch']
|
||||
NTabPane: typeof import('naive-ui')['NTabPane']
|
||||
NTabs: typeof import('naive-ui')['NTabs']
|
||||
NTooltip: typeof import('naive-ui')['NTooltip']
|
||||
NVirtualList: typeof import('naive-ui')['NVirtualList']
|
||||
RenderMessage: typeof import('./../components/rightBox/renderMessage/index.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
Text: typeof import('./../components/rightBox/renderMessage/Text.vue')['default']
|
||||
}
|
||||
}
|
||||
|
39
src/typings/env.d.ts
vendored
39
src/typings/env.d.ts
vendored
@ -8,30 +8,6 @@ interface ImportMetaEnv {
|
||||
readonly VITE_APP_NAME: string
|
||||
/** 项目标题 */
|
||||
readonly VITE_APP_TITLE: string
|
||||
/** 页面标题后缀*/
|
||||
readonly VITE_TITLE_SUFFIX: string
|
||||
/** 项目ICP备案号 */
|
||||
readonly VITE_APP_ICP: string
|
||||
/** 项目描述 */
|
||||
readonly VITE_APP_DESC: string
|
||||
/** 后端服务的环境类型 */
|
||||
readonly VITE_SERVICE_ENV?: ServiceEnvType
|
||||
/**
|
||||
* 权限路由模式:
|
||||
* - static - 前端声明的静态
|
||||
* - dynamic - 后端返回的动态
|
||||
*/
|
||||
readonly VITE_AUTH_ROUTE_MODE: 'static' | 'dynamic'
|
||||
/** 路由首页的路径 */
|
||||
readonly VITE_ROUTE_HOME_PATH: AuthRoute.RoutePath
|
||||
/** iconify图标作为组件的前缀 */
|
||||
readonly VITE_ICON_PREFIX: string
|
||||
/**
|
||||
* 本地SVG图标作为组件的前缀, 请注意一定要包含 VITE_ICON_PREFIX
|
||||
* - 格式 {VITE_ICON_PREFIX}-{本地图标集合名称}
|
||||
* - 例如:icon-local
|
||||
*/
|
||||
readonly VITE_ICON_LOCAL_PREFIX: string
|
||||
/** 开启请求代理 */
|
||||
readonly VITE_HTTP_PROXY?: 'Y' | 'N'
|
||||
/** 是否开启打包文件大小结果分析 */
|
||||
@ -40,19 +16,8 @@ interface ImportMetaEnv {
|
||||
readonly VITE_COMPRESS?: 'Y' | 'N'
|
||||
/** 压缩算法类型 */
|
||||
readonly VITE_COMPRESS_TYPE?: 'gzip' | 'brotliCompress' | 'deflate' | 'deflateRaw'
|
||||
/** 是否应用pwa */
|
||||
readonly VITE_PWA?: 'Y' | 'N'
|
||||
/**
|
||||
* 是否开启生产模式下的mock
|
||||
* @description 生产模式下会拦截XHR,导致无法获取response,不使用mock请求时设置为N
|
||||
*/
|
||||
readonly VITE_PROD_MOCK?: 'Y' | 'N'
|
||||
/** hash路由模式 */
|
||||
readonly VITE_HASH_ROUTE?: 'Y' | 'N'
|
||||
/** 是否应用自动生成路由的插件 */
|
||||
readonly VITE_SOYBEAN_ROUTE_PLUGIN?: 'Y' | 'N'
|
||||
/** 是否是部署的vercel */
|
||||
readonly VITE_VERCEL?: 'Y' | 'N'
|
||||
/** giteeToken */
|
||||
readonly VITE_GITEE_TOKEN: string
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
|
22
src/typings/stores.d.ts
vendored
22
src/typings/stores.d.ts
vendored
@ -15,6 +15,13 @@ declare namespace STO {
|
||||
}
|
||||
/** 是否启用ESC关闭窗口 */
|
||||
escClose: boolean
|
||||
/** 是否锁屏 */
|
||||
lockScreen: {
|
||||
/** 是否启用锁屏 */
|
||||
enable: boolean
|
||||
/** 锁屏密码 */
|
||||
password: string
|
||||
}
|
||||
/** 登录设置 */
|
||||
login: {
|
||||
autoLogin: boolean
|
||||
@ -38,15 +45,28 @@ declare namespace STO {
|
||||
/** 是否双击打开独立会话窗口 */
|
||||
isDouble: boolean
|
||||
}
|
||||
/** 界面设置 */
|
||||
page: {
|
||||
/** 是否开启阴影 */
|
||||
shadow: boolean
|
||||
}
|
||||
}
|
||||
|
||||
/** 置顶 */
|
||||
/** 置顶窗口列表 */
|
||||
type AlwaysOnTop = {
|
||||
/** 是否置顶窗口列表 */
|
||||
[key: string]: boolean
|
||||
}
|
||||
|
||||
/** 隐藏窗口列表 */
|
||||
type HideWindow = {
|
||||
/** 是否隐藏窗口列表 */
|
||||
[key: string]: boolean
|
||||
}
|
||||
|
||||
/** 历史内容 */
|
||||
type History = {
|
||||
/** emoji列表 */
|
||||
emoji: string[]
|
||||
}
|
||||
}
|
||||
|
@ -13,3 +13,8 @@ dayjs.extend(weekday)
|
||||
export const handRelativeTime = (time: string) => {
|
||||
return dayjs(time).fromNow()
|
||||
}
|
||||
|
||||
/** 获取指定日期的星期 */
|
||||
export const getWeekday = (time: string) => {
|
||||
return dayjs(time).format('ddd')
|
||||
}
|
||||
|
276
src/views/LockScreen.vue
Normal file
276
src/views/LockScreen.vue
Normal file
@ -0,0 +1,276 @@
|
||||
<template>
|
||||
<!-- 锁屏页面 -->
|
||||
<!-- // TODO 锁屏时隐藏其他窗口,解锁后再显示 (nyh -> 2024-07-14 01:39:01) -->
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
class="lock-bg select-none absolute top-0 left-0 w-full h-full z-9999 transition-all duration-300 ease-in-out">
|
||||
<ActionBar class="absolute top-0 right-0 z-99999" :current-label="appWindow.label" :shrink="false" />
|
||||
|
||||
<Transition name="slide-fade" appear>
|
||||
<!-- 壁纸界面 -->
|
||||
<div v-if="!isUnlockPage" @click.stop="isUnlockPage = true" class="size-full">
|
||||
<n-flex vertical align="center" :size="120" class="size-full mt-10%">
|
||||
<n-flex vertical align="center" :size="20" class="will-change-auto will-change-contents">
|
||||
<p class="text-(100px #f1f1f1) font-bold">{{ currentTime }}</p>
|
||||
<n-flex align="center" :size="30" class="text-(30px #f1f1f1)">
|
||||
<p>{{ currentMonthAndDate }}</p>
|
||||
<p>{{ currentWeekday }}</p>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
|
||||
<n-flex vertical justify="center" align="center" :size="20" class="tips">
|
||||
<svg><use href="#search"></use></svg>
|
||||
<p class="text-(16px #f1f1f1) text-center">
|
||||
这是一个开源的即时通讯(IM)应用,它采用了一些最新的前端技术,包括 Tauri、Vue3、Vite5、UnoCSS 和
|
||||
TypeScript。
|
||||
</p>
|
||||
<p class="text-(12px #909090) opacity-0">这个项目的目标是提供一个高效、稳定且易于使用的即时通讯平台。</p>
|
||||
<a
|
||||
@click.stop="$event.stopPropagation()"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://github.com/nongyehong/HuLa-IM-Tauri"
|
||||
class="no-underline text-(14px #f3f3f3) opacity-0">
|
||||
了解更多
|
||||
</a>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</div>
|
||||
|
||||
<!-- 解锁界面 -->
|
||||
<n-flex
|
||||
v-else
|
||||
data-tauri-drag-region
|
||||
vertical
|
||||
align="center"
|
||||
justify="center"
|
||||
:size="16"
|
||||
class="h-full backdrop-blur-md">
|
||||
<n-flex vertical align="center" justify="center" :size="30" class="mt--75px">
|
||||
<n-avatar round style="border: 2px solid #f1f1f1" :size="120" :src="login.accountInfo.avatar" />
|
||||
<p class="text-(24px #f1f1f1) font-bold">{{ login.accountInfo.name }}</p>
|
||||
|
||||
<!-- 密码输入框 -->
|
||||
<n-config-provider :theme="lightTheme">
|
||||
<n-input
|
||||
v-if="!isLogining && !isWrongPassword"
|
||||
ref="inputInstRef"
|
||||
style="
|
||||
width: 320px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.1);
|
||||
border-bottom-color: rgba(19, 152, 127, 1);
|
||||
background-color: #404040;
|
||||
color: #fff;
|
||||
"
|
||||
placeholder="锁屏密码"
|
||||
show-password-on="click"
|
||||
type="password"
|
||||
@keyup.enter.prevent="unlock"
|
||||
v-model:value="password">
|
||||
<template #suffix>
|
||||
<n-popover trigger="hover">
|
||||
<template #trigger>
|
||||
<svg
|
||||
@click.stop="unlock"
|
||||
class="size-16px color-#e3e3e3 mr-6px p-[4px_6px] rounded-8px cursor-pointer transition-all duration-300 ease-in-out hover:bg-#13987fe6">
|
||||
<use href="#arrow-right"></use>
|
||||
</svg>
|
||||
</template>
|
||||
<p>进入系统</p>
|
||||
</n-popover>
|
||||
</template>
|
||||
</n-input>
|
||||
</n-config-provider>
|
||||
|
||||
<!-- 登录时显示的文字 -->
|
||||
<n-flex vertical align="center" justify="center" :size="30" v-if="isLogining && !isWrongPassword">
|
||||
<img class="size-42px" src="@/assets/img/loading-bright.svg" alt="" />
|
||||
<p class="text-(20px #f1f1f1)">解锁中</p>
|
||||
</n-flex>
|
||||
|
||||
<!-- 密码不正常时显示 -->
|
||||
<n-flex v-if="isWrongPassword" vertical justify="center" align="center" :size="30">
|
||||
<p class="text-(18px #f1f1f1)">密码不正确, 请再试一次</p>
|
||||
<p
|
||||
@click="init"
|
||||
class="w-120px bg-[rgba(255,255,255,0.1)] backdrop-blur-xl cursor-pointer p-10px rounded-8px text-(14px #323232 center) font-bold">
|
||||
确定
|
||||
</p>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
|
||||
<n-flex v-if="!isLogining && !isWrongPassword" justify="space-around" align="center" :size="0" class="options">
|
||||
<p class="text-(14px #fefefe)" @click="isUnlockPage = false">返回</p>
|
||||
<p class="text-(14px #fefefe)" @click="logout">退出登录</p>
|
||||
<p class="text-(14px #fefefe)">忘记密码</p>
|
||||
<p class="text-(14px #fff)" @click="unlock">进入系统</p>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { appWindow } from '@tauri-apps/api/window'
|
||||
import { setting } from '@/stores/setting.ts'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useLogin } from '@/hooks/useLogin.ts'
|
||||
import { onKeyStroke } from '@vueuse/core'
|
||||
import { InputInst, lightTheme } from 'naive-ui'
|
||||
import { getWeekday } from '@/utils/Day.ts'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
const settingStore = setting()
|
||||
const { lockScreen, login } = storeToRefs(settingStore)
|
||||
const { logout } = useLogin()
|
||||
/** 解锁密码 */
|
||||
const password = ref('')
|
||||
/** 是否是解锁页面 */
|
||||
const isUnlockPage = ref(false)
|
||||
/** 是否是登录中 */
|
||||
const isLogining = ref(false)
|
||||
/** 密码不正确时显示 */
|
||||
const isWrongPassword = ref(false)
|
||||
/** 当前时间 */
|
||||
const currentTime = ref(dayjs().format('HH:mm'))
|
||||
/** 当前月份和日期 */
|
||||
const currentMonthAndDate = ref(dayjs().format('MM/DD'))
|
||||
// const currentMonthAndDate = ref(new Date().toLocaleDateString('chinese', { month: 'long', day: 'numeric' }))
|
||||
/** 当前星期 */
|
||||
const currentWeekday = ref(getWeekday(new Date().toLocaleString()))
|
||||
/** 计算当前时间的定时器 */
|
||||
let intervalId: NodeJS.Timeout | null = null
|
||||
/** 密码输入框实例 */
|
||||
const inputInstRef = ref<InputInst | null>(null)
|
||||
|
||||
watch(isUnlockPage, (val) => {
|
||||
if (val) {
|
||||
/** 延迟 300ms 后自动获取焦点,不然会触发一次回车事件 */
|
||||
setTimeout(() => {
|
||||
inputInstRef.value?.focus()
|
||||
}, 300)
|
||||
}
|
||||
})
|
||||
|
||||
watch(isWrongPassword, (val) => {
|
||||
if (val) {
|
||||
onKeyStroke('Enter', (e) => {
|
||||
e.preventDefault()
|
||||
init()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
/** 解锁 */
|
||||
const unlock = () => {
|
||||
if (password.value === '') {
|
||||
window.$message.error('请输入密码')
|
||||
} else {
|
||||
isLogining.value = true
|
||||
if (password.value === lockScreen.value.password) {
|
||||
setTimeout(() => {
|
||||
lockScreen.value.enable = false
|
||||
isLogining.value = false
|
||||
}, 1000)
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
isWrongPassword.value = true
|
||||
}, 300)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 重置登录状态 */
|
||||
const init = () => {
|
||||
if (isWrongPassword.value) {
|
||||
isWrongPassword.value = false
|
||||
isLogining.value = false
|
||||
setTimeout(() => {
|
||||
inputInstRef.value?.focus()
|
||||
}, 600)
|
||||
password.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
intervalId = setInterval(() => {
|
||||
currentTime.value = dayjs().format('HH:mm')
|
||||
currentMonthAndDate.value = dayjs().format('MM/DD')
|
||||
currentWeekday.value = getWeekday(new Date().toLocaleString())
|
||||
}, 1000)
|
||||
if (!isUnlockPage.value) {
|
||||
onKeyStroke('Enter', (e) => {
|
||||
e.preventDefault()
|
||||
isUnlockPage.value = true
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (intervalId) {
|
||||
clearInterval(intervalId)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.lock-bg {
|
||||
background-image: url('@/assets/img/lock_bg.jpg');
|
||||
background-size: cover; // 或者使用 contain,取决于你想要的效果
|
||||
background-position: center; // 确保图片居中
|
||||
background-repeat: no-repeat; // 防止图片重复
|
||||
}
|
||||
|
||||
.options {
|
||||
@apply w-320px;
|
||||
p {
|
||||
@apply cursor-pointer select-none;
|
||||
}
|
||||
}
|
||||
|
||||
.tips {
|
||||
@apply cursor-pointer w-240px p-12px rounded-8px transition-all duration-300 ease-in-out hover:bg-#323232;
|
||||
svg {
|
||||
@apply size-24px color-#f1f1f1 p-4px bg-#323232 rounded-8px;
|
||||
}
|
||||
&:hover {
|
||||
p {
|
||||
@apply opacity-100;
|
||||
}
|
||||
a {
|
||||
@apply opacity-100 hover:underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.hover-box),
|
||||
:deep(.action-close) {
|
||||
svg {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
:deep(.hover-box) {
|
||||
&:hover {
|
||||
background-color: #464646;
|
||||
}
|
||||
}
|
||||
:deep(.n-input .n-input__input-el, .n-input .n-input__textarea-el) {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/*
|
||||
进入和离开动画可以使用不同
|
||||
持续时间和速度曲线。
|
||||
*/
|
||||
.slide-fade-enter-active {
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.slide-fade-leave-active {
|
||||
transition: all 0.6s cubic-bezier(1, 0.5, 0.8, 1);
|
||||
}
|
||||
|
||||
.slide-fade-enter-from,
|
||||
.slide-fade-leave-to {
|
||||
transform: translateY(20px);
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
@ -35,7 +35,7 @@
|
||||
</n-flex>
|
||||
|
||||
<component :is="division" />
|
||||
<n-flex @click="exit(0)" align="center" :size="10" class="p-[8px_6px] rounded-4px hover:bg-[--tray-hover-e]">
|
||||
<n-flex @click="handleExit" align="center" :size="10" class="p-[8px_6px] rounded-4px hover:bg-[--tray-hover-e]">
|
||||
<span>退出</span>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
@ -51,14 +51,18 @@
|
||||
import { useWindow } from '@/hooks/useWindow.ts'
|
||||
import { invoke } from '@tauri-apps/api/tauri'
|
||||
import { exit } from '@tauri-apps/api/process'
|
||||
import { statusItem } from './home-window/onlineStatus/config.ts'
|
||||
import { statusItem } from '@/views/homeWindow/onlineStatus/config.ts'
|
||||
import { onlineStatus } from '@/stores/onlineStatus.ts'
|
||||
import { appWindow } from '@tauri-apps/api/window'
|
||||
import { listen } from '@tauri-apps/api/event'
|
||||
import { useWsLoginStore } from '@/stores/ws.ts'
|
||||
import { setting } from '@/stores/setting.ts'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
const { checkWinExist, createWebviewWindow, resizeWindow } = useWindow()
|
||||
const OLStatusStore = onlineStatus()
|
||||
const settingStore = setting()
|
||||
const { lockScreen } = storeToRefs(settingStore)
|
||||
const isLoginWin = ref(true)
|
||||
const loginStore = useWsLoginStore()
|
||||
const loginQrCode = computed(() => loginStore.loginQrCode)
|
||||
@ -68,6 +72,8 @@ const division = () => {
|
||||
}
|
||||
|
||||
const handleExit = () => {
|
||||
/** 退出时关闭锁屏 */
|
||||
lockScreen.value.enable = false
|
||||
exit(0)
|
||||
if (loginQrCode.value) {
|
||||
localStorage.removeItem('wsLogin')
|
||||
|
@ -1,70 +0,0 @@
|
||||
<template>
|
||||
<main class="size-full bg-[--right-bg-color]">
|
||||
<ActionBar :shrink="false" :max-w="false" :top-win-label="appWindow.label" :current-label="appWindow.label" />
|
||||
<article class="flex flex-col items-center text-[--text-color] size-full bg-[--right-bg-color]">
|
||||
<n-scrollbar
|
||||
style="max-height: calc(100vh - 20px)"
|
||||
class="w-full bg-[--center-bg-color] border-(solid 1px [--line-color]) h-full p-[10px_0] box-border rounded-4px">
|
||||
<n-flex justify="center">
|
||||
<!-- 动态内容框 -->
|
||||
<n-flex
|
||||
vertical
|
||||
v-for="item in dynamicList"
|
||||
:key="item.id"
|
||||
class="w-450px h-fit border-(solid 1px [--line-color]) shadow-md rounded-8px bg-[--right-bg-color] p-10px box-border">
|
||||
<n-flex align="center">
|
||||
<!-- 用户的头像和用户名以及个签 -->
|
||||
<img class="size-45px bg-#ccc rounded-50% select-none" :src="item.avatar" alt="" />
|
||||
<n-flex vertical style="flex: 1">
|
||||
<n-flex justify="space-between" align="center">
|
||||
<label class="text-14px flex items-center gap-5px">
|
||||
<span :class="item.isAuth ? 'text-#13987f' : ''">{{ item.user }}</span>
|
||||
<n-popover trigger="hover" v-if="item.isAuth">
|
||||
<template #trigger>
|
||||
<svg class="size-20px color-#13987f select-none outline-none cursor-pointer">
|
||||
<use href="#auth"></use>
|
||||
</svg>
|
||||
</template>
|
||||
<span>著名歌手</span>
|
||||
</n-popover>
|
||||
</label>
|
||||
|
||||
<span class="text-(12px #707070)">发布于:2021-01-01</span>
|
||||
</n-flex>
|
||||
|
||||
<span class="text-(12px #707070)">个性签名</span>
|
||||
</n-flex>
|
||||
|
||||
<!-- 个签照片墙 -->
|
||||
<n-flex vertical>
|
||||
<n-scrollbar style="max-height: 240px; user-select: none">
|
||||
<n-image-group>
|
||||
<n-flex>
|
||||
<n-image
|
||||
v-for="i in item.img"
|
||||
:key="i.url"
|
||||
:src="i.url"
|
||||
alt=""
|
||||
width="134px"
|
||||
height="120px"
|
||||
class="rounded-6px" />
|
||||
</n-flex>
|
||||
</n-image-group>
|
||||
</n-scrollbar>
|
||||
|
||||
<n-input />
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</n-scrollbar>
|
||||
</article>
|
||||
</main>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { dynamicList } from '@/mock'
|
||||
import { appWindow } from '@tauri-apps/api/window'
|
||||
import { useWindowState } from '@/hooks/useWindowState.ts'
|
||||
|
||||
useWindowState(appWindow.label)
|
||||
</script>
|
@ -1,6 +0,0 @@
|
||||
<template>
|
||||
<div>查找用户</div>
|
||||
</template>
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style scoped></style>
|
@ -1,7 +0,0 @@
|
||||
<template>
|
||||
<!-- 消息通知设置 -->
|
||||
<main class="bg-blue">消息提醒</main>
|
||||
</template>
|
||||
<script setup lang="ts"></script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
153
src/views/homeWindow/Dynamic.vue
Normal file
153
src/views/homeWindow/Dynamic.vue
Normal file
@ -0,0 +1,153 @@
|
||||
<template>
|
||||
<main class="size-full bg-[--right-bg-color]">
|
||||
<ActionBar :shrink="false" :max-w="false" :top-win-label="appWindow.label" :current-label="appWindow.label" />
|
||||
|
||||
<!-- 头部用户信息栏 -->
|
||||
<n-flex
|
||||
align="center"
|
||||
justify="center"
|
||||
:size="20"
|
||||
class="relative bg-[--left-active-color] h-160px w-full select-none">
|
||||
<n-avatar :size="120" round bordered :src="login.accountInfo.avatar" />
|
||||
<n-flex vertical justify="center" :size="20">
|
||||
<p class="text-(24px [--chat-text-color]) font-bold">{{ login.accountInfo.name }}</p>
|
||||
|
||||
<n-flex align="center" justify="space-between" :size="30" class="mt-5px">
|
||||
<template v-for="item in titleList" :key="item.label">
|
||||
<n-flex vertical align="center" class="cursor-pointer">
|
||||
<p class="text-[--text-color]">{{ item.total }}</p>
|
||||
<p class="text-(16px #808080)">{{ item.label }}</p>
|
||||
</n-flex>
|
||||
</template>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
|
||||
<div class="absolute top-30px right-30px cursor-pointer" @click="handleInfoTip">
|
||||
<n-badge :value="infoTip.value" :max="100" :show="infoTip.show">
|
||||
<svg class="size-24px color-[--text-color]"><use href="#remind"></use></svg>
|
||||
</n-badge>
|
||||
</div>
|
||||
</n-flex>
|
||||
|
||||
<!-- 动态列表 -->
|
||||
<div class="flex flex-col items-center text-[--text-color] size-full bg-[--right-bg-color]">
|
||||
<n-scrollbar
|
||||
style="max-height: calc(100vh - 180px)"
|
||||
class="w-full bg-[--center-bg-color] border-(solid 1px [--line-color]) h-full p-[10px_0] box-border rounded-4px">
|
||||
<n-flex justify="center">
|
||||
<!-- 动态内容框 -->
|
||||
<n-flex
|
||||
vertical
|
||||
v-for="item in dynamicList"
|
||||
:key="item.id"
|
||||
class="w-450px h-fit border-(solid 1px [--line-color]) custom-shadow rounded-8px bg-[--right-bg-color] p-10px box-border">
|
||||
<n-flex align="center">
|
||||
<!-- 用户的头像和用户名以及个签 -->
|
||||
<img class="size-45px bg-#ccc rounded-50% select-none" :src="item.avatar" alt="" />
|
||||
<n-flex vertical style="flex: 1">
|
||||
<n-flex justify="space-between" align="center">
|
||||
<label class="text-14px flex items-center gap-5px">
|
||||
<span :class="item.isAuth ? 'text-#13987f' : ''">{{ item.user }}</span>
|
||||
<n-popover trigger="hover" v-if="item.isAuth">
|
||||
<template #trigger>
|
||||
<svg class="size-20px color-#13987f select-none outline-none cursor-pointer">
|
||||
<use href="#auth"></use>
|
||||
</svg>
|
||||
</template>
|
||||
<span>著名歌手</span>
|
||||
</n-popover>
|
||||
</label>
|
||||
|
||||
<span class="text-(12px #707070)">发布于:2021-01-01</span>
|
||||
</n-flex>
|
||||
|
||||
<span class="text-(12px #707070)">个性签名</span>
|
||||
</n-flex>
|
||||
|
||||
<!-- 个签照片墙 -->
|
||||
<n-flex vertical>
|
||||
<n-scrollbar style="max-height: 240px; user-select: none">
|
||||
<n-image-group>
|
||||
<n-flex>
|
||||
<n-image
|
||||
v-for="i in item.img"
|
||||
:key="i.url"
|
||||
:src="i.url"
|
||||
alt=""
|
||||
width="134px"
|
||||
height="120px"
|
||||
class="rounded-6px" />
|
||||
</n-flex>
|
||||
</n-image-group>
|
||||
</n-scrollbar>
|
||||
|
||||
<n-input />
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</n-scrollbar>
|
||||
</div>
|
||||
|
||||
<!-- 弹出框 -->
|
||||
<n-modal v-model:show="infoTip.modalShow" class="w-450px border-rd-8px">
|
||||
<div class="bg-[--bg-popover] h-full p-6px box-border flex flex-col">
|
||||
<svg @click="infoTip.modalShow = false" class="w-12px h-12px ml-a cursor-pointer select-none">
|
||||
<use href="#close"></use>
|
||||
</svg>
|
||||
<n-virtual-list
|
||||
:items="dynamicCommentList"
|
||||
:item-size="40"
|
||||
class="max-h-500px w-full p-10px box-border select-none">
|
||||
<template #default="{ item }">
|
||||
<n-flex align="center" justify="space-between" class="mt-18px">
|
||||
<n-flex align="center">
|
||||
<n-avatar :size="36" round bordered :src="item.avatar" />
|
||||
<p>{{ item.user }}</p>
|
||||
<p class="text-(12px #707070)">{{ item.content }}</p>
|
||||
</n-flex>
|
||||
|
||||
<p class="text-(12px #707070)">2021-01-01</p>
|
||||
</n-flex>
|
||||
</template>
|
||||
</n-virtual-list>
|
||||
</div>
|
||||
</n-modal>
|
||||
</main>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { dynamicList, dynamicCommentList } from '@/mock'
|
||||
import { appWindow } from '@tauri-apps/api/window'
|
||||
import { useWindowState } from '@/hooks/useWindowState.ts'
|
||||
import { setting } from '@/stores/setting.ts'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
useWindowState(appWindow.label)
|
||||
const settingStore = setting()
|
||||
const { login } = storeToRefs(settingStore)
|
||||
const infoTip = ref({
|
||||
value: dynamicCommentList.length,
|
||||
show: true,
|
||||
modalShow: false
|
||||
})
|
||||
const titleList = [
|
||||
{
|
||||
label: '动态',
|
||||
total: 43
|
||||
},
|
||||
{
|
||||
label: '关注',
|
||||
total: 443
|
||||
},
|
||||
{
|
||||
label: '点赞',
|
||||
total: 99
|
||||
}
|
||||
]
|
||||
|
||||
/** 处理信息提示 */
|
||||
const handleInfoTip = () => {
|
||||
infoTip.value.show = false
|
||||
infoTip.value.modalShow = true
|
||||
}
|
||||
</script>
|
@ -7,20 +7,19 @@
|
||||
<ContextMenu @contextmenu="showMenu($event)" @select="handleSelect($event.label)" :menu="menuList">
|
||||
<n-collapse-item title="我的好友" name="1">
|
||||
<template #header-extra>
|
||||
<span class="text-(10px #707070)">1/1</span>
|
||||
<span class="text-(10px #707070)">0/0</span>
|
||||
</template>
|
||||
|
||||
<!-- 用户框 多套一层div来移除默认的右键事件然后覆盖掉因为margin空隙而导致右键可用 -->
|
||||
<div @contextmenu.stop="$event.preventDefault()">
|
||||
<n-flex
|
||||
v-slide
|
||||
:size="10"
|
||||
@click="handleClick(item.uid, RoomTypeEnum.SINGLE)"
|
||||
:class="{ active: activeItem === item.uid }"
|
||||
class="user-box w-full h-75px mb-5px"
|
||||
v-for="item in contactStore.contactsList"
|
||||
:key="item.uid">
|
||||
<n-flex v-slide align="center" :size="10" class="h-75px pl-6px pr-8px flex-1 truncate">
|
||||
<n-flex align="center" :size="10" class="h-75px pl-6px pr-8px flex-1 truncate">
|
||||
<n-avatar
|
||||
round
|
||||
bordered
|
42
src/views/homeWindow/SearchDetails.vue
Normal file
42
src/views/homeWindow/SearchDetails.vue
Normal file
@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<n-flex :size="14" vertical justify="center" class="p-14px text-(12px #909090)">
|
||||
<p>搜索建议</p>
|
||||
|
||||
<n-flex align="center" class="text-(12px #909090)">
|
||||
<p class="p-6px bg-#eee rounded-8px cursor-pointer">@我</p>
|
||||
<p class="p-6px bg-#eee rounded-8px cursor-pointer">特别关心</p>
|
||||
</n-flex>
|
||||
|
||||
<span class="w-full h-1px bg-[--line-color]"></span>
|
||||
|
||||
<n-flex align="center" justify="space-between">
|
||||
<p class="text-(12px #909090)">历史记录</p>
|
||||
<p class="cursor-pointer text-(12px #13987f)">清除</p>
|
||||
</n-flex>
|
||||
|
||||
<template v-for="(item, _index) in historyList" :key="_index">
|
||||
<n-flex align="center" :size="14" class="p-6px cursor-pointer rounded-8px hover:bg-[--bg-group-hover]">
|
||||
<n-avatar :size="38" round bordered :src="item.avatar" />
|
||||
<p class="text-(14px [--text-color])">{{ item.name }}</p>
|
||||
</n-flex>
|
||||
</template>
|
||||
</n-flex>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
const historyList = [
|
||||
{
|
||||
avatar: 'https://picsum.photos/140?1',
|
||||
name: '小瘪三'
|
||||
},
|
||||
{
|
||||
avatar: 'https://picsum.photos/140?2',
|
||||
name: '号啊玉'
|
||||
},
|
||||
{
|
||||
avatar: 'https://picsum.photos/140?3',
|
||||
name: '张三'
|
||||
}
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@ -16,8 +16,7 @@
|
||||
@click="onSelectSelectSession(item, item.type)"
|
||||
@dblclick="handleMsgDblclick(item)"
|
||||
@select="$event.click(item)">
|
||||
<!-- 消息框,使用v-slide自定义指令来自动抉择右键菜单位置 -->
|
||||
<n-flex v-slide :size="10" align="center" class="h-75px pl-6px pr-8px flex-1">
|
||||
<n-flex :size="10" align="center" class="h-75px pl-6px pr-8px flex-1">
|
||||
<n-avatar :color="'#fff'" :size="44" :src="item.avatar" bordered fallback-src="/logo.png" round />
|
||||
|
||||
<n-flex class="h-fit flex-1 truncate" justify="space-between" vertical>
|
@ -14,7 +14,7 @@
|
||||
:key="index">
|
||||
<div
|
||||
@click="handleTheme(item.code)"
|
||||
class="size-full rounded-8px cursor-pointer shadow-md"
|
||||
class="size-full rounded-8px cursor-pointer custom-shadow"
|
||||
:class="{ 'outline outline-2 outline-#13987f outline-offset': activeItem === item.code }">
|
||||
<component :is="item.model" />
|
||||
</div>
|
||||
@ -84,6 +84,20 @@
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
|
||||
<!-- 界面设置 -->
|
||||
<n-flex vertical class="text-(14px [--text-color])" :size="16">
|
||||
<span class="pl-10px">界面</span>
|
||||
|
||||
<n-flex class="item" :size="15" vertical>
|
||||
<!-- 发送信息 -->
|
||||
<n-flex align="center" justify="space-between">
|
||||
<span>是否开启阴影</span>
|
||||
|
||||
<n-switch size="small" v-model:value="page.shadow" />
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</n-flex>
|
||||
</template>
|
||||
<script setup lang="tsx">
|
||||
@ -94,7 +108,7 @@ import { topicsList } from './model.tsx'
|
||||
import { sendOptions } from './config.ts'
|
||||
|
||||
const settingStore = setting()
|
||||
const { themes, tips, escClose, chat } = storeToRefs(settingStore)
|
||||
const { themes, tips, escClose, chat, page } = storeToRefs(settingStore)
|
||||
const activeItem = ref<string>(themes.value.pattern)
|
||||
|
||||
/** 切换主题 */
|
||||
@ -106,6 +120,6 @@ const handleTheme = (code: string) => {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.item {
|
||||
@apply bg-[--bg-setting-item] rounded-12px size-full p-12px box-border border-(solid 1px [--line-color]) shadow-md;
|
||||
@apply bg-[--bg-setting-item] rounded-12px size-full p-12px box-border border-(solid 1px [--line-color]) custom-shadow;
|
||||
}
|
||||
</style>
|
@ -47,6 +47,6 @@ const clearInfo = () => {
|
||||
|
||||
<style scoped lang="scss">
|
||||
.item-box {
|
||||
@apply text-14px text-[--text-color] bg-[--bg-setting-item] rounded-8px p-10px border-(solid 1px [--line-color]) shadow-md;
|
||||
@apply text-14px text-[--text-color] bg-[--bg-setting-item] rounded-8px p-10px border-(solid 1px [--line-color]) custom-shadow;
|
||||
}
|
||||
</style>
|
@ -5,16 +5,6 @@ const sideOptions = ref<OPT.L.SettingSide[]>([
|
||||
label: '通用',
|
||||
icon: 'setting-config'
|
||||
},
|
||||
{
|
||||
url: '/remind',
|
||||
label: '消息通知',
|
||||
icon: 'remind'
|
||||
},
|
||||
{
|
||||
url: '/collection',
|
||||
label: '存储管理',
|
||||
icon: 'mini-sd-card'
|
||||
},
|
||||
{
|
||||
url: '/loginSetting',
|
||||
label: '登录设置',
|
@ -13,10 +13,12 @@
|
||||
</section>
|
||||
|
||||
<!-- 右边内容 -->
|
||||
<section class="bg-[--right-bg-color] flex-1 shadow-md">
|
||||
<section class="bg-[--right-bg-color] flex-1 custom-shadow border-l-(1px solid [--line-color])">
|
||||
<ActionBar :shrink="false" :max-w="false" />
|
||||
|
||||
<header class="header" style="box-shadow: 0 4px 4px var(--box-shadow-color)">{{ title }}</header>
|
||||
<header class="header" style="box-shadow: var(--shadow-enabled) 4px 4px var(--box-shadow-color)">
|
||||
{{ title }}
|
||||
</header>
|
||||
|
||||
<div class="flex-1 p-24px"><router-view /></div>
|
||||
</section>
|
||||
@ -80,6 +82,6 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.header {
|
||||
@apply w-full h-42px flex items-center pl-40px select-none text-18px color-[--text-color];
|
||||
@apply w-full h-42px flex items-center pl-40px select-none text-18px color-[--text-color] border-b-(1px solid [--line-color]);
|
||||
}
|
||||
</style>
|
@ -79,14 +79,14 @@
|
||||
<n-flex :size="12" align="center">
|
||||
<div
|
||||
@click="jump"
|
||||
class="bg-[--chat-bt-color] color-[--chat-text-color] size-fit p-[8px_9px] rounded-8px shadow-md cursor-pointer">
|
||||
class="bg-[--chat-bt-color] border-(1px solid [--line-color]) color-[--chat-text-color] size-fit p-[8px_9px] rounded-8px custom-shadow cursor-pointer">
|
||||
<svg class="size-18px"><use href="#settings"></use></svg>
|
||||
</div>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://github.com/nongyehong/HuLa-IM-Tauri"
|
||||
class="bg-[--chat-bt-color] color-[--chat-text-color] size-fit p-[8px_9px] rounded-8px shadow-md cursor-pointer">
|
||||
class="bg-[--chat-bt-color] border-(1px solid [--line-color]) color-[--chat-text-color] size-fit p-[8px_9px] rounded-8px custom-shadow cursor-pointer">
|
||||
<svg class="size-18px"><use href="#github"></use></svg>
|
||||
</a>
|
||||
</n-flex>
|
||||
@ -95,7 +95,7 @@
|
||||
:size="4"
|
||||
align="center"
|
||||
@click="add"
|
||||
class="bg-[--chat-bt-color] select-none text-(14px [--chat-text-color]) size-fit p-8px rounded-8px shadow-md cursor-pointer">
|
||||
class="bg-[--chat-bt-color] border-(1px solid [--line-color]) select-none text-(14px [--chat-text-color]) size-fit p-8px rounded-8px custom-shadow cursor-pointer">
|
||||
<svg class="size-18px"><use href="#plus"></use></svg>
|
||||
<p>新的聊天</p>
|
||||
</n-flex>
|
||||
@ -250,12 +250,12 @@ onMounted(() => {
|
||||
@apply bg-clip-text text-transparent bg-gradient-to-r from-#38BDF8 to-#13987F text-20px font-800;
|
||||
}
|
||||
.plugins {
|
||||
@apply size-fit bg-[--chat-bt-color] rounded-8px shadow-md p-[8px_14px]
|
||||
@apply size-fit bg-[--chat-bt-color] rounded-8px custom-shadow p-[8px_14px]
|
||||
flex items-center gap-10px select-none cursor-pointer
|
||||
text-14px color-[--chat-text-color];
|
||||
text-14px color-[--chat-text-color] border-(1px solid [--line-color]);
|
||||
}
|
||||
.chat-item {
|
||||
@apply relative bg-[--chat-bt-color] cursor-pointer shadow-md rounded-8px w-full h-65px;
|
||||
@apply relative bg-[--chat-bt-color] border-(1px solid [--line-color]) cursor-pointer custom-shadow rounded-8px w-full h-65px;
|
||||
&:hover {
|
||||
@apply bg-[--chat-hover-color];
|
||||
svg {
|
@ -1,5 +1,8 @@
|
||||
<template>
|
||||
<n-flex vertical :size="0" class="flex-1 truncate shadow-md select-none text-[--text-color]">
|
||||
<n-flex
|
||||
vertical
|
||||
:size="0"
|
||||
class="flex-1 truncate border-l-(1px solid [--line-color]) custom-shadow select-none text-[--text-color]">
|
||||
<!-- 右上角操作栏 -->
|
||||
<ActionBar class="w-full" :shrink="false" :current-label="appWindow.label" :top-win-label="appWindow.label" />
|
||||
|
@ -2,7 +2,9 @@
|
||||
<!-- 主体内容 -->
|
||||
<!-- // TODO 考虑是否需要添加一个欢迎页面,而不是直接使用聊天窗口 (nyh -> 2024-07-01 10:44:14)-->
|
||||
<main>
|
||||
<div class="flex truncate p-[14px_20px] justify-between items-center gap-50px">
|
||||
<div
|
||||
style="box-shadow: var(--shadow-enabled) 4px 4px var(--box-shadow-color)"
|
||||
class="flex truncate p-[14px_20px] justify-between items-center gap-50px">
|
||||
<n-flex :size="10" vertical class="truncate">
|
||||
<p
|
||||
v-if="!isEdit"
|
||||
@ -38,7 +40,7 @@
|
||||
<div class="h-1px bg-[--line-color]"></div>
|
||||
|
||||
<!-- 聊天信息框 -->
|
||||
<div class="w-full shadow-inner p-[28px_16px] box-border" style="height: calc(100vh - 300px)">
|
||||
<div class="w-full p-[28px_16px] box-border" style="height: calc(100vh - 300px)">
|
||||
<n-flex :size="6">
|
||||
<n-avatar
|
||||
class="rounded-8px"
|
||||
@ -61,7 +63,7 @@
|
||||
<n-flex
|
||||
vertical
|
||||
:size="6"
|
||||
style="box-shadow: 0 -4px 4px 0 rgba(0, 0, 0, 0.05)"
|
||||
style="box-shadow: var(--shadow-enabled) -4px 4px 0 rgba(0, 0, 0, 0.05)"
|
||||
class="size-full p-[14px_22px] box-border">
|
||||
<n-flex :size="26" class="options">
|
||||
<n-popover v-for="(item, index) in features" :key="index" trigger="hover" :show-arrow="false" placement="top">
|
||||
@ -136,7 +138,7 @@ onMounted(() => {
|
||||
<style scoped lang="scss">
|
||||
@import '@/styles/scss/chat-main';
|
||||
.right-btn {
|
||||
@apply size-fit cursor-pointer bg-[--chat-bt-color] color-[--chat-text-color] rounded-8px shadow-md p-[10px_11px];
|
||||
@apply size-fit border-(1px solid [--line-color]) cursor-pointer bg-[--chat-bt-color] color-[--chat-text-color] rounded-8px custom-shadow p-[10px_11px];
|
||||
svg {
|
||||
@apply size-18px;
|
||||
}
|
@ -129,7 +129,7 @@ const examplesList: Example = [
|
||||
|
||||
<style lang="scss">
|
||||
.examples {
|
||||
@apply w-300px h-fit rounded-12px p-10px box-border cursor-pointer border-(solid 1px [--line-color]) shadow-md;
|
||||
@apply w-300px h-fit rounded-12px p-10px box-border cursor-pointer border-(solid 1px [--line-color]) custom-shadow;
|
||||
&:hover {
|
||||
.search-item:not(:hover) {
|
||||
@apply blur-md scale-94;
|
@ -1,5 +1,7 @@
|
||||
<template>
|
||||
<div class="flex truncate p-[14px_20px] justify-between items-center gap-50px">
|
||||
<div
|
||||
style="box-shadow: var(--shadow-enabled) 4px 4px var(--box-shadow-color)"
|
||||
class="flex border-b-(1px solid [--line-color]) truncate p-[14px_20px] justify-between items-center gap-50px">
|
||||
<n-flex :size="10" vertical class="truncate">
|
||||
<p class="text-(22px [--chat-text-color]) truncate font-bold">设置</p>
|
||||
<p class="text-(14px #707070)">所有设置选项</p>
|
||||
@ -11,15 +13,14 @@
|
||||
</div>
|
||||
</n-flex>
|
||||
</div>
|
||||
<div class="h-1px bg-[--line-color]"></div>
|
||||
|
||||
<!-- 设置的主体内容 -->
|
||||
<n-scrollbar style="max-height: calc(100vh - 104px)">
|
||||
<n-flex vertical :size="20" class="p-[20px_0] shadow-inner">
|
||||
<n-flex vertical :size="20" class="p-[20px_0]">
|
||||
<div v-for="(key, index) in content" :key="index" class="flex flex-1 p-[0_20px]">
|
||||
<n-flex
|
||||
vertical
|
||||
class="w-full h-fit bg-[--bg-setting-item] border-(solid 1px [--line-color]) shadow-md rounded-8px p-10px">
|
||||
class="w-full h-fit bg-[--bg-setting-item] border-(solid 1px [--line-color]) custom-shadow rounded-8px p-10px">
|
||||
<n-flex vertical justify="center" v-for="(item, index) in key" :key="index">
|
||||
<n-flex justify="space-between" :size="0" align="center" class="p-8px">
|
||||
<n-flex vertical :size="4">
|
||||
@ -52,7 +53,7 @@ const handleClose = () => {
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.right-btn {
|
||||
@apply size-fit cursor-pointer bg-[--chat-bt-color] color-[--chat-text-color] rounded-8px shadow-md p-[10px_11px];
|
||||
@apply size-fit border-(1px solid [--line-color]) cursor-pointer bg-[--chat-bt-color] color-[--chat-text-color] rounded-8px custom-shadow p-[10px_11px];
|
||||
svg {
|
||||
@apply size-18px;
|
||||
}
|
@ -63,8 +63,8 @@ const loading = ref(true)
|
||||
const loadText = ref('加载中...')
|
||||
const QRCode = ref()
|
||||
const scanStatus = ref<{
|
||||
status: 'error' | 'success'
|
||||
icon: 'cloudError' | 'success'
|
||||
status: 'error' | 'success' | 'auth'
|
||||
icon: 'cloudError' | 'success' | 'Security'
|
||||
text: string
|
||||
show: boolean
|
||||
}>({ status: 'success', icon: 'success', text: '扫码成功', show: false })
|
||||
@ -72,7 +72,7 @@ const scanStatus = ref<{
|
||||
watchEffect(() => {
|
||||
// 等待授权中
|
||||
if (loginStatus.value === LoginStatus.Waiting) {
|
||||
loadText.value = '等待授权...'
|
||||
handleAuth()
|
||||
}
|
||||
})
|
||||
|
||||
@ -111,6 +111,18 @@ const handleError = (e: any) => {
|
||||
loadText.value = '请稍后再试'
|
||||
}
|
||||
|
||||
/** 处理授权场景 */
|
||||
const handleAuth = () => {
|
||||
loading.value = false
|
||||
scanStatus.value = {
|
||||
status: 'auth',
|
||||
icon: 'Security',
|
||||
text: '扫码成功,等待授权',
|
||||
show: true
|
||||
}
|
||||
loadText.value = '等待授权...'
|
||||
}
|
||||
|
||||
// TODO 做一个二维码过期时间重新刷新二维码的功能 (nyh -> 2024-01-27 00:37:18)
|
||||
onMounted(() => {
|
||||
if (!localStorage.getItem('wsLogin')) {
|
@ -11,6 +11,15 @@ export default defineConfig({
|
||||
},
|
||||
presets: [presetUno({ dark: 'class' })],
|
||||
transformers: [transformerDirectives(), transformerVariantGroup()],
|
||||
/** 自定义规则 */
|
||||
rules: [
|
||||
[
|
||||
/^custom-shadow$/,
|
||||
() => ({
|
||||
'box-shadow': 'var(--shadow-enabled) 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)'
|
||||
})
|
||||
]
|
||||
],
|
||||
/**
|
||||
* 快捷键命名标准
|
||||
* @default '布局样式 - 水平样式 - 垂直样式'
|
||||
|
Loading…
Reference in New Issue
Block a user