摸大鱼

 electron:打开链接默认将会使用内嵌打开
💄 一点点样式修正
📝 船新的 README
This commit is contained in:
stapxs 2023-03-07 15:57:33 +08:00
parent 9f1cf5d4c3
commit 7ccb1fc72b
15 changed files with 138 additions and 89 deletions

150
README.md
View File

@ -1,120 +1,114 @@
# stapxs-qq-lite
<p align="center">
<a href="https://blog.stapxs.cn" target="blank">
<img src="public/img/icons/icon.svg" alt="Logo" width="156" height="156">
</a>
<h2 align="center" style="font-weight: 600">Stapxs QQ Lite 2.0</h2>
简体中文 | [English (US)](README/en_US.md)
<br>
<br>
[![old](README/to-old-ver.png)](https://github.com/Stapxs/Stapxs-QQ-Lite)
<p align="center">
一个兼容 oicq-http2 的非官方网页 QQ 客户端
<br />
<a href="https://stapxs.github.io/Stapxs-QQ-Lite-2.0/" target="blank"><strong>🌎 访问 DEMO</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;
<a href="https://github.com/Stapxs/Stapxs-QQ-Lite-2.0/releases" target="blank"><strong>📦️ 下载程序</strong></a>&nbsp;&nbsp;|&nbsp;&nbsp;
<a href="https://github.com/Stapxs/Stapxs-QQ-Lite-2.0/issues/new?assignees=Stapxs&labels=%3Abug%3A+%E9%94%99%E8%AF%AF&template=----.md&title=%5B%E9%94%99%E8%AF%AF%5D" target="blank"><strong>💬 反馈问题</strong></a>
<br />
<br />
<strong>本网页应用仅供学习交流使用,请勿用于其他用途</strong><br>
<strong>版权争议请提出 issue 协商</strong>
</p>
</p>
![card](README/card.png)
<div align="center">
<h1>Stapxs QQ Lite 2.0</h1>
<h3>这是一个兼容 oicq-http2 的非官方网页版 QQ 客户端</h3>
<h3>使用 Vue 重新编写的全新版本</h3><br>
<strong>本网页应用仅供学习交流使用,请勿用于其他用途</strong><br>
<strong>版权争议请提出 issue 协商</strong>
</div>
## ✨ 特性支持
- ✅ 使用 Vue.js 全家桶开发,快乐前后端分离
- 🎨 自适应布局,竖版也能使用
- 🖥️ 支持 PWA都有 Electron 了(小声))
- 🌚 Light/Dark Mode 自动切换
- 🍱 该有的都有(虽然比不过官方端)
- 复杂消息显示、转发、回复、撤回
- 群文件、群公告、群设置(一小部分)、精华消息
- 图片、收藏表情、文件发送
- 📦️ 支持多种 bot我就是要用
- 🔥 水生火热但是更好看的 Electron 客户端
- 🥚 彩蛋!来更多的彩蛋!
- 🛠 更多特性开发中
## 快速使用
### 访问应用 - GitHub Pages
本仓库开启了 GitHub Pages, 所有向主分支提交的代码将会自动构建并发布。你可以直接访问 [这儿](https://stapxs.github.io/Stapxs-QQ-Lite-2.0) 来使用已经构建并部署的页面。
## ♿️ 快速使用
### > 运行服务
Stapxs QQ Lite 需要一个 QQ Bot 后端提供服务,你可以参考 [📖 这个文档](https://github.com/Stapxs/Stapxs-QQ-Lite-2.0/wiki/%E8%BF%9E%E6%8E%A5-oicq2-http) 布置它
### 运行服务 - oicq2-http
Stapxs QQ Lite 需要一个 QQ Bot 来支持功能,目前 2.0 版本只支持定制的 oicq-http2 克隆版本,你可以在 [这个仓库](https://github.com/Tim-Paik/oicq-http2) 下载到它
### > 访问应用
本仓库开启了 GitHub Pages, 所有向主分支提交的代码将会自动构建并发布。你可以直接访问 [🌎 这个页面](https://stapxs.github.io/Stapxs-QQ-Lite-2.0) 来使用已经构建并部署的页面
在开始运行服务前,请确认你的设备已经安装了 `yarn` 以用于运行在终端cmd、powershell、zsh 等)运行 `yarn -v` 来确认安装。
### > 安装应用
除了直接使用本仓库的构建页面,你也可以下载使用 electron 打包的功能**稍稍**更丰富的客户端版本,访问 [📦️ 这儿](https://github.com/Stapxs/Stapxs-QQ-Lite-2.0/releases) 查看构建列表。
如果一切没问题,那么将终端转到 oicq 的目录下,开始运行 Bot 服务。
## 💬 提醒和问题
- 更新依赖
运行 `yarn` 指令, yarn 将会为仓库进行初始化安装运行所需要的依赖:
### > 关于不安全连接
- 当使用 https 页面连接 ws 服务(反之相同)的情况下,连接将会失败;这是由于其中某一者是不安全的。在这种情况下,你可以选择将 ws 提升为 wss 或者将 https 降级为 http不安全来解决问题此处不提供解决方案。>> [Stapxs-QQ-Lite#32](https://github.com/Stapxs/Stapxs-QQ-Lite/issues/32)
![yarn](README/yarn.png)
- 配置设置文件
在 oicq 的目录下有一个示例的设置文件 `config.json`,你可以修改它来配置相关设置,你需要保证使用 Websocket 的选项是打开的,并且为了安全性需要设置连接密钥:
![config](README/config.png)
完成修改后把它放置到用户目录下的 `.oicq` 文件夹里Windows 用户应该为:`C://User/[name]/.oicq`Linux 用户则是:`/home/[name]/.oicq`,如果文件夹不存在请自行创建。
- 启动服务
准备工作都完成了。下面回到终端,运行指令 `yarn start [QQ]` 启动服务:
![run](README/run.png)
如果没什么问题的话, oicq-http2 应该会像上面这样完成启动,就快完成了!
- 连接服务
回到应用页面,输入地址和密钥连接服务,大功告成!
![connect](README/connect.png)
![connect](README/home.png)
## 其他提醒
### 关于不安全连接
- 当使用 https 页面连接 ws 服务(反之相同)的情况下,连接将会失败;这是由于其中某一者是不安全的。在这种情况下,你可以选择将 ws 提升为 wss 或者将 https 降级为 http不安全来解决问题此处不提供解决方案。[Stapxs-QQ-Lite#32](https://github.com/Stapxs/Stapxs-QQ-Lite/issues/32)
## 更多问题
### 我能使用其他 QQ Http Bot 吗
### > 我能使用其他 QQ Http Bot 吗
- 如果它兼容 [OneBot 11 协议](<https://github.com/botuniverse/onebot-11>), 你可以尝试连接它, 但是由于消息体格式和接口扩展的差异,大部分情况下都不能完全正常使用。
- 以下是已兼容的 Bot:
已经兼容的 Bot 都写在了文档里,可以去 [这里](https://github.com/Stapxs/Stapxs-QQ-Lite-2.0/wiki) 查看。
- [oicq http v2](https://github.com/Tim-Paik/oicq-http2):完全兼容
- 以下是计划兼容的 Bot
- [oicq http v1](https://github.com/takayama-lily/oicq/tree/master/http-api):完全兼容计划
- [go-cqhttp](https://github.com/Mrs4s/go-cqhttp):完全兼容计划
### 使用 Bot 是否有风险
### > 使用 Bot 是否有风险
- 如果你使用的是 oicq-http, 可以查看此处了解 [使用风险](<https://github.com/takayama-lily/oicq/wiki/98.%E5%85%B3%E4%BA%8E%E8%B4%A6%E5%8F%B7%E5%86%BB%E7%BB%93%E5%92%8C%E9%A3%8E%E6%8E%A7>), 如果你尝试使用其他 QQ Bot (参见上一条问题), 请自行参考它的文档。
### 我遇到了问题
### > 我遇到了问题
- 如果有什么奇奇怪怪的问题, 欢迎发起 [issue](<https://github.com/Stapxs/Stapxs-QQ-Lite/issues>) 询问! 如果有什么 BUG 和优化建议也可以哦!
## 构建应用
Stapxs QQ Lite 2.0 是一个基于 Vue 的单页应用,这意味着如果你想自行部署服务需要进行构建,你可以参考下面的 bash 进行构建:
## 📦️ 构建应用
### > 构建 Web 页面
Stapxs QQ Lite 2.0 是一个基于 Vue 的单页应用,这意味着如果你想自行部署到网页服务需要进行构建。
注意。在正式构建前,如果你的网站运作目录并不在根域名下,你需要修改(或增加)项目根目录下 `vue.config.js` 内导出的 `publicPath` 字段的值,它代表着最终你会运行在的目录,比如它在现在是 `/Stapxs-QQ-Lite-2.0/`;如果你本来就运行在根目录下,可以直接删去它。
如果你想使用 GitHub Action 自动构建本项目,可以直接参考本项目的自动构建配置,它在 [这儿](.github/workflows//vue-build.yml)。
下面是构建 Vue 应用的命令,构建结果将最终输出在 `dist` 下:
``` bash
# 安装依赖
yarn install
# 运行本地调试
yarn serve
# 代码检查和自动格式化
yarn lint
# 构建应用
yarn build
# 代码检查
yarn lint
```
### > 构建 Electron 客户端
`2.3.0` 版本后Stapxs QQ Lite 2.0 支持构建为 Electron 应用并补充部分平台特性的功能,你也可以自行构建。
下面是构建 Electron 应用的命令,构建结果将最终输出在 `dist_electron/out` 下:
``` bash
# electron 运行本地调试
yarn electron:serve
# electron 构建应用
yarn electron:build
```
你可以补充平台选项来指定构建某个平台:
\* 你可以查看文档了解构建流程详情 [vue-cli 文档](https://cli.vuejs.org/config/)。
```bash
yarn electron:build --linux
```
### > Github Actions
*为什么会有人对自动构建感兴趣,总之自动构建脚本我写了好久。感兴趣就自己去看好了(无端)。*
这是构建的一个例子:
![auto-build](README/auto-build.png)
![build](README/build.png)
## 🖼️ 截图
![1](README/pics/1.png)
![2](README/pics/2.png)
![3](README/pics/4.png)
![3](README/pics/3.png)
## 🎉 鸣谢
感谢这些小伙伴们在开发和文本中提供的支持 ——
## 鸣谢
<a href="https://github.com/Logic-Accepted"><img src="https://avatars.githubusercontent.com/u/36406453?s=48&v=4"></a>
<a href="https://github.com/doodlehuang"><img src="https://avatars.githubusercontent.com/u/25525621?s=48&v=4"></a>

BIN
README/auto-build.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
README/pics/1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 251 KiB

BIN
README/pics/2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

BIN
README/pics/3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

BIN
README/pics/4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

View File

@ -147,7 +147,7 @@
</TransitionGroup>
<Transition>
<div class="pop-box" v-if="runtimeData.popBoxList.length > 0">
<div class="pop-box-body ss-card"
<div :class="'pop-box-body ss-card' + (runtimeData.popBoxList[0].full ? ' full' : '')"
:style="'transform: translate(-50%, calc(-50% - ' + ((runtimeData.popBoxList.length > 3 ? 3 : runtimeData.popBoxList.length) * 10) + 'px))'">
<header v-show="runtimeData.popBoxList[0].title != undefined">
<div

View File

@ -345,7 +345,7 @@ textarea {
.merge-pan.show > div:last-child {
flex-direction: column;
display: flex;
width: 50%;
width: 450px;
}
.merge-pan > div:last-child > div:first-child {
align-items: center;

View File

@ -596,10 +596,17 @@ html, body {
margin-top: calc(50vh - 35px);
transition: all .3s;
position: absolute;
width: fit-content;
margin-left: 50%;
width: 360px;
min-width: 360px;
z-index: 20;
}
.pop-box-body.full {
height: calc(100vh - 120px);
transform: unset !important;
margin: 60px 20px 0 20px;
width: calc(100% - 80px);
}
.pop-box-body > header {
align-items: center;
display: flex;

View File

@ -278,5 +278,7 @@
"option_view_no_window": "去除窗口框架",
"option_view_no_window_tip": "沉浸式没圆角体验(无端",
"option_dev_restart": "重启应用",
"option_dev_restart_tip": "99% 的特性都能通过重启解决!"
"option_dev_restart_tip": "99% 的特性都能通过重启解决!",
"btn_close": "关闭",
"btn_open": "打开"
}

View File

@ -7,7 +7,7 @@ import path from 'path'
import installExtension, { VUEJS3_DEVTOOLS } from 'electron-devtools-installer'
import { Menu } from 'electron'
import { Menu, session } from 'electron'
import { app, protocol, BrowserWindow } from 'electron'
import { createProtocol } from 'vue-cli-plugin-electron-builder/lib'
@ -25,7 +25,7 @@ async function createWindow() {
Menu.setApplicationMenu(null)
regIpcListener()
// 创建窗口
let mainWindowState = windowStateKeeper({
const mainWindowState = windowStateKeeper({
defaultWidth: 1200,
defaultHeight: 800
})
@ -56,6 +56,15 @@ async function createWindow() {
win.loadURL('app://./index.html')
}
console.log('应用加载完成')
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
if(details.responseHeaders) {
// 绕过 CSP 限制X-Frame-Options 限制
details.responseHeaders['content-security-policy'] = ['*']
delete details.responseHeaders['x-frame-options']
}
callback({ cancel: false, responseHeaders: details.responseHeaders })
})
}
app.on('window-all-closed', () => {

View File

@ -37,7 +37,7 @@
<div v-for="(item, index) in data.message" :class="View.isMsgInline(item.type) ? 'msg-inline' : ''" :key="data.message_id + '-m-' + index">
<div v-if="item.type === undefined" ></div>
<span v-else-if="isDebugMsg" class="msg-text">{{ item }}</span>
<span v-else-if="item.type == 'text'" v-show="item.text !== ''" class="msg-text" v-html="parseText(item.text)"></span>
<span v-else-if="item.type == 'text'" @click="textClick" v-show="item.text !== ''" class="msg-text" v-html="parseText(item.text)"></span>
<img v-else-if="item.type == 'image'" :title="$t('chat_view_pic')" :alt="$t('chat_group_pic')" @load="scrollButtom" @error="imgLoadFail" @click="imgClick(data.message_id)" :class="imgStyle(data.message.length, index, item.asface)" :src="item.url">
<img v-else-if="item.type == 'face'" :alt="item.text" class="msg-face" :src="require('./../assets/img/qq-face/' + item.id + '.gif')" :title="item.text">
<span v-else-if="item.type == 'bface'" style="font-style: italic;opacity: 0.7;">[ {{ $t('chat_fun_menu_pic') }}{{ item.text }} ]</span>
@ -306,7 +306,7 @@ export default defineComponent({
text = ViewFuns.parseText(text)
//
const reg = /(http|ftp|https):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-\.,@?^=%&:/~\+#]*[\w\-\@?^=%&/~\+#])?/gi //eslint-disable-line
text = text.replaceAll(reg, '<a href="$&" target="_blank">$&</a>')
text = text.replaceAll(reg, '<a href="" data-link="$&" onclick="return false">$&</a>')
let linkList = text.match(reg)
if (linkList !== null && !this.gotLink) {
this.gotLink = true
@ -444,6 +444,19 @@ export default defineComponent({
fid: data.fid,
md5: data.md5
}, 'getVideoUrl_' + message_id)
},
/**
* 文本消息被点击
* @param event 事件
*/
textClick(event: Event) {
const target = event.target as HTMLElement
if(target.dataset.link) {
//
const link = target.dataset.link
Util.openLink(link)
}
}
},
mounted () {

View File

@ -86,8 +86,8 @@ export default function regIpcListener() {
ipcMain.on('sys:download', (evt, args) => {
const downloadPath = args.downloadPath
const fileName = args.fileName
let ext = path.extname(fileName)
let filters = [{ name: '全部文件', extensions: ['*'] }]
const ext = path.extname(fileName)
const filters = [{ name: '全部文件', extensions: ['*'] }]
if (ext && ext !== '.' && ext != null) {
const array = ext.match(/[a-zA-Z]+$/)
if (array) {

View File

@ -37,6 +37,7 @@ export interface RunTimeDataElem {
template?: any, // 填充模板(如果都有,优先填充 html
templateValue?: any, // 模板 props
data?: any, // 模板的附加传参,只有这一个
full?: boolean, // 是否填充整个页面
button?: { // 按钮
master?: boolean, // 是否高亮(主按钮)
fun?: (value: any) => void, // 按钮回调

View File

@ -79,7 +79,30 @@ export function isExternal(path: string): boolean {
* @param url
*/
export function openLink(url: string) {
window.open(url)
// 判断是不是 Electron是的话打开内嵌 iframe
if(runtimeData.tags.isElectron) {
const popInfo = {
html: `<iframe src="${url}" style="width: calc(100% + 80px);border: none;margin: -40px -40px -20px -40px;height: calc(100vh - 145px);border-radius: 7px;"></iframe>`,
full: true,
button: [
{
text: app.config.globalProperties.$t('btn_open'),
fun: () => {
window.open(url)
runtimeData.popBoxList.shift()
}
},
{
text: app.config.globalProperties.$t('btn_close'),
master: true,
fun: () => { runtimeData.popBoxList.shift() }
}
]
}
runtimeData.popBoxList.push(popInfo)
} else {
window.open(url)
}
}
/**