Compare commits
405 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
40b637b16e | ||
|
|
6e68f399b4 | ||
|
|
0be6c70e92 | ||
|
|
6c2d8fc163 | ||
|
|
a8193b8feb | ||
|
|
34159caf22 | ||
|
|
c75f406459 | ||
|
|
99dca06d44 | ||
|
|
d12c0c4207 | ||
|
|
915a5ed7d5 | ||
|
|
7bfc43c85f | ||
|
|
77ea022ddf | ||
|
|
93578f93f4 | ||
|
|
f129615ebe | ||
|
|
0e5b44baad | ||
|
|
3596475790 | ||
|
|
6218521dea | ||
|
|
65db8b5703 | ||
|
|
f5ff9a3648 | ||
|
|
cbbd50a2e3 | ||
|
|
b04647e65a | ||
|
|
d34d94faa6 | ||
|
|
4038d9560f | ||
|
|
006fc43498 | ||
|
|
47c9b2e1b0 | ||
|
|
dc3e5f0a59 | ||
|
|
01bda83fcd | ||
|
|
9ecb9c68fb | ||
|
|
4612d5180a | ||
|
|
cfb653796c | ||
|
|
d00cd5cb26 | ||
|
|
285a62c87e | ||
|
|
bcb0c6bc77 | ||
|
|
d1ab2d98eb | ||
|
|
c3d5328154 | ||
|
|
fc30588014 | ||
|
|
65b02001b2 | ||
|
|
cd011a172f | ||
|
|
bf913d9eff | ||
|
|
c2dd15fca1 | ||
|
|
b267863b58 | ||
|
|
d189fb100a | ||
|
|
dc6c5bef26 | ||
|
|
7208d51644 | ||
|
|
16359a968d | ||
|
|
d553f77533 | ||
|
|
bc25f5dfdf | ||
|
|
d40028340c | ||
|
|
4194d1cddd | ||
|
|
1fdd532133 | ||
|
|
71c62a3772 | ||
|
|
9be6cd5148 | ||
|
|
c6568969c7 | ||
|
|
f5b1a6ab05 | ||
|
|
5efe659cf5 | ||
|
|
b254fd5ce2 | ||
|
|
631a0ffff4 | ||
|
|
8b11e9a19e | ||
|
|
f6b006b000 | ||
|
|
3a26f420b8 | ||
|
|
0919e415ec | ||
|
|
030a07698d | ||
|
|
a7f2582df7 | ||
|
|
5f0a0e0371 | ||
|
|
28717fd0c7 | ||
|
|
7014ea176a | ||
|
|
b4f2da66be | ||
|
|
b53462cf6e | ||
|
|
8b40364722 | ||
|
|
6ee1824410 | ||
|
|
f63c2da37a | ||
|
|
9be0642ba5 | ||
|
|
55a922c7b3 | ||
|
|
50893929d6 | ||
|
|
03c94e791a | ||
|
|
96bb554813 | ||
|
|
1bc77de144 | ||
|
|
aa07c78fc8 | ||
|
|
52dda88d40 | ||
|
|
c555b309bd | ||
|
|
0a51225762 | ||
|
|
fab49b1dda | ||
|
|
57e422f2d3 | ||
|
|
50a1a3147e | ||
|
|
277115a30f | ||
|
|
7464de3adc | ||
|
|
3d725ddeef | ||
|
|
38d8f289e4 | ||
|
|
edfd6e6de2 | ||
|
|
a68ab6512e | ||
|
|
8383b88a44 | ||
|
|
27ff24f44e | ||
|
|
b111ecb227 | ||
|
|
ac17952cd3 | ||
|
|
e24978fdd7 | ||
|
|
a4d7579e3f | ||
|
|
52171b794a | ||
|
|
3c33f02e9d | ||
|
|
0a8823c40b | ||
|
|
a3f7e71638 | ||
|
|
7ebf4fb9ce | ||
|
|
c96bad3cdf | ||
|
|
0968c43f61 | ||
|
|
ae147c76ff | ||
|
|
0e916a2804 | ||
|
|
494565e131 | ||
|
|
c4430e1a6c | ||
|
|
26adfa11bf | ||
|
|
69ec57669e | ||
|
|
3556133585 | ||
|
|
a65181757d | ||
|
|
42d39a830e | ||
|
|
2e70c9617c | ||
|
|
6230bf94c5 | ||
|
|
47832ececb | ||
|
|
60e6003485 | ||
|
|
9133f289b4 | ||
|
|
76570e2f1b | ||
|
|
c9234a4b49 | ||
|
|
c1361fadda | ||
|
|
ec7af94f71 | ||
|
|
81690d6ce9 | ||
|
|
236b57864b | ||
|
|
22259ec34d | ||
|
|
5a4700753a | ||
|
|
cc862741dc | ||
|
|
779b32e8ad | ||
|
|
e3ce3bcfbe | ||
|
|
673053f181 | ||
|
|
b6eb77ae63 | ||
|
|
0e63255a7f | ||
|
|
f42408a363 | ||
|
|
897fc51ce3 | ||
|
|
6848b126c5 | ||
|
|
0ed9afd1bd | ||
|
|
26cca8298f | ||
|
|
58407af2ba | ||
|
|
3a0473a74f | ||
|
|
6e5124fe22 | ||
|
|
02bd022c62 | ||
|
|
f2538884ea | ||
|
|
8d121d4056 | ||
|
|
96438604ee | ||
|
|
63ccd675d0 | ||
|
|
2c08145c40 | ||
|
|
12effb5738 | ||
|
|
91bfb989be | ||
|
|
192de79fea | ||
|
|
82063f1b21 | ||
|
|
02b263439b | ||
|
|
7efaf3bb32 | ||
|
|
7dd5b082cf | ||
|
|
6320eaa3ac | ||
|
|
85b88b6b61 | ||
|
|
f285665f90 | ||
|
|
8f4399dc2f | ||
|
|
c8b8cc578d | ||
|
|
a142f52113 | ||
|
|
aa666a9662 | ||
|
|
0a4ac6abb7 | ||
|
|
96b0cb8aa0 | ||
|
|
b3a30720fa | ||
|
|
b711605bdc | ||
|
|
31efee2e97 | ||
|
|
569af135bd | ||
|
|
2975a0eaf9 | ||
|
|
d4ee87f324 | ||
|
|
c676a3037c | ||
|
|
e4790062c8 | ||
|
|
bb8a6982d0 | ||
|
|
80af98111b | ||
|
|
9a69d20949 | ||
|
|
5e52996a9e | ||
|
|
33d22d4970 | ||
|
|
170473fb2d | ||
|
|
67ccaea41e | ||
|
|
67d7e81ffa | ||
|
|
1788b40431 | ||
|
|
7f432cefb9 | ||
|
|
57e8c9c7cd | ||
|
|
c1b63af5f5 | ||
|
|
cf7f245a49 | ||
|
|
4824f30950 | ||
|
|
88fb1d8e62 | ||
|
|
e67ce9a438 | ||
|
|
976b9690d2 | ||
|
|
36735ace50 | ||
|
|
3aeea13526 | ||
|
|
6f33c3f5d6 | ||
|
|
53aab1ed0f | ||
|
|
b209040978 | ||
|
|
e74aeb9393 | ||
|
|
e53242613b | ||
|
|
bea7ba00f0 | ||
|
|
24d90b93e2 | ||
|
|
f380b0433d | ||
|
|
f7df6408ed | ||
|
|
10a77ee2a9 | ||
|
|
d5db894891 | ||
|
|
5a44076859 | ||
|
|
e78513cb80 | ||
|
|
2860c4cbe6 | ||
|
|
ebce9fa596 | ||
|
|
8080d0bb4e | ||
|
|
221e42d02b | ||
|
|
e06fd21a4b | ||
|
|
f42036c104 | ||
|
|
937bc4ead3 | ||
|
|
322a855ba2 | ||
|
|
7c4d537d67 | ||
|
|
b78e4240cb | ||
|
|
4f663dd761 | ||
|
|
b3bd5aded5 | ||
|
|
7714c53085 | ||
|
|
3a74cdc98b | ||
|
|
3631f511d4 | ||
|
|
5f7d528d9d | ||
|
|
85ceb8b938 | ||
|
|
b4b268a4d7 | ||
|
|
4b39f13fa9 | ||
|
|
4abcec08f4 | ||
|
|
4144f92631 | ||
|
|
fb8d759103 | ||
|
|
e215cda700 | ||
|
|
846fdcf145 | ||
|
|
ecdabc668d | ||
|
|
e8839974d4 | ||
|
|
2a864b6617 | ||
|
|
ada88a1c02 | ||
|
|
8fe16416f9 | ||
|
|
0daf06c06d | ||
|
|
3b697e7400 | ||
|
|
a543f8716b | ||
|
|
63703a029f | ||
|
|
22415e6c61 | ||
|
|
1a69e76fe7 | ||
|
|
7f916c4770 | ||
|
|
f76d36a74b | ||
|
|
ab0539a263 | ||
|
|
4104dea68e | ||
|
|
5aded9daa3 | ||
|
|
91d5bd80ff | ||
|
|
40d56a0155 | ||
|
|
54117fe51a | ||
|
|
fbd662e400 | ||
|
|
ccb31a81f8 | ||
|
|
dbb9366de6 | ||
|
|
6d7a4edae3 | ||
|
|
632068a74c | ||
|
|
4e78920f99 | ||
|
|
fdc85bbcbf | ||
|
|
67dafae9d6 | ||
|
|
989e5a5f9d | ||
|
|
a7e5bd0b80 | ||
|
|
da131746be | ||
|
|
8a7e80fe86 | ||
|
|
865dc61cd1 | ||
|
|
c8b96a8bce | ||
|
|
5546dbaa0e | ||
|
|
fd6312408b | ||
|
|
4f4c6de8a2 | ||
|
|
4506ba8cd3 | ||
|
|
9300e9fd9a | ||
|
|
a4eb8317da | ||
|
|
0e819de1bc | ||
|
|
9800f9e3da | ||
|
|
a0f6a17005 | ||
|
|
6087c7fed0 | ||
|
|
3fa0b472d2 | ||
|
|
1ce96ddae6 | ||
|
|
d4ef140c8e | ||
|
|
7de575e236 | ||
|
|
f0f0883a88 | ||
|
|
c1695a78d6 | ||
|
|
15e37eded3 | ||
|
|
57cd91e6d4 | ||
|
|
a178334d8e | ||
|
|
dd8ba7e8da | ||
|
|
d26df91960 | ||
|
|
f249763d41 | ||
|
|
1bada9ab30 | ||
|
|
a185ab2973 | ||
|
|
ce83bef0ed | ||
|
|
66135d8222 | ||
|
|
e99e069e55 | ||
|
|
327cdbc873 | ||
|
|
6eabba9679 | ||
|
|
c99f6cfcf2 | ||
|
|
0579a73c1c | ||
|
|
12b3c14299 | ||
|
|
c21da4292b | ||
|
|
3f9cdfd887 | ||
|
|
8dac2bc444 | ||
|
|
13ec6ec323 | ||
|
|
59aa854470 | ||
|
|
e0c3ea4456 | ||
|
|
d6a3727713 | ||
|
|
48cd32742c | ||
|
|
852ceba828 | ||
|
|
905c8be6eb | ||
|
|
fad98dcc9d | ||
|
|
8b5409de5a | ||
|
|
bcf1ad0870 | ||
|
|
617e88e0b5 | ||
|
|
c9e0840173 | ||
|
|
5e4f99da6c | ||
|
|
28bc303fcf | ||
|
|
91c63f281b | ||
|
|
7b3769b1db | ||
|
|
211f9f0c15 | ||
|
|
37ccf4dacb | ||
|
|
971167cad3 | ||
|
|
332bed3136 | ||
|
|
e2a9906de0 | ||
|
|
c5879e4376 | ||
|
|
22324f4c16 | ||
|
|
fa9c3b4f2f | ||
|
|
f411f17386 | ||
|
|
ab3a82300c | ||
|
|
dbb9162267 | ||
|
|
84d3e4f617 | ||
|
|
6209b53321 | ||
|
|
62a2bcf71d | ||
|
|
cdefe9d4a7 | ||
|
|
2bebad1112 | ||
|
|
9f186f1e9c | ||
|
|
a6873302f3 | ||
|
|
615f40d458 | ||
|
|
c4b49b34b8 | ||
|
|
d12fb47902 | ||
|
|
21132f475a | ||
|
|
a55e0a457d | ||
|
|
fa7b049316 | ||
|
|
f8f5bc476b | ||
|
|
e5c622cb89 | ||
|
|
aa70c41041 | ||
|
|
3cce9b67d4 | ||
|
|
c0342ea6d1 | ||
|
|
2813f4c062 | ||
|
|
9ca9de0d7e | ||
|
|
603db9de7f | ||
|
|
c4e72507e0 | ||
|
|
d06d1c177c | ||
|
|
4ff1cf68fc | ||
|
|
8d92933e43 | ||
|
|
9497fb1bb6 | ||
|
|
fc65d56977 | ||
|
|
fe4cba61e2 | ||
|
|
8144bea613 | ||
|
|
7a431d86d2 | ||
|
|
7ecfd86ffa | ||
|
|
66b9e7e9b3 | ||
|
|
6bed109f97 | ||
|
|
fbc5eed5c5 | ||
|
|
43b665652e | ||
|
|
5760d3ef0f | ||
|
|
e712b99287 | ||
|
|
85d88b6800 | ||
|
|
1a62a47935 | ||
|
|
689d842d58 | ||
|
|
8215e73a95 | ||
|
|
d3fc274f08 | ||
|
|
e4bcb8b518 | ||
|
|
9a942c483d | ||
|
|
e9fd223808 | ||
|
|
5dfc66fc21 | ||
|
|
bab82dc290 | ||
|
|
4c1125b9e1 | ||
|
|
85ef2d9687 | ||
|
|
b7fc815d58 | ||
|
|
ad1cc964c9 | ||
|
|
96a2b250a3 | ||
|
|
d72ab58f98 | ||
|
|
abd453f2f6 | ||
|
|
4b7283dbe8 | ||
|
|
f5a068fffc | ||
|
|
faf5dec08a | ||
|
|
e4070e249d | ||
|
|
fe5ec9677a | ||
|
|
5fdd5adef8 | ||
|
|
bc250ad4b8 | ||
|
|
22050b7488 | ||
|
|
6df906aa24 | ||
|
|
d2f20128bb | ||
|
|
cef19488d2 | ||
|
|
0ceb2de79d | ||
|
|
79feaaf801 | ||
|
|
34c005001d | ||
|
|
3508d7a472 | ||
|
|
1e58587b1c | ||
|
|
e99f952c28 | ||
|
|
b0742021b6 | ||
|
|
5e784f64a6 | ||
|
|
32aae08ef2 | ||
|
|
af46fc501b | ||
|
|
07c3a958fa | ||
|
|
ecdbf8765f | ||
|
|
5682943c24 | ||
|
|
5fbc5d3164 | ||
|
|
90af11a842 | ||
|
|
b644a65f22 | ||
|
|
6db82c8176 | ||
|
|
fa149fcaa9 | ||
|
|
9772e4b48a | ||
|
|
420e1a9d63 |
@@ -10,6 +10,7 @@ APP_URL=http://localhost
|
||||
APP_ID=
|
||||
APP_IPPR=
|
||||
APP_PORT=2222
|
||||
APP_SSL_PORT=
|
||||
APP_DEV_PORT=
|
||||
|
||||
LOG_CHANNEL=stack
|
||||
@@ -56,9 +57,6 @@ PUSHER_APP_KEY=
|
||||
PUSHER_APP_SECRET=
|
||||
PUSHER_APP_CLUSTER=mt1
|
||||
|
||||
JUKE_KEY_JOKE=
|
||||
JUKE_KEY_SOUP=
|
||||
|
||||
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
|
||||
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Publish Desktop
|
||||
name: Publish Mac
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -6,7 +6,7 @@ on:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
Build:
|
||||
runs-on: macos-latest
|
||||
environment: build
|
||||
|
||||
@@ -15,10 +15,10 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Use Node.js 16.x
|
||||
- name: Use Node.js 20.x
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 16.x
|
||||
node-version: 20.x
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
@@ -29,5 +29,6 @@ jobs:
|
||||
DP_KEY: ${{ secrets.DP_KEY }}
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
GH_REPOSITORY: ${{ github.repository }}
|
||||
run: ./cmd electron all
|
||||
run: |
|
||||
./cmd electron mac
|
||||
|
||||
30
.github/workflows/publish-desktop-win.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
||||
name: Publish Win
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
Build:
|
||||
runs-on: windows-latest
|
||||
environment: build
|
||||
|
||||
if: startsWith(github.event.ref, 'refs/tags/v')
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Use Node.js 20.x
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 20.x
|
||||
|
||||
- name: Build
|
||||
env:
|
||||
DP_KEY: ${{ secrets.DP_KEY }}
|
||||
GH_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
GH_REPOSITORY: ${{ github.repository }}
|
||||
shell: bash
|
||||
run: |
|
||||
./cmd electron win
|
||||
1
.gitignore
vendored
@@ -1,6 +1,7 @@
|
||||
/node_modules
|
||||
/public/hot
|
||||
/public/tmp
|
||||
/public/summary
|
||||
/public/uploads/*
|
||||
/public/.well-known
|
||||
/public/.user.ini
|
||||
|
||||
27
.prefetch
Normal file
@@ -0,0 +1,27 @@
|
||||
office/web-apps/apps/api/documents/api.js?hash={version}
|
||||
office/7.5.1-23/sdkjs/cell/css/main.css
|
||||
office/7.5.1-23/web-apps/apps/spreadsheeteditor/main/resources/css/app.css
|
||||
office/7.5.1-23/web-apps/vendor/requirejs/require.js
|
||||
office/7.5.1-23/web-apps/apps/spreadsheeteditor/main/app.js
|
||||
office/7.5.1-23/sdkjs/common/AllFonts.js
|
||||
office/7.5.1-23/web-apps/vendor/xregexp/xregexp-all-min.js
|
||||
office/7.5.1-23/web-apps/vendor/socketio/socket.io.min.js
|
||||
office/7.5.1-23/sdkjs/cell/sdk-all-min.js
|
||||
office/7.5.1-23/sdkjs/cell/sdk-all.js
|
||||
office/7.5.1-23/sdkjs/common/libfont/engine/fonts.js
|
||||
office/7.5.1-23/sdkjs/common/Charts/ChartStyles.js
|
||||
office/7.5.1-23/sdkjs/common/libfont/engine/fonts.wasm
|
||||
office/7.5.1-23/web-apps/apps/presentationeditor/main/resources/css/app.css
|
||||
office/7.5.1-23/web-apps/apps/presentationeditor/main/app.js
|
||||
office/7.5.1-23/sdkjs/slide/sdk-all-min.js
|
||||
office/7.5.1-23/sdkjs/slide/sdk-all.js
|
||||
office/7.5.1-23/sdkjs/slide/themes//themes.js
|
||||
office/7.5.1-23/web-apps/apps/documenteditor/main/resources/css/app.css
|
||||
office/7.5.1-23/web-apps/apps/documenteditor/main/app.js
|
||||
office/7.5.1-23/sdkjs/word/sdk-all-min.js
|
||||
office/7.5.1-23/sdkjs/word/sdk-all.js
|
||||
drawio/webapp/js/app.min.js
|
||||
drawio/webapp/js/stencils.min.js
|
||||
drawio/webapp/js/extensions.min.js
|
||||
drawio/webapp/js/shapes-14-6-5.min.js
|
||||
drawio/webapp/math/es5/output/svg/fonts/tex.js
|
||||
488
CHANGELOG.md
@@ -2,6 +2,477 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [0.36.26]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 关闭文件后无法再次打开
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化数据结构
|
||||
- 接龙优化为清空内容默认删除
|
||||
- 支持取消发送中的消息
|
||||
- 1. 强化接龙接口本地时间戳问题 2. 接龙消息点展开按钮后做缓存处理
|
||||
|
||||
## [0.36.15]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 消息太长导致菜单无法正常显示
|
||||
- 项目数量不正确的情况
|
||||
- 部分未读和待办信息不显示的情况
|
||||
- 审批中心修复loadIng效果
|
||||
|
||||
### Performance
|
||||
|
||||
- 自动识别md格式发送
|
||||
- 优化回复、转发消息数据结构
|
||||
- 优化iOS端数据读取失败的情况
|
||||
- 回复消息时自动@提及
|
||||
- 优化会话数据结构
|
||||
|
||||
## [0.35.90]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 子窗口出现重新登录的情况
|
||||
|
||||
### Performance
|
||||
|
||||
- 聊天文件发送进度
|
||||
- 拨打电话确认提示
|
||||
- 优化预加载文件
|
||||
|
||||
## [0.35.84]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 回复消息点击到原文无效
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化接口时间
|
||||
- 优化审批对话按钮配色
|
||||
|
||||
## [0.35.76]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 客户端无法打开excel文件
|
||||
- 修复投票实名逻辑
|
||||
|
||||
### Features
|
||||
|
||||
- 升级okr
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化文件功能按钮
|
||||
- 文件上传支持覆盖上传
|
||||
- 优化app等比显示
|
||||
- 优化发送文件预览
|
||||
- 消息发送中禁止右键菜单
|
||||
- 部分搜索框图标抖动
|
||||
- 优化复制功能
|
||||
- 优化pdf文件预览
|
||||
- 优化投票接口,加上事务锁
|
||||
- 优化接龙接口,加上事务锁
|
||||
- 优化转发消息样式
|
||||
- 接龙接口-强化排序
|
||||
- 审批按钮色微调
|
||||
- 统一审批中心的按钮色
|
||||
- 转发会议亮色皮肤问题,转发文件宽度铺满
|
||||
- 优化app数据交互
|
||||
|
||||
### Styling
|
||||
|
||||
- 调整代码格式
|
||||
|
||||
## [0.35.48]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 修复搜索偶尔无效的情况
|
||||
|
||||
### Features
|
||||
|
||||
- 升级okr
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化文件预览
|
||||
- 滑动列表自动隐藏键盘
|
||||
- 优化时间格式
|
||||
- 适配nodejs 20
|
||||
|
||||
## [0.35.40]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 部分手机出现非正常滚动到底部的情况
|
||||
- Dootask官网标题
|
||||
- Dootask官网调整
|
||||
- 修复导出任务统计没有按创建时间来的bug
|
||||
|
||||
### Features
|
||||
|
||||
- 新增不显示会话功能
|
||||
- 升级okr
|
||||
|
||||
### Performance
|
||||
|
||||
- 修改消息换行优化
|
||||
- 优化预加载资源
|
||||
- 优化pdf文件预览
|
||||
- 优化签到消息
|
||||
- 优化登录
|
||||
- 优化安装脚本
|
||||
- 优化消息时间格式
|
||||
- 优化app功能
|
||||
|
||||
## [0.35.20]
|
||||
|
||||
### Features
|
||||
|
||||
- 发起投票功能添加缓存记录选中效果
|
||||
- 导出的签到数据和审批数据换成xlsx,因老版本的xls会出现兼容性问题
|
||||
|
||||
### Performance
|
||||
|
||||
- 默认映射443端口
|
||||
- 优化子窗口
|
||||
- 优化发布接口 删除目录的逻辑
|
||||
|
||||
## [0.35.10]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 修改消息导致最后消息改变
|
||||
- 显示无关系的子任务、指定成员可见消息推送
|
||||
- 调整gemini机器人设置参数
|
||||
|
||||
### Performance
|
||||
|
||||
- 更新说明文档
|
||||
- 优化客户端
|
||||
- 默认关闭端到端加密传输
|
||||
|
||||
## [0.34.95]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 调整发送参数逻辑
|
||||
- 推送标题存在换行时不显示
|
||||
- 修复下载文件大小为0时报错
|
||||
- 更改其他版本的链接
|
||||
- 修复价格页面样式
|
||||
- 发布接口,调整缓存时间为两小时
|
||||
- 修复okr定时处理信息不发送
|
||||
- 项目已归档,任务面板也没有这三个任务,但是每次新增报告,都会弹任务出来
|
||||
- 修改下载页面按钮布局样式
|
||||
|
||||
### Features
|
||||
|
||||
- 升级okr
|
||||
- 优化内置浏览器
|
||||
- Gemini机器人添加代理参数
|
||||
- 发布接口只保留最近两个版本
|
||||
- 官网添加其他版本的按钮
|
||||
- 统一表为utf8mb4_unicode_ci
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化ai机器人
|
||||
- Okr和审批优化
|
||||
- 优化pdf在线预览
|
||||
- 优化客户端打开服务器链接
|
||||
- 签到设置,有些客户服务器安全体系会拦截 curl -sSL 关键字,优化为cmd不传值
|
||||
- 签到设置,有些客户服务器安全体系会拦截 curl -sSL 关键字,优化为base64返回
|
||||
|
||||
## [0.34.66]
|
||||
|
||||
### Performance
|
||||
|
||||
- 升级okr容器
|
||||
- 新增禁止私聊、群聊功能
|
||||
- 更换笑话和鸡汤接口
|
||||
|
||||
## [0.34.59]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 部分机型首次打开聊天窗口不显示聊天记录的问题
|
||||
|
||||
### Performance
|
||||
|
||||
- 修复一些问题
|
||||
- IOS打开键盘时看不见通知的情况
|
||||
- 优化系统参数
|
||||
- 优化菜单显示位置
|
||||
- 优化获取最近消息
|
||||
- 优化请求时间
|
||||
- 优化触摸长按和右键菜单共存
|
||||
|
||||
## [0.34.46]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 重复通知
|
||||
- 修复投票进度的算法
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化发送消息接口
|
||||
- 优化搜索提示
|
||||
- 优化消息列表
|
||||
- 优化消息保存覆盖
|
||||
- 优化快捷表情发送消息时关闭延迟的问题
|
||||
- 升级okr
|
||||
|
||||
## [0.34.28]
|
||||
|
||||
### Features
|
||||
|
||||
- 更新okr
|
||||
- 添加年度报告接口
|
||||
|
||||
### Performance
|
||||
|
||||
- 年度报告接口 - 查询条件优化
|
||||
- 升级okr, ai
|
||||
- 年度汇报接口返回用户头像
|
||||
- 年度报告接口 - 增加用户信息字段返回
|
||||
- 去掉未使用的引用
|
||||
- 优化滑动返回动画效果
|
||||
- 消息置顶滚动恢复
|
||||
- 优化消息Load效果
|
||||
|
||||
## [0.33.98]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 消息阅读回馈
|
||||
|
||||
### Performance
|
||||
|
||||
- 消息首次加载数据优化
|
||||
- 优化打包下载
|
||||
|
||||
## [0.33.91]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 修复置顶人员
|
||||
- 修复高危bug
|
||||
- 1.修复可见效数据取值,2.修复设置可见效指定人员不成功
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化打包下载
|
||||
- 去掉无用引用
|
||||
- 优化报告未读接口
|
||||
- 优化图片显示
|
||||
- 优化代码
|
||||
- 代码整理
|
||||
- 优化待审批流程数量接口
|
||||
- 代码优化
|
||||
- 优化未读消息提示
|
||||
- 优化预览消息
|
||||
- 优化缓存数据
|
||||
- 任务可见性用户 - 分表优化
|
||||
- 代码命名优化
|
||||
- 移动任务后,对应项目路径也要更改显示
|
||||
- 升级okr容器
|
||||
- 导出任务统计 - 下载地址换成按钮
|
||||
|
||||
## [0.33.58]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 修复已知问题
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化会话列表
|
||||
|
||||
## [0.33.54]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 撤回消息不删除消息的情况
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化录音load效果
|
||||
- 优化消息列表
|
||||
- 优化应用图标
|
||||
- 升级okr容器
|
||||
- 优化用户选择器
|
||||
- 优化对话列表接口数据
|
||||
- 优化未读消息提示动画
|
||||
- 优化消息更新机制
|
||||
- 优化缓存
|
||||
|
||||
## [0.33.41]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 更新导致的小问题
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化任务修改
|
||||
|
||||
## [0.33.34]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 版本验证有问题,先干掉
|
||||
- Android 无法回删输入框内的@(mention)内容
|
||||
- Android 长按重复事件
|
||||
- 合并修复
|
||||
|
||||
### Features
|
||||
|
||||
- 消息置顶功能 - 50%
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化发送消息时闪现2条一样的情况
|
||||
- 优化消息首页加载效果
|
||||
- 优化Android长按事件
|
||||
- 优化输入框自动高度
|
||||
- 点击消息页面会发生跳动的问题
|
||||
- 优化待办列表
|
||||
- 调整任务过多提示范围
|
||||
- 优化消息阅读规则
|
||||
- Okr版本升级
|
||||
- 1.数据库迁移文件修复 2.转发样式优化
|
||||
- 兼容okr1.1版本
|
||||
- 整体数据库索引和字段类型优化
|
||||
- 项目列表数据库查询优化
|
||||
|
||||
## [0.32.65]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 修复头像出现D的情况
|
||||
|
||||
### Documentation
|
||||
|
||||
- 更新文档
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化任务列表查询速度
|
||||
- 优化消息输入框内选择文本
|
||||
- 移动端修改任务详情确认提示
|
||||
- 优化发送录音消息抖动
|
||||
- 优化录音效果
|
||||
- 优化快捷键设置
|
||||
|
||||
## [0.32.55]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 聊天输入框内容为空时仍可以长安发送显示发送菜单
|
||||
- 文件页移动端滑动返回失败情况
|
||||
- 会员头像显示错乱
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化输入空换行时的兼容问题
|
||||
- 优化设置页面
|
||||
- 优化应用中心菜单排序
|
||||
- 更新录音插件
|
||||
- 更换移动任务图标
|
||||
- 优化设置返回跟滑动返回冲突
|
||||
- 优化键盘设置
|
||||
- 优化清除缓存数据
|
||||
- 优化阅读消息列表机制
|
||||
- 优化项目页面任务加载速度
|
||||
- 代码优化
|
||||
|
||||
## [0.32.35]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 修复重复SSE请求的问题
|
||||
- 部分pad设备横版和竖屏反过来
|
||||
- 用户选择组件,单选时不需要显示项目
|
||||
- 文件主题修复
|
||||
- 修复客户端版本更新按钮的显示问题
|
||||
- 标注取值bug修复
|
||||
- 项目权限 - 100%
|
||||
- 修复安装项目报错
|
||||
|
||||
### Features
|
||||
|
||||
- 新增以下为新消息提示
|
||||
- Okr结果分析 - 部门负责人也可以看
|
||||
- Okr1.1 兼容开发
|
||||
- 未读消息优化
|
||||
- 翻译
|
||||
- 添加投票功能 - 100%
|
||||
- 样式调优
|
||||
- 添加投票功能 30%
|
||||
- 接龙功能 - 100%
|
||||
- 添加接龙
|
||||
- 1.任务移动功能优化,2.导航样式优化
|
||||
- 新增压缩下载完成后系统机器人提醒
|
||||
- 添加一个 @我的 消息标签
|
||||
- 转发消息 - 添加单选模式
|
||||
- 转发消息 - 添加来源显示
|
||||
- 首页改版 - 100%
|
||||
- 新增项目任务创建权限功能 - 90%
|
||||
- 更换calendar
|
||||
- 首页改版
|
||||
|
||||
### Performance
|
||||
|
||||
- 优化消息阅读逻辑
|
||||
- 微应用优化
|
||||
- 优化未读消息机制
|
||||
- 优化重连时消息列表跳回第一页的情况
|
||||
- 优化消息更新太快导致不更新数据的情况
|
||||
- 机器人添加清空上下文菜单
|
||||
- 优化翻译
|
||||
- 发送消息失败时再次编辑改为重新发送
|
||||
- 优化通过消息设置待办功能
|
||||
- 优化扫一扫登录功能
|
||||
- 优化头像
|
||||
- 兼容okr1.1版本
|
||||
- 接龙和投票的样式优化
|
||||
- 逻辑强化
|
||||
- 搜索消息时按esc取消搜索
|
||||
- 接龙优化
|
||||
- 移动设备优化消息输入框菜单
|
||||
- 优化消息输入框@所有人暗黑样式
|
||||
- 优化@人名换行的情况
|
||||
- 样式优化
|
||||
- 优化导出任务统计
|
||||
- 客户端下载按钮,仪表盘不显示
|
||||
- 细节优化
|
||||
|
||||
## [0.32.17]
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 修复打包下载问题
|
||||
- 1. 修复 windows端 右键发送 是直接发送了,没有出现使用md格式发送 2.其他bug修复
|
||||
- 修复统一打包下载命名
|
||||
|
||||
### Features
|
||||
|
||||
- 添加项目权限功能 - 30%
|
||||
|
||||
### Performance
|
||||
|
||||
- 审批版本更新
|
||||
- Okr 和 审批中心弄一些演示数据
|
||||
- 项目邀请页 - 安卓用opne方式打开
|
||||
|
||||
## [0.32.9]
|
||||
|
||||
### Bug Fixes
|
||||
@@ -28,7 +499,6 @@ All notable changes to this project will be documented in this file.
|
||||
- 文件共享只读禁止下载文件
|
||||
- 保存任务详情至文件的方法 添加失败日志
|
||||
- 翻译
|
||||
- 翻译
|
||||
- 新增实现文件夹下载以及多文件压缩下载功能
|
||||
- 任务可以筛选未设置时间的
|
||||
- 临时账号可以主动跟机器人聊天
|
||||
@@ -37,14 +507,11 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
- Office只读模式,隐藏下载按钮
|
||||
- 优化实现文件夹下载以及多文件压缩下载功能
|
||||
- 优化实现文件夹下载以及多文件压缩下载功能
|
||||
- 机器人设置页面,点点点看不到内容,需要给弹窗看详细内容
|
||||
- 文件选中后,移动端页面宽度放不下对应内容
|
||||
- 文件选中后,移动端页面宽度放不下对应内容,没有滚动条
|
||||
- 压缩下载改名打包下载
|
||||
- 复制链接去除主题语言参数
|
||||
- 优化实现文件夹下载以及多文件压缩下载功能
|
||||
- 优化实现文件夹下载以及多文件压缩下载功能
|
||||
|
||||
## [0.31.75]
|
||||
|
||||
@@ -186,14 +653,12 @@ All notable changes to this project will be documented in this file.
|
||||
- 去掉test信息
|
||||
- 按照dootask启动原始尺寸截取使用说明的图
|
||||
- 修改边栏目录滚动效果
|
||||
- 修改边栏目录滚动效果
|
||||
- 官网使用说明的图重新截取更换
|
||||
- 优化官网布局与样式
|
||||
- 修复下载英文页面跳转
|
||||
|
||||
### Features
|
||||
|
||||
- 新增创建聊天关联id
|
||||
- 新增创建聊天关联id
|
||||
- Okr信息面板新增"打开OKR"按钮
|
||||
- 新增OKR信息推送
|
||||
@@ -280,7 +745,6 @@ All notable changes to this project will be documented in this file.
|
||||
- 优化会员选择器
|
||||
- 优化图片压缩
|
||||
- 回复图片显示图片搜略图
|
||||
- 优化会员选择器
|
||||
- 会员选择下拉框提示
|
||||
|
||||
## [0.27.26]
|
||||
@@ -311,7 +775,6 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
- 修复获取聊天列表的接口
|
||||
- 添加密码账号长度限制
|
||||
- 添加密码账号长度限制
|
||||
- Dootask对接系统分享 - 添加头像返回
|
||||
- 兼容加密bug问题处理
|
||||
- Dootask对接系统分享
|
||||
@@ -361,7 +824,6 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- 审批流程静态页
|
||||
- 审批流程静态页
|
||||
|
||||
### Performance
|
||||
@@ -1646,7 +2108,6 @@ All notable changes to this project will be documented in this file.
|
||||
- 优化甘特图
|
||||
- 优化任务列表切换显示
|
||||
- 更新icon图标库
|
||||
- 更新icon图标库
|
||||
- 已删除任务详情任务描述改为只读
|
||||
- 已删除任务操作文案及显示优化
|
||||
- 项目--删除任务查看详情页功能
|
||||
@@ -1737,7 +2198,6 @@ All notable changes to this project will be documented in this file.
|
||||
- 报表导出任务没有流程日志判断优化
|
||||
- 修复登录页设置下拉显示不全的情况
|
||||
- 处理回滚后异常代码
|
||||
- 处理回滚后异常代码
|
||||
- 【系统设置】邮件设置提前小时数双向绑定无效问题修改
|
||||
|
||||
### Features
|
||||
@@ -1825,7 +2285,6 @@ All notable changes to this project will be documented in this file.
|
||||
- 调整消息置顶标识位置
|
||||
- 消息列表详情增加'置顶'标识
|
||||
- 项目列表置顶优化
|
||||
- 项目列表置顶优化
|
||||
- 【文件】剪切后加'取消剪切'按钮
|
||||
- 消息会话右键时隐藏滚动条
|
||||
- 页面高度足够时只滚动项目部分
|
||||
@@ -1926,7 +2385,6 @@ All notable changes to this project will be documented in this file.
|
||||
- 该文件版本已经改变了。该页面将被重新加载
|
||||
- 点击切换语言一级菜单出现的兼容问题
|
||||
- 上传文件夹
|
||||
- 该文件版本已经改变了。该页面将被重新加载
|
||||
- 团队管理新增身份筛选项
|
||||
- 任务文件支持更多格式上传
|
||||
|
||||
@@ -1964,7 +2422,6 @@ All notable changes to this project will be documented in this file.
|
||||
- 任务中没有聊天记录时,发送图片无法成功
|
||||
- 修复消息撤回文字提示在第一条时会被顶部遮住的问题
|
||||
- 修复个人对话为空时无法重复打开该对话的问题
|
||||
- 修复消息撤回文字提示在第一条时会被顶部遮住的问题
|
||||
- MacOS客户端首次不加载角标的问题
|
||||
- 工作流列表接口用作筛选时不用传多余参数
|
||||
|
||||
@@ -2065,7 +2522,6 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
- 工作报告优化
|
||||
- 规范代码
|
||||
- 工作报告优化
|
||||
- 共享文件删除、移动改为仅限所有者或创建者操作
|
||||
|
||||
## [0.6.38]
|
||||
@@ -2345,7 +2801,6 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
### Performance
|
||||
|
||||
- 客户端新窗口打开文件
|
||||
- 客户端窗口标题
|
||||
- 领取任务流程
|
||||
- 到期时间格式化
|
||||
@@ -2358,7 +2813,6 @@ All notable changes to this project will be documented in this file.
|
||||
|
||||
- 客户端文件窗口样式
|
||||
- Iview
|
||||
- Iview
|
||||
- 排序箭头颜色
|
||||
|
||||
## [0.4.28]
|
||||
|
||||
32
README.md
@@ -2,7 +2,7 @@
|
||||
|
||||
English | **[中文文档](./README_CN.md)**
|
||||
|
||||
- [Screenshot Preview](README_PREVIEW.md)
|
||||
- [Screenshot preview](./README_PREVIEW.md)
|
||||
- [Demo site](http://www.dootask.com/)
|
||||
|
||||
**QQ Group**
|
||||
@@ -12,8 +12,9 @@ Group No.: `546574618`
|
||||
## Setup
|
||||
|
||||
- `Docker v20.10+` & `Docker Compose v2.0+` must be installed
|
||||
- System: `Centos/Debian/Ubuntu/macOS`
|
||||
- System: `Centos/Debian/Ubuntu/macOS/Windows`
|
||||
- Hardware suggestion: 2 cores and above 4G memory
|
||||
- Special note: Windows users please use `git bash` or `cmder` to run the command
|
||||
|
||||
### Deployment (Pro Edition)
|
||||
|
||||
@@ -28,7 +29,7 @@ git clone -b pro --depth=1 https://gitee.com/aipaw/dootask.git
|
||||
# 2、Enter directory
|
||||
cd dootask
|
||||
|
||||
# 3、Installation(Custom port installation: ./cmd install --port 2222)
|
||||
# 3、Installation(Custom port installation, as: ./cmd install --port 80)
|
||||
./cmd install
|
||||
```
|
||||
|
||||
@@ -42,7 +43,8 @@ cd dootask
|
||||
### Change port
|
||||
|
||||
```bash
|
||||
./cmd port 2222
|
||||
# This method only replaces the HTTP port. To replace the HTTPS port, please read the SSL configuration below
|
||||
./cmd port 80
|
||||
```
|
||||
|
||||
### Change App Url
|
||||
@@ -66,11 +68,13 @@ cd dootask
|
||||
|
||||
### Development compilation
|
||||
|
||||
- `NodeJs 20+` must be installed
|
||||
|
||||
```bash
|
||||
# Development mode, Mac OS only
|
||||
# Development
|
||||
./cmd dev
|
||||
|
||||
# Production projects, macOS only
|
||||
# Production (This is web client. For App/PC/Mac clients, Please read README-CLIENT.md)
|
||||
./cmd prod
|
||||
```
|
||||
|
||||
@@ -84,11 +88,19 @@ cd dootask
|
||||
./cmd redis "your command" # To run a redis command
|
||||
./cmd composer "your command" # To run a composer command
|
||||
./cmd supervisorctl "your command" # To run a supervisorctl command
|
||||
./cmd test "your command" # To run a phpunit command
|
||||
./cmd mysql "your command" # To run a mysql command (backup: Backup database, recovery: Restore database)
|
||||
```
|
||||
|
||||
### NGINX PROXY SSL
|
||||
### SSL configuration
|
||||
|
||||
#### Method 1: Automatic configuration
|
||||
|
||||
```bash
|
||||
# Running commands in a project
|
||||
./cmd https
|
||||
```
|
||||
|
||||
#### Or Method 2: Nginx Agent Configuration
|
||||
|
||||
```bash
|
||||
# 1、Nginx config add
|
||||
@@ -96,8 +108,8 @@ proxy_set_header X-Forwarded-Host $http_host;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
||||
# 2、Running commands in a project
|
||||
./cmd https
|
||||
# 2、Running commands in a project (If you unconfigure the NGINX agent, run: ./cmd https close)
|
||||
./cmd https agent
|
||||
```
|
||||
|
||||
## Upgrade
|
||||
|
||||
@@ -3,19 +3,23 @@
|
||||
## 1、App客户端
|
||||
|
||||
#### 1.1、说明
|
||||
|
||||
目录 `resources/mobile`,使用`eeui.app`框架,遵从eeui的开发文档进行打包开发app
|
||||
|
||||
#### 1.2、编译App
|
||||
1. 在项目目录执行 `./cmd appbuild` 编译
|
||||
|
||||
1. 在项目目录执行 `./cmd appbuild [build|setting]` 编译
|
||||
2. 进入 `resources/mobile` eeui框架内打包Android或iOS应用
|
||||
|
||||
|
||||
## 2、PC/Mac客户端
|
||||
|
||||
#### 2.1、说明
|
||||
|
||||
目录 `electron`,使用`electron`框架,遵从electron的开发文档进行打包客户端
|
||||
|
||||
#### 2.2、编译客户端
|
||||
在项目目录执行 `./cmd electron` 根据提示编译
|
||||
|
||||
在项目目录执行 `./cmd electron [dev]` 根据提示编译
|
||||
|
||||
|
||||
|
||||
32
README_CN.md
@@ -2,7 +2,7 @@
|
||||
|
||||
**[English](./README.md)** | 中文文档
|
||||
|
||||
- [截图预览](README_PREVIEW.md)
|
||||
- [截图预览](./README_PREVIEW.md)
|
||||
- [演示站点](http://www.dootask.com/)
|
||||
|
||||
**QQ交流群**
|
||||
@@ -12,8 +12,9 @@
|
||||
## 安装程序
|
||||
|
||||
- 必须安装:`Docker v20.10+` 和 `Docker Compose v2.0+`
|
||||
- 支持环境:`Centos/Debian/Ubuntu/macOS`
|
||||
- 支持环境:`Centos/Debian/Ubuntu/macOS/Windows`
|
||||
- 硬件建议:2核4G以上
|
||||
- 特别说明:Windows 用户请使用 `git bash` 或者 `cmder` 运行命令
|
||||
|
||||
### 部署项目(Pro版)
|
||||
|
||||
@@ -28,7 +29,7 @@ git clone -b pro --depth=1 https://gitee.com/aipaw/dootask.git
|
||||
# 2、进入目录
|
||||
cd dootask
|
||||
|
||||
# 3、一键安装项目(自定义端口安装 ./cmd install --port 2222)
|
||||
# 3、一键安装项目(自定义端口安装,如:./cmd install --port 80)
|
||||
./cmd install
|
||||
```
|
||||
|
||||
@@ -42,7 +43,8 @@ cd dootask
|
||||
### 更换端口
|
||||
|
||||
```bash
|
||||
./cmd port 2222
|
||||
# 此方法仅更换http端口,更换https端口请阅读下面SSL配置
|
||||
./cmd port 80
|
||||
```
|
||||
|
||||
### 更换URL
|
||||
@@ -66,11 +68,13 @@ cd dootask
|
||||
|
||||
### 开发编译
|
||||
|
||||
- 请确保你已经安装了 `NodeJs 20+`
|
||||
|
||||
```bash
|
||||
# 开发模式,仅限macOS
|
||||
# 开发模式
|
||||
./cmd dev
|
||||
|
||||
# 编译项目,仅限macOS
|
||||
# 编译项目(这是网页端的,App/Pc/Mac客户端请查看 README_CLIENT.md)
|
||||
./cmd prod
|
||||
```
|
||||
|
||||
@@ -85,11 +89,19 @@ cd dootask
|
||||
./cmd redis "your command" # 运行 redis 命令
|
||||
./cmd composer "your command" # 运行 composer 命令
|
||||
./cmd supervisorctl "your command" # 运行 supervisorctl 命令
|
||||
./cmd test "your command" # 运行 phpunit 命令
|
||||
./cmd mysql "your command" # 运行 mysql 命令 (backup: 备份数据库,recovery: 还原数据库)
|
||||
```
|
||||
|
||||
### NGINX 代理 SSL
|
||||
### SSL 配置
|
||||
|
||||
#### 方法1:自动配置
|
||||
|
||||
```bash
|
||||
# 在项目下运行命令,根据提示执行即可
|
||||
./cmd https
|
||||
```
|
||||
|
||||
#### (或者)方法2:Nginx 代理配置
|
||||
|
||||
```bash
|
||||
# 1、Nginx 代理配置添加
|
||||
@@ -97,8 +109,8 @@ proxy_set_header X-Forwarded-Host $http_host;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
|
||||
# 2、在项目下运行命令
|
||||
./cmd https
|
||||
# 2、在项目下运行命令(如果取消 Nginx 代理配置请运行:./cmd https close)
|
||||
./cmd https agent
|
||||
```
|
||||
|
||||
## 升级更新
|
||||
|
||||
@@ -22,5 +22,5 @@
|
||||
|
||||
## 编译App
|
||||
|
||||
1. 执行 `./cmd appbuild` 或 `./cmd appbuild setting` 编译
|
||||
1. 执行 `./cmd appbuild [setting]` 编译
|
||||
2. 进入 `resources/mobile` eeui框架内打包Android或iOS应用
|
||||
|
||||
@@ -19,6 +19,7 @@ use App\Models\UserDepartment;
|
||||
use App\Models\WebSocketDialogMsg;
|
||||
use App\Module\BillMultipleExport;
|
||||
use Hhxsv5\LaravelS\Swoole\Task\Task;
|
||||
|
||||
/**
|
||||
* @apiDefine approve
|
||||
*
|
||||
@@ -27,6 +28,7 @@ use Hhxsv5\LaravelS\Swoole\Task\Task;
|
||||
class ApproveController extends AbstractController
|
||||
{
|
||||
private $flow_url = '';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->flow_url = env('FLOW_URL') ?: 'http://approve';
|
||||
@@ -71,7 +73,7 @@ class ApproveController extends AbstractController
|
||||
{
|
||||
User::auth();
|
||||
$data['name'] = Request::input('name');
|
||||
$ret = Ihttp::ihttp_post($this->flow_url.'/api/v1/workflow/procdef/findAll', json_encode($data));
|
||||
$ret = Ihttp::ihttp_post($this->flow_url . '/api/v1/workflow/procdef/findAll', json_encode($data));
|
||||
$procdef = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
|
||||
if (!$procdef || $procdef['status'] != 200 || $ret['ret'] == 0) {
|
||||
// info($ret);
|
||||
@@ -98,7 +100,7 @@ class ApproveController extends AbstractController
|
||||
{
|
||||
User::auth('admin');
|
||||
$data['id'] = Request::input('id');
|
||||
$ret = Ihttp::ihttp_get($this->flow_url.'/api/v1/workflow/procdef/delById?'.http_build_query($data));
|
||||
$ret = Ihttp::ihttp_get($this->flow_url . '/api/v1/workflow/procdef/delById?' . http_build_query($data));
|
||||
$procdef = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
|
||||
if (!$procdef || $procdef['status'] != 200) {
|
||||
return Base::retError($procdef['message'] ?? '删除失败');
|
||||
@@ -131,7 +133,7 @@ class ApproveController extends AbstractController
|
||||
//
|
||||
$var = json_decode(Request::input('var'), true);
|
||||
$data['var'] = $var;
|
||||
$ret = Ihttp::ihttp_post($this->flow_url.'/api/v1/workflow/process/start', json_encode(Base::arrayKeyToCamel($data)));
|
||||
$ret = Ihttp::ihttp_post($this->flow_url . '/api/v1/workflow/process/start', json_encode(Base::arrayKeyToCamel($data)));
|
||||
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
|
||||
if (!$process || $process['status'] != 200) {
|
||||
return Base::retError($process['message'] ?? '启动失败');
|
||||
@@ -193,7 +195,7 @@ class ApproveController extends AbstractController
|
||||
|
||||
$processInst = $this->getProcessById($data['proc_inst_id']);
|
||||
|
||||
$ret = Ihttp::ihttp_post($this->flow_url.'/api/v1/workflow/process/addGlobalComment', json_encode(Base::arrayKeyToCamel($data)));
|
||||
$ret = Ihttp::ihttp_post($this->flow_url . '/api/v1/workflow/process/addGlobalComment', json_encode(Base::arrayKeyToCamel($data)));
|
||||
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
|
||||
if (!$process || $process['status'] != 200) {
|
||||
return Base::retError($process['message'] ?? '添加失败');
|
||||
@@ -201,11 +203,11 @@ class ApproveController extends AbstractController
|
||||
|
||||
// 推送通知
|
||||
$botUser = User::botGetOrCreate('approval-alert');
|
||||
foreach ( $processInst['userids'] as $id) {
|
||||
if($id != $user->userid){
|
||||
foreach ($processInst['userids'] as $id) {
|
||||
if ($id != $user->userid) {
|
||||
$dialog = WebSocketDialog::checkUserDialog($botUser, $id);
|
||||
$processInst['comment_user_id'] = $user->userid;
|
||||
$processInst['comment_content'] = json_decode($data['content'],true)['content'];
|
||||
$processInst['comment_content'] = json_decode($data['content'], true)['content'];
|
||||
$this->approveMsg('approve_comment_notifier', $dialog, $botUser, $processInst, $processInst);
|
||||
}
|
||||
}
|
||||
@@ -237,7 +239,7 @@ class ApproveController extends AbstractController
|
||||
$data['task_id'] = intval(Request::input('task_id'));
|
||||
$data['pass'] = Request::input('pass');
|
||||
$data['comment'] = Request::input('comment');
|
||||
$ret = Ihttp::ihttp_post($this->flow_url.'/api/v1/workflow/task/complete', json_encode(Base::arrayKeyToCamel($data)));
|
||||
$ret = Ihttp::ihttp_post($this->flow_url . '/api/v1/workflow/task/complete', json_encode(Base::arrayKeyToCamel($data)));
|
||||
$task = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
|
||||
if (!$task || $task['status'] != 200) {
|
||||
return Base::retError($task['message'] ?? '审批失败');
|
||||
@@ -260,12 +262,12 @@ class ApproveController extends AbstractController
|
||||
$this->approveMsg('approve_reviewer', $dialog, $botUser, $val, $process, $pass);
|
||||
}
|
||||
// 发起人
|
||||
if($process['is_finished'] == true) {
|
||||
if ($process['is_finished'] == true) {
|
||||
$dialog = WebSocketDialog::checkUserDialog($botUser, $process['start_user_id']);
|
||||
if (!empty($dialog)) {
|
||||
$this->approveMsg('approve_submitter', $dialog, $botUser, ['userid' => $data['userid']], $process, $pass);
|
||||
}
|
||||
}else if ($process['candidate']) {
|
||||
} else if ($process['candidate']) {
|
||||
// 下个审批人
|
||||
$userid = explode(',', $process['candidate']);
|
||||
$toUser = User::whereIn('userid', $userid)->get()->toArray();
|
||||
@@ -277,7 +279,7 @@ class ApproveController extends AbstractController
|
||||
if (empty($dialog)) {
|
||||
continue;
|
||||
}
|
||||
$this->approveMsg('approve_reviewer', $dialog, $botUser, $val, $process,'start');
|
||||
$this->approveMsg('approve_reviewer', $dialog, $botUser, $val, $process, 'start');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -291,7 +293,7 @@ class ApproveController extends AbstractController
|
||||
}
|
||||
}
|
||||
}
|
||||
return Base::retSuccess( $pass == 'pass' ? '已通过' : '已拒绝', $task);
|
||||
return Base::retSuccess($pass == 'pass' ? '已通过' : '已拒绝', $task);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -315,7 +317,7 @@ class ApproveController extends AbstractController
|
||||
$data['userid'] = (string)$user->userid;
|
||||
$data['task_id'] = intval(Request::input('task_id'));
|
||||
$data['proc_inst_id'] = intval(Request::input('proc_inst_id'));
|
||||
$ret = Ihttp::ihttp_post($this->flow_url.'/api/v1/workflow/task/withdraw', json_encode(Base::arrayKeyToCamel($data)));
|
||||
$ret = Ihttp::ihttp_post($this->flow_url . '/api/v1/workflow/task/withdraw', json_encode(Base::arrayKeyToCamel($data)));
|
||||
$task = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
|
||||
if (!$task || $task['status'] != 200) {
|
||||
return Base::retError($task['message'] ?? '撤回失败');
|
||||
@@ -365,7 +367,7 @@ class ApproveController extends AbstractController
|
||||
$data['sort'] = Request::input('sort');
|
||||
$data['pageIndex'] = intval(Request::input('page'));
|
||||
$data['pageSize'] = intval(Request::input('page_size'));
|
||||
$ret = Ihttp::ihttp_post($this->flow_url.'/api/v1/workflow/process/findTask', json_encode(Base::arrayKeyToCamel($data)));
|
||||
$ret = Ihttp::ihttp_post($this->flow_url . '/api/v1/workflow/process/findTask', json_encode(Base::arrayKeyToCamel($data)));
|
||||
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
|
||||
if (!$process || $process['status'] != 200) {
|
||||
return Base::retError($process['message'] ?? '查询失败');
|
||||
@@ -379,7 +381,7 @@ class ApproveController extends AbstractController
|
||||
}
|
||||
$val['userimg'] = User::getAvatar($info->userid, $info->userimg, $info->email, $info->nickname);
|
||||
}
|
||||
return Base::retSuccess('success',$res);
|
||||
return Base::retSuccess('success', $res);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -403,12 +405,12 @@ class ApproveController extends AbstractController
|
||||
{
|
||||
$user = User::auth();
|
||||
$data['userid'] = (string)$user->userid;
|
||||
$data['username'] = Request::input('username');
|
||||
$data['username'] = Request::input('username');
|
||||
$data['procName'] = Request::input('proc_def_name'); //分类
|
||||
$data['state'] = intval(Request::input('state')); //状态
|
||||
$data['pageIndex'] = intval(Request::input('page'));
|
||||
$data['pageSize'] = intval(Request::input('page_size'));
|
||||
$ret = Ihttp::ihttp_post($this->flow_url.'/api/v1/workflow/process/startByMyselfAll', json_encode($data));
|
||||
$ret = Ihttp::ihttp_post($this->flow_url . '/api/v1/workflow/process/startByMyselfAll', json_encode($data));
|
||||
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
|
||||
if (!$process || $process['status'] != 200) {
|
||||
return Base::retError($process['message'] ?? '查询失败');
|
||||
@@ -446,7 +448,7 @@ class ApproveController extends AbstractController
|
||||
$data['userid'] = (string)$user->userid;
|
||||
$data['pageIndex'] = intval(Request::input('page'));
|
||||
$data['pageSize'] = intval(Request::input('page_size'));
|
||||
$ret = Ihttp::ihttp_post($this->flow_url.'/api/v1/workflow/process/startByMyself', json_encode($data));
|
||||
$ret = Ihttp::ihttp_post($this->flow_url . '/api/v1/workflow/process/startByMyself', json_encode($data));
|
||||
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
|
||||
if (!$process || $process['status'] != 200) {
|
||||
return Base::retError($process['message'] ?? '查询失败');
|
||||
@@ -490,7 +492,7 @@ class ApproveController extends AbstractController
|
||||
$data['pageIndex'] = intval(Request::input('page'));
|
||||
$data['pageSize'] = intval(Request::input('page_size'));
|
||||
|
||||
$ret = Ihttp::ihttp_post($this->flow_url.'/api/v1/workflow/process/findProcNotify', json_encode($data));
|
||||
$ret = Ihttp::ihttp_post($this->flow_url . '/api/v1/workflow/process/findProcNotify', json_encode($data));
|
||||
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
|
||||
if (!$process || $process['status'] != 200) {
|
||||
return Base::retError($process['message'] ?? '查询失败');
|
||||
@@ -525,7 +527,7 @@ class ApproveController extends AbstractController
|
||||
{
|
||||
User::auth();
|
||||
$proc_inst_id = Request::input('proc_inst_id');
|
||||
$ret = Ihttp::ihttp_get($this->flow_url.'/api/v1/workflow/identitylink/findParticipant?procInstId=' . $proc_inst_id);
|
||||
$ret = Ihttp::ihttp_get($this->flow_url . '/api/v1/workflow/identitylink/findParticipant?procInstId=' . $proc_inst_id);
|
||||
$identitylink = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
|
||||
if (!$identitylink || $identitylink['status'] != 200) {
|
||||
return Base::retError($identitylink['message'] ?? '查询失败');
|
||||
@@ -568,7 +570,7 @@ class ApproveController extends AbstractController
|
||||
$data['sort'] = Request::input('sort');
|
||||
$data['pageIndex'] = intval(Request::input('page'));
|
||||
$data['pageSize'] = intval(Request::input('page_size'));
|
||||
$ret = Ihttp::ihttp_post($this->flow_url.'/api/v1/workflow/procHistory/findTask', json_encode(Base::arrayKeyToCamel($data)));
|
||||
$ret = Ihttp::ihttp_post($this->flow_url . '/api/v1/workflow/procHistory/findTask', json_encode(Base::arrayKeyToCamel($data)));
|
||||
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
|
||||
if (!$process || $process['status'] != 200) {
|
||||
return Base::retError($process['message'] ?? '查询失败');
|
||||
@@ -606,7 +608,7 @@ class ApproveController extends AbstractController
|
||||
$data['userid'] = (string)$user->userid;
|
||||
$data['pageIndex'] = intval(Request::input('page'));
|
||||
$data['pageSize'] = intval(Request::input('page_size'));
|
||||
$ret = Ihttp::ihttp_post($this->flow_url.'/api/v1/workflow/procHistory/startByMyself', json_encode($data));
|
||||
$ret = Ihttp::ihttp_post($this->flow_url . '/api/v1/workflow/procHistory/startByMyself', json_encode($data));
|
||||
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
|
||||
if (!$process || $process['status'] != 200) {
|
||||
return Base::retError($process['message'] ?? '查询失败');
|
||||
@@ -650,7 +652,7 @@ class ApproveController extends AbstractController
|
||||
$data['pageIndex'] = intval(Request::input('page'));
|
||||
$data['pageSize'] = intval(Request::input('page_size'));
|
||||
|
||||
$ret = Ihttp::ihttp_post($this->flow_url.'/api/v1/workflow/procHistory/findProcNotify', json_encode($data));
|
||||
$ret = Ihttp::ihttp_post($this->flow_url . '/api/v1/workflow/procHistory/findProcNotify', json_encode($data));
|
||||
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
|
||||
if (!$process || $process['status'] != 200) {
|
||||
return Base::retError($process['message'] ?? '查询失败');
|
||||
@@ -685,7 +687,7 @@ class ApproveController extends AbstractController
|
||||
{
|
||||
User::auth();
|
||||
$proc_inst_id = Request::input('proc_inst_id');
|
||||
$ret = Ihttp::ihttp_get($this->flow_url.'/api/v1/workflow/identitylinkHistory/findParticipant?procInstId=' . $proc_inst_id);
|
||||
$ret = Ihttp::ihttp_get($this->flow_url . '/api/v1/workflow/identitylinkHistory/findParticipant?procInstId=' . $proc_inst_id);
|
||||
$identitylink = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
|
||||
if (!$identitylink || $identitylink['status'] != 200) {
|
||||
return Base::retError($identitylink['message'] ?? '查询失败');
|
||||
@@ -749,7 +751,7 @@ class ApproveController extends AbstractController
|
||||
$data['isFinished'] = intval(Request::input('is_finished')); //是否完成
|
||||
$date = Request::input('date');
|
||||
$data['startTime'] = $date[0]; //开始时间
|
||||
$data['endTime'] =Carbon::parse($date[1])->addDay()->toDateString(); //结束时间 + 1天
|
||||
$data['endTime'] = Carbon::parse($date[1])->addDay()->toDateString(); //结束时间 + 1天
|
||||
//
|
||||
if (empty($name) || empty($date)) {
|
||||
return Base::retError('参数错误');
|
||||
@@ -761,7 +763,7 @@ class ApproveController extends AbstractController
|
||||
return Base::retError('日期范围限制最大35天');
|
||||
}
|
||||
//
|
||||
$ret = Ihttp::ihttp_post($this->flow_url.'/api/v1/workflow/process/findAllProcIns', json_encode($data));
|
||||
$ret = Ihttp::ihttp_post($this->flow_url . '/api/v1/workflow/process/findAllProcIns', json_encode($data));
|
||||
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
|
||||
if (!$process || $process['status'] != 200) {
|
||||
return Base::retError($process['message'] ?? '查询失败');
|
||||
@@ -856,7 +858,7 @@ class ApproveController extends AbstractController
|
||||
BillExport::create()->setTitle($title)->setHeadings($headings)->setData($datas)->setStyles(["A1:Y1" => ["font" => ["bold" => true]]])
|
||||
];
|
||||
//
|
||||
$fileName = '审批记录_' . Base::time() . '.xls';
|
||||
$fileName = '审批记录_' . Base::time() . '.xlsx';
|
||||
$filePath = "temp/approve/export/" . date("Ym", Base::time());
|
||||
$export = new BillMultipleExport($sheets);
|
||||
$res = $export->store($filePath . "/" . $fileName);
|
||||
@@ -864,7 +866,7 @@ class ApproveController extends AbstractController
|
||||
return Base::retError('导出失败,' . $fileName . '!');
|
||||
}
|
||||
$xlsPath = storage_path("app/" . $filePath . "/" . $fileName);
|
||||
$zipFile = "app/" . $filePath . "/" . Base::rightDelete($fileName, '.xls') . ".zip";
|
||||
$zipFile = "app/" . $filePath . "/" . Base::rightDelete($fileName, '.xlsx') . ".zip";
|
||||
$zipPath = storage_path($zipFile);
|
||||
if (file_exists($zipPath)) {
|
||||
Base::deleteDirAndFile($zipPath, true);
|
||||
@@ -888,7 +890,8 @@ class ApproveController extends AbstractController
|
||||
}
|
||||
}
|
||||
|
||||
function getStateDescription($state) {
|
||||
function getStateDescription($state)
|
||||
{
|
||||
$state_map = array(
|
||||
0 => '全部',
|
||||
1 => '审批中',
|
||||
@@ -950,13 +953,13 @@ class ApproveController extends AbstractController
|
||||
}
|
||||
// 审批记录
|
||||
$name = $val['username'] . '|';
|
||||
$call = $val['step'] == 0 ? '发起审批'. '|' : '同意' . '|';
|
||||
$time =$val['step'] == 0 ? $process['start_time'] . '|' : '';
|
||||
$call = $val['step'] == 0 ? '发起审批' . '|' : '同意' . '|';
|
||||
$time = $val['step'] == 0 ? $process['start_time'] . '|' : '';
|
||||
$comment = $val['step'] == 0 ? '' : ($val['comment'] ?? '') . '|';
|
||||
$res['approval_record'] .= $name . $call . $time . $comment;
|
||||
}
|
||||
}
|
||||
$res['historical_approver'] = trim(implode(';', $historical_approver), ';');
|
||||
$res['historical_approver'] = trim(implode(';', $historical_approver), ';');
|
||||
$res['approved_node'] = $approved_node;
|
||||
$res['approved_num'] = $approved_num;
|
||||
$res['historical_agent'] = $res['historical_approver'];
|
||||
@@ -975,9 +978,9 @@ class ApproveController extends AbstractController
|
||||
'department' => $process['department'],
|
||||
'type' => $process['var']['type'],
|
||||
'start_time' => $process['var']['start_time'],
|
||||
'start_day_of_week' => '周'.Base::getTimeWeek(Carbon::parse($process['var']['start_time'])->timestamp),
|
||||
'start_day_of_week' => '周' . Base::getTimeWeek(Carbon::parse($process['var']['start_time'])->timestamp),
|
||||
'end_time' => $process['var']['end_time'],
|
||||
'end_day_of_week' => '周'.Base::getTimeWeek(Carbon::parse($process['var']['end_time'])->timestamp),
|
||||
'end_day_of_week' => '周' . Base::getTimeWeek(Carbon::parse($process['var']['end_time'])->timestamp),
|
||||
'description' => $process['var']['description'],
|
||||
'comment_nickname' => $process['comment_user_id'] ? User::userid2nickname($process['comment_user_id']) : '',
|
||||
'comment_content' => $process['comment_content'] ?? ''
|
||||
@@ -988,11 +991,11 @@ class ApproveController extends AbstractController
|
||||
$msg_action = null;
|
||||
if ($action == 'withdraw' || $action == 'pass' || $action == 'refuse') {
|
||||
// 任务完成,给发起人发送消息
|
||||
if($type == 'approve_submitter' && $action != 'withdraw'){
|
||||
if ($type == 'approve_submitter' && $action != 'withdraw') {
|
||||
return WebSocketDialogMsg::sendMsg($msg_action, $dialog->id, 'text', ['text' => $text], $botUser->userid, false, false, true);
|
||||
}
|
||||
// 查找最后一条消息msg_id
|
||||
$msg_action = 'update-'.$toUser['msg_id'];
|
||||
$msg_action = 'change-' . $toUser['msg_id'];
|
||||
}
|
||||
//
|
||||
try {
|
||||
@@ -1006,9 +1009,9 @@ class ApproveController extends AbstractController
|
||||
$proc_msg->save();
|
||||
}
|
||||
// 更新工作报告 未读数量
|
||||
if($type == 'approve_reviewer' && $toUser['userid']){
|
||||
if ($type == 'approve_reviewer' && $toUser['userid']) {
|
||||
$params = [
|
||||
'userid' => [ $toUser['userid'], User::auth()->userid() ],
|
||||
'userid' => [$toUser['userid'], User::auth()->userid()],
|
||||
'msg' => [
|
||||
'type' => 'approve',
|
||||
'action' => 'unread',
|
||||
@@ -1027,7 +1030,7 @@ class ApproveController extends AbstractController
|
||||
public function getProcessById($id)
|
||||
{
|
||||
$data['id'] = intval($id);
|
||||
$ret = Ihttp::ihttp_get($this->flow_url."/api/v1/workflow/process/findById?".http_build_query($data));
|
||||
$ret = Ihttp::ihttp_get($this->flow_url . "/api/v1/workflow/process/findById?" . http_build_query($data));
|
||||
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
|
||||
if (!$process || $process['status'] != 200) {
|
||||
throw new ApiException($process['message'] ?? '查询失败');
|
||||
@@ -1046,14 +1049,14 @@ class ApproveController extends AbstractController
|
||||
$val['node_user_list'][$k]['userimg'] = User::getAvatar($info->userid, $info->userimg, $info->email, $info->nickname);
|
||||
$res['userids'][] = $item['target_id'];
|
||||
}
|
||||
}else if($val['aprover_id']){
|
||||
} else if ($val['aprover_id']) {
|
||||
$info = User::whereUserid($val['aprover_id'])->first();
|
||||
$val['userimg'] = $info ? User::getAvatar($info->userid, $info->userimg, $info->email, $info->nickname) : '';
|
||||
$res['userids'][] = $val['aprover_id'];
|
||||
}
|
||||
}
|
||||
// 全局评论
|
||||
if(isset($res['global_comments'])){
|
||||
if (isset($res['global_comments'])) {
|
||||
foreach ($res['global_comments'] as $k => &$globalComment) {
|
||||
$info = User::whereUserid($globalComment['user_id'])->first();
|
||||
if (!$info) {
|
||||
@@ -1094,7 +1097,7 @@ class ApproveController extends AbstractController
|
||||
public function getUserProcessParticipantById($id)
|
||||
{
|
||||
$data['procInstId'] = intval($id);
|
||||
$ret = Ihttp::ihttp_get($this->flow_url."/api/v1/workflow/identitylink/findParticipantAll?".http_build_query($data));
|
||||
$ret = Ihttp::ihttp_get($this->flow_url . "/api/v1/workflow/identitylink/findParticipantAll?" . http_build_query($data));
|
||||
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
|
||||
if (!$process || $process['status'] != 200) {
|
||||
throw new ApiException($process['message'] ?? '查询失败');
|
||||
@@ -1110,20 +1113,44 @@ class ApproveController extends AbstractController
|
||||
* @apiGroup system
|
||||
* @apiName user__status
|
||||
*
|
||||
* @apiParam {String} userid
|
||||
* @apiParam {String} userid
|
||||
*
|
||||
* @apiSuccess {String}
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function user__status()
|
||||
{
|
||||
$data['userid'] = intval(Request::input('userid'));
|
||||
$ret = Ihttp::ihttp_get($this->flow_url.'/api/v1/workflow/process/getUserApprovalStatus?'.http_build_query($data));
|
||||
$procdef = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
|
||||
$ret = Ihttp::ihttp_get($this->flow_url . '/api/v1/workflow/process/getUserApprovalStatus?' . http_build_query($data));
|
||||
$procdef = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
|
||||
if (isset($procdef['status']) && $procdef['status'] == 200) {
|
||||
return Base::retSuccess('success', isset($procdef['data']["proc_def_name"]) ? $procdef['data']["proc_def_name"] : '');
|
||||
}
|
||||
}
|
||||
return Base::retSuccess('success', '');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @api {get} api/approve/process/doto 21. 查询需要我审批的流程数量
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup approve
|
||||
* @apiName process__doto
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function process__doto()
|
||||
{
|
||||
$user = User::auth();
|
||||
$ret = Ihttp::ihttp_get($this->flow_url . '/api/v1/workflow/process/findTaskTotal?userid=' . $user->userid);
|
||||
$process = json_decode($ret['ret'] == 1 ? $ret['data'] : '{}', true);
|
||||
if (!$process || $process['status'] != 200) {
|
||||
return Base::retError($process['message'] ?? '查询失败');
|
||||
}
|
||||
return Base::retSuccess('success', $process['data']);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Models\WebSocketDialogMsg;
|
||||
use App\Models\WebSocketDialog;
|
||||
use App\Exceptions\ApiException;
|
||||
use App\Models\AbstractModel;
|
||||
use App\Models\File;
|
||||
@@ -11,6 +13,8 @@ use App\Models\FileUser;
|
||||
use App\Models\User;
|
||||
use App\Module\Base;
|
||||
use App\Module\Ihttp;
|
||||
use Response;
|
||||
use Session;
|
||||
use Swoole\Coroutine;
|
||||
use Carbon\Carbon;
|
||||
use Redirect;
|
||||
@@ -695,6 +699,9 @@ class FileController extends AbstractController
|
||||
* @apiName content__upload
|
||||
*
|
||||
* @apiParam {Number} [pid] 父级ID
|
||||
* @apiParam {Number} [cover] 覆盖已存在的文件
|
||||
* - 0:不覆盖,保留两者(默认)
|
||||
* - 1:覆盖
|
||||
* @apiParam {String} [files] 文件名
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
@@ -705,8 +712,9 @@ class FileController extends AbstractController
|
||||
{
|
||||
$user = User::auth();
|
||||
$pid = intval(Request::input('pid'));
|
||||
$overwrite = intval(Request::input('cover'));
|
||||
$webkitRelativePath = Request::input('webkitRelativePath');
|
||||
$data = (new File)->contentUpload($user, $pid, $webkitRelativePath);
|
||||
$data = (new File)->contentUpload($user, $pid, $webkitRelativePath, $overwrite);
|
||||
return Base::retSuccess($data['data']['name'] . ' 上传成功', $data['addItem']);
|
||||
}
|
||||
|
||||
@@ -973,49 +981,7 @@ class FileController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/file/download/check 19. 检测下载
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup file
|
||||
* @apiName download__check
|
||||
*
|
||||
* @apiParam {Array} [ids] 文件ID,格式: [id, id2, id3]
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function download__check(){
|
||||
$user = User::auth();
|
||||
$ids = Request::input('ids');
|
||||
|
||||
if (!is_array($ids) || empty($ids)) {
|
||||
return Base::retError('请选择下载的文件或文件夹');
|
||||
}
|
||||
|
||||
if (count($ids) > 100) {
|
||||
return Base::retError('一次最多只能下载100个文件或文件夹');
|
||||
}
|
||||
|
||||
$files = [];
|
||||
$totalSize = 0;
|
||||
AbstractModel::transaction(function() use ($user, $ids, &$files, &$totalSize) {
|
||||
foreach ($ids as $k => $id) {
|
||||
$files[] = File::getFilesTree(intval($id), $user, 1);
|
||||
$totalSize += $files[$k]->totalSize;
|
||||
}
|
||||
});
|
||||
|
||||
if ($totalSize > File::zipMaxSize) {
|
||||
throw new ApiException('文件总大小已超过1GB,请分批下载');
|
||||
}
|
||||
|
||||
return Base::retSuccess('success');
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/file/download/pack 20. 打包文件
|
||||
* @api {get} api/file/download/pack 19. 打包文件
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -1031,89 +997,110 @@ class FileController extends AbstractController
|
||||
*/
|
||||
public function download__pack()
|
||||
{
|
||||
$key = Request::input('key');
|
||||
if ($key) {
|
||||
$userid = Session::get('file::pack:userid');
|
||||
if (empty($userid)) {
|
||||
return Base::ajaxError("请求已过期,请重新导出!", [], 0, 502);
|
||||
}
|
||||
//
|
||||
$array = Base::string2array(base64_decode(urldecode($key)));
|
||||
$file = $array['file'];
|
||||
if (empty($file) || !file_exists(storage_path($file))) {
|
||||
return Base::ajaxError("文件不存在!", [], 0, 502);
|
||||
}
|
||||
return Response::download(storage_path($file));
|
||||
}
|
||||
|
||||
$user = User::auth();
|
||||
$ids = Request::input('ids');
|
||||
$downName = Request::input('name');
|
||||
$fileName = Request::input('name');
|
||||
$fileName = preg_replace("/[\/\\\:\*\?\"\<\>\|]/", "", $fileName);
|
||||
if (empty($fileName)) {
|
||||
$fileName = 'Package_' . $user->userid;
|
||||
}
|
||||
$fileName .= '_' . Base::time() . '.zip';
|
||||
|
||||
$filePath = "temp/file/pack/" . date("Ym", Base::time());
|
||||
$zipFile = "app/" . $filePath . "/" . $fileName;
|
||||
$zipPath = storage_path($zipFile);
|
||||
|
||||
if (!is_array($ids) || empty($ids)) {
|
||||
abort(403, "Please select the file or folder to download.");
|
||||
return Base::retError('请选择下载的文件或文件夹');
|
||||
}
|
||||
if (count($ids) > 100) {
|
||||
abort(403, "You can download a maximum of 100 files or folders at a time.");
|
||||
return Base::retError('一次最多可以下载100个文件或文件夹');
|
||||
}
|
||||
|
||||
$botUser = User::botGetOrCreate('system-msg');
|
||||
if (empty($botUser)) {
|
||||
return Base::retError('系统机器人不存在');
|
||||
}
|
||||
$dialog = WebSocketDialog::checkUserDialog($botUser, $user->userid);
|
||||
|
||||
$files = [];
|
||||
$totalSize = 0;
|
||||
AbstractModel::transaction(function() use ($user, $ids, &$files, &$totalSize) {
|
||||
foreach ($ids as $k => $id) {
|
||||
$files[] = File::getFilesTree(intval($id), $user, 1);
|
||||
$totalSize += $files[$k]->totalSize;
|
||||
}
|
||||
});
|
||||
|
||||
if ($totalSize > File::zipMaxSize) {
|
||||
abort(403, "The total size of the file exceeds 1GB. Please download it in batches.");
|
||||
foreach ($ids as $k => $id) {
|
||||
$files[] = File::getFilesTree(intval($id), $user, 1);
|
||||
$totalSize += $files[$k]->totalSize;
|
||||
}
|
||||
|
||||
if ($totalSize > File::zipMaxSize) {
|
||||
return Base::retError('文件总大小已超过1GB,请分批下载');
|
||||
}
|
||||
|
||||
$base64 = base64_encode(Base::array2string([
|
||||
'file' => $zipFile,
|
||||
]));
|
||||
$fileUrl = Base::fillUrl('api/file/download/pack?key=' . urlencode($base64));
|
||||
Session::put('file::pack:userid', $user->userid);
|
||||
|
||||
$zip = new \ZipArchive();
|
||||
$zipName = 'temp/download/' . date("Ym") . '/' . $user->userid . '/' . $downName;
|
||||
$zipPath = storage_path('app/'.$zipName);
|
||||
Base::makeDir(dirname($zipPath));
|
||||
|
||||
if ($zip->open($zipPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
|
||||
abort(403, "Failed to create compressed file.");
|
||||
return Base::retError('创建压缩文件失败');
|
||||
}
|
||||
|
||||
go(function() use ($zip, $files, $downName) {
|
||||
go(function () use ($zipPath, $fileUrl, $zip, $files, $fileName, $botUser, $dialog) {
|
||||
Coroutine::sleep(0.1);
|
||||
// 压缩进度
|
||||
$progress = 0;
|
||||
$zip->registerProgressCallback(0.05, function($ratio) use ($downName, &$progress) {
|
||||
$zip->registerProgressCallback(0.05, function ($ratio) use ($fileUrl, $fileName, &$progress) {
|
||||
$progress = round($ratio * 100);
|
||||
File::filePushMsg('compress', [
|
||||
'name'=> $downName,
|
||||
'name' => $fileName,
|
||||
'url' => $fileUrl,
|
||||
'progress' => $progress
|
||||
]);
|
||||
});
|
||||
|
||||
//
|
||||
foreach ($files as $file) {
|
||||
File::addFileTreeToZip($zip, $file);
|
||||
}
|
||||
$zip->close();
|
||||
//
|
||||
if ($progress < 100) {
|
||||
File::filePushMsg('compress', [
|
||||
'name'=> $downName,
|
||||
'name' => $fileName,
|
||||
'url' => $fileUrl,
|
||||
'progress' => 100
|
||||
]);
|
||||
}
|
||||
//
|
||||
$text = "<b>文件下载打包已完成。</b>";
|
||||
$text .= "\n\n";
|
||||
$text .= "文件名:{$fileName}";
|
||||
$text .= "\n";
|
||||
$text .= "文件大小:".Base::twoFloat(filesize($zipPath) / 1024, true)."KB";
|
||||
$text .= "\n";
|
||||
$text .= '<a href="' . $fileUrl . '" target="_blank"><button type="button" class="ivu-btn ivu-btn-warning" style="margin-top: 10px;"><span>立即下载</span></button></a>';
|
||||
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'text', ['text' => $text], $botUser->userid, false, false, true);
|
||||
});
|
||||
|
||||
return Base::retSuccess('success');
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/file/download/confirm 21. 确认下载
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup file
|
||||
* @apiName download__confirm
|
||||
*
|
||||
* @apiParam {String} [name] 下载文件名
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function download__confirm()
|
||||
{
|
||||
$user = User::auth();
|
||||
$downName = Request::input('name');
|
||||
$zipName = 'temp/download/' . date("Ym") . '/' . $user->userid . '/' . $downName;
|
||||
$zipPath = storage_path('app/'.$zipName);
|
||||
if (!file_exists($zipPath)) {
|
||||
abort(403, "The file does not exist.");
|
||||
}
|
||||
return response()->download($zipPath);
|
||||
return Base::retSuccess('success', [
|
||||
'name' => $fileName,
|
||||
'url' => $fileUrl,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ use App\Module\Doo;
|
||||
use App\Models\File;
|
||||
use App\Models\User;
|
||||
use App\Module\Base;
|
||||
use Swoole\Coroutine;
|
||||
use App\Models\Deleted;
|
||||
use App\Models\Project;
|
||||
use App\Module\TimeRange;
|
||||
@@ -30,9 +31,12 @@ use App\Models\ProjectTaskFile;
|
||||
use App\Models\ProjectTaskUser;
|
||||
use App\Models\WebSocketDialog;
|
||||
use App\Exceptions\ApiException;
|
||||
use App\Models\ProjectPermission;
|
||||
use App\Models\WebSocketDialogMsg;
|
||||
use App\Module\BillMultipleExport;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use App\Models\ProjectTaskFlowChange;
|
||||
use App\Models\ProjectTaskVisibilityUser;
|
||||
|
||||
/**
|
||||
* @apiDefine project
|
||||
@@ -575,6 +579,8 @@ class ProjectController extends AbstractController
|
||||
$project = Project::userProject($project_id);
|
||||
//
|
||||
if ($only_column) {
|
||||
//
|
||||
ProjectPermission::userTaskPermission($project, ProjectPermission::TASK_LIST_SORT);
|
||||
// 排序列表
|
||||
$index = 0;
|
||||
foreach ($sort as $item) {
|
||||
@@ -760,6 +766,8 @@ class ProjectController extends AbstractController
|
||||
// 项目
|
||||
$project = Project::userProject($project_id);
|
||||
//
|
||||
ProjectPermission::userTaskPermission($project, ProjectPermission::TASK_LIST_ADD);
|
||||
//
|
||||
if (empty($name)) {
|
||||
return Base::retError('列表名称不能为空');
|
||||
}
|
||||
@@ -809,7 +817,9 @@ class ProjectController extends AbstractController
|
||||
return Base::retError('列表不存在');
|
||||
}
|
||||
// 项目
|
||||
Project::userProject($column->project_id);
|
||||
$project = Project::userProject($column->project_id);
|
||||
//
|
||||
ProjectPermission::userTaskPermission($project, ProjectPermission::TASK_LIST_UPDATE);
|
||||
//
|
||||
if (Arr::exists($data, 'name') && $column->name != $data['name']) {
|
||||
$column->addLog("修改列表名称:{$column->name} => {$data['name']}");
|
||||
@@ -849,7 +859,9 @@ class ProjectController extends AbstractController
|
||||
return Base::retError('列表不存在');
|
||||
}
|
||||
// 项目
|
||||
Project::userProject($column->project_id, true, true);
|
||||
$project = Project::userProject($column->project_id);
|
||||
//
|
||||
ProjectPermission::userTaskPermission($project, ProjectPermission::TASK_LIST_REMOVE);
|
||||
//
|
||||
$column->deleteColumn();
|
||||
return Base::retSuccess('删除成功', ['id' => $column->id]);
|
||||
@@ -876,10 +888,10 @@ class ProjectController extends AbstractController
|
||||
public function column__one()
|
||||
{
|
||||
User::auth();
|
||||
//
|
||||
//
|
||||
$column_id = intval(Request::input('column_id'));
|
||||
$deleted = Request::input('deleted', 'no');
|
||||
//
|
||||
//
|
||||
$builder = ProjectColumn::whereId($column_id);
|
||||
if ($deleted == 'all') {
|
||||
$builder->withTrashed();
|
||||
@@ -890,10 +902,10 @@ class ProjectController extends AbstractController
|
||||
if (empty($column)) {
|
||||
return Base::retError('列表不存在');
|
||||
}
|
||||
//
|
||||
//
|
||||
return Base::retSuccess('success', $column);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @api {get} api/project/task/lists 19. 任务列表
|
||||
@@ -1016,17 +1028,25 @@ class ProjectController extends AbstractController
|
||||
$builder->orderBy('project_tasks.' . $column, $direction);
|
||||
}
|
||||
// 任务可见性条件
|
||||
$builder->leftJoin('project_users', function ($query) {
|
||||
$query->on('project_tasks.project_id', '=', 'project_users.project_id')->where('project_users.owner', 1);
|
||||
$builder->leftJoin('project_users', function ($query) use($userid) {
|
||||
$query->on('project_tasks.project_id', '=', 'project_users.project_id');
|
||||
$query->where('project_users.owner', 1);
|
||||
$query->where('project_users.userid', $userid);
|
||||
});
|
||||
$builder->leftJoin('project_task_users as project_p_task_users', function ($query) {
|
||||
$query->on('project_p_task_users.task_pid', '=', 'project_tasks.parent_id');
|
||||
$builder->leftJoin('project_task_users as project_sub_task_users', function ($query) use($userid) {
|
||||
$query->on('project_sub_task_users.task_pid', '=', 'project_tasks.parent_id');
|
||||
$query->where('project_sub_task_users.userid', $userid);
|
||||
});
|
||||
$builder->leftJoin('project_task_visibility_users', function ($query) use($userid) {
|
||||
$query->on('project_task_visibility_users.task_id', '=', 'project_tasks.id');
|
||||
$query->where('project_task_visibility_users.userid', $userid);
|
||||
});
|
||||
$builder->where(function ($query) use ($userid) {
|
||||
$query->where("project_tasks.visibility", 1);
|
||||
$query->orWhere("project_users.userid", $userid);
|
||||
$query->orWhere("project_task_users.userid", $userid);
|
||||
$query->orWhere("project_p_task_users.userid", $userid);
|
||||
$query->orWhere("project_task_visibility_users.userid", $userid);
|
||||
$query->orWhere("project_sub_task_users.userid", $userid);
|
||||
});
|
||||
// 优化子查询汇总
|
||||
$builder->leftJoinSub(function ($query) {
|
||||
@@ -1180,170 +1200,191 @@ class ProjectController extends AbstractController
|
||||
if (Carbon::parse($time[1])->timestamp - Carbon::parse($time[0])->timestamp > 90 * 86400) {
|
||||
return Base::retError('时间范围限制最大90天');
|
||||
}
|
||||
$headings = [];
|
||||
$headings[] = '任务ID';
|
||||
$headings[] = '父级任务ID';
|
||||
$headings[] = '所属项目';
|
||||
$headings[] = '任务标题';
|
||||
$headings[] = '任务开始时间';
|
||||
$headings[] = '任务结束时间';
|
||||
$headings[] = '完成时间';
|
||||
$headings[] = '归档时间';
|
||||
$headings[] = '任务计划用时';
|
||||
$headings[] = '实际完成用时';
|
||||
$headings[] = '超时时间';
|
||||
$headings[] = '开发用时';
|
||||
$headings[] = '验收/测试用时';
|
||||
$headings[] = '负责人';
|
||||
$headings[] = '创建人';
|
||||
$headings[] = '状态';
|
||||
$datas = [];
|
||||
$botUser = User::botGetOrCreate('system-msg');
|
||||
if (empty($botUser)) {
|
||||
return Base::retError('系统机器人不存在');
|
||||
}
|
||||
$dialog = WebSocketDialog::checkUserDialog($botUser, $user->userid);
|
||||
//
|
||||
$builder = ProjectTask::select(['project_tasks.*', 'project_task_users.userid as ownerid'])
|
||||
->join('project_task_users', 'project_tasks.id', '=', 'project_task_users.task_id')
|
||||
->where('project_task_users.owner', 1)
|
||||
->whereIn('project_task_users.userid', $userid)
|
||||
->betweenTime(Carbon::parse($time[0])->startOfDay(), Carbon::parse($time[1])->endOfDay(), $type);
|
||||
$builder->orderByDesc('project_tasks.id')->chunk(100, function ($tasks) use (&$datas) {
|
||||
/** @var ProjectTask $task */
|
||||
foreach ($tasks as $task) {
|
||||
$flowChanges = ProjectTaskFlowChange::whereTaskId($task->id)->get();
|
||||
$testTime = 0;//验收/测试时间
|
||||
$taskStartTime = $task->start_at ? Carbon::parse($task->start_at)->timestamp : Carbon::parse($task->created_at)->timestamp;
|
||||
$taskCompleteTime = $task->complete_at ? Carbon::parse($task->complete_at)->timestamp : time();
|
||||
$totalTime = $taskCompleteTime - $taskStartTime; //开发测试总用时
|
||||
foreach ($flowChanges as $change) {
|
||||
if (!str_contains($change->before_flow_item_name, 'end')) {
|
||||
$upOne = ProjectTaskFlowChange::where('id', '<', $change->id)->whereTaskId($task->id)->orderByDesc('id')->first();
|
||||
if ($upOne) {
|
||||
if (str_contains($change->before_flow_item_name, 'test') || str_contains($change->before_flow_item_name, '测试') || strpos($change->before_flow_item_name, '验收') !== false) {
|
||||
$testCtime = Carbon::parse($change->created_at)->timestamp;
|
||||
$tTime = Carbon::parse($upOne->created_at)->timestamp;
|
||||
$tMinusNum = $testCtime - $tTime;
|
||||
$testTime += $tMinusNum;
|
||||
go(function () use ($user, $userid, $time, $type, $botUser, $dialog) {
|
||||
Coroutine::sleep(0.1);
|
||||
$headings = [];
|
||||
$headings[] = '任务ID';
|
||||
$headings[] = '父级任务ID';
|
||||
$headings[] = '所属项目';
|
||||
$headings[] = '任务标题';
|
||||
$headings[] = '任务开始时间';
|
||||
$headings[] = '任务结束时间';
|
||||
$headings[] = '完成时间';
|
||||
$headings[] = '归档时间';
|
||||
$headings[] = '任务计划用时';
|
||||
$headings[] = '实际完成用时';
|
||||
$headings[] = '超时时间';
|
||||
$headings[] = '开发用时';
|
||||
$headings[] = '验收/测试用时';
|
||||
$headings[] = '负责人';
|
||||
$headings[] = '创建人';
|
||||
$headings[] = '状态';
|
||||
$datas = [];
|
||||
//
|
||||
$text = '<b>导出任务统计已完成。</b>';
|
||||
$text .= "\n\n";
|
||||
//
|
||||
$builder = ProjectTask::select(['project_tasks.*', 'project_task_users.userid as ownerid'])
|
||||
->join('project_task_users', 'project_tasks.id', '=', 'project_task_users.task_id')
|
||||
->where('project_task_users.owner', 1)
|
||||
->whereIn('project_task_users.userid', $userid)
|
||||
->betweenTime(Carbon::parse($time[0])->startOfDay(), Carbon::parse($time[1])->endOfDay(), $type);
|
||||
$builder->orderByDesc('project_tasks.id')->chunk(100, function ($tasks) use (&$datas) {
|
||||
/** @var ProjectTask $task */
|
||||
foreach ($tasks as $task) {
|
||||
$flowChanges = ProjectTaskFlowChange::whereTaskId($task->id)->get();
|
||||
$testTime = 0;//验收/测试时间
|
||||
$taskStartTime = $task->start_at ? Carbon::parse($task->start_at)->timestamp : Carbon::parse($task->created_at)->timestamp;
|
||||
$taskCompleteTime = $task->complete_at ? Carbon::parse($task->complete_at)->timestamp : time();
|
||||
$totalTime = $taskCompleteTime - $taskStartTime; //开发测试总用时
|
||||
foreach ($flowChanges as $change) {
|
||||
if (!str_contains($change->before_flow_item_name, 'end')) {
|
||||
$upOne = ProjectTaskFlowChange::where('id', '<', $change->id)->whereTaskId($task->id)->orderByDesc('id')->first();
|
||||
if ($upOne) {
|
||||
if (str_contains($change->before_flow_item_name, 'test') || str_contains($change->before_flow_item_name, '测试') || strpos($change->before_flow_item_name, '验收') !== false) {
|
||||
$testCtime = Carbon::parse($change->created_at)->timestamp;
|
||||
$tTime = Carbon::parse($upOne->created_at)->timestamp;
|
||||
$tMinusNum = $testCtime - $tTime;
|
||||
$testTime += $tMinusNum;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!$task->complete_at) {
|
||||
$lastChange = ProjectTaskFlowChange::whereTaskId($task->id)->orderByDesc('id')->first();
|
||||
$nowTime = time();
|
||||
$unFinishTime = $nowTime - Carbon::parse($lastChange->created_at)->timestamp;
|
||||
if (str_contains($lastChange->after_flow_item_name, 'test') || str_contains($lastChange->after_flow_item_name, '测试') || strpos($lastChange->after_flow_item_name, '验收') !== false) {
|
||||
$testTime += $unFinishTime;
|
||||
if (!$task->complete_at) {
|
||||
$lastChange = ProjectTaskFlowChange::whereTaskId($task->id)->orderByDesc('id')->first();
|
||||
$nowTime = time();
|
||||
$unFinishTime = $nowTime - Carbon::parse($lastChange->created_at)->timestamp;
|
||||
if (str_contains($lastChange->after_flow_item_name, 'test') || str_contains($lastChange->after_flow_item_name, '测试') || strpos($lastChange->after_flow_item_name, '验收') !== false) {
|
||||
$testTime += $unFinishTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
$developTime = $totalTime - $testTime;//开发时间
|
||||
$planTime = '-';//任务计划用时
|
||||
$overTime = '-';//超时时间
|
||||
if ($task->end_at) {
|
||||
$startTime = Carbon::parse($task->start_at)->timestamp;
|
||||
$endTime = Carbon::parse($task->end_at)->timestamp;
|
||||
$planTotalTime = $endTime - $startTime;
|
||||
$residueTime = $planTotalTime - $totalTime;
|
||||
if ($residueTime < 0) {
|
||||
$overTime = Base::timeFormat(abs($residueTime));
|
||||
$developTime = $totalTime - $testTime;//开发时间
|
||||
$planTime = '-';//任务计划用时
|
||||
$overTime = '-';//超时时间
|
||||
if ($task->end_at) {
|
||||
$startTime = Carbon::parse($task->start_at)->timestamp;
|
||||
$endTime = Carbon::parse($task->end_at)->timestamp;
|
||||
$planTotalTime = $endTime - $startTime;
|
||||
$residueTime = $planTotalTime - $totalTime;
|
||||
if ($residueTime < 0) {
|
||||
$overTime = Base::timeFormat(abs($residueTime));
|
||||
}
|
||||
$planTime = Base::timeDiff($startTime, $endTime);
|
||||
}
|
||||
$planTime = Base::timeDiff($startTime, $endTime);
|
||||
}
|
||||
$actualTime = $task->complete_at ? $totalTime : 0;//实际完成用时
|
||||
$statusText = '未完成';
|
||||
if ($task->flow_item_name) {
|
||||
if (str_contains($task->flow_item_name, '已取消')) {
|
||||
$statusText = '已取消';
|
||||
$actualTime = 0;
|
||||
$testTime = 0;
|
||||
$developTime = 0;
|
||||
$overTime = '-';
|
||||
} elseif (str_contains($task->flow_item_name, '已完成')) {
|
||||
$actualTime = $task->complete_at ? $totalTime : 0;//实际完成用时
|
||||
$statusText = '未完成';
|
||||
if ($task->flow_item_name) {
|
||||
if (str_contains($task->flow_item_name, '已取消')) {
|
||||
$statusText = '已取消';
|
||||
$actualTime = 0;
|
||||
$testTime = 0;
|
||||
$developTime = 0;
|
||||
$overTime = '-';
|
||||
} elseif (str_contains($task->flow_item_name, '已完成')) {
|
||||
$statusText = '已完成';
|
||||
}
|
||||
} elseif ($task->complete_at) {
|
||||
$statusText = '已完成';
|
||||
}
|
||||
} elseif ($task->complete_at) {
|
||||
$statusText = '已完成';
|
||||
}
|
||||
if (!isset($datas[$task->ownerid])) {
|
||||
$datas[$task->ownerid] = [
|
||||
'index' => 1,
|
||||
'nickname' => Base::filterEmoji(User::userid2nickname($task->ownerid)),
|
||||
'styles' => ["A1:P1" => ["font" => ["bold" => true]]],
|
||||
'data' => [],
|
||||
if (!isset($datas[$task->ownerid])) {
|
||||
$datas[$task->ownerid] = [
|
||||
'index' => 1,
|
||||
'nickname' => Base::filterEmoji(User::userid2nickname($task->ownerid)),
|
||||
'styles' => ["A1:P1" => ["font" => ["bold" => true]]],
|
||||
'data' => [],
|
||||
];
|
||||
}
|
||||
$datas[$task->ownerid]['index']++;
|
||||
if ($statusText === '未完成') {
|
||||
$datas[$task->ownerid]['styles']["P{$datas[$task->ownerid]['index']}"] = ["font" => ["color" => ["rgb" => "ff0000"]]]; // 未完成
|
||||
} elseif ($statusText === '已完成' && $task->end_at && Carbon::parse($task->complete_at)->gt($task->end_at)) {
|
||||
$datas[$task->ownerid]['styles']["P{$datas[$task->ownerid]['index']}"] = ["font" => ["color" => ["rgb" => "436FF6"]]]; // 已完成超期
|
||||
}
|
||||
$datas[$task->ownerid]['data'][] = [
|
||||
$task->id,
|
||||
$task->parent_id ?: '-',
|
||||
Base::filterEmoji($task->project?->name) ?: '-',
|
||||
Base::filterEmoji($task->name),
|
||||
$task->start_at ?: '-',
|
||||
$task->end_at ?: '-',
|
||||
$task->complete_at ?: '-',
|
||||
$task->archived_at ?: '-',
|
||||
$planTime ?: '-',
|
||||
$actualTime ? Base::timeFormat($actualTime) : '-',
|
||||
$overTime,
|
||||
$developTime > 0 ? Base::timeFormat($developTime) : '-',
|
||||
$testTime > 0 ? Base::timeFormat($testTime) : '-',
|
||||
Base::filterEmoji(User::userid2nickname($task->ownerid)) . " (ID: {$task->ownerid})",
|
||||
Base::filterEmoji(User::userid2nickname($task->userid)) . " (ID: {$task->userid})",
|
||||
$statusText
|
||||
];
|
||||
}
|
||||
$datas[$task->ownerid]['index']++;
|
||||
if ($statusText === '未完成') {
|
||||
$datas[$task->ownerid]['styles']["P{$datas[$task->ownerid]['index']}"] = ["font" => ["color" => ["rgb" => "ff0000"]]]; // 未完成
|
||||
} elseif ($statusText === '已完成' && $task->end_at && Carbon::parse($task->complete_at)->gt($task->end_at)) {
|
||||
$datas[$task->ownerid]['styles']["P{$datas[$task->ownerid]['index']}"] = ["font" => ["color" => ["rgb" => "436FF6"]]]; // 已完成超期
|
||||
}
|
||||
$datas[$task->ownerid]['data'][] = [
|
||||
$task->id,
|
||||
$task->parent_id ?: '-',
|
||||
Base::filterEmoji($task->project?->name) ?: '-',
|
||||
Base::filterEmoji($task->name),
|
||||
$task->start_at ?: '-',
|
||||
$task->end_at ?: '-',
|
||||
$task->complete_at ?: '-',
|
||||
$task->archived_at ?: '-',
|
||||
$planTime ?: '-',
|
||||
$actualTime ? Base::timeFormat($actualTime) : '-',
|
||||
$overTime,
|
||||
$developTime > 0 ? Base::timeFormat($developTime) : '-',
|
||||
$testTime > 0 ? Base::timeFormat($testTime) : '-',
|
||||
Base::filterEmoji(User::userid2nickname($task->ownerid)) . " (ID: {$task->ownerid})",
|
||||
Base::filterEmoji(User::userid2nickname($task->userid)) . " (ID: {$task->userid})",
|
||||
$statusText
|
||||
];
|
||||
});
|
||||
if (empty($datas)) {
|
||||
$text .= '没有任何数据';
|
||||
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'text', ['text' => $text], $botUser->userid, false, false, true);
|
||||
return;
|
||||
}
|
||||
});
|
||||
if (empty($datas)) {
|
||||
return Base::retError('没有任何数据');
|
||||
}
|
||||
//
|
||||
$sheets = [];
|
||||
foreach ($userid as $ownerid) {
|
||||
$data = $datas[$ownerid] ?? [
|
||||
//
|
||||
$sheets = [];
|
||||
foreach ($userid as $ownerid) {
|
||||
$data = $datas[$ownerid] ?? [
|
||||
'nickname' => Base::filterEmoji(User::userid2nickname($ownerid)),
|
||||
'styles' => ["A1:P1" => ["font" => ["bold" => true]]],
|
||||
'data' => [],
|
||||
];
|
||||
$title = (count($sheets) + 1) . "." . ($data['nickname'] ?: $ownerid);
|
||||
$sheets[] = BillExport::create()->setTitle($title)->setHeadings($headings)->setData($data['data'])->setStyles($data['styles']);
|
||||
}
|
||||
//
|
||||
$fileName = User::userid2nickname($userid[0]) ?: $userid[0];
|
||||
if (count($userid) > 1) {
|
||||
$fileName .= "等" . count($userid) . "位成员";
|
||||
}
|
||||
$fileName .= '任务统计_' . Base::time() . '.xls';
|
||||
$filePath = "temp/task/export/" . date("Ym", Base::time());
|
||||
$export = new BillMultipleExport($sheets);
|
||||
$res = $export->store($filePath . "/" . $fileName);
|
||||
if ($res != 1) {
|
||||
return Base::retError('导出失败,' . $fileName . '!');
|
||||
}
|
||||
$xlsPath = storage_path("app/" . $filePath . "/" . $fileName);
|
||||
$zipFile = "app/" . $filePath . "/" . Base::rightDelete($fileName, '.xls') . ".zip";
|
||||
$zipPath = storage_path($zipFile);
|
||||
if (file_exists($zipPath)) {
|
||||
Base::deleteDirAndFile($zipPath, true);
|
||||
}
|
||||
try {
|
||||
Madzipper::make($zipPath)->add($xlsPath)->close();
|
||||
} catch (\Throwable) {
|
||||
}
|
||||
//
|
||||
if (file_exists($zipPath)) {
|
||||
$base64 = base64_encode(Base::array2string([
|
||||
'file' => $zipFile,
|
||||
]));
|
||||
Session::put('task::export:userid', $user->userid);
|
||||
return Base::retSuccess('success', [
|
||||
'size' => Base::twoFloat(filesize($zipPath) / 1024, true),
|
||||
'url' => Base::fillUrl('api/project/task/down?key=' . urlencode($base64)),
|
||||
]);
|
||||
} else {
|
||||
return Base::retError('打包失败,请稍后再试...');
|
||||
}
|
||||
$title = (count($sheets) + 1) . "." . ($data['nickname'] ?: $ownerid);
|
||||
$sheets[] = BillExport::create()->setTitle($title)->setHeadings($headings)->setData($data['data'])->setStyles($data['styles']);
|
||||
}
|
||||
//
|
||||
$fileName = User::userid2nickname($userid[0]) ?: $userid[0];
|
||||
if (count($userid) > 1) {
|
||||
$fileName .= '等' . count($userid) . '位成员任务统计';
|
||||
}
|
||||
$fileName .= '_' . Base::time() . '.xls';
|
||||
$filePath = "temp/task/export/" . date("Ym", Base::time());
|
||||
$export = new BillMultipleExport($sheets);
|
||||
$res = $export->store($filePath . "/" . $fileName);
|
||||
if ($res != 1) {
|
||||
$text .= "导出失败,{$fileName}!";
|
||||
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'text', ['text' => $text], $botUser->userid, false, false, true);
|
||||
return;
|
||||
}
|
||||
//
|
||||
$xlsPath = storage_path("app/" . $filePath . "/" . $fileName);
|
||||
$zipFile = "app/" . $filePath . "/" . Base::rightDelete($fileName, '.xls') . ".zip";
|
||||
$zipPath = storage_path($zipFile);
|
||||
if (file_exists($zipPath)) {
|
||||
Base::deleteDirAndFile($zipPath, true);
|
||||
}
|
||||
try {
|
||||
Madzipper::make($zipPath)->add($xlsPath)->close();
|
||||
} catch (\Throwable) {
|
||||
}
|
||||
//
|
||||
if (file_exists($zipPath)) {
|
||||
$base64 = base64_encode(Base::array2string([
|
||||
'file' => $zipFile,
|
||||
]));
|
||||
$fileUrl = Base::fillUrl('api/project/task/down?key=' . urlencode($base64));
|
||||
Session::put('task::export:userid', $user->userid);
|
||||
$text .= "文件名:{$fileName}";
|
||||
$text .= "\n";
|
||||
$text .= "文件大小:" . Base::twoFloat(filesize($zipPath) / 1024, true) . "KB";
|
||||
$text .= "\n";
|
||||
$text .= '<a href="' . $fileUrl . '" target="_blank"><button type="button" class="ivu-btn ivu-btn-warning" style="margin-top: 10px;"><span>立即下载</span></button></a>';
|
||||
} else {
|
||||
$text .= '打包失败,请稍后再试...';
|
||||
}
|
||||
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'text', ['text' => $text], $botUser->userid, false, false, true);
|
||||
});
|
||||
return Base::retSuccess('success', ['msg' => '正在打包,请留意系统消息。']);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1507,13 +1548,15 @@ class ProjectController extends AbstractController
|
||||
$archived = Request::input('archived', 'no');
|
||||
//
|
||||
$isArchived = str_replace(['all', 'yes', 'no'], [null, false, true], $archived);
|
||||
$task = ProjectTask::userTask($task_id, $isArchived, true, false, ['taskUser', 'taskTag']);
|
||||
$task = ProjectTask::userTask($task_id, $isArchived, true, ['taskUser', 'taskTag']);
|
||||
// 项目可见性
|
||||
$project_userid = ProjectUser::whereProjectId($task->project_id)->whereOwner(1)->value('userid'); // 项目负责人
|
||||
if ($task->visibility != 1 && $user->userid != $project_userid) {
|
||||
$visibleUserids = ProjectTaskUser::whereTaskId($task_id)->pluck('userid')->toArray(); // 是否任务负责人、协助人、可见人
|
||||
$subVisibleUserids = ProjectTaskUser::whereTaskPid($task_id)->pluck('userid')->toArray(); // 是否子任务负责人、协助人
|
||||
if (!in_array($user->userid, $visibleUserids) && !in_array($user->userid, $subVisibleUserids)) {
|
||||
$taskUserids = ProjectTaskUser::whereTaskId($task_id)->pluck('userid')->toArray(); //任务负责人、协助人
|
||||
$subTaskUserids = ProjectTaskUser::whereTaskPid($task_id)->pluck('userid')->toArray(); //子任务负责人、协助人
|
||||
$visibleUserids = ProjectTaskVisibilityUser::whereTaskId($task_id)->pluck('userid')->toArray(); //可见人
|
||||
$visibleUserids = array_merge($taskUserids, $subTaskUserids, $visibleUserids);
|
||||
if (!in_array($user->userid, $visibleUserids)) {
|
||||
return Base::retError('无任务权限');
|
||||
}
|
||||
}
|
||||
@@ -1521,7 +1564,7 @@ class ProjectController extends AbstractController
|
||||
$data = $task->toArray();
|
||||
$data['project_name'] = $task->project?->name;
|
||||
$data['column_name'] = $task->projectColumn?->name;
|
||||
$data['visibility_appointor'] = $task->visibility == 1 ? [0] : ProjectTaskUser::whereTaskId($task_id)->whereOwner(2)->pluck('userid');
|
||||
$data['visibility_appointor'] = $task->visibility == 1 ? [0] : ProjectTaskVisibilityUser::whereTaskId($task_id)->pluck('userid');
|
||||
return Base::retSuccess('success', $data);
|
||||
}
|
||||
|
||||
@@ -1603,7 +1646,9 @@ class ProjectController extends AbstractController
|
||||
return Base::retError('文件不存在或已被删除');
|
||||
}
|
||||
//
|
||||
$task = ProjectTask::userTask($file->task_id, true, true, true);
|
||||
$task = ProjectTask::userTask($file->task_id);
|
||||
//
|
||||
ProjectPermission::userTaskPermission(Project::userProject($task->project_id), ProjectPermission::TASK_UPDATE, $task);
|
||||
//
|
||||
$task->pushMsg('filedelete', $file);
|
||||
$file->delete();
|
||||
@@ -1732,6 +1777,8 @@ class ProjectController extends AbstractController
|
||||
$column_id = $data['column_id'];
|
||||
// 项目
|
||||
$project = Project::userProject($project_id);
|
||||
//
|
||||
ProjectPermission::userTaskPermission($project, ProjectPermission::TASK_ADD);
|
||||
// 列表
|
||||
$column = null;
|
||||
$newColumn = null;
|
||||
@@ -1808,11 +1855,13 @@ class ProjectController extends AbstractController
|
||||
$task_id = intval(Request::input('task_id'));
|
||||
$name = Request::input('name');
|
||||
//
|
||||
$task = ProjectTask::userTask($task_id, true, true, true);
|
||||
$task = ProjectTask::userTask($task_id);
|
||||
if ($task->complete_at) {
|
||||
return Base::retError('主任务已完成无法添加子任务');
|
||||
}
|
||||
//
|
||||
ProjectPermission::userTaskPermission(Project::userProject($task->project_id), ProjectPermission::TASK_ADD);
|
||||
//
|
||||
$task = ProjectTask::addTask([
|
||||
'name' => $name,
|
||||
'parent_id' => $task->id,
|
||||
@@ -1867,18 +1916,26 @@ class ProjectController extends AbstractController
|
||||
$param = Request::input();
|
||||
$task_id = intval($param['task_id']);
|
||||
//
|
||||
$task = ProjectTask::userTask($task_id, true, true, 2);
|
||||
$task = ProjectTask::userTask($task_id);
|
||||
//
|
||||
$project = Project::userProject($task->project_id);
|
||||
if (Arr::exists($param, 'flow_item_id')) {
|
||||
ProjectPermission::userTaskPermission($project, ProjectPermission::TASK_STATUS, $task);
|
||||
}else{
|
||||
ProjectPermission::userTaskPermission($project, ProjectPermission::TASK_UPDATE, $task);
|
||||
}
|
||||
//
|
||||
$taskUser = ProjectTaskUser::select(['userid', 'owner'])->whereTaskId($task_id)->get();
|
||||
$owners = $taskUser->where('owner', 1)->pluck('userid')->toArray(); // 负责人
|
||||
$assists = $taskUser->where('owner', 0)->pluck('userid')->toArray(); // 协助人
|
||||
|
||||
$owners = $taskUser->where('owner', 1)->pluck('userid')->toArray();
|
||||
$assists = $taskUser->where('owner', 0)->pluck('userid')->toArray();
|
||||
$visible = ProjectTaskVisibilityUser::whereTaskId($task->id)->pluck('userid')->toArray();
|
||||
// 更新任务
|
||||
$updateMarking = [];
|
||||
$task->updateTask($param, $updateMarking);
|
||||
//
|
||||
$data = ProjectTask::oneTask($task->id)->toArray();
|
||||
$data['update_marking'] = $updateMarking ?: json_decode('{}');
|
||||
$data['visibility_appointor'] = $data['visibility'] == 1 ? [] : ProjectTaskUser::whereTaskId($task->id)->whereOwner(2)->pluck('userid');
|
||||
$data['visibility_appointor'] = $data['visibility'] == 1 ? [] : ProjectTaskVisibilityUser::whereTaskId($task->id)->pluck('userid');
|
||||
$task->pushMsg('update', $data);
|
||||
// 可见性推送
|
||||
if ($task->parent_id == 0) {
|
||||
@@ -1888,10 +1945,9 @@ class ProjectController extends AbstractController
|
||||
$task->pushMsgVisibleAdd($data);
|
||||
}
|
||||
if ($param['visibility_appointor']) {
|
||||
$oldVisibleUserIds = $taskUser->where('owner', 2)->pluck('userid')->toArray() ?? [];
|
||||
$newVisibleUserIds = $param['visibility_appointor'] ?? [];
|
||||
$deleteUserIds = array_diff($oldVisibleUserIds, $newVisibleUserIds, $subUserids);
|
||||
$addUserIds = array_diff($newVisibleUserIds, $oldVisibleUserIds);
|
||||
$deleteUserIds = array_diff($visible, $newVisibleUserIds, $subUserids);
|
||||
$addUserIds = array_diff($newVisibleUserIds, $visible);
|
||||
$task->pushMsgVisibleUpdate($data, $deleteUserIds, $addUserIds);
|
||||
}
|
||||
if ($data['visibility'] != 1 && empty($param['visibility_appointor'])) {
|
||||
@@ -2003,12 +2059,15 @@ class ProjectController extends AbstractController
|
||||
$task_id = intval(Request::input('task_id'));
|
||||
$type = Request::input('type', 'add');
|
||||
//
|
||||
$task = ProjectTask::userTask($task_id, $type == 'add', true, true);
|
||||
$task = ProjectTask::userTask($task_id, $type == 'add');
|
||||
//
|
||||
if ($task->parent_id > 0) {
|
||||
return Base::retError('子任务不支持此功能');
|
||||
}
|
||||
//
|
||||
$project = Project::userProject($task->project_id);
|
||||
ProjectPermission::userTaskPermission($project, ProjectPermission::TASK_ARCHIVED, $task);
|
||||
//
|
||||
if ($type == 'recovery') {
|
||||
$task->archivedTask(null);
|
||||
} elseif ($type == 'add') {
|
||||
@@ -2045,7 +2104,11 @@ class ProjectController extends AbstractController
|
||||
$task_id = intval(Request::input('task_id'));
|
||||
$type = Request::input('type', 'delete');
|
||||
//
|
||||
$task = ProjectTask::userTask($task_id, null, $type !== 'recovery', true);
|
||||
$task = ProjectTask::userTask($task_id, null, $type !== 'recovery');
|
||||
//
|
||||
$project = Project::userProject($task->project_id);
|
||||
ProjectPermission::userTaskPermission($project, ProjectPermission::TASK_REMOVE, $task);
|
||||
//
|
||||
if ($type == 'recovery') {
|
||||
$task->restoreTask();
|
||||
return Base::retSuccess('操作成功', ['id' => $task->id]);
|
||||
@@ -2080,7 +2143,7 @@ class ProjectController extends AbstractController
|
||||
return Base::retError('记录不存在');
|
||||
}
|
||||
//
|
||||
$task = ProjectTask::userTask($projectLog->task_id, true, true, true);
|
||||
$task = ProjectTask::userTask($projectLog->task_id);
|
||||
//
|
||||
$record = $projectLog->record;
|
||||
if ($record['flow'] && is_array($record['flow'])) {
|
||||
@@ -2123,6 +2186,7 @@ class ProjectController extends AbstractController
|
||||
* @apiName task__flow
|
||||
*
|
||||
* @apiParam {Number} task_id 任务ID
|
||||
* @apiParam {Number} project_id 项目ID - 存在时只返回这个项目的
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
@@ -2133,17 +2197,23 @@ class ProjectController extends AbstractController
|
||||
User::auth();
|
||||
//
|
||||
$task_id = intval(Request::input('task_id'));
|
||||
$project_id = intval(Request::input('project_id'));
|
||||
//
|
||||
$projectTask = ProjectTask::select(['id', 'project_id', 'complete_at', 'flow_item_id', 'flow_item_name'])->withTrashed()->find($task_id);
|
||||
if (empty($projectTask)) {
|
||||
return Base::retError('任务不存在', [ 'task_id' => $task_id ], -4002);
|
||||
return Base::retError('任务不存在', ['task_id' => $task_id], -4002);
|
||||
}
|
||||
//
|
||||
$projectFlowItem = $projectTask->flow_item_id ? ProjectFlowItem::with(['projectFlow'])->find($projectTask->flow_item_id) : null;
|
||||
if ($projectFlowItem?->projectFlow) {
|
||||
$projectFlow = $projectFlowItem->projectFlow;
|
||||
$projectFlowItem = null;
|
||||
if ($project_id) {
|
||||
$projectFlow = ProjectFlow::whereProjectId($project_id)->orderByDesc('id')->first();
|
||||
} else {
|
||||
$projectFlow = ProjectFlow::whereProjectId($projectTask->project_id)->orderByDesc('id')->first();
|
||||
$projectFlowItem = $projectTask->flow_item_id ? ProjectFlowItem::with(['projectFlow'])->find($projectTask->flow_item_id) : null;
|
||||
if ($projectFlowItem?->projectFlow) {
|
||||
$projectFlow = $projectFlowItem->projectFlow;
|
||||
} else {
|
||||
$projectFlow = ProjectFlow::whereProjectId($projectTask->project_id)->orderByDesc('id')->first();
|
||||
}
|
||||
}
|
||||
if (empty($projectFlow)) {
|
||||
return Base::retSuccess('success', [
|
||||
@@ -2197,7 +2267,7 @@ class ProjectController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/project/task/move 35. 任务移动
|
||||
* @api {get} api/project/task/move 38. 任务移动
|
||||
*
|
||||
* @apiDescription 需要token身份(限:项目、任务负责人)
|
||||
* @apiVersion 1.0.0
|
||||
@@ -2207,6 +2277,9 @@ class ProjectController extends AbstractController
|
||||
* @apiParam {Number} task_id 任务ID
|
||||
* @apiParam {Number} project_id 项目ID
|
||||
* @apiParam {Number} column_id 列ID
|
||||
* @apiParam {Number} flow_item_id 工作流id
|
||||
* @apiParam {Array} owner 负责人
|
||||
* @apiParam {Array} assist 协助人
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
@@ -2219,26 +2292,40 @@ class ProjectController extends AbstractController
|
||||
$task_id = intval(Request::input('task_id'));
|
||||
$project_id = intval(Request::input('project_id'));
|
||||
$column_id = intval(Request::input('column_id'));
|
||||
//
|
||||
$task = ProjectTask::userTask($task_id, true, true, 2);
|
||||
//
|
||||
$flow_item_id = intval(Request::input('flow_item_id'));
|
||||
$owner = Request::input('owner', []);
|
||||
$assist = Request::input('assist', []);
|
||||
//
|
||||
$task = ProjectTask::userTask($task_id);
|
||||
//
|
||||
$project = Project::userProject($task->project_id);
|
||||
ProjectPermission::userTaskPermission($project, ProjectPermission::TASK_MOVE, $task);
|
||||
//
|
||||
if( $task->project_id == $project_id && $task->column_id == $column_id){
|
||||
return Base::retSuccess('移动成功', ['id' => $task_id]);
|
||||
}
|
||||
//
|
||||
//
|
||||
$project = Project::userProject($project_id);
|
||||
$column = ProjectColumn::whereProjectId($project->id)->whereId($column_id)->first();
|
||||
if (empty($column)) {
|
||||
return Base::retError('列表不存在');
|
||||
}
|
||||
//
|
||||
$task->moveTask($project_id,$column_id);
|
||||
//
|
||||
return Base::retSuccess('移动成功', ['id' => $task_id]);
|
||||
if($flow_item_id){
|
||||
$flowItem = projectFlowItem::whereProjectId($project->id)->whereId($flow_item_id)->first();
|
||||
if (empty($flowItem)) {
|
||||
return Base::retError('任务状态不存在');
|
||||
}
|
||||
}
|
||||
//
|
||||
$task->moveTask($project_id, $column_id, $flow_item_id, $owner, $assist);
|
||||
//
|
||||
$task = ProjectTask::userTask($task_id);
|
||||
//
|
||||
return Base::retSuccess('移动成功', $task);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/project/flow/list 38. 工作流列表
|
||||
* @api {get} api/project/flow/list 39. 工作流列表
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -2264,7 +2351,7 @@ class ProjectController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/project/flow/save 39. 保存工作流
|
||||
* @api {post} api/project/flow/save 40. 保存工作流
|
||||
*
|
||||
* @apiDescription 需要token身份(限:项目负责人)
|
||||
* @apiVersion 1.0.0
|
||||
@@ -2298,7 +2385,7 @@ class ProjectController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/project/flow/delete 40. 删除工作流
|
||||
* @api {get} api/project/flow/delete 41. 删除工作流
|
||||
*
|
||||
* @apiDescription 需要token身份(限:项目负责人)
|
||||
* @apiVersion 1.0.0
|
||||
@@ -2330,7 +2417,7 @@ class ProjectController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/project/log/lists 41. 获取项目、任务日志
|
||||
* @api {get} api/project/log/lists 42. 获取项目、任务日志
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -2383,7 +2470,7 @@ class ProjectController extends AbstractController
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/project/top 42. 项目置顶
|
||||
* @api {get} api/project/top 43. 项目置顶
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
@@ -2411,4 +2498,74 @@ class ProjectController extends AbstractController
|
||||
'top_at' => $projectUser->top_at?->toDateTimeString(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/project/permission 44. 获取项目权限设置
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup project
|
||||
* @apiName permission
|
||||
*
|
||||
* @apiParam {Number} project_id 项目ID
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function permission()
|
||||
{
|
||||
$user = User::auth();
|
||||
$projectId = intval(Request::input('project_id'), 0);
|
||||
$projectUser = ProjectUser::whereUserid($user->userid)->whereProjectId($projectId)->first();
|
||||
if (!$projectUser) {
|
||||
return Base::retError("项目不存在");
|
||||
}
|
||||
$projectPermission = ProjectPermission::initPermissions($projectId);
|
||||
return Base::retSuccess("success", $projectPermission);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/project/permission/update 45. 项目权限设置
|
||||
*
|
||||
* @apiDescription 需要token身份
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup project
|
||||
* @apiName permission__update
|
||||
*
|
||||
* @apiParam {Number} project_id 项目ID
|
||||
* @apiParam {Array} task_add 添加任务权限
|
||||
* @apiParam {Array} task_update 修改任务权限
|
||||
* @apiParam {Array} task_remove 删除任务权限
|
||||
* @apiParam {Array} task_update_complete 标记完成权限
|
||||
* @apiParam {Array} task_archived 归档任务权限
|
||||
* @apiParam {Array} task_move 移动任务权限
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function permission__update()
|
||||
{
|
||||
$user = User::auth();
|
||||
$projectId = intval(Request::input('project_id'), 0);
|
||||
$projectUser = ProjectUser::whereUserid($user->userid)->whereProjectId($projectId)->first();
|
||||
if (!$projectUser) {
|
||||
return Base::retError("项目不存在");
|
||||
}
|
||||
$permissions = Request::only([
|
||||
ProjectPermission::TASK_LIST_ADD,
|
||||
ProjectPermission::TASK_LIST_UPDATE,
|
||||
ProjectPermission::TASK_LIST_REMOVE,
|
||||
ProjectPermission::TASK_LIST_SORT,
|
||||
ProjectPermission::TASK_ADD,
|
||||
ProjectPermission::TASK_UPDATE,
|
||||
ProjectPermission::TASK_REMOVE,
|
||||
ProjectPermission::TASK_STATUS,
|
||||
ProjectPermission::TASK_ARCHIVED,
|
||||
ProjectPermission::TASK_MOVE,
|
||||
]);
|
||||
$projectPermission = ProjectPermission::updatePermissions($projectId, Base::newArrayRecursive('intval', $permissions));
|
||||
return Base::retSuccess("success", $projectPermission);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,15 +2,8 @@
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\UserBot;
|
||||
use App\Models\UserCheckinMac;
|
||||
use App\Models\UserCheckinRecord;
|
||||
use App\Models\WebSocketDialog;
|
||||
use App\Models\WebSocketDialogMsg;
|
||||
use App\Module\Base;
|
||||
use Cache;
|
||||
use Carbon\Carbon;
|
||||
use Request;
|
||||
|
||||
/**
|
||||
|
||||
@@ -13,7 +13,6 @@ use App\Module\Doo;
|
||||
use App\Tasks\PushTask;
|
||||
use Carbon\Carbon;
|
||||
use Hhxsv5\LaravelS\Swoole\Task\Task;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Request;
|
||||
@@ -317,13 +316,16 @@ class ReportController extends AbstractController
|
||||
// 未完成的任务
|
||||
$unfinishedContent = "";
|
||||
$unfinished_task = ProjectTask::query()
|
||||
->whereNull("complete_at")
|
||||
->whereNotNull("start_at")
|
||||
->where("end_at", "<", $end_time->toDateTimeString())
|
||||
->join("projects", "projects.id", "=", "project_tasks.project_id")
|
||||
->whereNull("projects.archived_at")
|
||||
->whereNull("project_tasks.complete_at")
|
||||
->whereNotNull("project_tasks.start_at")
|
||||
->where("project_tasks.end_at", "<", $end_time->toDateTimeString())
|
||||
->whereHas("taskUser", function ($query) use ($user) {
|
||||
$query->where("userid", $user->userid);
|
||||
})
|
||||
->orderByDesc("id")
|
||||
->select("project_tasks.*")
|
||||
->orderByDesc("project_tasks.id")
|
||||
->get();
|
||||
if ($unfinished_task->isNotEmpty()) {
|
||||
foreach ($unfinished_task as $task) {
|
||||
@@ -474,10 +476,13 @@ class ReportController extends AbstractController
|
||||
{
|
||||
$user = User::auth();
|
||||
//
|
||||
$data = Report::whereHas("Receives", function (Builder $query) use ($user) {
|
||||
$query->where("userid", $user->userid)->where("read", 0);
|
||||
})->orderByDesc('created_at')->paginate(Base::getPaginate(50, 20));
|
||||
return Base::retSuccess("success", $data);
|
||||
$total = Report::select('reports.id')
|
||||
->join('report_receives', 'report_receives.rid', '=', 'reports.id')
|
||||
->where('report_receives.userid', $user->userid)
|
||||
->where('report_receives.read', 0)
|
||||
->count();
|
||||
//
|
||||
return Base::retSuccess("success", compact("total"));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -40,7 +40,7 @@ class SystemController extends AbstractController
|
||||
* @apiParam {String} type
|
||||
* - get: 获取(默认)
|
||||
* - all: 获取所有(需要管理员权限)
|
||||
* - save: 保存设置(参数:['reg', 'reg_identity', 'reg_invite', 'login_code', 'password_policy', 'project_invite', 'chat_information', 'anon_message', 'auto_archived', 'archived_day', 'task_visible', 'task_default_time', 'all_group_mute', 'all_group_autoin', 'image_compress', 'image_save_local', 'start_home'])
|
||||
* - save: 保存设置(参数:['reg', 'reg_identity', 'reg_invite', 'login_code', 'password_policy', 'project_invite', 'chat_information', 'anon_message', 'e2e_message', 'auto_archived', 'archived_day', 'task_visible', 'task_default_time', 'all_group_mute', 'all_group_autoin', 'user_private_chat_mute', 'user_group_chat_mute', 'image_compress', 'image_save_local', 'start_home'])
|
||||
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
@@ -65,12 +65,15 @@ class SystemController extends AbstractController
|
||||
'project_invite',
|
||||
'chat_information',
|
||||
'anon_message',
|
||||
'e2e_message',
|
||||
'auto_archived',
|
||||
'archived_day',
|
||||
'task_visible',
|
||||
'task_default_time',
|
||||
'all_group_mute',
|
||||
'all_group_autoin',
|
||||
'user_private_chat_mute',
|
||||
'user_group_chat_mute',
|
||||
'image_compress',
|
||||
'image_save_local',
|
||||
'start_home',
|
||||
@@ -108,18 +111,22 @@ class SystemController extends AbstractController
|
||||
$setting['project_invite'] = $setting['project_invite'] ?: 'open';
|
||||
$setting['chat_information'] = $setting['chat_information'] ?: 'optional';
|
||||
$setting['anon_message'] = $setting['anon_message'] ?: 'open';
|
||||
$setting['e2e_message'] = $setting['e2e_message'] ?: 'close';
|
||||
$setting['auto_archived'] = $setting['auto_archived'] ?: 'close';
|
||||
$setting['archived_day'] = floatval($setting['archived_day']) ?: 7;
|
||||
$setting['task_visible'] = $setting['task_visible'] ?: 'close';
|
||||
$setting['task_default_time'] = $setting['task_default_time'] ? Base::json2array($setting['task_default_time']) : ['09:00', '18:00'];
|
||||
$setting['all_group_mute'] = $setting['all_group_mute'] ?: 'open';
|
||||
$setting['all_group_autoin'] = $setting['all_group_autoin'] ?: 'yes';
|
||||
$setting['user_private_chat_mute'] = $setting['user_private_chat_mute'] ?: 'open';
|
||||
$setting['user_group_chat_mute'] = $setting['user_group_chat_mute'] ?: 'open';
|
||||
$setting['image_compress'] = $setting['image_compress'] ?: 'open';
|
||||
$setting['image_save_local'] = $setting['image_save_local'] ?: 'open';
|
||||
$setting['start_home'] = $setting['start_home'] ?: 'close';
|
||||
$setting['file_upload_limit'] = $setting['file_upload_limit'] ?: '';
|
||||
$setting['unclaimed_task_reminder'] = $setting['unclaimed_task_reminder'] ?: 'close';
|
||||
$setting['unclaimed_task_reminder_time'] = $setting['unclaimed_task_reminder_time'] ?: '';
|
||||
$setting['server_version'] = Base::getVersion();
|
||||
//
|
||||
return Base::retSuccess('success', $setting ?: json_decode('{}'));
|
||||
}
|
||||
@@ -267,7 +274,10 @@ class SystemController extends AbstractController
|
||||
'wenxin_secret',
|
||||
'wenxin_model',
|
||||
'qianwen_key',
|
||||
'qianwen_model'
|
||||
'qianwen_model',
|
||||
'gemini_key',
|
||||
'gemini_model',
|
||||
'gemini_agency',
|
||||
];
|
||||
|
||||
if ($type == 'save') {
|
||||
@@ -307,11 +317,18 @@ class SystemController extends AbstractController
|
||||
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'text', ['text' => "设置成功"], $botUser->userid, true, false, true);
|
||||
}
|
||||
}
|
||||
if ($backup['gemini_key'] != $setting['gemini_key']) {
|
||||
$botUser = User::botGetOrCreate('ai-gemini');
|
||||
if ($botUser && $dialog = WebSocketDialog::checkUserDialog($botUser, $user->userid)) {
|
||||
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'text', ['text' => "设置成功"], $botUser->userid, true, false, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
//
|
||||
$setting['openai_model'] = $setting['openai_model'] ?: 'gpt-3.5-turbo';
|
||||
$setting['wenxin_model'] = $setting['wenxin_model'] ?: 'eb-instant';
|
||||
$setting['qianwen_model'] = $setting['qianwen_model'] ?: 'qwen-v1';
|
||||
$setting['gemini_model'] = $setting['gemini_model'] ?: 'gemini-1.0-pro';
|
||||
if (env("SYSTEM_SETTING") == 'disabled') {
|
||||
foreach ($keys as $item) {
|
||||
if (strlen($setting[$item]) > 12) {
|
||||
@@ -385,6 +402,9 @@ class SystemController extends AbstractController
|
||||
$setting['edit'] = $setting['edit'] ?: 'close';
|
||||
$setting['modes'] = is_array($setting['modes']) ? $setting['modes'] : [];
|
||||
$setting['cmd'] = "curl -sSL '" . Base::fillUrl("api/public/checkin/install?key={$setting['key']}") . "' | sh";
|
||||
if (Base::judgeClientVersion('0.34.67')) {
|
||||
$setting['cmd'] = base64_encode($setting['cmd']);
|
||||
}
|
||||
//
|
||||
return Base::retSuccess('success', $setting ?: json_decode('{}'));
|
||||
}
|
||||
@@ -1191,7 +1211,7 @@ class SystemController extends AbstractController
|
||||
if (count($users) > 1) {
|
||||
$fileName .= "等" . count($userid) . "位成员";
|
||||
}
|
||||
$fileName .= '签到记录_' . Base::time() . '.xls';
|
||||
$fileName .= '签到记录_' . Base::time() . '.xlsx';
|
||||
$filePath = "temp/checkin/export/" . date("Ym", Base::time());
|
||||
$export = new BillMultipleExport($sheets);
|
||||
$res = $export->store($filePath . "/" . $fileName);
|
||||
@@ -1199,7 +1219,7 @@ class SystemController extends AbstractController
|
||||
return Base::retError('导出失败,' . $fileName . '!');
|
||||
}
|
||||
$xlsPath = storage_path("app/" . $filePath . "/" . $fileName);
|
||||
$zipFile = "app/" . $filePath . "/" . Base::rightDelete($fileName, '.xls') . ".zip";
|
||||
$zipFile = "app/" . $filePath . "/" . Base::rightDelete($fileName, '.xlsx') . ".zip";
|
||||
$zipPath = storage_path($zipFile);
|
||||
if (file_exists($zipPath)) {
|
||||
Base::deleteDirAndFile($zipPath, true);
|
||||
@@ -1256,8 +1276,14 @@ class SystemController extends AbstractController
|
||||
* @apiGroup system
|
||||
* @apiName version
|
||||
*
|
||||
* @apiSuccess {String} version
|
||||
* @apiSuccess {String} publish
|
||||
* @apiSuccessExample {json} Success-Response:
|
||||
{
|
||||
"version": "0.0.1",
|
||||
"publish": {
|
||||
"provider": "generic",
|
||||
"url": ""
|
||||
}
|
||||
}
|
||||
*/
|
||||
public function version()
|
||||
{
|
||||
@@ -1279,4 +1305,52 @@ class SystemController extends AbstractController
|
||||
}
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/prefetch 25. 预加载的资源
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
* @apiName prefetch
|
||||
*
|
||||
* @apiSuccessExample {array} Success-Response:
|
||||
[
|
||||
"https://......",
|
||||
"https://......",
|
||||
"......",
|
||||
]
|
||||
*/
|
||||
public function prefetch()
|
||||
{
|
||||
$userAgent = strtolower(Request::server('HTTP_USER_AGENT'));
|
||||
$isMain = str_contains($userAgent, 'maintaskwindow');
|
||||
$isApp = str_contains($userAgent, 'kuaifan_eeui');
|
||||
$version = Base::getVersion();
|
||||
$array = [];
|
||||
|
||||
if ($isMain || $isApp) {
|
||||
$path = 'js/build/';
|
||||
$list = Base::readDir(public_path($path), false);
|
||||
foreach ($list as $item) {
|
||||
if (is_file($item) && filesize($item) > 50 * 1024) {
|
||||
$array[] = $path . basename($item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($isMain) {
|
||||
$file = base_path('.prefetch');
|
||||
if (file_exists($file)) {
|
||||
$content = file_get_contents($file);
|
||||
$items = explode("\n", $content);
|
||||
$array = array_merge($array, $items);
|
||||
}
|
||||
}
|
||||
|
||||
return array_map(function($item) use ($version) {
|
||||
$url = trim($item);
|
||||
$url = str_replace('{version}', $version, $url);
|
||||
return url($url);
|
||||
}, array_values(array_filter($array)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ use App\Models\UserDepartment;
|
||||
use App\Models\WebSocketDialog;
|
||||
use App\Models\UserCheckinRecord;
|
||||
use App\Models\WebSocketDialogMsg;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use App\Models\UserEmailVerification;
|
||||
use App\Module\AgoraIO\AgoraTokenGenerator;
|
||||
|
||||
@@ -570,9 +571,9 @@ class UsersController extends AbstractController
|
||||
$list->transform(function (User $userInfo) use ($user, $state) {
|
||||
$tags = [];
|
||||
$dep = $userInfo->getDepartmentName();
|
||||
$dep = array_filter(explode(",", $dep), function($item) {
|
||||
$dep = array_values(array_filter(explode(",", $dep), function($item) {
|
||||
return preg_match("/\(M\)$/", $item);
|
||||
});
|
||||
}));
|
||||
if ($dep) {
|
||||
$tags[] = preg_replace("/\(M\)$/", "", trim($dep[0])) . Doo::translate("负责人");
|
||||
}
|
||||
@@ -1100,10 +1101,17 @@ class UsersController extends AbstractController
|
||||
];
|
||||
$row = UmengAlias::where($inArray);
|
||||
if ($row->exists()) {
|
||||
$row->update(['updated_at' => Carbon::now()]);
|
||||
$row->update([
|
||||
'ua' => $data['userAgent'],
|
||||
'device' => $data['deviceModel'],
|
||||
'updated_at' => Carbon::now()
|
||||
]);
|
||||
return Base::retSuccess('别名已存在');
|
||||
}
|
||||
$row = UmengAlias::createInstance($inArray);
|
||||
$row = UmengAlias::createInstance(array_merge($inArray, [
|
||||
'ua' => $data['userAgent'],
|
||||
'device' => $data['deviceModel'],
|
||||
]));
|
||||
if ($row->save()) {
|
||||
return Base::retSuccess('添加成功');
|
||||
} else {
|
||||
@@ -1971,7 +1979,7 @@ class UsersController extends AbstractController
|
||||
'name' => Doo::translate('文件'),
|
||||
];
|
||||
}
|
||||
$dialogList = (new WebSocketDialog)->getDialogList($user->userid);
|
||||
$dialogList = WebSocketDialog::getDialogList($user->userid);
|
||||
foreach ($dialogList['data'] as $dialog) {
|
||||
if ($dialog['avatar']) {
|
||||
$avatar = url($dialog['avatar']);
|
||||
@@ -2002,4 +2010,176 @@ class UsersController extends AbstractController
|
||||
// 返回
|
||||
return Base::retSuccess('success', $lists);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/users/annual/report 34. 年度报告
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup users
|
||||
* @apiName annual__report
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function annual__report()
|
||||
{
|
||||
$user = User::auth();
|
||||
//
|
||||
global $_A;
|
||||
if (!isset($_A["__annual__report_".$user->userid])) {
|
||||
$year = '2023';
|
||||
$time = '2300-01-01 00:00:01';
|
||||
$prefix = \DB::getTablePrefix();
|
||||
$hireTimestamp = strtotime($user->created_at);
|
||||
DB::statement("SET SQL_MODE=''");
|
||||
|
||||
// 我的任务
|
||||
$taskDb = DB::table('project_tasks as t')
|
||||
->join('project_task_users as tu', 't.id', '=', 'tu.task_id')
|
||||
->where('tu.owner', 1)
|
||||
->whereYear('t.created_at', $year)
|
||||
->where('tu.userid', $user->userid);
|
||||
|
||||
// 我的任务 - 时长(分钟)
|
||||
$durationTaskDb = $taskDb->clone()
|
||||
->selectRaw("
|
||||
{$prefix}t.id,
|
||||
{$prefix}t.flow_item_name,
|
||||
{$prefix}t.name as task_name,
|
||||
{$prefix}p.name as project_name,
|
||||
{$prefix}c.name as project_column_name,
|
||||
{$prefix}t.start_at,
|
||||
{$prefix}t.end_at,
|
||||
{$prefix}t.complete_at,
|
||||
{$prefix}t.created_at,
|
||||
ifnull(TIMESTAMPDIFF(MINUTE, {$prefix}t.start_at, {$prefix}t.complete_at), 0) as duration
|
||||
")
|
||||
->leftJoin('projects as p', 'p.id', '=', 't.project_id')
|
||||
->leftJoin('project_columns as c', 'c.id', '=', 't.column_id')
|
||||
->whereNotNull('t.start_at')
|
||||
->whereNotNull('t.complete_at');
|
||||
|
||||
// 最多聊天用户
|
||||
$longestChat = DB::table('web_socket_dialogs as d')
|
||||
->selectRaw("
|
||||
{$prefix}d.id,
|
||||
{$prefix}d.name as dialog_name,
|
||||
{$prefix}d.type as dialog_type,
|
||||
{$prefix}d.group_type as dialog_group_type,
|
||||
{$prefix}m.chat_num,
|
||||
{$prefix}u.userid,
|
||||
{$prefix}u.email as user_email,
|
||||
{$prefix}u.nickname as user_nickname,
|
||||
ifnull({$prefix}d.avatar, {$prefix}u.userimg) as avatar
|
||||
")
|
||||
->leftJoinSub(function ($query) use ($user, $year) {
|
||||
$query->select('web_socket_dialog_msgs.dialog_id', DB::raw('count(*) as chat_num'))
|
||||
->from('web_socket_dialog_msgs')
|
||||
->where('web_socket_dialog_msgs.userid', $user->userid)
|
||||
->whereYear('web_socket_dialog_msgs.created_at', $year)
|
||||
->groupBy('web_socket_dialog_msgs.dialog_id');
|
||||
}, 'm', 'm.dialog_id', '=', 'd.id')
|
||||
->leftJoin('web_socket_dialog_users as du', function ($query) use ($user) {
|
||||
$query->on('d.id', '=', 'du.dialog_id');
|
||||
$query->where('du.userid', '!=', $user->userid);
|
||||
$query->where('d.type', 'user');
|
||||
})
|
||||
->leftJoin('users as u', 'du.userid', '=', 'u.userid')
|
||||
->where('d.type', '!=', 'user')
|
||||
->orWhere('u.bot', 0)
|
||||
->orderByDesc('m.chat_num')
|
||||
->first();
|
||||
if (!empty($longestChat)) {
|
||||
if ($longestChat->avatar) {
|
||||
$longestChat->avatar = url($longestChat->avatar);
|
||||
} else if ($longestChat->dialog_type == 'user') {
|
||||
$longestChat->avatar = User::getAvatar($longestChat->userid, $longestChat->avatar, $longestChat->user_email, $longestChat->user_nickname);
|
||||
} else {
|
||||
$longestChat->avatar = match ($longestChat->dialog_group_type) {
|
||||
'department' => url("images/avatar/default_group_department.png"),
|
||||
'project' => url("images/avatar/default_group_project.png"),
|
||||
'task' => url("images/avatar/default_group_task.png"),
|
||||
default => url("images/avatar/default_group_people.png"),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 最晚在线时间
|
||||
$timezone = config('app.timezone');
|
||||
$latestOnline = UserCheckinRecord::whereUserid($user->userid)
|
||||
->whereYear(DB::raw('from_unixtime(report_time)'), $year)
|
||||
->orderByRaw("TIME_FORMAT(DATE_ADD(CONVERT_TZ(from_unixtime(report_time), 'UTC', '$timezone'), INTERVAL 18 HOUR), '%H%i%s') desc")
|
||||
->first();
|
||||
|
||||
//
|
||||
$_A["__annual__report_".$user->userid] = [
|
||||
// 本人信息
|
||||
'user' => [
|
||||
'userid' => $user->userid,
|
||||
'email' => $user->email,
|
||||
'nickname' => $user->nickname,
|
||||
'avatar' => User::getAvatar($user->userid, $user->userimg, $user->email, $user->nickname)
|
||||
],
|
||||
// 入职时间(年月日)
|
||||
'hire_date' => date("Y-m-d", $hireTimestamp),
|
||||
// 在职时间(天为单位)
|
||||
'tenure_days' => floor((strtotime(date('Y-m-d')) - $hireTimestamp) / (24 * 60 * 60)),
|
||||
// 最晚在线时间
|
||||
'latest_online_time' => date("Y-m-d H:i:s", $latestOnline->report_time),
|
||||
// 跟谁聊天最多(发消息的次数。可以是群、私聊、机器人除外)
|
||||
'longest_chat_user' => $longestChat,
|
||||
// 跟所有ai机器人聊天的次数
|
||||
'chat_al_num' => DB::table('web_socket_dialog_msgs as m')
|
||||
->join('web_socket_dialogs as d', 'd.id', '=', 'm.dialog_id')
|
||||
->join('web_socket_dialog_users as du', 'd.id', '=', 'du.dialog_id')
|
||||
->join('users as u', 'du.userid', '=', 'u.userid')
|
||||
->where('u.email', 'like', "%ai-%")
|
||||
->where('u.bot', 1)
|
||||
->where('m.userid', $user->userid)
|
||||
->whereYear('m.created_at', $year)
|
||||
->count(),
|
||||
// 文件创建数量
|
||||
'file_created_num' => File::whereCreatedId($user->userid)->whereYear('created_at', $year)->count(),
|
||||
// 参与过的项目
|
||||
'projects' => DB::table('projects as p')
|
||||
->select('p.id', 'p.name')
|
||||
->join('project_users as pu', 'p.id', '=', 'pu.project_id')
|
||||
->join('project_task_users as ptu', 'p.id', '=', 'ptu.project_id')
|
||||
->where(function($query) use ($user,$year) {
|
||||
$query->where('pu.userid', $user->userid);
|
||||
$query->whereYear('pu.created_at', $year);
|
||||
})
|
||||
->orWhere(function($query) use ($user,$year) {
|
||||
$query->where('ptu.userid', $user->userid);
|
||||
$query->whereYear('ptu.created_at', $year);
|
||||
})
|
||||
->groupBy('p.id')
|
||||
->take(100)
|
||||
->get(),
|
||||
// 任务统计
|
||||
'tasks' => [
|
||||
// 总数量
|
||||
'total' => $taskDb->count(),
|
||||
// 完成数量
|
||||
'completed' => $taskDb->clone()->whereNotNUll('t.complete_at')->count(),
|
||||
// 超时数量
|
||||
'overtime' => $taskDb->clone()->whereRaw("ifnull({$prefix}t.complete_at,'$time') > ifnull({$prefix}t.end_at,'$time')")->count(),
|
||||
// 做得最久的任务
|
||||
'longest_task' => $durationTaskDb->clone()->orderByDesc('duration')->first(),
|
||||
// 做得最快的任务
|
||||
'fastest_task' => $durationTaskDb->clone()->orderBy('duration')->first(),
|
||||
// 每个月完成多少个任务
|
||||
'month_completed_task' => $taskDb->clone()
|
||||
->selectRaw("MONTH({$prefix}t.complete_at) AS month, COUNT({$prefix}t.id) AS num")
|
||||
->whereNotNUll('t.complete_at')
|
||||
->whereYear('t.complete_at', $year)
|
||||
->groupBy('month')
|
||||
->get()
|
||||
]
|
||||
];
|
||||
}
|
||||
//
|
||||
return Base::retSuccess('success', $_A["__annual__report_".$user->userid]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use Cache;
|
||||
use Request;
|
||||
use Redirect;
|
||||
use Response;
|
||||
use App\Module\Doo;
|
||||
use App\Models\File;
|
||||
use App\Module\Base;
|
||||
use App\Tasks\LoopTask;
|
||||
@@ -37,9 +38,8 @@ class IndexController extends InvokeController
|
||||
if ($action) {
|
||||
$app .= "__" . $action;
|
||||
}
|
||||
if ($app === 'manifest.txt') {
|
||||
$app = 'manifest';
|
||||
$child = 'txt';
|
||||
if ($app == 'default') {
|
||||
return '';
|
||||
}
|
||||
if (!method_exists($this, $app)) {
|
||||
$app = method_exists($this, $method) ? $method : 'main';
|
||||
@@ -68,49 +68,7 @@ class IndexController extends InvokeController
|
||||
'version' => Base::getVersion(),
|
||||
'style' => $style,
|
||||
'script' => $script,
|
||||
])->header('Link', "<" . url('manifest.txt') . ">; rel=\"prefetch\"");
|
||||
}
|
||||
|
||||
/**
|
||||
* Manifest
|
||||
* @param $child
|
||||
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\Response|string
|
||||
*/
|
||||
public function manifest($child = '')
|
||||
{
|
||||
if (empty($child)) {
|
||||
$murl = url('manifest.txt');
|
||||
return response($murl)->header('Link', "<{$murl}>; rel=\"prefetch\"");
|
||||
}
|
||||
$array = [
|
||||
"office/web-apps/apps/api/documents/api.js?hash=" . Base::getVersion(),
|
||||
"office/7.5.1-23/web-apps/vendor/requirejs/require.js",
|
||||
"office/7.5.1-23/web-apps/apps/api/documents/api.js",
|
||||
"office/7.5.1-23/sdkjs/common/AllFonts.js",
|
||||
"office/7.5.1-23/web-apps/vendor/xregexp/xregexp-all-min.js",
|
||||
"office/7.5.1-23/web-apps/vendor/sockjs/sockjs.min.js",
|
||||
"office/7.5.1-23/web-apps/vendor/jszip/jszip.min.js",
|
||||
"office/7.5.1-23/web-apps/vendor/jszip-utils/jszip-utils.min.js",
|
||||
"office/7.5.1-23/sdkjs/common/libfont/wasm/fonts.js",
|
||||
"office/7.5.1-23/sdkjs/common/Charts/ChartStyles.js",
|
||||
"office/7.5.1-23/sdkjs/slide/themes//themes.js",
|
||||
|
||||
"office/7.5.1-23/web-apps/apps/presentationeditor/main/app.js",
|
||||
"office/7.5.1-23/sdkjs/slide/sdk-all-min.js",
|
||||
"office/7.5.1-23/sdkjs/slide/sdk-all.js",
|
||||
|
||||
"office/7.5.1-23/web-apps/apps/documenteditor/main/app.js",
|
||||
"office/7.5.1-23/sdkjs/word/sdk-all-min.js",
|
||||
"office/7.5.1-23/sdkjs/word/sdk-all.js",
|
||||
|
||||
"office/7.5.1-23/web-apps/apps/spreadsheeteditor/main/app.js",
|
||||
"office/7.5.1-23/sdkjs/cell/sdk-all-min.js",
|
||||
"office/7.5.1-23/sdkjs/cell/sdk-all.js",
|
||||
];
|
||||
foreach ($array as &$item) {
|
||||
$item = url($item);
|
||||
}
|
||||
return implode(PHP_EOL, $array);
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -217,6 +175,7 @@ class IndexController extends InvokeController
|
||||
public function desktop__publish($name = '')
|
||||
{
|
||||
$publishVersion = Request::header('publish-version');
|
||||
$fileNum = Request::get('file_num', 1);
|
||||
$latestFile = public_path("uploads/desktop/latest");
|
||||
$latestVersion = file_exists($latestFile) ? trim(file_get_contents($latestFile)) : "0.0.1";
|
||||
if (strtolower($name) === 'latest') {
|
||||
@@ -224,6 +183,7 @@ class IndexController extends InvokeController
|
||||
}
|
||||
// 上传
|
||||
if (preg_match("/^\d+\.\d+\.\d+$/", $publishVersion)) {
|
||||
$uploadSuccessFileNum = (int)Cache::get($publishVersion, 0);
|
||||
$publishKey = Request::header('publish-key');
|
||||
if ($publishKey !== env('APP_KEY')) {
|
||||
return Base::retError("key error");
|
||||
@@ -238,6 +198,20 @@ class IndexController extends InvokeController
|
||||
]);
|
||||
if (Base::isSuccess($res)) {
|
||||
file_put_contents($latestFile, $publishVersion);
|
||||
$uploadSuccessFileNum = $uploadSuccessFileNum + 1;
|
||||
Cache::set($publishVersion, $uploadSuccessFileNum, 7200);
|
||||
}
|
||||
if ($uploadSuccessFileNum >= $fileNum){
|
||||
$directoryPath = public_path("uploads/desktop");
|
||||
$files = array_filter(scandir($directoryPath), function($file) use($directoryPath) {
|
||||
return preg_match("/^\d+\.\d+\.\d+$/", $file) && is_dir($directoryPath . '/' . $file) && $file != '.' && $file != '..';
|
||||
});
|
||||
sort($files);
|
||||
foreach ($files as $key => $file) {
|
||||
if ($file != $publishVersion && $key < count($files) - 2) {
|
||||
Base::deleteDirAndFile($directoryPath . '/' . $file);
|
||||
}
|
||||
}
|
||||
}
|
||||
return $res;
|
||||
}
|
||||
@@ -249,14 +223,15 @@ class IndexController extends InvokeController
|
||||
$lists = Base::readDir($dirPath);
|
||||
$files = [];
|
||||
foreach ($lists as $file) {
|
||||
if (str_ends_with($file, '.yml') || str_ends_with($file, '.yaml')) {
|
||||
if (str_ends_with($file, '.yml') || str_ends_with($file, '.yaml') || str_ends_with($file, '.blockmap')) {
|
||||
continue;
|
||||
}
|
||||
$fileName = Base::leftDelete($file, $dirPath);
|
||||
$fileSize = filesize($file);
|
||||
$files[] = [
|
||||
'name' => substr($fileName, 1),
|
||||
'time' => date("Y-m-d H:i:s", filemtime($file)),
|
||||
'size' => Base::readableBytes(filesize($file)),
|
||||
'size' => $fileSize > 0 ? Base::readableBytes($fileSize) : 0,
|
||||
'url' => Base::fillUrl($path . $fileName),
|
||||
];
|
||||
}
|
||||
@@ -273,10 +248,11 @@ class IndexController extends InvokeController
|
||||
continue;
|
||||
}
|
||||
$fileName = Base::leftDelete($file, $dirPath);
|
||||
$fileSize = filesize($file);
|
||||
$apkFile = [
|
||||
'name' => substr($fileName, 1),
|
||||
'time' => date("Y-m-d H:i:s", filemtime($file)),
|
||||
'size' => Base::readableBytes(filesize($file)),
|
||||
'size' => $fileSize > 0 ? Base::readableBytes($fileSize) : 0,
|
||||
'url' => Base::fillUrl($path . $fileName),
|
||||
];
|
||||
}
|
||||
@@ -321,45 +297,90 @@ class IndexController extends InvokeController
|
||||
$data = parse_url($key);
|
||||
$path = Arr::get($data, 'path');
|
||||
$file = public_path($path);
|
||||
// 防止 ../ 穿越获取到系统文件
|
||||
if (!str_starts_with(realpath($file), public_path())) {
|
||||
abort(404);
|
||||
}
|
||||
//
|
||||
if (file_exists($file)) {
|
||||
parse_str($data['query'], $query);
|
||||
$name = Arr::get($query, 'name');
|
||||
$ext = strtolower(Arr::get($query, 'ext'));
|
||||
$userAgent = strtolower(Request::server('HTTP_USER_AGENT'));
|
||||
if ($ext === 'pdf'
|
||||
&& (str_contains($userAgent, 'electron') || str_contains($userAgent, 'chrome'))) {
|
||||
if (!file_exists($file)) {
|
||||
abort(404);
|
||||
}
|
||||
//
|
||||
parse_str($data['query'], $query);
|
||||
$name = Arr::get($query, 'name');
|
||||
$ext = strtolower(Arr::get($query, 'ext'));
|
||||
$userAgent = strtolower(Request::server('HTTP_USER_AGENT'));
|
||||
if ($ext === 'pdf') {
|
||||
// 文件超过 10m 不支持在线预览,提示下载
|
||||
if (filesize($file) > 10 * 1024 * 1024) {
|
||||
return view('download', [
|
||||
'name' => $name,
|
||||
'size' => Base::readableBytes(filesize($file)),
|
||||
'url' => Base::fillUrl($path),
|
||||
'button' => Doo::translate('点击下载'),
|
||||
]);
|
||||
}
|
||||
// 浏览器类型
|
||||
$browser = 'none';
|
||||
if (str_contains($userAgent, 'chrome') || str_contains($userAgent, 'android_kuaifan_eeui')) {
|
||||
$browser = str_contains($userAgent, 'android_kuaifan_eeui') ? 'android-mobile' : 'chrome-desktop';
|
||||
} elseif (str_contains($userAgent, 'safari') || str_contains($userAgent, 'ios_kuaifan_eeui')) {
|
||||
$browser = str_contains($userAgent, 'ios_kuaifan_eeui') ? 'safari-mobile' : 'safari-desktop';
|
||||
}
|
||||
// electron 直接在线预览查看
|
||||
if (str_contains($userAgent, 'electron') || str_contains($browser, 'desktop')) {
|
||||
return Response::download($file, $name, [
|
||||
'Content-Type' => 'application/pdf'
|
||||
], 'inline');
|
||||
}
|
||||
//
|
||||
if (in_array($ext, File::localExt)) {
|
||||
$url = Base::fillUrl($path);
|
||||
} else {
|
||||
$url = 'http://' . env('APP_IPPR') . '.3/' . $path;
|
||||
// EEUI App 直接在线预览查看
|
||||
if (str_contains($userAgent, 'eeui') && Base::judgeClientVersion("0.34.47")) {
|
||||
if ($browser === 'safari-mobile') {
|
||||
$redirectUrl = Base::fillUrl($path);
|
||||
return <<<EOF
|
||||
<script>
|
||||
window.top.postMessage({
|
||||
action: "eeuiAppSendMessage",
|
||||
data: [
|
||||
{
|
||||
action: 'setPageData',
|
||||
data: {
|
||||
showProgress: true,
|
||||
titleFixed: true,
|
||||
urlFixed: true,
|
||||
}
|
||||
},
|
||||
{
|
||||
action: 'createTarget',
|
||||
url: "{$redirectUrl}",
|
||||
}
|
||||
]
|
||||
}, "*")
|
||||
</script>
|
||||
EOF;
|
||||
}
|
||||
}
|
||||
if ($ext !== 'pdf') {
|
||||
$url = Base::urlAddparameter($url, [
|
||||
'fullfilename' => $name . '.' . $ext
|
||||
]);
|
||||
}
|
||||
$toUrl = Base::fillUrl("fileview/onlinePreview?url=" . urlencode(base64_encode($url)));
|
||||
return Redirect::to($toUrl, 301);
|
||||
}
|
||||
return abort(404);
|
||||
//
|
||||
if (in_array($ext, File::localExt)) {
|
||||
$url = Base::fillUrl($path);
|
||||
} else {
|
||||
$url = 'http://' . env('APP_IPPR') . '.3/' . $path;
|
||||
}
|
||||
$url = Base::urlAddparameter($url, [
|
||||
'fullfilename' => Base::rightDelete($name, '.' . $ext) . '_' . filemtime($file) . '.' . $ext
|
||||
]);
|
||||
$redirectUrl = Base::fillUrl("fileview/onlinePreview?url=" . urlencode(base64_encode($url)));
|
||||
return Redirect::to($redirectUrl, 301);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置语言和皮肤
|
||||
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View
|
||||
* 保存配置 (todo 已废弃)
|
||||
* @return string
|
||||
*/
|
||||
public function setting__theme_language()
|
||||
public function storage__synch()
|
||||
{
|
||||
return view('setting', [
|
||||
'theme' => Request::input('theme'),
|
||||
'language' => Request::input('language')
|
||||
]);
|
||||
return '<!-- Deprecated -->';
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -190,9 +190,10 @@ class File extends AbstractModel
|
||||
* @param user $user
|
||||
* @param int $pid
|
||||
* @param string $webkitRelativePath
|
||||
* @param bool $overwrite
|
||||
* @return array
|
||||
*/
|
||||
public function contentUpload($user, int $pid, $webkitRelativePath)
|
||||
public function contentUpload($user, int $pid, $webkitRelativePath, $overwrite = false)
|
||||
{
|
||||
$userid = $user->userid;
|
||||
if ($pid > 0) {
|
||||
@@ -283,17 +284,25 @@ class File extends AbstractModel
|
||||
if ($data['ext'] == 'markdown') {
|
||||
$data['ext'] = 'md';
|
||||
}
|
||||
$file = File::createInstance([
|
||||
$file = null;
|
||||
$params = [
|
||||
'pid' => $pid,
|
||||
'name' => Base::rightDelete($data['name'], '.' . $data['ext']),
|
||||
'type' => $type,
|
||||
'ext' => $data['ext'],
|
||||
'userid' => $userid,
|
||||
'created_id' => $user->userid,
|
||||
]);
|
||||
$file->handleDuplicateName();
|
||||
];
|
||||
if ($overwrite) {
|
||||
$file = self::wherePid($params['pid'])->whereExt($params['ext'])->whereName($params['name'])->first();
|
||||
}
|
||||
if (!$file) {
|
||||
$overwrite = false;
|
||||
$file = File::createInstance($params);
|
||||
$file->handleDuplicateName();
|
||||
}
|
||||
// 开始创建
|
||||
return AbstractModel::transaction(function () use ($addItem, $webkitRelativePath, $type, $user, $data, $file) {
|
||||
return AbstractModel::transaction(function () use ($overwrite, $addItem, $webkitRelativePath, $type, $user, $data, $file) {
|
||||
$file->size = $data['size'] * 1024;
|
||||
$file->saveBeforePP();
|
||||
//
|
||||
@@ -321,11 +330,12 @@ class File extends AbstractModel
|
||||
$tmpRow->pushMsg('add', $tmpRow);
|
||||
//
|
||||
$data = File::handleImageUrl($tmpRow->toArray());
|
||||
$data['full_name'] = $webkitRelativePath ?: $data['name'];
|
||||
$data['full_name'] = $webkitRelativePath ?: ($data['name'] . '.' . $data['ext']);
|
||||
$data['overwrite'] = $overwrite ? 1 : 0;
|
||||
//
|
||||
$addItem[] = $data;
|
||||
|
||||
return ['data'=>$data,'addItem'=>$addItem];
|
||||
return ['data' => $data, 'addItem' => $addItem];
|
||||
});
|
||||
}
|
||||
|
||||
@@ -926,12 +936,10 @@ class File extends AbstractModel
|
||||
*/
|
||||
public static function filePushMsg($action, $data = null, $userid = null)
|
||||
{
|
||||
//
|
||||
$userid = User::auth()->userid();
|
||||
if (empty($userid)) {
|
||||
return;
|
||||
}
|
||||
//
|
||||
$msg = [
|
||||
'type' => 'file',
|
||||
'action' => $action,
|
||||
@@ -941,7 +949,6 @@ class File extends AbstractModel
|
||||
'userid' => $userid,
|
||||
'msg' => $msg
|
||||
];
|
||||
$task = new PushTask($params, false);
|
||||
Task::deliver($task);
|
||||
Task::deliver(new PushTask($params));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ class FileContent extends AbstractModel
|
||||
'name' => $name,
|
||||
'ext' => $fileExt
|
||||
]));
|
||||
return Base::fillUrl("online/preview/{$name}?key={$key}");
|
||||
return Base::fillUrl("online/preview/{$name}?key={$key}&version=" . Base::getVersion() . "&__=" . Base::msecTime());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,7 +8,7 @@ use App\Module\Base;
|
||||
* App\Models\FileLink
|
||||
*
|
||||
* @property int $id
|
||||
* @property int|null $file_id 文件ID
|
||||
* @property int|null $file_id 项目ID
|
||||
* @property int|null $num 累计访问
|
||||
* @property string|null $code 链接码
|
||||
* @property int|null $userid 会员ID
|
||||
|
||||
@@ -13,7 +13,7 @@ use App\Module\Base;
|
||||
* @property string|null $name 名称
|
||||
* @property string|null $status 状态
|
||||
* @property array $turns 可流转
|
||||
* @property array $userids 状态负责人ID
|
||||
* @property array $userids 自动负责人ID
|
||||
* @property string|null $usertype 流转模式
|
||||
* @property int|null $userlimit 限制负责人
|
||||
* @property int|null $columnid 对应的项目列表
|
||||
|
||||
@@ -10,7 +10,7 @@ use App\Module\Base;
|
||||
* @property int $id
|
||||
* @property int|null $project_id 项目ID
|
||||
* @property int|null $column_id 列表ID
|
||||
* @property int|null $task_id 任务ID
|
||||
* @property int|null $task_id 项目ID
|
||||
* @property int|null $userid 会员ID
|
||||
* @property string|null $detail 详细信息
|
||||
* @property array $record 记录数据
|
||||
|
||||
201
app/Models/ProjectPermission.php
Normal file
@@ -0,0 +1,201 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Exceptions\ApiException;
|
||||
use App\Module\Base;
|
||||
|
||||
/**
|
||||
* App\Models\ProjectPermission
|
||||
*
|
||||
* @property int $id
|
||||
* @property int|null $project_id 项目ID
|
||||
* @property string $permissions 权限
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectPermission newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectPermission newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectPermission query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectPermission whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectPermission whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectPermission wherePermissions($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectPermission whereProjectId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectPermission whereUpdatedAt($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class ProjectPermission extends AbstractModel
|
||||
{
|
||||
|
||||
const TASK_LIST_ADD = 'task_list_add'; // 添加列
|
||||
const TASK_LIST_UPDATE = 'task_list_update'; // 修改列
|
||||
const TASK_LIST_REMOVE = 'task_list_remove'; // 删除列
|
||||
const TASK_LIST_SORT = 'task_list_sort'; // 列表排序
|
||||
const TASK_ADD = 'task_add'; // 任务添加
|
||||
const TASK_UPDATE = 'task_update'; // 任务更新
|
||||
const TASK_STATUS = 'task_status'; // 任务状态
|
||||
const TASK_REMOVE = 'task_remove'; // 任务删除
|
||||
const TASK_ARCHIVED = 'task_archived'; // 任务归档
|
||||
const TASK_MOVE = 'task_move'; // 任务移动
|
||||
|
||||
// 权限列表
|
||||
const PERMISSIONS = [
|
||||
'project_leader' => 1, // 项目负责人
|
||||
'project_member' => 2, // 项目成员
|
||||
'task_leader' => 3, // 任务负责人
|
||||
'task_assist' => 4, // 任务协助人
|
||||
];
|
||||
|
||||
// 权限描述
|
||||
const PERMISSIONS_DESC = [
|
||||
1 => "项目负责人",
|
||||
2 => "项目成员",
|
||||
3 => "任务负责人",
|
||||
4 => "任务协助人",
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $fillable = ['project_id', 'permissions'];
|
||||
|
||||
/**
|
||||
* 权限
|
||||
* @param $value
|
||||
* @return string
|
||||
*/
|
||||
public function getPermissionsAttribute($value)
|
||||
{
|
||||
return Base::json2array($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取权限值
|
||||
*
|
||||
* @param int $projectId
|
||||
* @param string $key
|
||||
* @return object|array
|
||||
*/
|
||||
public static function getPermission($projectId, $key = '')
|
||||
{
|
||||
$projectPermission = self::initPermissions($projectId);
|
||||
$currentPermissions = $projectPermission->permissions;
|
||||
if ($key) {
|
||||
if (!isset($currentPermissions[$key])) {
|
||||
throw new ApiException('项目权限设置不存在');
|
||||
}
|
||||
return $currentPermissions[$key];
|
||||
}
|
||||
return $projectPermission;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化项目权限
|
||||
*
|
||||
* @param int $projectId
|
||||
* @return ProjectPermission
|
||||
*/
|
||||
public static function initPermissions($projectId)
|
||||
{
|
||||
$permissions = [
|
||||
self::TASK_LIST_ADD => $projectTaskList = [self::PERMISSIONS['project_leader'], self::PERMISSIONS['project_member']],
|
||||
self::TASK_LIST_UPDATE => $projectTaskList,
|
||||
self::TASK_LIST_REMOVE => [self::PERMISSIONS['project_leader']],
|
||||
self::TASK_LIST_SORT => $projectTaskList,
|
||||
self::TASK_ADD => $projectTaskList,
|
||||
self::TASK_UPDATE => [self::PERMISSIONS['project_leader'], self::PERMISSIONS['task_leader'], self::PERMISSIONS['task_assist']],
|
||||
self::TASK_STATUS => $taskStatus = [self::PERMISSIONS['project_leader'], self::PERMISSIONS['task_leader']],
|
||||
self::TASK_REMOVE => $taskStatus,
|
||||
self::TASK_ARCHIVED => $taskStatus,
|
||||
self::TASK_MOVE => $taskStatus
|
||||
];
|
||||
return self::firstOrCreate(
|
||||
['project_id' => $projectId],
|
||||
['permissions' => Base::array2json($permissions)]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新项目权限
|
||||
*
|
||||
* @param int $projectId
|
||||
* @param array $permissions
|
||||
* @return ProjectPermission
|
||||
*/
|
||||
public static function updatePermissions($projectId, $newPermissions)
|
||||
{
|
||||
$projectPermission = self::initPermissions($projectId);
|
||||
$currentPermissions = $projectPermission->permissions;
|
||||
$mergedPermissions = empty($newPermissions) ? $currentPermissions : array_merge($currentPermissions, $newPermissions);
|
||||
|
||||
$projectPermission->permissions = Base::array2json($mergedPermissions);
|
||||
$projectPermission->save();
|
||||
|
||||
return $projectPermission;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否有执行特定动作的权限
|
||||
* @param string $action 动作名称
|
||||
* @param Project $project 项目实例
|
||||
* @param ProjectTask $task 任务实例
|
||||
* @return bool
|
||||
*/
|
||||
public static function userTaskPermission(Project $project, $action, ProjectTask $task = null)
|
||||
{
|
||||
$userid = User::userid();
|
||||
$permissions = self::getPermission($project->id, $action);
|
||||
switch ($action) {
|
||||
// 任务添加,任务更新, 任务状态, 任务删除, 任务完成, 任务归档, 任务移动
|
||||
case self::TASK_LIST_ADD:
|
||||
case self::TASK_LIST_UPDATE:
|
||||
case self::TASK_LIST_REMOVE:
|
||||
case self::TASK_LIST_SORT:
|
||||
case self::TASK_ADD:
|
||||
case self::TASK_UPDATE:
|
||||
case self::TASK_STATUS:
|
||||
case self::TASK_REMOVE:
|
||||
case self::TASK_ARCHIVED:
|
||||
case self::TASK_MOVE:
|
||||
$verify = false;
|
||||
// 项目负责人
|
||||
if (in_array(self::PERMISSIONS['project_leader'], $permissions)) {
|
||||
if ($project->owner) {
|
||||
$verify = true;
|
||||
}
|
||||
}
|
||||
// 项目成员
|
||||
if (!$verify && in_array(self::PERMISSIONS['project_member'], $permissions)) {
|
||||
$user = ProjectUser::whereProjectId($project->id)->whereUserid(intval($userid))->first();
|
||||
if (!empty($user)) {
|
||||
$verify = true;
|
||||
}
|
||||
}
|
||||
// 任务负责人
|
||||
if (!$verify && $task && in_array(self::PERMISSIONS['task_leader'], $permissions)) {
|
||||
if ($task->isOwner()) {
|
||||
$verify = true;
|
||||
}
|
||||
}
|
||||
// 任务协助人
|
||||
if (!$verify && $task && in_array(self::PERMISSIONS['task_assist'], $permissions)) {
|
||||
if ($task->isAssister()) {
|
||||
$verify = true;
|
||||
}
|
||||
}
|
||||
//
|
||||
if (!$verify) {
|
||||
$desc = [];
|
||||
rsort($permissions);
|
||||
foreach ($permissions as $permission) {
|
||||
$desc[] = self::PERMISSIONS_DESC[$permission];
|
||||
}
|
||||
$desc = array_reverse($desc);
|
||||
throw new ApiException(sprintf("仅限%s操作", implode('、', $desc)));
|
||||
}
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -2,15 +2,16 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Exceptions\ApiException;
|
||||
use DB;
|
||||
use Arr;
|
||||
use Request;
|
||||
use Carbon\Carbon;
|
||||
use App\Module\Base;
|
||||
use App\Tasks\PushTask;
|
||||
use Arr;
|
||||
use Carbon\Carbon;
|
||||
use DB;
|
||||
use App\Exceptions\ApiException;
|
||||
use Hhxsv5\LaravelS\Swoole\Task\Task;
|
||||
use App\Models\ProjectTaskVisibilityUser;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Request;
|
||||
|
||||
/**
|
||||
* App\Models\ProjectTask
|
||||
@@ -32,7 +33,7 @@ use Request;
|
||||
* @property int|null $archived_follow 跟随项目归档(项目取消归档时任务也取消归档)
|
||||
* @property string|null $complete_at 完成时间
|
||||
* @property int|null $userid 创建人
|
||||
* @property int|null $is_all_visible 是否所有人可见
|
||||
* @property int|null $visibility 任务可见性:1-项目人员 2-任务人员 3-指定成员
|
||||
* @property int|null $p_level 优先级
|
||||
* @property string|null $p_name 优先级名称
|
||||
* @property string|null $p_color 优先级颜色
|
||||
@@ -81,7 +82,6 @@ use Request;
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereFlowItemId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereFlowItemName($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereIsAllVisible($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereLoop($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereLoopAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereName($value)
|
||||
@@ -94,6 +94,7 @@ use Request;
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereStartAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereUserid($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask whereVisibility($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask withTrashed()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTask withoutTrashed()
|
||||
* @mixin \Eloquent
|
||||
@@ -328,13 +329,7 @@ class ProjectTask extends AbstractModel
|
||||
$query->where(function ($q1) use ($start, $end, $type) {
|
||||
switch ($type) {
|
||||
case 'createdTime':
|
||||
$q1->where(function ($q2) use ($start) {
|
||||
$q2->where('project_tasks.created_at', '>=', $start);
|
||||
})->orWhere(function ($q2) use ($end) {
|
||||
$q2->where('project_tasks.created_at', '<=', $end);
|
||||
})->orWhere(function ($q2) use ($start, $end) {
|
||||
$q2->where('project_tasks.created_at', '>', $start)->where('project_tasks.created_at', '<', $end);
|
||||
});
|
||||
$q1->where('project_tasks.created_at', '>=', $start)->where('project_tasks.created_at', '<=', $end);
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -494,13 +489,12 @@ class ProjectTask extends AbstractModel
|
||||
])->save();
|
||||
}
|
||||
|
||||
// 可见性
|
||||
foreach ($visibility_userids as $uid) {
|
||||
ProjectTaskUser::createInstance([
|
||||
ProjectTaskVisibilityUser::createInstance([
|
||||
'project_id' => $task->project_id,
|
||||
'task_id' => $task->id,
|
||||
'task_pid' => $task->parent_id ?: $task->id,
|
||||
'userid' => $uid,
|
||||
'owner' => 2,
|
||||
'userid' => $uid
|
||||
])->save();
|
||||
}
|
||||
|
||||
@@ -549,15 +543,12 @@ class ProjectTask extends AbstractModel
|
||||
*/
|
||||
public function updateTask($data, &$updateMarking = [])
|
||||
{
|
||||
//
|
||||
AbstractModel::transaction(function () use ($data, &$updateMarking) {
|
||||
// 主任务
|
||||
$mainTask = $this->parent_id > 0 ? self::find($this->parent_id) : null;
|
||||
// 工作流
|
||||
if (Arr::exists($data, 'flow_item_id')) {
|
||||
$isProjectOwner = $this->useridInTheProject(User::userid()) === 2;
|
||||
if (!$isProjectOwner && !$this->isOwner()) {
|
||||
throw new ApiException('仅限项目或任务负责人修改任务状态');
|
||||
}
|
||||
if ($this->flow_item_id == $data['flow_item_id']) {
|
||||
throw new ApiException('任务状态未发生改变');
|
||||
}
|
||||
@@ -578,6 +569,7 @@ class ProjectTask extends AbstractModel
|
||||
throw new ApiException("当前状态[{$currentFlowItem->name}]不可流转到[{$newFlowItem->name}]");
|
||||
}
|
||||
if ($currentFlowItem->userlimit) {
|
||||
$isProjectOwner = $this->useridInTheProject(User::userid()) === 2;
|
||||
if (!$isProjectOwner && !in_array(User::userid(), $currentFlowItem->userids)) {
|
||||
throw new ApiException("当前状态[{$currentFlowItem->name}]仅限状态负责人或项目负责人修改");
|
||||
}
|
||||
@@ -729,16 +721,14 @@ class ProjectTask extends AbstractModel
|
||||
ProjectTask::whereId($data['task_id'])->update(['visibility' => $data["visibility"]]);
|
||||
ProjectTask::whereParentId($data['task_id'])->update(['visibility' => $data["visibility"]]);
|
||||
}
|
||||
ProjectTaskUser::whereTaskId($data['task_id'])->whereOwner(2)->delete();
|
||||
ProjectTaskVisibilityUser::whereTaskId($data['task_id'])->delete();
|
||||
if (Arr::exists($data, 'visibility_appointor')) {
|
||||
foreach ($data['visibility_appointor'] as $uid) {
|
||||
if ($uid) {
|
||||
ProjectTaskUser::createInstance([
|
||||
ProjectTaskVisibilityUser::createInstance([
|
||||
'project_id' => $this->project_id,
|
||||
'task_id' => $this->id,
|
||||
'task_pid' => $this->parent_id ?: $this->id,
|
||||
'userid' => $uid,
|
||||
'owner' => 2,
|
||||
'userid' => $uid
|
||||
])->save();
|
||||
}
|
||||
}
|
||||
@@ -1434,53 +1424,62 @@ class ProjectTask extends AbstractModel
|
||||
}
|
||||
//
|
||||
$array = [];
|
||||
if (empty($data['parent_id'])) {
|
||||
if (Arr::exists($data, 'owner') || Arr::exists($data, 'assist')) {
|
||||
$taskUser = ProjectTaskUser::select(['userid', 'owner'])->whereTaskId($data['id'])->get();
|
||||
// 负责人
|
||||
$owners = $taskUser->where('owner', 1)->pluck('userid')->toArray();
|
||||
$owners = array_intersect($userids, $owners);
|
||||
if ($owners) {
|
||||
$array[] = [
|
||||
'userid' => array_values($owners),
|
||||
'data' => array_merge($data, [
|
||||
'owner' => 1,
|
||||
'assist' => 1,
|
||||
])
|
||||
];
|
||||
}
|
||||
// 协助人
|
||||
$assists = $taskUser->where('owner', 0)->pluck('userid')->toArray();
|
||||
$assists = array_intersect($userids, $assists);
|
||||
if ($assists) {
|
||||
$array[] = [
|
||||
'userid' => array_values($assists),
|
||||
'data' => array_merge($data, [
|
||||
'owner' => 0,
|
||||
'assist' => 1,
|
||||
])
|
||||
];
|
||||
}
|
||||
// 项目成员(其他人)
|
||||
if ($data['visibility'] == 1) {
|
||||
// 全部可见
|
||||
if (Arr::exists($data, 'owner') || Arr::exists($data, 'assist')) {
|
||||
$taskUser = ProjectTaskUser::select(['userid', 'owner'])->whereTaskId($data['id'])->get();
|
||||
// 负责人
|
||||
$owners = $taskUser->where('owner', 1)->pluck('userid')->toArray();
|
||||
$owners = array_intersect($userids, $owners);
|
||||
if ($owners) {
|
||||
$array[] = [
|
||||
'userid' => array_values($owners),
|
||||
'data' => array_merge($data, [
|
||||
'owner' => 1,
|
||||
'assist' => 1,
|
||||
])
|
||||
];
|
||||
}
|
||||
// 协助人
|
||||
$assists = $taskUser->where('owner', 0)->pluck('userid')->toArray();
|
||||
$assists = array_intersect($userids, $assists);
|
||||
if ($assists) {
|
||||
$array[] = [
|
||||
'userid' => array_values($assists),
|
||||
'data' => array_merge($data, [
|
||||
'owner' => 0,
|
||||
'assist' => 1,
|
||||
])
|
||||
];
|
||||
}
|
||||
// 其他人
|
||||
switch ($data['visibility']) {
|
||||
case 1:
|
||||
// 项目人员,除了负责人、协助人项目其他人
|
||||
$userids = array_diff($userids, $owners, $assists);
|
||||
} else {
|
||||
// 指定可见
|
||||
$userids = $taskUser->pluck('userid')->toArray();
|
||||
}
|
||||
$data = array_merge($data, [
|
||||
'owner' => 0,
|
||||
'assist' => 0,
|
||||
]);
|
||||
break;
|
||||
case 2:
|
||||
// 任务人员,除了负责人、协助人
|
||||
$userids = [];
|
||||
break;
|
||||
case 3:
|
||||
// 指定成员
|
||||
$specifys = ProjectTaskVisibilityUser::select(['userid'])->whereTaskId($data['id'])->pluck('userid')->toArray();
|
||||
$userids = array_diff($specifys, $owners, $assists);
|
||||
break;
|
||||
default:
|
||||
$userids = [];
|
||||
break;
|
||||
}
|
||||
if ($userids) {
|
||||
$array[] = [
|
||||
'userid' => array_values($userids),
|
||||
'data' => array_merge($data, [
|
||||
'owner' => 0,
|
||||
'assist' => 0,
|
||||
])
|
||||
];
|
||||
}
|
||||
}
|
||||
//
|
||||
$array[] = [
|
||||
'userid' => array_values($userids),
|
||||
'data' => $data
|
||||
];
|
||||
//
|
||||
foreach ($array as $item) {
|
||||
$params = [
|
||||
'ignoreFd' => Request::header('fd'),
|
||||
@@ -1521,7 +1520,10 @@ class ProjectTask extends AbstractModel
|
||||
if ($pushUserIds) {
|
||||
$userids = $pushUserIds;
|
||||
} elseif ($this->visibility != 1) {
|
||||
$userids = ProjectTaskUser::select(['userid', 'owner'])->whereTaskId($this->id)->orWhere('task_pid', '=', $this->id)->pluck('userid')->toArray();
|
||||
$userids = ProjectTaskUser::whereTaskId($this->id)->orWhere('task_pid', '=', $this->id)->pluck('userid')->toArray();
|
||||
if ($this->visibility == 3) {
|
||||
$userids = array_merge($userids, ProjectTaskVisibilityUser::whereTaskId($this->id)->pluck('userid')->toArray());
|
||||
}
|
||||
} else {
|
||||
$userids = ProjectUser::whereProjectId($this->project_id)->pluck('userid')->toArray(); // 项目成员
|
||||
}
|
||||
@@ -1667,13 +1669,21 @@ class ProjectTask extends AbstractModel
|
||||
|
||||
/**
|
||||
* 移动任务
|
||||
* @param int $project_id
|
||||
* @param int $column_id
|
||||
* @param int $project_id
|
||||
* @param int $column_id
|
||||
* @param int $flowItemId
|
||||
* @param array $owner
|
||||
* @param array $assist
|
||||
* @return bool
|
||||
*/
|
||||
public function moveTask(int $projectId, int $columnId)
|
||||
public function moveTask(int $projectId, int $columnId,int $flowItemId = 0,array $owner = [], array $assist = [])
|
||||
{
|
||||
AbstractModel::transaction(function () use($projectId, $columnId) {
|
||||
AbstractModel::transaction(function () use($projectId, $columnId, $flowItemId, $owner, $assist) {
|
||||
$newTaskUser = array_merge($owner, $assist);
|
||||
//
|
||||
$this->project_id = $projectId;
|
||||
$this->column_id = $columnId;
|
||||
$this->flow_item_id = $flowItemId;
|
||||
// 任务内容
|
||||
if($this->content){
|
||||
$this->content->project_id = $projectId;
|
||||
@@ -1690,15 +1700,24 @@ class ProjectTask extends AbstractModel
|
||||
$taskTag->save();
|
||||
}
|
||||
// 任务用户
|
||||
$this->updateTask(['owner' => $owner]);
|
||||
$this->updateTask(['assist' => $assist]);
|
||||
foreach ($this->taskUser as $taskUser){
|
||||
$taskUser->project_id = $projectId;
|
||||
$taskUser->save();
|
||||
if( in_array($taskUser->id, $newTaskUser) ){
|
||||
$taskUser->project_id = $projectId;
|
||||
$taskUser->save();
|
||||
}
|
||||
}
|
||||
//
|
||||
$this->project_id = $projectId;
|
||||
$this->column_id = $columnId;
|
||||
//
|
||||
if($flowItemId){
|
||||
$flowItem = projectFlowItem::whereProjectId($projectId)->whereId($flowItemId)->first();
|
||||
$this->flow_item_name = $flowItem->status . "|" . $flowItem->name;
|
||||
}else{
|
||||
$this->flow_item_name = '';
|
||||
}
|
||||
//
|
||||
$this->save();
|
||||
//
|
||||
//
|
||||
$this->addLog("移动{任务}");
|
||||
});
|
||||
$this->pushMsg('update');
|
||||
@@ -1728,14 +1747,10 @@ class ProjectTask extends AbstractModel
|
||||
* @param int $task_id
|
||||
* @param bool $archived true:仅限未归档, false:仅限已归档, null:不限制
|
||||
* @param bool $trashed true:仅限未删除, false:仅限已删除, null:不限制
|
||||
* @param int|bool $permission
|
||||
* - 0|false 限制:项目成员、任务成员、任务群聊成员(任务成员 = 任务创建人+任务协助人+任务负责人)
|
||||
* - 1|true 限制:项目负责人、任务成员
|
||||
* - 2 已有负责人才限制true (子任务时如果是主任务负责人也可以)
|
||||
* @param array $with
|
||||
* @return self
|
||||
*/
|
||||
public static function userTask($task_id, $archived = true, $trashed = true, $permission = false, $with = [])
|
||||
public static function userTask($task_id, $archived = true, $trashed = true, $with = [])
|
||||
{
|
||||
$builder = self::with($with)->allData()->where("project_tasks.id", intval($task_id));
|
||||
if ($trashed === false) {
|
||||
@@ -1758,7 +1773,7 @@ class ProjectTask extends AbstractModel
|
||||
try {
|
||||
$project = Project::userProject($task->project_id);
|
||||
} catch (\Throwable $e) {
|
||||
if ($task->owner !== null || (!$permission && $task->permission(4))) {
|
||||
if ($task->owner !== null || $task->permission(4)) {
|
||||
$project = Project::find($task->project_id);
|
||||
if (empty($project)) {
|
||||
throw new ApiException('项目不存在或已被删除', [ 'task_id' => $task_id ], -4002);
|
||||
@@ -1768,13 +1783,6 @@ class ProjectTask extends AbstractModel
|
||||
}
|
||||
}
|
||||
//
|
||||
if ($permission >= 2) {
|
||||
$permission = $task->hasOwner() ? 1 : 0;
|
||||
}
|
||||
if ($permission && !$project->owner && !$task->permission(3)) {
|
||||
throw new ApiException('仅限项目负责人、任务负责人、协助人员或任务创建者操作');
|
||||
}
|
||||
//
|
||||
return $task;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +60,8 @@ class ProjectTaskContent extends AbstractModel
|
||||
*/
|
||||
public static function saveContent($task_id, $content)
|
||||
{
|
||||
@ini_set("pcre.backtrack_limit", 999999999);
|
||||
//
|
||||
$oldContent = $content;
|
||||
$path = 'uploads/task/content/' . date("Ym") . '/' . $task_id . '/';
|
||||
//
|
||||
@@ -80,10 +82,7 @@ class ProjectTaskContent extends AbstractModel
|
||||
$publicPath = public_path($filePath);
|
||||
Base::makeDir(dirname($publicPath));
|
||||
$result = file_put_contents($publicPath, $content);
|
||||
if(!$result){
|
||||
info("保存任务详情至文件失败");
|
||||
info($publicPath);
|
||||
info($oldContent);
|
||||
if(!$result && $oldContent){
|
||||
throw new ApiException("保存任务详情至文件失败,请重试");
|
||||
}
|
||||
//
|
||||
|
||||
37
app/Models/ProjectTaskVisibilityUser.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
/**
|
||||
* App\Models\ProjectTaskVisibilityUser
|
||||
*
|
||||
* @property int $id
|
||||
* @property int|null $project_id 项目ID
|
||||
* @property int|null $task_id 任务ID
|
||||
* @property int|null $userid 成员ID
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property-read \App\Models\ProjectTask|null $projectTask
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskVisibilityUser newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskVisibilityUser newQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskVisibilityUser query()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskVisibilityUser whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskVisibilityUser whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskVisibilityUser whereProjectId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskVisibilityUser whereTaskId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskVisibilityUser whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|ProjectTaskVisibilityUser whereUserid($value)
|
||||
* @mixin \Eloquent
|
||||
*/
|
||||
class ProjectTaskVisibilityUser extends AbstractModel
|
||||
{
|
||||
|
||||
/**
|
||||
* @return \Illuminate\Database\Eloquent\Relations\HasOne
|
||||
*/
|
||||
public function projectTask(): \Illuminate\Database\Eloquent\Relations\HasOne
|
||||
{
|
||||
return $this->hasOne(ProjectTask::class, 'id', 'task_id');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -31,6 +31,16 @@ class UmengAlias extends AbstractModel
|
||||
{
|
||||
protected $table = 'umeng_alias';
|
||||
|
||||
/**
|
||||
* 推送内容处理
|
||||
* @param $string
|
||||
* @return string
|
||||
*/
|
||||
private static function specialCharacters($string)
|
||||
{
|
||||
return str_replace(["\r\n", "\r", "\n"], '', $string);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取推送配置
|
||||
* @return array|false
|
||||
@@ -73,9 +83,9 @@ class UmengAlias extends AbstractModel
|
||||
return false;
|
||||
}
|
||||
//
|
||||
$title = $array['title'] ?: ''; // 标题
|
||||
$subtitle = $array['subtitle'] ?: ''; // 副标题(iOS)
|
||||
$body = $array['body'] ?: ''; // 通知内容
|
||||
$title = self::specialCharacters($array['title'] ?: ''); // 标题
|
||||
$subtitle = self::specialCharacters($array['subtitle'] ?: ''); // 副标题(iOS)
|
||||
$body = self::specialCharacters($array['body'] ?: ''); // 通知内容
|
||||
$description = $array['description'] ?: 'no description'; // 描述
|
||||
$extra = is_array($array['extra']) ? $array['extra'] : []; // 额外参数
|
||||
$seconds = intval($array['seconds']) ?: 86400; // 有效时间(单位:秒)
|
||||
|
||||
@@ -290,6 +290,26 @@ class User extends AbstractModel
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查发送聊天内容前必须设置昵称、电话
|
||||
* @return void
|
||||
*/
|
||||
public function checkChatInformation()
|
||||
{
|
||||
if ($this->bot) {
|
||||
return;
|
||||
}
|
||||
$chatInformation = Base::settingFind('system', 'chat_information');
|
||||
if ($chatInformation == 'required') {
|
||||
if (empty($this->getRawOriginal('nickname'))) {
|
||||
throw new ApiException('请设置昵称', [], -2);
|
||||
}
|
||||
if (empty($this->getRawOriginal('tel'))) {
|
||||
throw new ApiException('请设置联系电话', [], -3);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** ***************************************************************************************** */
|
||||
/** ***************************************************************************************** */
|
||||
/** ***************************************************************************************** */
|
||||
@@ -539,6 +559,8 @@ class User extends AbstractModel
|
||||
return url("images/avatar/default_openai.png");
|
||||
case 'ai-claude@bot.system':
|
||||
return url("images/avatar/default_claude.png");
|
||||
case 'ai-gemini@bot.system':
|
||||
return url("images/avatar/default_gemini.png");
|
||||
case 'bot-manager@bot.system':
|
||||
return url("images/avatar/default_bot.png");
|
||||
case 'meeting-alert@bot.system':
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace App\Models;
|
||||
use App\Module\Base;
|
||||
use App\Module\Doo;
|
||||
use App\Module\Extranet;
|
||||
use App\Tasks\JokeSoupTask;
|
||||
use Cache;
|
||||
use Carbon\Carbon;
|
||||
|
||||
@@ -57,6 +58,7 @@ class UserBot extends AbstractModel
|
||||
'ai-claude' => 'Claude',
|
||||
'ai-wenxin' => '文心一言',
|
||||
'ai-qianwen' => '通义千问',
|
||||
'ai-gemini' => 'Gemini',
|
||||
'bot-manager' => '机器人管理',
|
||||
'meeting-alert' => '会议通知',
|
||||
'okr-alert' => 'OKR提醒',
|
||||
@@ -114,6 +116,16 @@ class UserBot extends AbstractModel
|
||||
'label' => Doo::translate('我的机器人')
|
||||
],
|
||||
],
|
||||
'ai-openai@bot.system',
|
||||
'ai-claude@bot.system',
|
||||
'ai-wenxin@bot.system',
|
||||
'ai-gemini@bot.system',
|
||||
'ai-qianwen@bot.system' => [
|
||||
[
|
||||
'key' => '%3A.clear',
|
||||
'label' => Doo::translate('清空上下文')
|
||||
]
|
||||
],
|
||||
default => [],
|
||||
};
|
||||
|
||||
@@ -218,8 +230,8 @@ class UserBot extends AbstractModel
|
||||
if ($checkins && $botUser = User::botGetOrCreate('check-in')) {
|
||||
$getJokeSoup = function($type) {
|
||||
$pre = $type == "up" ? "每日开心:" : "心灵鸡汤:";
|
||||
$key = $type == "up" ? "JokeSoupTask:jokes" : "JokeSoupTask:soups";
|
||||
$array = Base::json2array(Cache::get($key));
|
||||
$key = $type == "up" ? "jokes" : "soups";
|
||||
$array = Base::json2array(Cache::get(JokeSoupTask::keyName($key)));
|
||||
if ($array) {
|
||||
$item = $array[array_rand($array)];
|
||||
if ($item) {
|
||||
|
||||
@@ -10,6 +10,7 @@ use Cache;
|
||||
use Carbon\Carbon;
|
||||
use Hhxsv5\LaravelS\Swoole\Task\Task;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* App\Models\WebSocketDialog
|
||||
@@ -19,9 +20,10 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
* @property string|null $group_type 聊天室类型
|
||||
* @property string|null $name 对话名称
|
||||
* @property string $avatar 头像(群)
|
||||
* @property string|null $last_at 最后消息时间
|
||||
* @property int|null $owner_id 群主用户ID
|
||||
* @property int|null $link_id 关联id
|
||||
* @property int|null $top_userid 置顶的用户ID
|
||||
* @property int|null $top_msg_id 置顶的消息ID
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property \Illuminate\Support\Carbon|null $deleted_at
|
||||
@@ -36,10 +38,11 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereDeletedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereGroupType($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereLastAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereLinkId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereName($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereOwnerId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereTopMsgId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereTopUserid($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereType($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog whereUpdatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialog withTrashed()
|
||||
@@ -76,9 +79,9 @@ class WebSocketDialog extends AbstractModel
|
||||
* @param $deleted
|
||||
* @return array
|
||||
*/
|
||||
public function getDialogList($userid, $updated = "", $deleted = "")
|
||||
public static function getDialogList($userid, $updated = "", $deleted = "")
|
||||
{
|
||||
$builder = WebSocketDialog::select(['web_socket_dialogs.*', 'u.top_at', 'u.mark_unread', 'u.silence', 'u.color', 'u.updated_at as user_at'])
|
||||
$builder = WebSocketDialog::select(['web_socket_dialogs.*', 'u.top_at', 'u.last_at', 'u.mark_unread', 'u.silence', 'u.hide', 'u.color', 'u.updated_at as user_at'])
|
||||
->join('web_socket_dialog_users as u', 'web_socket_dialogs.id', '=', 'u.dialog_id')
|
||||
->where('u.userid', $userid);
|
||||
if ($updated) {
|
||||
@@ -86,7 +89,7 @@ class WebSocketDialog extends AbstractModel
|
||||
}
|
||||
$list = $builder
|
||||
->orderByDesc('u.top_at')
|
||||
->orderByDesc('web_socket_dialogs.last_at')
|
||||
->orderByDesc('u.last_at')
|
||||
->paginate(Base::getPaginate(100, 50));
|
||||
$list->transform(function (WebSocketDialog $item) use ($userid) {
|
||||
return $item->formatData($userid);
|
||||
@@ -99,6 +102,74 @@ class WebSocketDialog extends AbstractModel
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表外的未读对话 和 列表外的待办对话
|
||||
* @param $userid
|
||||
* @param $unreadAt
|
||||
* @param $todoAt
|
||||
* @return WebSocketDialog[]
|
||||
*/
|
||||
public static function getDialogBeyond($userid, $unreadAt, $todoAt)
|
||||
{
|
||||
DB::statement("SET SQL_MODE=''");
|
||||
$ids = [];
|
||||
$array = [];
|
||||
if ($unreadAt) {
|
||||
// 未读对话
|
||||
$list = WebSocketDialog::select(['web_socket_dialogs.*', 'u.top_at', 'u.last_at', 'u.mark_unread', 'u.silence', 'u.hide', 'u.color', 'u.updated_at as user_at'])
|
||||
->join('web_socket_dialog_users as u', 'web_socket_dialogs.id', '=', 'u.dialog_id')
|
||||
->join('web_socket_dialog_msg_reads as r', 'web_socket_dialogs.id', '=', 'r.dialog_id')
|
||||
->where('u.userid', $userid)
|
||||
->where('r.userid', $userid)
|
||||
->where('r.read_at')
|
||||
->where('u.last_at', '<', $unreadAt)
|
||||
->groupBy('u.dialog_id')
|
||||
->take(20)
|
||||
->get();
|
||||
$list->transform(function (WebSocketDialog $item) use ($userid, &$ids, &$array) {
|
||||
if (!in_array($item->id, $ids)) {
|
||||
$ids[] = $item->id;
|
||||
$array[] = $item->formatData($userid);
|
||||
}
|
||||
});
|
||||
// 标记未读会话
|
||||
$list = WebSocketDialog::select(['web_socket_dialogs.*', 'u.top_at', 'u.last_at', 'u.mark_unread', 'u.silence', 'u.hide', 'u.color', 'u.updated_at as user_at'])
|
||||
->join('web_socket_dialog_users as u', 'web_socket_dialogs.id', '=', 'u.dialog_id')
|
||||
->where('u.userid', $userid)
|
||||
->where('u.mark_unread', 1)
|
||||
->where('u.last_at', '<', $unreadAt)
|
||||
->take(20)
|
||||
->get();
|
||||
$list->transform(function (WebSocketDialog $item) use ($userid, &$ids, &$array) {
|
||||
if (!in_array($item->id, $ids)) {
|
||||
$ids[] = $item->id;
|
||||
$array[] = $item->formatData($userid);
|
||||
}
|
||||
});
|
||||
}
|
||||
if ($todoAt) {
|
||||
// 待办会话
|
||||
$list = WebSocketDialog::select(['web_socket_dialogs.*', 'u.top_at', 'u.last_at', 'u.mark_unread', 'u.silence', 'u.hide', 'u.color', 'u.updated_at as user_at'])
|
||||
->join('web_socket_dialog_users as u', 'web_socket_dialogs.id', '=', 'u.dialog_id')
|
||||
->join('web_socket_dialog_msg_todos as t', 'web_socket_dialogs.id', '=', 't.dialog_id')
|
||||
->where('u.userid', $userid)
|
||||
->where('t.userid', $userid)
|
||||
->where('t.done_at')
|
||||
->where('u.last_at', '<', $todoAt)
|
||||
->groupBy('u.dialog_id')
|
||||
->take(20)
|
||||
->get();
|
||||
$list->transform(function (WebSocketDialog $item) use ($userid, &$ids, &$array) {
|
||||
if (!in_array($item->id, $ids)) {
|
||||
$ids[] = $item->id;
|
||||
$array[] = $item->formatData($userid);
|
||||
}
|
||||
});
|
||||
}
|
||||
return $array;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 格式化对话
|
||||
* @param int $userid 会员ID
|
||||
@@ -115,17 +186,23 @@ class WebSocketDialog extends AbstractModel
|
||||
};
|
||||
//
|
||||
$time = Carbon::parse($this->user_at ?? $dialogUserFun('updated_at'));
|
||||
$this->hide = $this->hide ?? $dialogUserFun('hide');
|
||||
$this->top_at = $this->top_at ?? $dialogUserFun('top_at');
|
||||
$this->last_at = $this->last_at ?? $dialogUserFun('last_at');
|
||||
$this->user_at = $time->toDateTimeString('millisecond');
|
||||
$this->user_ms = $time->valueOf();
|
||||
//
|
||||
if (isset($this->search_msg_id)) {
|
||||
// 最后消息 (搜索预览消息)
|
||||
$this->last_msg = WebSocketDialogMsg::whereDialogId($this->id)->find($this->search_msg_id);
|
||||
$this->last_at = $this->last_msg?->created_at;
|
||||
$this->last_at = $this->last_msg ? Carbon::parse($this->last_msg->created_at)->format('Y-m-d H:i:s') : null;
|
||||
} else {
|
||||
// 未读信息
|
||||
$this->generateUnread($userid, $hasData);
|
||||
if (Base::judgeClientVersion("0.34.0")) {
|
||||
$this->generateUnread($userid);
|
||||
} else {
|
||||
$this->generateUnread_03398($userid, $hasData);
|
||||
}
|
||||
// 未读标记
|
||||
$this->mark_unread = $this->mark_unread ?? $dialogUserFun('mark_unread');
|
||||
// 是否免打扰
|
||||
@@ -161,9 +238,13 @@ class WebSocketDialog extends AbstractModel
|
||||
$this->dialog_delete = 1;
|
||||
}
|
||||
$this->dialog_user = $dialog_user;
|
||||
$this->dialog_mute = Base::settingFind('system', 'user_private_chat_mute');
|
||||
break;
|
||||
case "group":
|
||||
switch ($this->group_type) {
|
||||
case 'user':
|
||||
$this->dialog_mute = Base::settingFind('system', 'user_group_chat_mute');
|
||||
break;
|
||||
case 'project':
|
||||
$this->group_info = Project::withTrashed()->select(['id', 'name', 'archived_at', 'deleted_at'])->whereDialogId($this->id)->first()?->cancelAppend()->cancelHidden();
|
||||
if ($this->group_info) {
|
||||
@@ -184,7 +265,7 @@ class WebSocketDialog extends AbstractModel
|
||||
break;
|
||||
case 'all':
|
||||
$this->name = Doo::translate('全体成员');
|
||||
$this->all_group_mute = Base::settingFind('system', 'all_group_mute');
|
||||
$this->dialog_mute = Base::settingFind('system', 'all_group_mute');
|
||||
break;
|
||||
}
|
||||
break;
|
||||
@@ -192,10 +273,26 @@ class WebSocketDialog extends AbstractModel
|
||||
if ($hasData === true) {
|
||||
$msgBuilder = WebSocketDialogMsg::whereDialogId($this->id);
|
||||
$this->has_tag = $msgBuilder->clone()->where('tag', '>', 0)->exists();
|
||||
$this->has_todo = $msgBuilder->clone()->where('todo', '>', 0)->exists();
|
||||
$this->has_image = $msgBuilder->clone()->whereMtype('image')->exists();
|
||||
$this->has_file = $msgBuilder->clone()->whereMtype('file')->exists();
|
||||
$this->has_link = $msgBuilder->clone()->whereLink(1)->exists();
|
||||
$this->has_todo = $msgBuilder->clone()->where('todo', '>', 0)->exists();
|
||||
Cache::forever("Dialog::tag:" . $this->id, Base::array2json([
|
||||
'has_tag' => $this->has_tag,
|
||||
'has_todo' => $this->has_todo,
|
||||
'has_image' => $this->has_image,
|
||||
'has_file' => $this->has_file,
|
||||
'has_link' => $this->has_link,
|
||||
]));
|
||||
} else {
|
||||
$tagData = Base::json2array(Cache::get("Dialog::tag:" . $this->id));
|
||||
if ($tagData) {
|
||||
$this->has_tag = !!$tagData['has_tag'];
|
||||
$this->has_todo = !!$tagData['has_todo'];
|
||||
$this->has_image = !!$tagData['has_image'];
|
||||
$this->has_file = !!$tagData['has_file'];
|
||||
$this->has_link = !!$tagData['has_link'];
|
||||
}
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
@@ -203,10 +300,29 @@ class WebSocketDialog extends AbstractModel
|
||||
/**
|
||||
* 生成未读数据
|
||||
* @param $userid
|
||||
* @return $this
|
||||
*/
|
||||
public function generateUnread($userid)
|
||||
{
|
||||
$builder = WebSocketDialogMsgRead::whereDialogId($this->id)->whereUserid($userid)->whereReadAt(null);
|
||||
// 未读消息
|
||||
$this->unread = $builder->count();
|
||||
// 最早一条未读消息
|
||||
$this->unread_one = $this->unread > 0 ? intval($builder->clone()->orderBy('msg_id')->value('msg_id')) : 0;
|
||||
// @我的消息
|
||||
$this->mention = $this->unread > 0 ? $builder->clone()->whereMention(1)->count() : 0;
|
||||
// @我的消息(id集合)
|
||||
$this->mention_ids = $this->mention > 0 ? $builder->clone()->whereMention(1)->orderByDesc('msg_id')->take(20)->pluck('msg_id')->toArray() : [];
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成未读数据 // todo: 旧版兼容,后续删除
|
||||
* @param $userid
|
||||
* @param $positionData
|
||||
* @return $this
|
||||
*/
|
||||
public function generateUnread($userid, $positionData = false)
|
||||
public function generateUnread_03398($userid, $positionData = false)
|
||||
{
|
||||
$builder = WebSocketDialogMsgRead::whereDialogId($this->id)->whereUserid($userid)->whereReadAt(null);
|
||||
$this->unread = $builder->count();
|
||||
@@ -214,12 +330,14 @@ class WebSocketDialog extends AbstractModel
|
||||
if ($positionData) {
|
||||
$array = [];
|
||||
// @我的消息
|
||||
if ($this->mention > 0
|
||||
&& $mention_id = intval($builder->clone()->whereMention(1)->orderByDesc('msg_id')->value('msg_id'))) {
|
||||
$array[] = [
|
||||
'msg_id' => $mention_id,
|
||||
'label' => Doo::translate('@我的消息'),
|
||||
];
|
||||
if ($this->mention > 0) {
|
||||
$list = $builder->clone()->whereMention(1)->orderByDesc('msg_id')->take(20)->get();
|
||||
foreach ($list as $item) {
|
||||
$array[] = [
|
||||
'msg_id' => $item->msg_id,
|
||||
'label' => Doo::translate('@我的消息'),
|
||||
];
|
||||
}
|
||||
}
|
||||
// 最早一条未读消息
|
||||
if ($this->unread > 0
|
||||
@@ -386,17 +504,37 @@ class WebSocketDialog extends AbstractModel
|
||||
*/
|
||||
public function checkMute($userid)
|
||||
{
|
||||
if ($this->group_type === 'all') {
|
||||
$allGroupMute = Base::settingFind('system', 'all_group_mute');
|
||||
switch ($allGroupMute) {
|
||||
case 'all':
|
||||
throw new ApiException('当前会话全员禁言');
|
||||
case 'user':
|
||||
if (!User::find($userid)?->isAdmin()) {
|
||||
throw new ApiException('当前会话禁言');
|
||||
$muteMsgTip = null;
|
||||
$systemConfig = Base::setting('system');
|
||||
switch ($this->type) {
|
||||
case 'user':
|
||||
if ($systemConfig['user_private_chat_mute'] === 'close') {
|
||||
$muteMsgTip = '个人会话禁言';
|
||||
}
|
||||
break;
|
||||
|
||||
case 'group':
|
||||
if ($this->group_type === 'user') {
|
||||
if ($systemConfig['user_group_chat_mute'] === 'close') {
|
||||
$muteMsgTip = '个人群组禁言';
|
||||
}
|
||||
} elseif ($this->group_type === 'all') {
|
||||
if ($systemConfig['all_group_mute'] === 'close') {
|
||||
$muteMsgTip = '当前会话全员禁言';
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
if ($muteMsgTip === null) {
|
||||
return;
|
||||
}
|
||||
if ($userid) {
|
||||
$user = User::find($userid);
|
||||
if ($user?->bot || $user?->isAdmin()) { // 机器人或管理员不受禁言
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw new ApiException($muteMsgTip);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -454,20 +592,6 @@ class WebSocketDialog extends AbstractModel
|
||||
Task::deliver($task);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新对话最后消息时间
|
||||
* @return WebSocketDialogMsg|\Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Model|\Illuminate\Database\Query\Builder|object|null
|
||||
*/
|
||||
public function updateMsgLastAt()
|
||||
{
|
||||
$lastMsg = WebSocketDialogMsg::whereDialogId($this->id)->orderByDesc('id')->first();
|
||||
if ($lastMsg) {
|
||||
$this->last_at = $lastMsg->created_at;
|
||||
$this->save();
|
||||
}
|
||||
return $lastMsg;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对话(同时检验对话身份)
|
||||
* @param $dialog_id
|
||||
@@ -502,6 +626,7 @@ class WebSocketDialog extends AbstractModel
|
||||
return $dialog;
|
||||
}
|
||||
if (!WebSocketDialogUser::whereDialogId($dialog->id)->whereUserid($userid)->exists()) {
|
||||
WebSocketDialogMsgRead::forceRead($dialog_id, $userid);
|
||||
throw new ApiException('不在成员列表内', ['dialog_id' => $dialog_id], -4003);
|
||||
}
|
||||
return $dialog;
|
||||
@@ -523,7 +648,6 @@ class WebSocketDialog extends AbstractModel
|
||||
'name' => $name ?: '',
|
||||
'group_type' => $group_type,
|
||||
'owner_id' => $owner_id,
|
||||
'last_at' => in_array($group_type, ['user', 'department', 'all']) ? Carbon::now() : null,
|
||||
]);
|
||||
$dialog->save();
|
||||
foreach (is_array($userid) ? $userid : [$userid] as $value) {
|
||||
@@ -531,7 +655,8 @@ class WebSocketDialog extends AbstractModel
|
||||
WebSocketDialogUser::createInstance([
|
||||
'dialog_id' => $dialog->id,
|
||||
'userid' => $value,
|
||||
'important' => !in_array($group_type, ['user', 'all'])
|
||||
'important' => !in_array($group_type, ['user', 'all']),
|
||||
'last_at' => in_array($group_type, ['user', 'department', 'all']) ? Carbon::now() : null,
|
||||
])->save();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,11 +31,12 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
* @property int|null $modify 是否编辑
|
||||
* @property int|null $reply_num 有多少条回复
|
||||
* @property int|null $reply_id 回复ID
|
||||
* @property int|null $forward_id 转发ID
|
||||
* @property int|null $forward_num 被转发多少次
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
* @property \Illuminate\Support\Carbon|null $deleted_at
|
||||
* @property-read int|mixed $percentage
|
||||
* @property-read \App\Models\WebSocketDialogMsg|null $reply_data
|
||||
* @property-read \App\Models\WebSocketDialog|null $webSocketDialog
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg newModelQuery()
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg newQuery()
|
||||
@@ -46,6 +47,8 @@ use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereDialogId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereDialogType($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereEmoji($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereForwardId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereForwardNum($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereKey($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsg whereLink($value)
|
||||
@@ -71,7 +74,6 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
|
||||
protected $appends = [
|
||||
'percentage',
|
||||
'reply_data',
|
||||
];
|
||||
|
||||
protected $hidden = [
|
||||
@@ -99,21 +101,6 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
return $this->appendattrs['percentage'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 回复消息详情
|
||||
* @return WebSocketDialogMsg|null
|
||||
*/
|
||||
public function getReplyDataAttribute()
|
||||
{
|
||||
if (!isset($this->appendattrs['reply_data'])) {
|
||||
$this->appendattrs['reply_data'] = null;
|
||||
if ($this->reply_id > 0) {
|
||||
$this->appendattrs['reply_data'] = self::find($this->reply_id, ['id', 'userid', 'type', 'msg'])?->cancelAppend() ?: null;
|
||||
}
|
||||
}
|
||||
return $this->appendattrs['reply_data'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息格式化
|
||||
* @param $value
|
||||
@@ -124,13 +111,9 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
if (is_array($value)) {
|
||||
return $value;
|
||||
}
|
||||
$value = Base::json2array($value);
|
||||
if ($this->type === 'file') {
|
||||
$value['type'] = in_array($value['ext'], ['jpg', 'jpeg', 'webp', 'png', 'gif']) ? 'img' : 'file';
|
||||
$value['path'] = Base::fillUrl($value['path']);
|
||||
$value['thumb'] = Base::fillUrl($value['thumb'] ?: Base::extIcon($value['ext']));
|
||||
} else if ($this->type === 'record') {
|
||||
$value['path'] = Base::fillUrl($value['path']);
|
||||
$value = $this->formatDataMsg($this->type, $value);
|
||||
if (isset($value['reply_data'])) {
|
||||
$value['reply_data']['msg'] = $this->formatDataMsg($value['reply_data']['type'], $value['reply_data']['msg']);
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
@@ -148,6 +131,27 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
return Base::json2array($value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理消息数据
|
||||
* @param $type
|
||||
* @param $msg
|
||||
* @return mixed
|
||||
*/
|
||||
private function formatDataMsg($type, $msg)
|
||||
{
|
||||
if (!is_array($msg)) {
|
||||
$msg = Base::json2array($msg);
|
||||
}
|
||||
if ($type === 'file') {
|
||||
$msg['type'] = in_array($msg['ext'], ['jpg', 'jpeg', 'webp', 'png', 'gif']) ? 'img' : 'file';
|
||||
$msg['path'] = Base::fillUrl($msg['path']);
|
||||
$msg['thumb'] = Base::fillUrl($msg['thumb'] ?: Base::extIcon($msg['ext']));
|
||||
} else if ($type === 'record') {
|
||||
$msg['path'] = Base::fillUrl($msg['path']);
|
||||
}
|
||||
return $msg;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取占比
|
||||
* @param bool|int $increment 是否新增阅读数
|
||||
@@ -368,13 +372,25 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
* 转发消息
|
||||
* @param array|int $dialogids
|
||||
* @param array|int $userids
|
||||
* @param User $user 发送的会员
|
||||
* @param User $user 发送的会员
|
||||
* @param int $showSource 是否显示原发送者信息
|
||||
* @param string $leaveMessage 转发留言
|
||||
* @return mixed
|
||||
*/
|
||||
public function forwardMsg($dialogids, $userids, $user)
|
||||
public function forwardMsg($dialogids, $userids, $user, $showSource = 1, $leaveMessage = '')
|
||||
{
|
||||
return AbstractModel::transaction(function() use ($dialogids, $user, $userids) {
|
||||
$originalMsg = Base::json2array($this->getRawOriginal('msg'));
|
||||
return AbstractModel::transaction(function () use ($dialogids, $user, $userids, $showSource, $leaveMessage) {
|
||||
$msgData = Base::json2array($this->getRawOriginal('msg'));
|
||||
$forwardData = is_array($msgData['forward_data']) ? $msgData['forward_data'] : [];
|
||||
$forwardId = $forwardData['id'] ?: $this->id;
|
||||
$forwardUserid = $forwardData['userid'] ?: $this->userid;
|
||||
$msgData['forward_data'] = [
|
||||
'id' => $forwardId, // 转发的消息ID(原始)
|
||||
'userid' => $forwardUserid, // 转发的消息会员ID(原始)
|
||||
'parent_id' => $this->id, // 转发的消息ID
|
||||
'parent_userid' => $this->userid, // 转发的消息会员ID
|
||||
'show' => $showSource, // 是否显示原发送者信息
|
||||
];
|
||||
$msgs = [];
|
||||
$already = [];
|
||||
if ($dialogids) {
|
||||
@@ -382,11 +398,14 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
$dialogids = [$dialogids];
|
||||
}
|
||||
foreach ($dialogids as $dialogid) {
|
||||
$res = self::sendMsg(null, $dialogid, $this->type, $originalMsg, $user->userid);
|
||||
$res = self::sendMsg('forward-' . $forwardId, $dialogid, $this->type, $msgData, $user->userid);
|
||||
if (Base::isSuccess($res)) {
|
||||
$msgs[] = $res['data'];
|
||||
$already[] = $dialogid;
|
||||
}
|
||||
if ($leaveMessage) {
|
||||
self::sendMsg(null, $dialogid, 'text', ['text' => $leaveMessage], $user->userid);
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($userids) {
|
||||
@@ -399,13 +418,19 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
}
|
||||
$dialog = WebSocketDialog::checkUserDialog($user, $userid);
|
||||
if ($dialog && !in_array($dialog->id, $already)) {
|
||||
$res = self::sendMsg(null, $dialog->id, $this->type, $originalMsg, $user->userid);
|
||||
$res = self::sendMsg('forward-' . $forwardId, $dialog->id, $this->type, $msgData, $user->userid);
|
||||
if (Base::isSuccess($res)) {
|
||||
$msgs[] = $res['data'];
|
||||
}
|
||||
if ($leaveMessage) {
|
||||
self::sendMsg(null, $dialog->id, 'text', ['text' => $leaveMessage], $user->userid);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (count($msgs) > 0) {
|
||||
$this->increment('forward_num', count($msgs));
|
||||
}
|
||||
return Base::retSuccess('转发成功', [
|
||||
'msgs' => $msgs
|
||||
]);
|
||||
@@ -427,9 +452,8 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
WebSocketDialogMsgTodo::whereIn('msg_id', $ids)->delete();
|
||||
self::whereIn('id', $ids)->delete();
|
||||
//
|
||||
$dialogDatas = WebSocketDialog::whereIn('id', $dialogIds)->get();
|
||||
foreach ($dialogDatas as $dialogData) {
|
||||
$dialogData->updateMsgLastAt();
|
||||
foreach ($dialogIds as $dialogId) {
|
||||
WebSocketDialogUser::updateMsgLastAt($dialogId);
|
||||
}
|
||||
foreach ($replyIds as $id) {
|
||||
self::whereId($id)->update(['reply_num' => self::whereReplyId($id)->count()]);
|
||||
@@ -470,7 +494,7 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
'data' => [
|
||||
'id' => $this->id,
|
||||
'dialog_id' => $this->dialog_id,
|
||||
'last_msg' => $dialogData->updateMsgLastAt(),
|
||||
'last_msg' => WebSocketDialogUser::updateMsgLastAt($this->dialog_id),
|
||||
'update_read' => $deleteRead ? 1 : 0
|
||||
],
|
||||
]
|
||||
@@ -497,6 +521,8 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
}
|
||||
switch ($data['type']) {
|
||||
case 'text':
|
||||
case 'word-chain':
|
||||
case 'vote':
|
||||
return $this->previewTextMsg($data['msg']['text'], $preserveHtml);
|
||||
case 'record':
|
||||
return "[语音]";
|
||||
@@ -510,6 +536,9 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
case 'tag':
|
||||
$action = $data['msg']['action'] === 'remove' ? '取消标注' : '标注';
|
||||
return "[{$action}] {$this->previewMsg(false, $data['msg']['data'])}";
|
||||
case 'top':
|
||||
$action = $data['msg']['action'] === 'remove' ? '取消置顶' : '置顶';
|
||||
return "[{$action}] {$this->previewMsg(false, $data['msg']['data'])}";
|
||||
case 'todo':
|
||||
$action = $data['msg']['action'] === 'remove' ? '取消待办' : ($data['msg']['action'] === 'done' ? '完成' : '设待办');
|
||||
return "[{$action}] {$this->previewMsg(false, $data['msg']['data'])}";
|
||||
@@ -772,6 +801,7 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
* - reply-98:回复消息ID=98
|
||||
* - update-99:更新消息ID=99(标记修改)
|
||||
* - change-99:更新消息ID=99(不标记修改)
|
||||
* - forward-99:转发消息ID=99
|
||||
* @param int $dialog_id 会话ID(即 聊天室ID)
|
||||
* @param string $type 消息类型
|
||||
* @param array $msg 发送的消息
|
||||
@@ -809,9 +839,10 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
$push_silence = !in_array($type, ["text", "file", "record", "meeting"]);
|
||||
}
|
||||
//
|
||||
$update_id = preg_match("/^update-(\d+)$/", $action, $match) ? $match[1] : 0;
|
||||
$change_id = preg_match("/^change-(\d+)$/", $action, $match) ? $match[1] : 0;
|
||||
$reply_id = preg_match("/^reply-(\d+)$/", $action, $match) ? $match[1] : 0;
|
||||
$update_id = intval(preg_match("/^update-(\d+)$/", $action, $match) ? $match[1] : 0);
|
||||
$change_id = intval(preg_match("/^change-(\d+)$/", $action, $match) ? $match[1] : 0);
|
||||
$reply_id = intval(preg_match("/^reply-(\d+)$/", $action, $match) ? $match[1] : 0);
|
||||
$forward_id = intval(preg_match("/^forward-(\d+)$/", $action, $match) ? $match[1] : 0);
|
||||
$sender = $sender === null ? User::userid() : $sender;
|
||||
//
|
||||
$dialog = WebSocketDialog::find($dialog_id);
|
||||
@@ -833,10 +864,10 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
if (empty($dialogMsg)) {
|
||||
throw new ApiException('消息不存在');
|
||||
}
|
||||
if ($dialogMsg->type !== 'text') {
|
||||
if ($dialogMsg->type !== 'text' && $dialogMsg->type !== 'vote') {
|
||||
throw new ApiException('此消息不支持此操作');
|
||||
}
|
||||
if ($dialogMsg->userid != $sender) {
|
||||
if ($dialogMsg->userid != $sender && $dialogMsg->type !== 'vote') {
|
||||
throw new ApiException('仅支持修改自己的消息');
|
||||
}
|
||||
//
|
||||
@@ -850,6 +881,11 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
$dialogMsg->key = $dialogMsg->generateMsgKey();
|
||||
$dialogMsg->save();
|
||||
//
|
||||
WebSocketDialogUser::whereDialogId($dialog->id)->whereUserid($sender)->whereHide(1)->change([
|
||||
'hide' => 0, // 修改消息时,显示会话(仅自己)
|
||||
'updated_at' => Carbon::now()->toDateTimeString('millisecond'),
|
||||
]);
|
||||
//
|
||||
$dialogMsg->msgJoinGroup($dialog);
|
||||
//
|
||||
$dialog->pushMsg('update', array_merge($updateData, [
|
||||
@@ -859,14 +895,28 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
return Base::retSuccess('修改成功', $dialogMsg);
|
||||
} else {
|
||||
// 发送
|
||||
if ($reply_id && !self::whereId($reply_id)->increment('reply_num')) {
|
||||
throw new ApiException('回复的消息不存在');
|
||||
if ($reply_id) {
|
||||
// 回复
|
||||
$replyRow = self::whereId($reply_id)->whereDialogId($dialog_id)->first();
|
||||
if (empty($replyRow)) {
|
||||
throw new ApiException('回复的消息不存在');
|
||||
}
|
||||
$replyMsg = Base::json2array($replyRow->getRawOriginal('msg'));
|
||||
unset($replyMsg['reply_data']);
|
||||
$msg['reply_data'] = [
|
||||
'id' => $replyRow->id,
|
||||
'userid' => $replyRow->userid,
|
||||
'type' => $replyRow->type,
|
||||
'msg' => $replyMsg,
|
||||
];
|
||||
$replyRow->increment('reply_num');
|
||||
}
|
||||
//
|
||||
$dialogMsg = self::createInstance([
|
||||
'dialog_id' => $dialog_id,
|
||||
'dialog_type' => $dialog->type,
|
||||
'reply_id' => $reply_id,
|
||||
'forward_id' => $forward_id,
|
||||
'userid' => $sender,
|
||||
'type' => $type,
|
||||
'mtype' => $mtype,
|
||||
@@ -874,13 +924,16 @@ class WebSocketDialogMsg extends AbstractModel
|
||||
'msg' => $msg,
|
||||
'read' => 0,
|
||||
]);
|
||||
AbstractModel::transaction(function () use ($dialog, $dialogMsg) {
|
||||
$dialog->last_at = Carbon::now();
|
||||
$dialog->save();
|
||||
AbstractModel::transaction(function () use ($dialogMsg) {
|
||||
$dialogMsg->send = 1;
|
||||
$dialogMsg->key = $dialogMsg->generateMsgKey();
|
||||
$dialogMsg->save();
|
||||
WebSocketDialogUser::whereDialogId($dialog->id)->change(['updated_at' => Carbon::now()->toDateTimeString('millisecond')]);
|
||||
//
|
||||
WebSocketDialogUser::whereDialogId($dialogMsg->dialog_id)->change([
|
||||
'hide' => 0, // 有新消息时,显示会话(会话内所有会员)
|
||||
'last_at' => Carbon::now(),
|
||||
'updated_at' => Carbon::now()->toDateTimeString('millisecond'),
|
||||
]);
|
||||
});
|
||||
//
|
||||
$task = new WebSocketDialogMsgTask($dialogMsg->id);
|
||||
|
||||
@@ -47,6 +47,20 @@ class WebSocketDialogMsgRead extends AbstractModel
|
||||
return $this->hasOne(WebSocketDialogMsg::class, 'id', 'msg_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 强制标记成阅读
|
||||
* @param $dialogId
|
||||
* @param $userId
|
||||
* @return void
|
||||
*/
|
||||
public static function forceRead($dialogId, $userId)
|
||||
{
|
||||
self::whereDialogId($dialogId)
|
||||
->whereUserid($userId)
|
||||
->whereNull('read_at')
|
||||
->update(['read_at' => Carbon::now()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 仅标记成阅读
|
||||
* @param $list
|
||||
|
||||
@@ -11,10 +11,12 @@ use Carbon\Carbon;
|
||||
* @property int|null $dialog_id 对话ID
|
||||
* @property int|null $userid 会员ID
|
||||
* @property string|null $top_at 置顶时间
|
||||
* @property string|null $last_at 最后消息时间
|
||||
* @property int|null $mark_unread 是否标记为未读:0否,1是
|
||||
* @property int|null $silence 是否免打扰:0否,1是
|
||||
* @property int|null $hide 不显示会话:0否,1是
|
||||
* @property int|null $inviter 邀请人
|
||||
* @property int|null $important 是否不可移出(项目、任务、部门人员)
|
||||
* @property int|null $important 是否不可移出(项目、任务人员)
|
||||
* @property string|null $color 颜色
|
||||
* @property \Illuminate\Support\Carbon|null $created_at
|
||||
* @property \Illuminate\Support\Carbon|null $updated_at
|
||||
@@ -25,9 +27,11 @@ use Carbon\Carbon;
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereColor($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereCreatedAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereDialogId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereHide($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereId($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereImportant($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereInviter($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereLastAt($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereMarkUnread($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereSilence($value)
|
||||
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogUser whereTopAt($value)
|
||||
@@ -46,4 +50,17 @@ class WebSocketDialogUser extends AbstractModel
|
||||
{
|
||||
return $this->hasOne(WebSocketDialog::class, 'id', 'dialog_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新对话最后消息时间
|
||||
* @return WebSocketDialogMsg|\Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Model|\Illuminate\Database\Query\Builder|object|null
|
||||
*/
|
||||
public static function updateMsgLastAt($dialogId)
|
||||
{
|
||||
$lastMsg = WebSocketDialogMsg::whereDialogId($dialogId)->orderByDesc('id')->first();
|
||||
if ($lastMsg) {
|
||||
WebSocketDialogUser::whereDialogId($dialogId)->change(['last_at' => $lastMsg->created_at]);
|
||||
}
|
||||
return $lastMsg;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,7 +133,8 @@ class Base
|
||||
|
||||
/**
|
||||
* 判断客户端版本
|
||||
* @param $min
|
||||
* @param $min // 最小版本(满足此版本返回true)
|
||||
* @param null $clientVersion
|
||||
* @return bool
|
||||
*/
|
||||
public static function judgeClientVersion($min, $clientVersion = null)
|
||||
@@ -687,6 +688,22 @@ class Base
|
||||
return $string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归处理数组
|
||||
*
|
||||
* @param string $callback 如:'intval'、'trim'、'addslashes'、'stripslashes'、'htmlspecialchars'
|
||||
* @param array $array
|
||||
* @return array
|
||||
*/
|
||||
public static function newArrayRecursive($callback, $array)
|
||||
{
|
||||
$func = function ($item) use (&$func, &$callback) {
|
||||
return is_array($item) ? array_map($func, $item) : call_user_func($callback, $item);
|
||||
};
|
||||
|
||||
return array_map($func, $array);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重MD5加密
|
||||
* @param $text
|
||||
@@ -2101,7 +2118,7 @@ class Base
|
||||
}
|
||||
$scaleName = "";
|
||||
if ($param['fileName']) {
|
||||
$fileName = $param['fileName'];
|
||||
$fileName = basename($param['fileName']);
|
||||
} else {
|
||||
if ($param['scale'] && is_array($param['scale'])) {
|
||||
list($width, $height) = $param['scale'];
|
||||
@@ -2260,7 +2277,7 @@ class Base
|
||||
if ($param['fileName'] === true) {
|
||||
$fileName = $file->getClientOriginalName();
|
||||
} elseif ($param['fileName']) {
|
||||
$fileName = $param['fileName'];
|
||||
$fileName = basename($param['fileName']);
|
||||
} else {
|
||||
if ($param['scale'] && is_array($param['scale'])) {
|
||||
list($width, $height) = $param['scale'];
|
||||
@@ -2456,13 +2473,18 @@ class Base
|
||||
*/
|
||||
public static function extIcon($ext)
|
||||
{
|
||||
return match ($ext) {
|
||||
"docx" => 'images/ext/doc.png',
|
||||
"xlsx" => 'images/ext/xls.png',
|
||||
"pptx" => 'images/ext/ppt.png',
|
||||
"ai", "avi", "bmp", "cdr", "doc", "eps", "gif", "mov", "mp3", "mp4", "pdf", "ppt", "pr", "psd", "rar", "svg", "tif", "txt", "xls", "zip" => 'images/ext/' . $ext . '.png',
|
||||
default => 'images/ext/file.png',
|
||||
};
|
||||
if ($ext == "docx") {
|
||||
$ext = 'doc';
|
||||
} elseif ($ext == "xlsx") {
|
||||
$ext = 'xls';
|
||||
} elseif ($ext == "pptx") {
|
||||
$ext = 'ppt';
|
||||
}
|
||||
if (in_array($ext, ["ai", "avi", "bmp", "cdr", "doc", "eps", "gif", "mov", "mp3", "mp4", "pdf", "ppt", "pr", "psd", "rar", "svg", "tif", "txt", "xls", "zip"])) {
|
||||
return 'images/ext/' . $ext . '.png';
|
||||
} else {
|
||||
return 'images/ext/file.png';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -174,19 +174,16 @@ class Extranet
|
||||
|
||||
/**
|
||||
* 随机笑话接口
|
||||
* @return array
|
||||
* @return string
|
||||
*/
|
||||
public static function randJoke(): array
|
||||
public static function randJoke(): string
|
||||
{
|
||||
$jokeKey = env("JUKE_KEY_JOKE");
|
||||
if ($jokeKey) {
|
||||
$data = self::curl("http://v.juhe.cn/joke/randJoke.php?key=" . $jokeKey);
|
||||
$data = Base::json2array($data);
|
||||
if ($data['reason'] === 'success') {
|
||||
return $data['result'];
|
||||
}
|
||||
$data = self::curl("https://hmajax.itheima.net/api/randjoke");
|
||||
$data = Base::json2array($data);
|
||||
if ($data['message'] === '获取成功' && $text = trim($data['data'])) {
|
||||
return $text;
|
||||
}
|
||||
return [];
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -195,13 +192,10 @@ class Extranet
|
||||
*/
|
||||
public static function soups(): string
|
||||
{
|
||||
$soupKey = env("JUKE_KEY_SOUP");
|
||||
if ($soupKey) {
|
||||
$data = self::curl("https://apis.juhe.cn/fapig/soup/query?key=" . $soupKey);
|
||||
$data = Base::json2array($data);
|
||||
if ($data['reason'] === 'success' && $text = trim($data['result']['text'])) {
|
||||
return $text;
|
||||
}
|
||||
$data = self::curl("https://hmajax.itheima.net/api/ambition");
|
||||
$data = Base::json2array($data);
|
||||
if ($data['message'] === '获取成功' && $text = trim($data['data'])) {
|
||||
return $text;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ class TimeRange
|
||||
public function __construct($data)
|
||||
{
|
||||
if (is_array($data)) {
|
||||
$range = explode("-", str_replace([",", "|"], "-", $data['timerange']));
|
||||
$range = $this->format($data['timerange']);
|
||||
if ($data['updated_at'] || $data['at_after']) {
|
||||
$range[0] = $data['updated_at'] ?: $data['at_after'];
|
||||
}
|
||||
@@ -23,14 +23,27 @@ class TimeRange
|
||||
$range[1] = $data['deleted_at'];
|
||||
}
|
||||
} else {
|
||||
$range = explode("-", str_replace([",", "|"], "-", $data));
|
||||
$range = $this->format($data);
|
||||
}
|
||||
//
|
||||
$updated = Base::isNumber($range[0]) ? intval($range[0]) : trim($range[0]);
|
||||
$deleted = Base::isNumber($range[1]) ? intval($range[1]) : trim($range[1]);
|
||||
//
|
||||
$this->updated = $updated ? Carbon::parse($updated) : null;
|
||||
$this->deleted = $deleted ? Carbon::parse($deleted) : null;
|
||||
$timezone = config('app.timezone');
|
||||
$this->updated = $updated ? Carbon::parse($updated)->setTimezone($timezone) : null;
|
||||
$this->deleted = $deleted ? Carbon::parse($deleted)->setTimezone($timezone) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $timerange
|
||||
* - 格式1:2021-01-01 00:00:00,2021-01-01 23:59:59
|
||||
* - 格式2:1612051200-1612137599
|
||||
* @return array
|
||||
*/
|
||||
private function format($timerange)
|
||||
{
|
||||
$search = str_contains($timerange, ":") ? ["|"] : ["|", "-"];
|
||||
return explode(",", str_replace($search, ",", $timerange));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Observers;
|
||||
|
||||
use App\Models\Deleted;
|
||||
use App\Models\WebSocketDialogUser;
|
||||
use Carbon\Carbon;
|
||||
|
||||
class WebSocketDialogUserObserver
|
||||
{
|
||||
@@ -15,6 +16,18 @@ class WebSocketDialogUserObserver
|
||||
*/
|
||||
public function created(WebSocketDialogUser $webSocketDialogUser)
|
||||
{
|
||||
if (!$webSocketDialogUser->last_at) {
|
||||
if (in_array($webSocketDialogUser->webSocketDialog?->group_type, ['user', 'department', 'all'])) {
|
||||
$webSocketDialogUser->last_at = Carbon::now();
|
||||
$webSocketDialogUser->save();
|
||||
} else {
|
||||
$item = WebSocketDialogUser::whereDialogId($webSocketDialogUser->dialog_id)->orderByDesc('last_at')->first();
|
||||
if ($item?->last_at) {
|
||||
$webSocketDialogUser->last_at = $item->last_at;
|
||||
$webSocketDialogUser->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
Deleted::forget('dialog', $webSocketDialogUser->dialog_id, $webSocketDialogUser->userid);
|
||||
}
|
||||
|
||||
|
||||
@@ -172,8 +172,11 @@ class WebSocketService implements WebSocketHandlerInterface
|
||||
*/
|
||||
public function onClose(Server $server, $fd, $reactorId)
|
||||
{
|
||||
Task::deliver(new LineTask($this->getUserid($fd), false)); // 通知离线
|
||||
$this->deleteUser($fd);
|
||||
$userid = $this->getUserid($fd);
|
||||
if($userid){
|
||||
Task::deliver(new LineTask($userid, false)); // 通知离线
|
||||
$this->deleteUser($fd);
|
||||
}
|
||||
}
|
||||
|
||||
/** ****************************************************************************** */
|
||||
|
||||
@@ -71,6 +71,9 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
}
|
||||
if (preg_match("/<span[^>]*?data-quick-key=([\"'])(.*?)\\1[^>]*?>(.*?)<\/span>/is", $original, $match)) {
|
||||
$command = $match[2];
|
||||
if (str_starts_with($command, '%3A.')) {
|
||||
$command = ":" . substr($command, 4);
|
||||
}
|
||||
} else {
|
||||
$command = trim(strip_tags($original));
|
||||
}
|
||||
@@ -313,12 +316,12 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
$nameKey = $isManager ? $array[2] : $array[1];
|
||||
$data = $this->botManagerOne($botId, $msg->userid);
|
||||
if ($data) {
|
||||
$list = WebSocketDialog::select(['web_socket_dialogs.*', 'u.top_at', 'u.mark_unread', 'u.silence', 'u.color', 'u.updated_at as user_at'])
|
||||
$list = WebSocketDialog::select(['web_socket_dialogs.*', 'u.top_at', 'u.last_at', 'u.mark_unread', 'u.silence', 'u.hide', 'u.color', 'u.updated_at as user_at'])
|
||||
->join('web_socket_dialog_users as u', 'web_socket_dialogs.id', '=', 'u.dialog_id')
|
||||
->where('web_socket_dialogs.name', 'LIKE', "%{$nameKey}%")
|
||||
->where('u.userid', $data->userid)
|
||||
->orderByDesc('u.top_at')
|
||||
->orderByDesc('web_socket_dialogs.last_at')
|
||||
->orderByDesc('u.last_at')
|
||||
->take(20)
|
||||
->get();
|
||||
if ($list->isEmpty()) {
|
||||
@@ -377,6 +380,7 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
'openai_agency' => $setting['openai_agency'],
|
||||
'openai_model' => $setting['openai_model'],
|
||||
'server_url' => $serverUrl,
|
||||
'chunk_size' => 7,
|
||||
];
|
||||
if (empty($extras['openai_key'])) {
|
||||
$error = 'Robot disabled.';
|
||||
@@ -434,6 +438,24 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
$error = 'The client version is low (required version ≥ v0.29.12).';
|
||||
}
|
||||
break;
|
||||
// Gemini 机器人
|
||||
case 'ai-gemini@bot.system':
|
||||
$setting = Base::setting('aibotSetting');
|
||||
$webhookUrl = "{$serverUrl}/ai/gemini/send";
|
||||
$extras = [
|
||||
'gemini_key' => $setting['gemini_key'],
|
||||
'gemini_model' => $setting['gemini_model'],
|
||||
'gemini_agency' => $setting['gemini_agency'],
|
||||
'gemini_timeout' => 20,
|
||||
'server_url' => $serverUrl,
|
||||
];
|
||||
if (empty($extras['gemini_key'])) {
|
||||
$error = 'Robot disabled.';
|
||||
} elseif (in_array($this->client['platform'], ['win', 'mac', 'web'])
|
||||
&& !Base::judgeClientVersion("0.29.11", $this->client['version'])) {
|
||||
$error = 'The client version is low (required version ≥ v0.29.12).';
|
||||
}
|
||||
break;
|
||||
// 其他机器人
|
||||
default:
|
||||
$userBot = UserBot::whereBotId($botUser->userid)->first();
|
||||
@@ -449,7 +471,7 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
}
|
||||
//
|
||||
try {
|
||||
$res = Ihttp::ihttp_post($webhookUrl, [
|
||||
$data = [
|
||||
'text' => $command,
|
||||
'token' => User::generateToken($botUser),
|
||||
'dialog_id' => $dialog->id,
|
||||
@@ -460,7 +482,8 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
'bot_uid' => $botUser->userid,
|
||||
'version' => Base::getVersion(),
|
||||
'extras' => Base::array2json($extras)
|
||||
], 10);
|
||||
];
|
||||
$res = Ihttp::ihttp_post($webhookUrl, $data);
|
||||
if ($userBot) {
|
||||
$userBot->webhook_num++;
|
||||
$userBot->save();
|
||||
|
||||
@@ -7,6 +7,7 @@ use App\Models\TaskWorker;
|
||||
use App\Models\Tmp;
|
||||
use App\Models\WebSocketTmpMsg;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\File as SupportFile;
|
||||
|
||||
/**
|
||||
* 删除过期临时数据任务
|
||||
@@ -102,10 +103,12 @@ class DeleteTmpTask extends AbstractTask
|
||||
*/
|
||||
case 'file_pack':
|
||||
{
|
||||
$path = storage_path('app/temp/download/');
|
||||
$path = public_path('tmp/file/');
|
||||
if (!SupportFile::exists($path)) {
|
||||
return;
|
||||
}
|
||||
$dirIterator = new \RecursiveDirectoryIterator($path);
|
||||
$iterator = new \RecursiveIteratorIterator($dirIterator);
|
||||
|
||||
foreach ($iterator as $file) {
|
||||
if ($file->isFile()) {
|
||||
$time = $file->getMTime();
|
||||
|
||||
@@ -13,13 +13,15 @@ use Carbon\Carbon;
|
||||
/**
|
||||
* 获取笑话、心灵鸡汤
|
||||
*
|
||||
* 在.env添加笑话 JUKE_KEY_JOKE
|
||||
* 在.env添加鸡汤 JUKE_KEY_SOUP
|
||||
*
|
||||
* 每日小时采集1次
|
||||
* 每分钟采集1次
|
||||
*/
|
||||
class JokeSoupTask extends AbstractTask
|
||||
{
|
||||
public static function keyName($key)
|
||||
{
|
||||
return "JokeSoupTask-v2:{$key}";
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
@@ -27,27 +29,25 @@ class JokeSoupTask extends AbstractTask
|
||||
|
||||
public function start()
|
||||
{
|
||||
// 判断每小时执行一次
|
||||
if (Cache::get("JokeSoupTask:YmdH") == date("YmdH")) {
|
||||
// 判断每分钟执行一次
|
||||
if (Cache::get(self::keyName("YmdHi")) == date("YmdHi")) {
|
||||
return;
|
||||
}
|
||||
Cache::put("JokeSoupTask:YmdH", date("YmdH"), Carbon::now()->addDay());
|
||||
Cache::put(self::keyName("YmdHi"), date("YmdHi"), Carbon::now()->addDay());
|
||||
//
|
||||
$array = Base::json2array(Cache::get("JokeSoupTask:jokes"));
|
||||
$array = Base::json2array(Cache::get(self::keyName("jokes")));
|
||||
$data = Extranet::randJoke();
|
||||
foreach ($data as $item) {
|
||||
if ($text = trim($item['content'])) {
|
||||
$array[] = $text;
|
||||
}
|
||||
if ($data) {
|
||||
$array[] = $data;
|
||||
}
|
||||
Cache::forever("JokeSoupTask:jokes", Base::array2json(array_slice($array, -100)));
|
||||
Cache::forever(self::keyName("jokes"), Base::array2json(array_slice($array, -200)));
|
||||
//
|
||||
$array = Base::json2array(Cache::get("JokeSoupTask:soups"));
|
||||
$array = Base::json2array(Cache::get(self::keyName("soups")));
|
||||
$data = Extranet::soups();
|
||||
if ($data) {
|
||||
$array[] = $data;
|
||||
}
|
||||
Cache::forever("JokeSoupTask:soups", Base::array2json(array_slice($array, -24)));
|
||||
Cache::forever(self::keyName("soups"), Base::array2json(array_slice($array, -200)));
|
||||
}
|
||||
|
||||
public function end()
|
||||
|
||||
@@ -31,34 +31,34 @@ class UnclaimedTaskRemindTask extends AbstractTask
|
||||
return;
|
||||
}
|
||||
//
|
||||
$times = explode(':',date('H:i'));
|
||||
$reminderTimes = explode(':',$setting['unclaimed_task_reminder_time']);
|
||||
if( !isset($times[1]) || !isset($reminderTimes[1]) || $times[0] != $reminderTimes[0]){
|
||||
$times = explode(':', date('H:i'));
|
||||
$reminderTimes = explode(':', $setting['unclaimed_task_reminder_time']);
|
||||
if (!isset($times[1]) || !isset($reminderTimes[1]) || $times[0] != $reminderTimes[0]) {
|
||||
return;
|
||||
}
|
||||
// 执行一次
|
||||
if (Cache::get("UnclaimedTaskRemindTask:His",0)) {
|
||||
if (Cache::get("UnclaimedTaskRemindTask:His", 0)) {
|
||||
return;
|
||||
}
|
||||
if( $times[1] >= $reminderTimes[1] - 1 && $times[1] <= $reminderTimes[1] + 1){
|
||||
if ($times[1] >= intval($reminderTimes[1]) - 1 && $times[1] <= intval($reminderTimes[1]) + 1) {
|
||||
//
|
||||
Cache::put("UnclaimedTaskRemindTask:His", date('H:i:s'), Carbon::now()->addMinutes(5));
|
||||
//
|
||||
Project::whereNull('deleted_at')->whereNull('archived_at')->chunk(100,function($projects) {
|
||||
Project::whereNull('deleted_at')->whereNull('archived_at')->chunk(100, function ($projects) {
|
||||
foreach ($projects as $project) {
|
||||
//
|
||||
$projectTasks = ProjectTask::select('project_tasks.id','project_tasks.name')
|
||||
$projectTasks = ProjectTask::select('project_tasks.id', 'project_tasks.name')
|
||||
->leftJoin('project_task_users', function ($query) {
|
||||
$query->on('project_tasks.id', '=', 'project_task_users.task_id');
|
||||
})
|
||||
->where('project_tasks.project_id',$project->id)
|
||||
->where('project_tasks.project_id', $project->id)
|
||||
->whereNull('project_tasks.deleted_at')
|
||||
->whereNull('project_tasks.archived_at')
|
||||
->whereNull('project_task_users.id')
|
||||
->limit(10)
|
||||
->get();
|
||||
//
|
||||
if( !$projectTasks->isEmpty() ){
|
||||
if (!$projectTasks->isEmpty()) {
|
||||
$botUser = User::botGetOrCreate('task-alert');
|
||||
if (empty($botUser)) {
|
||||
return;
|
||||
@@ -69,10 +69,10 @@ class UnclaimedTaskRemindTask extends AbstractTask
|
||||
}
|
||||
//
|
||||
$taskHtml = '<span style="line-height: 26px;">任务待领取</span> <br/>';
|
||||
foreach($projectTasks as $projectTask){
|
||||
foreach ($projectTasks as $projectTask) {
|
||||
$taskHtml .= "<span class=\"mention task\" style=\"line-height: 26px;\" data-id=\"{$projectTask->id}\">#{$projectTask->name}</span> <br/>";
|
||||
}
|
||||
WebSocketDialogMsg::sendMsg(null, $project->dialog_id , 'text', [
|
||||
WebSocketDialogMsg::sendMsg(null, $project->dialog_id, 'text', [
|
||||
'text' => $taskHtml
|
||||
], $botUser->userid);
|
||||
}
|
||||
@@ -83,7 +83,5 @@ class UnclaimedTaskRemindTask extends AbstractTask
|
||||
|
||||
public function end()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -38,12 +38,17 @@ class WebSocketDialogMsgTask extends AbstractTask
|
||||
{
|
||||
parent::__construct(...func_get_args());
|
||||
$this->id = $id;
|
||||
$this->ignoreFd = $ignoreFd === null ? Request::header('fd') : $ignoreFd;
|
||||
$this->client = [
|
||||
'version' => Base::headerOrInput('version'),
|
||||
'language' => Base::headerOrInput('language'),
|
||||
'platform' => Base::headerOrInput('platform'),
|
||||
];
|
||||
// 判断是否有Request方法,兼容go协程请求
|
||||
$this->ignoreFd = $ignoreFd;
|
||||
$this->client = [];
|
||||
if (method_exists(request(), "header")) {
|
||||
$this->ignoreFd = $ignoreFd === null ? Request::header('fd') : $ignoreFd;
|
||||
$this->client = [
|
||||
'version' => Base::headerOrInput('version'),
|
||||
'language' => Base::headerOrInput('language'),
|
||||
'platform' => Base::headerOrInput('platform'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
169
bin/https
Normal file
@@ -0,0 +1,169 @@
|
||||
#!/bin/sh
|
||||
|
||||
#fonts color
|
||||
Green="\033[32m"
|
||||
Yellow="\033[33m"
|
||||
Red="\033[31m"
|
||||
GreenBG="\033[42;37m"
|
||||
YellowBG="\033[43;37m"
|
||||
RedBG="\033[41;37m"
|
||||
Font="\033[0m"
|
||||
|
||||
#notification information
|
||||
OK="${Green}[OK]${Font}"
|
||||
Warn="${Yellow}[警告]${Font}"
|
||||
Error="${Red}[错误]${Font}"
|
||||
|
||||
cd "$(
|
||||
cd "$(dirname "$0")" || exit
|
||||
pwd
|
||||
)" || exit
|
||||
|
||||
#================================================================
|
||||
#================================================================
|
||||
|
||||
success() {
|
||||
echo -e "${OK} ${GreenBG}$1${Font}"
|
||||
}
|
||||
|
||||
warning() {
|
||||
echo -e "${Warn} ${YellowBG}$1${Font}"
|
||||
}
|
||||
|
||||
error() {
|
||||
echo -e "${Error} ${RedBG}$1${Font}"
|
||||
}
|
||||
|
||||
info() {
|
||||
echo -e "$1"
|
||||
}
|
||||
|
||||
env_get() {
|
||||
local key=$1
|
||||
local value=`cat $(dirname "$PWD")/.env | grep "^$key=" | awk -F '=' '{print $2}'`
|
||||
echo "$value"
|
||||
}
|
||||
|
||||
env_set() {
|
||||
local key=$1
|
||||
local val=$2
|
||||
local exist=`cat $(dirname "$PWD")/.env | grep "^$key="`
|
||||
if [ -z "$exist" ]; then
|
||||
echo "$key=$val" >> $(dirname "$PWD")/.env
|
||||
else
|
||||
sed -i "/^${key}=/c\\${key}=${val}" $(dirname "$PWD")/.env
|
||||
fi
|
||||
}
|
||||
|
||||
#================================================================
|
||||
#================================================================
|
||||
|
||||
check() {
|
||||
while [ -z "$domain" ]; do
|
||||
read -rp "请输入你的域名: " domain
|
||||
# 判断域名是否合法
|
||||
if [ -z "$domain" ]; then
|
||||
error "域名不能为空"
|
||||
elif [ -z "$(echo "$domain" | grep -E '^[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)+$')" ]; then
|
||||
error "域名格式不正确"
|
||||
domain=""
|
||||
fi
|
||||
done
|
||||
|
||||
success "正在域名DNS解析IP..."
|
||||
local domain_ip=$(ping -c 1 "${domain}" 2>/dev/null | grep PING | cut -d "(" -f2 | cut -d ")" -f1)
|
||||
local local_ip=$(curl -sk ip.sb)
|
||||
info "域名DNS解析IP: ${domain_ip}"
|
||||
info "本机IP: ${local_ip}"
|
||||
|
||||
sleep 2
|
||||
if [[ "$(echo "${local_ip}" | tr '.' '+' | bc)" == "$(echo "${domain_ip}" | tr '.' '+' | bc)" ]]; then
|
||||
success "域名DNS解析IP 与 本机IP 匹配"
|
||||
sleep 2
|
||||
else
|
||||
warning "域名DNS解析IP 与 本机IP 不匹配,是否继续操作? [Y/n]"
|
||||
read -r continue_next
|
||||
[[ -z ${continue_next} ]] && continue_next="Y"
|
||||
case $continue_next in
|
||||
[yY][eE][sS] | [yY])
|
||||
success "继续操作"
|
||||
sleep 2
|
||||
;;
|
||||
*)
|
||||
error "操作终止"
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
}
|
||||
|
||||
install() {
|
||||
local sitePath="$(dirname "$PWD")/docker/nginx/site"
|
||||
local sslPath="$sitePath/ssl"
|
||||
if [[ -f "$sslPath/$domain.key" && -f "$sslPath/$domain.crt" ]]; then
|
||||
warning "$domain 证书文件已存在,是否删除并继续操作? [Y/n]"
|
||||
read -r continue_install
|
||||
[[ -z ${continue_install} ]] && continue_install="Y"
|
||||
case $continue_install in
|
||||
[yY][eE][sS] | [yY])
|
||||
rm -f "$sslPath/$domain.key"
|
||||
rm -f "$sslPath/$domain.crt"
|
||||
success "继续操作"
|
||||
sleep 2
|
||||
;;
|
||||
*)
|
||||
error "操作终止"
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
apk add --no-cache openssl socat
|
||||
curl https://get.acme.sh | sh
|
||||
if [[ 0 -ne $? ]]; then
|
||||
error "安装证书生成脚本失败"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if /root/.acme.sh/acme.sh --issue -d "${domain}" -w "$(dirname "$PWD")/public" --standalone -k ec-256 --force --test; then
|
||||
success "SSL 证书测试签发成功,开始正式签发"
|
||||
rm -rf "/root/.acme.sh/${domain}_ecc"
|
||||
sleep 2
|
||||
else
|
||||
error "SSL 证书测试签发失败"
|
||||
rm -rf "root/.acme.sh/${domain}_ecc"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if /root/.acme.sh/acme.sh --issue -d "${domain}" -w "$(dirname "$PWD")/public" --server letsencrypt --standalone -k ec-256 --force; then
|
||||
success "SSL 证书生成成功"
|
||||
sleep 2
|
||||
mkdir -p $sslPath
|
||||
if /root/.acme.sh/acme.sh --installcert -d "${domain}" --fullchainpath "${sslPath}/${domain}.crt" --keypath "${sslPath}/${domain}.key" --ecc --force; then
|
||||
success "SSL 证书配置成功"
|
||||
sleep 2
|
||||
fi
|
||||
else
|
||||
error "SSL 证书生成失败"
|
||||
rm -rf "/root/.acme.sh/${domain}_ecc"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
env_set "APP_URL" "https://${domain}"
|
||||
|
||||
cat >${sitePath}/ssl.conf <<EOF
|
||||
server_name ${domain};
|
||||
listen 443 ssl;
|
||||
ssl_certificate /etc/nginx/conf.d/site/ssl/${domain}.crt;
|
||||
ssl_certificate_key /etc/nginx/conf.d/site/ssl/${domain}.key;
|
||||
ssl_protocols TLSv1.1 TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:HIGH:!aNULL:!MD5:!RC4:!DHE;
|
||||
ssl_prefer_server_ciphers on;
|
||||
ssl_session_cache shared:SSL:10m;
|
||||
ssl_session_timeout 10m;
|
||||
error_page 497 https://\$host\$request_uri;
|
||||
EOF
|
||||
}
|
||||
|
||||
check
|
||||
install
|
||||
25
version.js → bin/version.js
vendored
@@ -17,12 +17,33 @@ function runExec(command, cb) {
|
||||
});
|
||||
}
|
||||
|
||||
function removeDuplicateLines(log) {
|
||||
const logs = log.split(/(\n## \[.*?\])/)
|
||||
if (logs) {
|
||||
log = logs.map(str => {
|
||||
const array = [];
|
||||
const items = str.split("\n");
|
||||
items.some(item => {
|
||||
if (/^-/.test(item)) {
|
||||
if (array.indexOf(item) === -1) {
|
||||
array.push(item);
|
||||
}
|
||||
} else {
|
||||
array.push(item);
|
||||
}
|
||||
})
|
||||
return array.join("\n");
|
||||
}).join('');
|
||||
}
|
||||
return log;
|
||||
}
|
||||
|
||||
runExec("git rev-list --count HEAD $(git branch | sed -n -e 's/^\* \(.*\)/\1/p')", function (err, response) {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
return;
|
||||
}
|
||||
const num = 1238 + parseInt(response)
|
||||
const num = 1250 + parseInt(response)
|
||||
if (isNaN(num) || Math.floor(num % 100) < 0) {
|
||||
console.error("get version error " + response);
|
||||
return;
|
||||
@@ -43,7 +64,7 @@ runExec("git rev-list --count HEAD $(git branch | sed -n -e 's/^\* \(.*\)/\1/p')
|
||||
console.error("Change file does not exist");
|
||||
return "";
|
||||
}
|
||||
let newContent = fs.readFileSync(changeFile, 'utf8');
|
||||
let newContent = removeDuplicateLines(fs.readFileSync(changeFile, 'utf8'));
|
||||
if (newContent.indexOf("## [Unreleased]") !== -1) {
|
||||
newContent = newContent.replace("## [Unreleased]", `## [${ver}]`);
|
||||
} else {
|
||||
152
cmd
@@ -2,13 +2,16 @@
|
||||
|
||||
#fonts color
|
||||
Green="\033[32m"
|
||||
Yellow="\033[33m"
|
||||
Red="\033[31m"
|
||||
GreenBG="\033[42;37m"
|
||||
YellowBG="\033[43;37m"
|
||||
RedBG="\033[41;37m"
|
||||
Font="\033[0m"
|
||||
|
||||
#notification information
|
||||
OK="${Green}[OK]${Font}"
|
||||
Warn="${Yellow}[警告]${Font}"
|
||||
Error="${Red}[错误]${Font}"
|
||||
|
||||
cur_path="$(pwd)"
|
||||
@@ -17,14 +20,30 @@ COMPOSE="docker-compose"
|
||||
|
||||
judge() {
|
||||
if [[ 0 -eq $? ]]; then
|
||||
echo -e "${OK} ${GreenBG} $1 完成 ${Font}"
|
||||
success "$1 完成"
|
||||
sleep 1
|
||||
else
|
||||
echo -e "${Error} ${RedBG} $1 失败${Font}"
|
||||
error "$1 失败"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
success() {
|
||||
echo -e "${OK} ${GreenBG}$1${Font}"
|
||||
}
|
||||
|
||||
warning() {
|
||||
echo -e "${Warn} ${YellowBG}$1${Font}"
|
||||
}
|
||||
|
||||
error() {
|
||||
echo -e "${Error} ${RedBG}$1${Font}"
|
||||
}
|
||||
|
||||
info() {
|
||||
echo -e "$1"
|
||||
}
|
||||
|
||||
rand() {
|
||||
local min=$1
|
||||
local max=$(($2-$min+1))
|
||||
@@ -51,7 +70,7 @@ restart_php() {
|
||||
$COMPOSE stop php
|
||||
$COMPOSE start php
|
||||
else
|
||||
echo -e "$RES"
|
||||
info "$RES"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -69,21 +88,21 @@ switch_debug() {
|
||||
check_docker() {
|
||||
docker --version &> /dev/null
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${Error} ${RedBG} 未安装 Docker!${Font}"
|
||||
error "未安装 Docker!"
|
||||
exit 1
|
||||
fi
|
||||
docker-compose version &> /dev/null
|
||||
if [ $? -ne 0 ]; then
|
||||
docker compose version &> /dev/null
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${Error} ${RedBG} 未安装 Docker-compose!${Font}"
|
||||
error "未安装 Docker-compose!"
|
||||
exit 1
|
||||
fi
|
||||
COMPOSE="docker compose"
|
||||
fi
|
||||
if [[ -n `$COMPOSE version | grep -E "\sv*1"` ]]; then
|
||||
if [[ -n `$COMPOSE version | grep -E "\sv1"` ]]; then
|
||||
$COMPOSE version
|
||||
echo -e "${Error} ${RedBG} Docker-compose 版本过低,请升级至v2+!${Font}"
|
||||
error "Docker-compose 版本过低,请升级至v2+!"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
@@ -91,7 +110,17 @@ check_docker() {
|
||||
check_node() {
|
||||
npm --version &> /dev/null
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${Error} ${RedBG} 未安装nodejs!${Font}"
|
||||
error "未安装 npm!"
|
||||
exit 1
|
||||
fi
|
||||
node --version &> /dev/null
|
||||
if [ $? -ne 0 ]; then
|
||||
error "未安装 Node.js!"
|
||||
exit 1
|
||||
fi
|
||||
if [[ -n `node --version | grep -E "v1"` ]]; then
|
||||
node --version
|
||||
error "Node.js 版本过低,请升级至v20+!"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
@@ -154,7 +183,7 @@ run_exec() {
|
||||
local cmd=$2
|
||||
local name=`docker_name $container`
|
||||
if [ -z "$name" ]; then
|
||||
echo -e "${Error} ${RedBG} 没有找到 $container 容器! ${Font}"
|
||||
error "没有找到 $container 容器!"
|
||||
exit 1
|
||||
fi
|
||||
docker exec -it "$name" /bin/sh -c "$cmd"
|
||||
@@ -170,7 +199,7 @@ run_mysql() {
|
||||
filename="${cur_path}/docker/mysql/backup/${database}_$(date "+%Y%m%d%H%M%S").sql.gz"
|
||||
run_exec mariadb "exec mysqldump --databases $database -u$username -p$password" | gzip > $filename
|
||||
judge "备份数据库"
|
||||
[ -f "$filename" ] && echo -e "备份文件:$filename"
|
||||
[ -f "$filename" ] && info "备份文件:$filename"
|
||||
elif [ "$1" = "recovery" ]; then
|
||||
database=$(env_get DB_DATABASE)
|
||||
username=$(env_get DB_USERNAME)
|
||||
@@ -179,19 +208,19 @@ run_mysql() {
|
||||
mkdir -p ${cur_path}/docker/mysql/backup
|
||||
list=`ls -1 "${cur_path}/docker/mysql/backup" | grep ".sql.gz"`
|
||||
if [ -z "$list" ]; then
|
||||
echo -e "${Error} ${RedBG} 没有备份文件!${Font}"
|
||||
error "没有备份文件!"
|
||||
exit 1
|
||||
fi
|
||||
echo "$list"
|
||||
read -rp "请输入备份文件名称还原:" inputname
|
||||
filename="${cur_path}/docker/mysql/backup/${inputname}"
|
||||
if [ ! -f "$filename" ]; then
|
||||
echo -e "${Error} ${RedBG} 备份文件:${inputname} 不存在! ${Font}"
|
||||
error "备份文件:${inputname} 不存在!"
|
||||
exit 1
|
||||
fi
|
||||
container_name=`docker_name mariadb`
|
||||
if [ -z "$container_name" ]; then
|
||||
echo -e "${Error} ${RedBG} 没有找到 mariadb 容器! ${Font}"
|
||||
error "没有找到 mariadb 容器!"
|
||||
exit 1
|
||||
fi
|
||||
docker cp $filename $container_name:/
|
||||
@@ -201,6 +230,49 @@ run_mysql() {
|
||||
fi
|
||||
}
|
||||
|
||||
https_auto() {
|
||||
restart_nginx="n"
|
||||
if [[ "$(env_get APP_PORT)" != "80" ]]; then
|
||||
warning "HTTP服务端口不是80,是否修改并继续操作? [Y/n]"
|
||||
read -r continue_http
|
||||
[[ -z ${continue_http} ]] && continue_http="Y"
|
||||
case $continue_http in
|
||||
[yY][eE][sS] | [yY])
|
||||
success "继续操作"
|
||||
env_set "APP_PORT" "80"
|
||||
restart_nginx="y"
|
||||
;;
|
||||
*)
|
||||
error "操作终止"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
if [[ "$(env_get APP_SSL_PORT)" != "443" ]]; then
|
||||
warning "HTTPS服务端口不是443,是否修改并继续操作? [Y/n]"
|
||||
read -r continue_https
|
||||
[[ -z ${continue_https} ]] && continue_https="Y"
|
||||
case $continue_https in
|
||||
[yY][eE][sS] | [yY])
|
||||
success "继续操作"
|
||||
env_set "APP_SSL_PORT" "443"
|
||||
restart_nginx="y"
|
||||
;;
|
||||
*)
|
||||
error "操作终止"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
if [[ "$restart_nginx" == "y" ]]; then
|
||||
$COMPOSE up -d
|
||||
fi
|
||||
docker run -it --rm -v $(pwd):/work nginx:alpine sh "/work/bin/https"
|
||||
if [[ 0 -eq $? ]]; then
|
||||
run_exec nginx "nginx -s reload"
|
||||
fi
|
||||
}
|
||||
|
||||
env_get() {
|
||||
local key=$1
|
||||
local value=`cat ${cur_path}/.env | grep "^$key=" | awk -F '=' '{print $2}'`
|
||||
@@ -220,7 +292,7 @@ env_set() {
|
||||
docker run -it --rm -v ${cur_path}:/www alpine sh -c "sed -i "/^${key}=/c\\${key}=${val}" /www/.env"
|
||||
fi
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${Error} ${RedBG} 设置env参数失败!${Font}"
|
||||
error "设置env参数失败!"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
@@ -293,7 +365,7 @@ if [ $# -gt 0 ]; then
|
||||
run_exec php "composer config --unset repos.packagist"
|
||||
fi
|
||||
if [ ! -f "${cur_path}/vendor/autoload.php" ]; then
|
||||
echo -e "${Error} ${RedBG}composer install 失败,请重试! ${Font}"
|
||||
error "composer install 失败,请重试!"
|
||||
exit 1
|
||||
fi
|
||||
[[ -z "$(env_get APP_KEY)" ]] && run_exec php "php artisan key:generate"
|
||||
@@ -303,7 +375,7 @@ if [ $# -gt 0 ]; then
|
||||
while [ ! -f "${cur_path}/docker/mysql/data/$(env_get DB_DATABASE)/db.opt" ]; do
|
||||
((remaining=$remaining-1))
|
||||
if [ $remaining -lt 0 ]; then
|
||||
echo -e "${Error} ${RedBG} 数据库初始化失败! ${Font}"
|
||||
error "数据库初始化失败!"
|
||||
exit 1
|
||||
fi
|
||||
chmod -R 775 "${cur_path}/docker/mysql/data"
|
||||
@@ -311,19 +383,21 @@ if [ $# -gt 0 ]; then
|
||||
done
|
||||
run_exec php "php artisan migrate --seed"
|
||||
if [ ! -f "${cur_path}/docker/mysql/data/$(env_get DB_DATABASE)/$(env_get DB_PREFIX)migrations.ibd" ]; then
|
||||
echo -e "${Error} ${RedBG} 数据库安装失败! ${Font}"
|
||||
error "数据库安装失败!"
|
||||
exit 1
|
||||
fi
|
||||
# 设置初始化密码
|
||||
res=`run_exec mariadb "sh /etc/mysql/repassword.sh"`
|
||||
$COMPOSE up -d
|
||||
restart_php
|
||||
echo -e "${OK} ${GreenBG} 安装完成 ${Font}"
|
||||
echo -e "地址: http://${GreenBG}127.0.0.1:$(env_get APP_PORT)${Font}"
|
||||
echo -e "$res"
|
||||
success "安装完成"
|
||||
info "地址: http://${GreenBG}127.0.0.1:$(env_get APP_PORT)${Font}"
|
||||
info "$res"
|
||||
elif [[ "$1" == "update" ]]; then
|
||||
shift 1
|
||||
run_mysql backup
|
||||
if [[ "$@" != "nobackup" ]]; then
|
||||
run_mysql backup
|
||||
fi
|
||||
if [[ -z "$(arg_get local)" ]]; then
|
||||
git fetch --all
|
||||
git reset --hard origin/$(git branch | sed -n -e 's/^\* \(.*\)/\1/p')
|
||||
@@ -340,10 +414,10 @@ if [ $# -gt 0 ]; then
|
||||
[[ -z ${uninstall} ]] && uninstall="Y"
|
||||
case $uninstall in
|
||||
[yY][eE][sS] | [yY])
|
||||
echo -e "${RedBG} 开始卸载... ${Font}"
|
||||
info "${RedBG}开始卸载...${Font}"
|
||||
;;
|
||||
*)
|
||||
echo -e "${GreenBG} 终止卸载。 ${Font}"
|
||||
info "${GreenBG}终止卸载。${Font}"
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
@@ -352,7 +426,7 @@ if [ $# -gt 0 ]; then
|
||||
rm -rf "./docker/mysql/data"
|
||||
rm -rf "./docker/log/supervisor"
|
||||
find "./storage/logs" -name "*.log" | xargs rm -rf
|
||||
echo -e "${OK} ${GreenBG} 卸载完成 ${Font}"
|
||||
success "卸载完成"
|
||||
elif [[ "$1" == "reinstall" ]]; then
|
||||
shift 1
|
||||
./cmd uninstall $@
|
||||
@@ -362,20 +436,20 @@ if [ $# -gt 0 ]; then
|
||||
shift 1
|
||||
env_set APP_PORT "$1"
|
||||
$COMPOSE up -d
|
||||
echo -e "${OK} ${GreenBG} 修改成功 ${Font}"
|
||||
echo -e "地址: http://${GreenBG}127.0.0.1:$(env_get APP_PORT)${Font}"
|
||||
success "修改成功"
|
||||
info "地址: http://${GreenBG}127.0.0.1:$(env_get APP_PORT)${Font}"
|
||||
elif [[ "$1" == "url" ]]; then
|
||||
shift 1
|
||||
env_set APP_URL "$1"
|
||||
restart_php
|
||||
echo -e "${OK} ${GreenBG} 修改成功 ${Font}"
|
||||
success "修改成功"
|
||||
elif [[ "$1" == "env" ]]; then
|
||||
shift 1
|
||||
if [ -n "$1" ]; then
|
||||
env_set $1 "$2"
|
||||
fi
|
||||
restart_php
|
||||
echo -e "${OK} ${GreenBG} 修改成功 ${Font}"
|
||||
success "修改成功"
|
||||
elif [[ "$1" == "repassword" ]]; then
|
||||
shift 1
|
||||
run_exec mariadb "sh /etc/mysql/repassword.sh \"$@\""
|
||||
@@ -391,6 +465,16 @@ if [ $# -gt 0 ]; then
|
||||
elif [[ "$1" == "electron" ]]; then
|
||||
shift 1
|
||||
run_electron $@
|
||||
elif [[ "$1" == "eeui" ]]; then
|
||||
shift 1
|
||||
cli="$@"
|
||||
por=""
|
||||
if [[ "$cli" == "build" ]]; then
|
||||
cli="build --simple"
|
||||
elif [[ "$cli" == "dev" ]]; then
|
||||
por="-p 8880:8880"
|
||||
fi
|
||||
docker run -it --rm -v ${cur_path}/resources/mobile:/work -w /work ${por} kuaifan/eeui-cli:0.0.1 eeui ${cli}
|
||||
elif [[ "$1" == "doc" ]]; then
|
||||
shift 1
|
||||
run_exec php "php app/Http/Controllers/Api/apidoc.php"
|
||||
@@ -398,13 +482,15 @@ if [ $# -gt 0 ]; then
|
||||
elif [[ "$1" == "debug" ]]; then
|
||||
shift 1
|
||||
switch_debug "$@"
|
||||
echo "success"
|
||||
info "success"
|
||||
elif [[ "$1" == "https" ]]; then
|
||||
shift 1
|
||||
if [[ "$@" == "auto" ]]; then
|
||||
if [[ "$1" == "agent" ]] || [[ "$1" == "true" ]]; then
|
||||
env_set APP_SCHEME "true"
|
||||
elif [[ "$1" == "close" ]] || [[ "$1" == "auto" ]]; then
|
||||
env_set APP_SCHEME "auto"
|
||||
else
|
||||
env_set APP_SCHEME "true"
|
||||
https_auto
|
||||
fi
|
||||
restart_php
|
||||
elif [[ "$1" == "artisan" ]]; then
|
||||
@@ -421,9 +507,9 @@ if [ $# -gt 0 ]; then
|
||||
e="redis $@" && run_exec redis "$e"
|
||||
elif [[ "$1" == "mysql" ]]; then
|
||||
shift 1
|
||||
if [ "$1" = "backup" ]; then
|
||||
if [[ "$1" == "backup" ]] || [[ "$1" == "b" ]]; then
|
||||
run_mysql backup
|
||||
elif [ "$1" = "recovery" ]; then
|
||||
elif [[ "$1" == "recovery" ]] || [[ "$1" == "r" ]]; then
|
||||
run_mysql recovery
|
||||
else
|
||||
e="mysql $@" && run_exec mariadb "$e"
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"ext-libxml": "*",
|
||||
"ext-openssl": "*",
|
||||
"ext-simplexml": "*",
|
||||
"ext-zip": "*",
|
||||
"directorytree/ldaprecord-laravel": "^2.7",
|
||||
"fideloper/proxy": "^4.4.1",
|
||||
"firebase/php-jwt": "^6.9",
|
||||
|
||||
950
composer.lock
generated
@@ -306,7 +306,7 @@ return [
|
||||
'reload_async' => true,
|
||||
'max_wait_time' => 60,
|
||||
'enable_reuse_port' => true,
|
||||
'enable_coroutine' => false,
|
||||
'enable_coroutine' => true,
|
||||
'upload_tmp_dir' => @is_writable('/dev/shm/') ? '/dev/shm' : '/tmp',
|
||||
'http_compression' => false,
|
||||
],
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateProjectPermissionsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
if (Schema::hasTable('project_permissions'))
|
||||
return;
|
||||
|
||||
Schema::create('project_permissions', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->bigInteger('project_id')->nullable()->default(0)->comment('项目ID');
|
||||
$table->text('permissions')->nullable()->comment('权限');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('project_permissions');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class WebSocketDialogMsgsAddForwardId extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('web_socket_dialog_msgs', function (Blueprint $table) {
|
||||
if (!Schema::hasColumn('web_socket_dialog_msgs', 'forward_id')) {
|
||||
$table->bigInteger('forward_id')->nullable()->default(0)->after('reply_id')->comment('转发ID');
|
||||
$table->bigInteger('forward_num')->nullable()->default(0)->after('forward_id')->comment('被转发多少次');
|
||||
$table->boolean('forward_show')->nullable()->default(1)->after('forward_num')->comment('是否显示转发的来源');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('web_socket_dialog_msgs', function (Blueprint $table) {
|
||||
$table->dropColumn("forward_id");
|
||||
$table->dropColumn("forward_num");
|
||||
$table->dropColumn("forward_show");
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AddIndexSome20231217 extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('project_task_users', function (Blueprint $table) {
|
||||
$table->index('task_id');
|
||||
$table->index('task_pid');
|
||||
});
|
||||
Schema::table('project_tasks', function (Blueprint $table) {
|
||||
$table->index('visibility');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return voidw
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('project_task_users', function (Blueprint $table) {
|
||||
$table->dropIndex(['task_id']);
|
||||
$table->dropIndex(['task_pid']);
|
||||
});
|
||||
Schema::table('project_tasks', function (Blueprint $table) {
|
||||
$table->dropIndex(['visibility']);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class UpdateOwnerAddIndexSome20231217 extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
// 项目相关
|
||||
Schema::table('projects', function (Blueprint $table) {
|
||||
$table->index('userid');
|
||||
$table->index('dialog_id');
|
||||
});
|
||||
Schema::table('project_users', function (Blueprint $table) {
|
||||
$table->index('userid');
|
||||
$table->index('project_id');
|
||||
$table->index(['project_id','userid']);
|
||||
$table->index('owner');
|
||||
$table->integer('owner')->change();
|
||||
});
|
||||
Schema::table('project_tasks', function (Blueprint $table) {
|
||||
$table->index('parent_id');
|
||||
$table->index('dialog_id');
|
||||
$table->index('userid');
|
||||
$table->integer('visibility')->change();
|
||||
});
|
||||
Schema::table('project_task_users', function (Blueprint $table) {
|
||||
$table->index(['task_id','userid']);
|
||||
$table->index('owner');
|
||||
$table->integer('owner')->change();
|
||||
});
|
||||
Schema::table('project_task_files', function (Blueprint $table) {
|
||||
$table->index('project_id');
|
||||
$table->index('task_id');
|
||||
});
|
||||
Schema::table('project_task_tags', function (Blueprint $table) {
|
||||
$table->index('project_id');
|
||||
$table->index('task_id');
|
||||
});
|
||||
Schema::table('project_task_contents', function (Blueprint $table) {
|
||||
$table->index('project_id');
|
||||
$table->index('task_id');
|
||||
});
|
||||
Schema::table('project_task_push_logs', function (Blueprint $table) {
|
||||
$table->index('userid');
|
||||
$table->index('task_id');
|
||||
});
|
||||
Schema::table('project_task_flow_changes', function (Blueprint $table) {
|
||||
$table->index('userid');
|
||||
$table->index('task_id');
|
||||
});
|
||||
|
||||
// 聊天相关
|
||||
Schema::table('web_socket_dialogs', function (Blueprint $table) {
|
||||
$table->index('owner_id');
|
||||
$table->index('link_id');
|
||||
});
|
||||
Schema::table('web_socket_dialog_msgs', function (Blueprint $table) {
|
||||
$table->integer('link')->change();
|
||||
$table->integer('modify')->change();
|
||||
$table->integer('forward_show')->change();
|
||||
});
|
||||
Schema::table('web_socket_dialog_users', function (Blueprint $table) {
|
||||
$table->index('dialog_id');
|
||||
$table->index('userid');
|
||||
$table->integer('mark_unread')->change();
|
||||
$table->integer('silence')->change();
|
||||
$table->integer('important')->change();
|
||||
});
|
||||
Schema::table('web_socket_dialog_msg_todos', function (Blueprint $table) {
|
||||
$table->index('msg_id');
|
||||
$table->index('userid');
|
||||
});
|
||||
Schema::table('web_socket_dialog_msg_reads', function (Blueprint $table) {
|
||||
$table->index('dialog_id');
|
||||
$table->integer('mention')->change();
|
||||
$table->integer('silence')->change();
|
||||
$table->integer('email')->change();
|
||||
$table->integer('after')->change();
|
||||
});
|
||||
|
||||
// 文件相关
|
||||
Schema::table('files', function (Blueprint $table) {
|
||||
$table->index('pid');
|
||||
$table->index('cid');
|
||||
$table->integer('share')->change();
|
||||
});
|
||||
Schema::table('file_users', function (Blueprint $table) {
|
||||
$table->index('file_id');
|
||||
$table->index('userid');
|
||||
$table->integer('permission')->change();
|
||||
});
|
||||
Schema::table('file_links', function (Blueprint $table) {
|
||||
$table->index('file_id');
|
||||
$table->index('userid');
|
||||
});
|
||||
Schema::table('file_contents', function (Blueprint $table) {
|
||||
$table->index('fid');
|
||||
$table->index('userid');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return voidw
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
// 项目相关
|
||||
Schema::table('projects', function (Blueprint $table) {
|
||||
$table->dropIndex(['userid']);
|
||||
$table->dropIndex(['dialog_id']);
|
||||
});
|
||||
Schema::table('project_users', function (Blueprint $table) {
|
||||
$table->dropIndex(['userid']);
|
||||
$table->dropIndex(['project_id']);
|
||||
$table->dropIndex(['owner']);
|
||||
$table->dropIndex(['project_id','userid']);
|
||||
});
|
||||
Schema::table('project_tasks', function (Blueprint $table) {
|
||||
$table->dropIndex(['parent_id']);
|
||||
$table->dropIndex(['dialog_id']);
|
||||
$table->dropIndex(['userid']);
|
||||
});
|
||||
Schema::table('project_task_users', function (Blueprint $table) {
|
||||
$table->dropIndex(['owner']);
|
||||
$table->dropIndex(['task_id','userid']);
|
||||
});
|
||||
Schema::table('project_task_files', function (Blueprint $table) {
|
||||
$table->dropIndex(['project_id']);
|
||||
$table->dropIndex(['task_id']);
|
||||
});
|
||||
Schema::table('project_task_tags', function (Blueprint $table) {
|
||||
$table->dropIndex(['project_id']);
|
||||
$table->dropIndex(['task_id']);
|
||||
});
|
||||
Schema::table('project_task_contents', function (Blueprint $table) {
|
||||
$table->dropIndex(['project_id']);
|
||||
$table->dropIndex(['task_id']);
|
||||
});
|
||||
Schema::table('project_task_push_logs', function (Blueprint $table) {
|
||||
$table->dropIndex(['userid']);
|
||||
$table->dropIndex(['task_id']);
|
||||
});
|
||||
Schema::table('project_task_flow_changes', function (Blueprint $table) {
|
||||
$table->dropIndex(['userid']);
|
||||
$table->dropIndex(['task_id']);
|
||||
});
|
||||
|
||||
// 聊天相关
|
||||
Schema::table('web_socket_dialogs', function (Blueprint $table) {
|
||||
$table->dropIndex(['owner_id']);
|
||||
$table->dropIndex(['link_id']);
|
||||
});
|
||||
Schema::table('web_socket_dialog_users', function (Blueprint $table) {
|
||||
$table->dropIndex(['dialog_id']);
|
||||
$table->dropIndex(['userid']);
|
||||
});
|
||||
Schema::table('web_socket_dialog_msg_todos', function (Blueprint $table) {
|
||||
$table->dropIndex(['msg_id']);
|
||||
$table->dropIndex(['userid']);
|
||||
});
|
||||
Schema::table('web_socket_dialog_msg_reads', function (Blueprint $table) {
|
||||
$table->dropIndex(['dialog_id']);
|
||||
});
|
||||
|
||||
// 文件相关
|
||||
Schema::table('files', function (Blueprint $table) {
|
||||
$table->dropIndex(['pid']);
|
||||
$table->dropIndex(['cid']);
|
||||
});
|
||||
Schema::table('file_users', function (Blueprint $table) {
|
||||
$table->dropIndex(['file_id']);
|
||||
$table->dropIndex(['userid']);
|
||||
});
|
||||
Schema::table('file_links', function (Blueprint $table) {
|
||||
$table->dropIndex(['file_id']);
|
||||
$table->dropIndex(['userid']);
|
||||
});
|
||||
Schema::table('file_contents', function (Blueprint $table) {
|
||||
$table->dropIndex(['fid']);
|
||||
$table->dropIndex(['userid']);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AddWebSocketDialogMsgsTopAt extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('web_socket_dialog_msgs', function (Blueprint $table) {
|
||||
if (!Schema::hasColumn('web_socket_dialog_msgs', 'top')) {
|
||||
$table->bigInteger('top')->nullable()->default(0)->after('send')->comment('置顶的会员ID');
|
||||
$table->timestamp('top_at')->nullable()->after('top')->comment('置顶时间');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('web_socket_dialog_msgs', function (Blueprint $table) {
|
||||
$table->dropColumn("top");
|
||||
$table->dropColumn("top_at");
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateProjectTaskVisibilityUsersTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('project_task_visibility_users', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->bigInteger('project_id')->index()->nullable()->default(0)->comment('项目ID');
|
||||
$table->bigInteger('task_id')->index()->nullable()->default(0)->comment('任务ID');
|
||||
$table->bigInteger('userid')->index()->nullable()->default(0)->comment('成员ID');
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
// 迁移旧数据
|
||||
DB::table('project_task_users')->where("owner",2)->orderBy('id')->chunk(100, function ($data) {
|
||||
foreach ($data as $item) {
|
||||
DB::table('project_task_visibility_users')->insert([
|
||||
'project_id' => $item->project_id,
|
||||
'task_id' => $item->task_id,
|
||||
'userid' => $item->userid,
|
||||
'created_at' => $item->created_at,
|
||||
'updated_at' => $item->updated_at
|
||||
]);
|
||||
}
|
||||
});
|
||||
DB::table('project_task_users')->where("owner",2)->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('project_task_visibility_users');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AddWebSocketDialogsTop extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('web_socket_dialog_msgs', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('web_socket_dialog_msgs', 'top')) {
|
||||
$table->dropColumn("top");
|
||||
$table->dropColumn("top_at");
|
||||
}
|
||||
});
|
||||
//
|
||||
Schema::table('web_socket_dialogs', function (Blueprint $table) {
|
||||
$table->bigInteger('top_userid')->nullable()->default(0)->after('link_id')->comment('置顶的用户ID');
|
||||
$table->bigInteger('top_msg_id')->nullable()->default(0)->after('top_userid')->comment('置顶的消息ID');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('web_socket_dialog_msgs', function (Blueprint $table) {
|
||||
if (!Schema::hasColumn('web_socket_dialog_msgs', 'top')) {
|
||||
$table->bigInteger('top')->nullable()->default(0)->after('send')->comment('置顶的会员ID');
|
||||
$table->timestamp('top_at')->nullable()->after('top')->comment('置顶时间');
|
||||
}
|
||||
});
|
||||
//
|
||||
Schema::table('web_socket_dialogs', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('web_socket_dialogs', 'top_msg_id')) {
|
||||
$table->dropColumn("top_msg_id");
|
||||
}
|
||||
if (Schema::hasColumn('web_socket_dialogs', 'top_userid')) {
|
||||
$table->dropColumn("top_userid");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
@error_reporting(E_ALL & ~E_NOTICE & ~E_WARNING);
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\WebSocketDialog;
|
||||
use App\Module\Base;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class AllGroupMuteHandle extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
$systemConfig = Base::setting('system');
|
||||
if ($systemConfig['all_group_mute'] == 'user' || $systemConfig['all_group_mute'] == 'all') {
|
||||
$systemConfig['all_group_mute'] = 'close';
|
||||
Base::setting('system', $systemConfig);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class ChangeCharsetToUtf8mb4 extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
$pre = DB::connection()->getTablePrefix();
|
||||
$tables = [
|
||||
"approve_execution",
|
||||
"approve_execution_history",
|
||||
"approve_identitylink",
|
||||
"approve_identitylink_history",
|
||||
"approve_proc_inst",
|
||||
"approve_proc_inst_history",
|
||||
"approve_proc_msgs",
|
||||
"approve_procdef",
|
||||
"approve_procdef_history",
|
||||
"approve_task",
|
||||
"approve_task_history"
|
||||
];
|
||||
foreach ($tables as $table) {
|
||||
if (Schema::hasTable($table)) {
|
||||
Schema::getConnection()->getPdo()->exec("ALTER TABLE `{$pre}{$table}` CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AddWebSocketDialogUsersAddHide extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('web_socket_dialog_users', function (Blueprint $table) {
|
||||
if (!Schema::hasColumn('web_socket_dialog_users', 'hide')) {
|
||||
$table->integer('hide')->nullable()->default(0)->after('silence')->comment('不显示会话:0否,1是');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('web_socket_dialog_users', function (Blueprint $table) {
|
||||
$table->dropColumn("hide");
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AddIndexSome20240315 extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('web_socket_dialog_msg_reads', function (Blueprint $table) {
|
||||
$table->index(['dialog_id', 'userid', 'read_at', 'msg_id'], 'IDEX_dialog_id_userid_read_at_msg_id');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return voidw
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('web_socket_dialog_msg_reads', function (Blueprint $table) {
|
||||
$table->dropIndex('IDEX_dialog_id_userid_read_at_msg_id');
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
use App\Models\WebSocketDialog;
|
||||
use App\Models\WebSocketDialogUser;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AddWebSocketDialogUsersLastAt extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
$isAdd = false;
|
||||
Schema::table('web_socket_dialog_users', function (Blueprint $table) use (&$isAdd) {
|
||||
if (!Schema::hasColumn('web_socket_dialog_users', 'last_at')) {
|
||||
$isAdd = true;
|
||||
$table->timestamp('last_at')->nullable()->after('top_at')->comment('最后消息时间');
|
||||
}
|
||||
});
|
||||
if ($isAdd) {
|
||||
// 更新数据
|
||||
WebSocketDialog::chunk(100, function ($dialogs) {
|
||||
/** @var WebSocketDialog $dialog */
|
||||
foreach ($dialogs as $dialog) {
|
||||
WebSocketDialogUser::whereDialogId($dialog->id)->update(['last_at' => $dialog->last_at]);
|
||||
}
|
||||
});
|
||||
|
||||
// 删除表字段
|
||||
Schema::table('web_socket_dialogs', function (Blueprint $table) {
|
||||
$table->dropColumn("last_at");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
// 回滚数据 - 无法回滚
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AddIndexSome20240317 extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('web_socket_dialog_users', function (Blueprint $table) {
|
||||
$table->dropIndex(['userid']);
|
||||
$table->index(['userid', 'dialog_id']);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return voidw
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
// 回滚数据 - 无法回滚
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
use App\Module\Base;
|
||||
use App\Models\WebSocketDialogMsg;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class ChangeForwardData extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('web_socket_dialog_msgs', function (Blueprint $table) {
|
||||
if (Schema::hasColumn('web_socket_dialog_msgs', 'forward_show')) {
|
||||
WebSocketDialogMsg::where("forward_id", ">", 0)->chunk(100, function ($items) {
|
||||
/** @var WebSocketDialogMsg $item */
|
||||
foreach ($items as $item) {
|
||||
$msg = Base::json2array($item->getRawOriginal('msg'));
|
||||
$msg['forward_data'] = [
|
||||
'id' => $item->forward_id,
|
||||
'userid' => 0,
|
||||
'parent_id' => $item->forward_id,
|
||||
'parent_userid' => 0,
|
||||
'show' => 0,
|
||||
];
|
||||
$original = WebSocketDialogMsg::select(['id', 'userid', 'forward_show'])->whereId($item->forward_id)->withTrashed()->first();
|
||||
if ($original) {
|
||||
$msg['forward_data']['userid'] = $original->userid;
|
||||
$msg['forward_data']['parent_userid'] = $original->userid;
|
||||
$msg['forward_data']['show'] = $original->forward_show;
|
||||
}
|
||||
$item->msg = Base::array2json($msg);
|
||||
$item->save();
|
||||
}
|
||||
});
|
||||
$table->dropColumn("forward_show");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
// 回滚数据 - 无法回滚
|
||||
}
|
||||
}
|
||||
49
database/migrations/2024_03_18_153504_change_reply_data.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
use App\Module\Base;
|
||||
use App\Models\WebSocketDialogMsg;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class ChangeReplyData extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
WebSocketDialogMsg::where("reply_id", ">", 0)->chunk(100, function ($items) {
|
||||
/** @var WebSocketDialogMsg $item */
|
||||
foreach ($items as $item) {
|
||||
$msg = Base::json2array($item->getRawOriginal('msg'));
|
||||
$msg['reply_data'] = [
|
||||
'id' => $item->reply_id,
|
||||
'userid' => 0,
|
||||
'type' => '',
|
||||
'msg' => [],
|
||||
];
|
||||
$original = WebSocketDialogMsg::whereId($item->reply_id)->withTrashed()->first();
|
||||
if ($original) {
|
||||
$replyMsg = Base::json2array($original->getRawOriginal('msg'));
|
||||
unset($replyMsg['reply_data']);
|
||||
$msg['reply_data']['userid'] = $original->userid;
|
||||
$msg['reply_data']['type'] = $original->type;
|
||||
$msg['reply_data']['msg'] = $replyMsg;
|
||||
}
|
||||
$item->msg = Base::array2json($msg);
|
||||
$item->save();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
// 回滚数据 - 无法回滚
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class AddUmengAliasDevice extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
//
|
||||
Schema::table('umeng_alias', function (Blueprint $table) {
|
||||
if (!Schema::hasColumn('umeng_alias', 'device')) {
|
||||
$table->text('ua')->nullable()->after('platform')->comment('userAgent');
|
||||
$table->string('device', 100)->nullable()->default('')->after('platform')->comment('设备类型');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
//
|
||||
Schema::table('umeng_alias', function (Blueprint $table) {
|
||||
$table->dropColumn("ua");
|
||||
$table->dropColumn("device");
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ class WebSocketDialogUsersTableSeeder extends Seeder
|
||||
'id' => 1,
|
||||
'dialog_id' => 1,
|
||||
'userid' => 1,
|
||||
'last_at' => seeders_at('2021-07-01 10:46:37'),
|
||||
'created_at' => seeders_at('2021-07-01 10:46:37'),
|
||||
'updated_at' => seeders_at('2021-07-01 10:46:37'),
|
||||
),
|
||||
@@ -34,6 +35,7 @@ class WebSocketDialogUsersTableSeeder extends Seeder
|
||||
'id' => 2,
|
||||
'dialog_id' => 1,
|
||||
'userid' => 2,
|
||||
'last_at' => seeders_at('2021-07-01 10:46:37'),
|
||||
'created_at' => seeders_at('2021-07-01 10:46:37'),
|
||||
'updated_at' => seeders_at('2021-07-01 10:46:37'),
|
||||
),
|
||||
@@ -42,6 +44,7 @@ class WebSocketDialogUsersTableSeeder extends Seeder
|
||||
'id' => 3,
|
||||
'dialog_id' => 2,
|
||||
'userid' => 1,
|
||||
'last_at' => seeders_at('2021-07-01 10:46:47'),
|
||||
'created_at' => seeders_at('2021-07-01 10:46:47'),
|
||||
'updated_at' => seeders_at('2021-07-01 10:46:47'),
|
||||
),
|
||||
@@ -50,6 +53,7 @@ class WebSocketDialogUsersTableSeeder extends Seeder
|
||||
'id' => 4,
|
||||
'dialog_id' => 3,
|
||||
'userid' => 1,
|
||||
'last_at' => seeders_at('2021-07-01 10:46:47'),
|
||||
'created_at' => seeders_at('2021-07-01 10:46:47'),
|
||||
'updated_at' => seeders_at('2021-07-01 10:46:47'),
|
||||
),
|
||||
@@ -58,6 +62,7 @@ class WebSocketDialogUsersTableSeeder extends Seeder
|
||||
'id' => 5,
|
||||
'dialog_id' => 4,
|
||||
'userid' => 1,
|
||||
'last_at' => seeders_at('2021-07-01 10:47:45'),
|
||||
'created_at' => seeders_at('2021-07-01 10:47:45'),
|
||||
'updated_at' => seeders_at('2021-07-01 10:47:45'),
|
||||
),
|
||||
@@ -66,6 +71,7 @@ class WebSocketDialogUsersTableSeeder extends Seeder
|
||||
'id' => 6,
|
||||
'dialog_id' => 5,
|
||||
'userid' => 1,
|
||||
'last_at' => seeders_at('2021-07-01 10:47:45'),
|
||||
'created_at' => seeders_at('2021-07-01 10:47:45'),
|
||||
'updated_at' => seeders_at('2021-07-01 10:47:45'),
|
||||
),
|
||||
@@ -74,6 +80,7 @@ class WebSocketDialogUsersTableSeeder extends Seeder
|
||||
'id' => 7,
|
||||
'dialog_id' => 6,
|
||||
'userid' => 1,
|
||||
'last_at' => seeders_at('2021-07-01 11:02:57'),
|
||||
'created_at' => seeders_at('2021-07-01 11:02:57'),
|
||||
'updated_at' => seeders_at('2021-07-01 11:02:57'),
|
||||
),
|
||||
@@ -82,6 +89,7 @@ class WebSocketDialogUsersTableSeeder extends Seeder
|
||||
'id' => 8,
|
||||
'dialog_id' => 7,
|
||||
'userid' => 1,
|
||||
'last_at' => seeders_at('2021-07-01 11:02:57'),
|
||||
'created_at' => seeders_at('2021-07-01 11:02:57'),
|
||||
'updated_at' => seeders_at('2021-07-01 11:02:57'),
|
||||
),
|
||||
@@ -90,6 +98,7 @@ class WebSocketDialogUsersTableSeeder extends Seeder
|
||||
'id' => 9,
|
||||
'dialog_id' => 8,
|
||||
'userid' => 1,
|
||||
'last_at' => seeders_at('2021-07-01 11:43:01'),
|
||||
'created_at' => seeders_at('2021-07-01 11:43:01'),
|
||||
'updated_at' => seeders_at('2021-07-01 11:43:01'),
|
||||
),
|
||||
@@ -98,6 +107,7 @@ class WebSocketDialogUsersTableSeeder extends Seeder
|
||||
'id' => 10,
|
||||
'dialog_id' => 9,
|
||||
'userid' => 1,
|
||||
'last_at' => seeders_at('2021-07-01 11:43:01'),
|
||||
'created_at' => seeders_at('2021-07-01 11:43:01'),
|
||||
'updated_at' => seeders_at('2021-07-01 11:43:01'),
|
||||
),
|
||||
@@ -106,6 +116,7 @@ class WebSocketDialogUsersTableSeeder extends Seeder
|
||||
'id' => 11,
|
||||
'dialog_id' => 10,
|
||||
'userid' => 1,
|
||||
'last_at' => seeders_at('2021-07-01 15:33:23'),
|
||||
'created_at' => seeders_at('2021-07-01 15:33:23'),
|
||||
'updated_at' => seeders_at('2021-07-01 15:33:23'),
|
||||
),
|
||||
@@ -114,6 +125,7 @@ class WebSocketDialogUsersTableSeeder extends Seeder
|
||||
'id' => 12,
|
||||
'dialog_id' => 11,
|
||||
'userid' => 1,
|
||||
'last_at' => seeders_at('2021-07-01 15:33:23'),
|
||||
'created_at' => seeders_at('2021-07-01 15:33:23'),
|
||||
'updated_at' => seeders_at('2021-07-01 15:33:23'),
|
||||
),
|
||||
@@ -122,6 +134,7 @@ class WebSocketDialogUsersTableSeeder extends Seeder
|
||||
'id' => 13,
|
||||
'dialog_id' => 12,
|
||||
'userid' => 1,
|
||||
'last_at' => seeders_at('2021-07-01 15:37:06'),
|
||||
'created_at' => seeders_at('2021-07-01 15:37:06'),
|
||||
'updated_at' => seeders_at('2021-07-01 15:37:06'),
|
||||
),
|
||||
@@ -130,6 +143,7 @@ class WebSocketDialogUsersTableSeeder extends Seeder
|
||||
'id' => 14,
|
||||
'dialog_id' => 13,
|
||||
'userid' => 1,
|
||||
'last_at' => seeders_at('2021-07-01 15:37:06'),
|
||||
'created_at' => seeders_at('2021-07-01 15:37:06'),
|
||||
'updated_at' => seeders_at('2021-07-01 15:37:06'),
|
||||
),
|
||||
@@ -138,6 +152,7 @@ class WebSocketDialogUsersTableSeeder extends Seeder
|
||||
'id' => 15,
|
||||
'dialog_id' => 14,
|
||||
'userid' => 1,
|
||||
'last_at' => seeders_at('2021-07-01 16:15:28'),
|
||||
'created_at' => seeders_at('2021-07-01 16:15:28'),
|
||||
'updated_at' => seeders_at('2021-07-01 16:15:28'),
|
||||
),
|
||||
@@ -146,6 +161,7 @@ class WebSocketDialogUsersTableSeeder extends Seeder
|
||||
'id' => 16,
|
||||
'dialog_id' => 15,
|
||||
'userid' => 1,
|
||||
'last_at' => seeders_at('2021-07-01 16:15:28'),
|
||||
'created_at' => seeders_at('2021-07-01 16:15:28'),
|
||||
'updated_at' => seeders_at('2021-07-01 16:15:28'),
|
||||
),
|
||||
@@ -154,6 +170,7 @@ class WebSocketDialogUsersTableSeeder extends Seeder
|
||||
'id' => 17,
|
||||
'dialog_id' => 7,
|
||||
'userid' => 2,
|
||||
'last_at' => seeders_at('2021-07-01 16:22:42'),
|
||||
'created_at' => seeders_at('2021-07-01 16:22:42'),
|
||||
'updated_at' => seeders_at('2021-07-01 16:22:42'),
|
||||
),
|
||||
@@ -162,6 +179,7 @@ class WebSocketDialogUsersTableSeeder extends Seeder
|
||||
'id' => 18,
|
||||
'dialog_id' => 5,
|
||||
'userid' => 2,
|
||||
'last_at' => seeders_at('2021-07-01 16:23:15'),
|
||||
'created_at' => seeders_at('2021-07-01 16:23:15'),
|
||||
'updated_at' => seeders_at('2021-07-01 16:23:15'),
|
||||
),
|
||||
@@ -170,6 +188,7 @@ class WebSocketDialogUsersTableSeeder extends Seeder
|
||||
'id' => 19,
|
||||
'dialog_id' => 15,
|
||||
'userid' => 2,
|
||||
'last_at' => seeders_at('2021-07-01 16:23:40'),
|
||||
'created_at' => seeders_at('2021-07-01 16:23:40'),
|
||||
'updated_at' => seeders_at('2021-07-01 16:23:40'),
|
||||
),
|
||||
@@ -178,6 +197,7 @@ class WebSocketDialogUsersTableSeeder extends Seeder
|
||||
'id' => 20,
|
||||
'dialog_id' => 11,
|
||||
'userid' => 2,
|
||||
'last_at' => seeders_at('2021-07-01 16:29:38'),
|
||||
'created_at' => seeders_at('2021-07-01 16:29:38'),
|
||||
'updated_at' => seeders_at('2021-07-01 16:29:38'),
|
||||
),
|
||||
@@ -186,6 +206,7 @@ class WebSocketDialogUsersTableSeeder extends Seeder
|
||||
'id' => 21,
|
||||
'dialog_id' => 16,
|
||||
'userid' => 2,
|
||||
'last_at' => seeders_at('2021-07-01 16:56:05'),
|
||||
'created_at' => seeders_at('2021-07-01 16:56:05'),
|
||||
'updated_at' => seeders_at('2021-07-01 16:56:05'),
|
||||
),
|
||||
@@ -194,6 +215,7 @@ class WebSocketDialogUsersTableSeeder extends Seeder
|
||||
'id' => 22,
|
||||
'dialog_id' => 16,
|
||||
'userid' => 1,
|
||||
'last_at' => seeders_at('2021-07-01 16:56:05'),
|
||||
'created_at' => seeders_at('2021-07-01 16:56:05'),
|
||||
'updated_at' => seeders_at('2021-07-01 16:56:05'),
|
||||
),
|
||||
@@ -202,6 +224,7 @@ class WebSocketDialogUsersTableSeeder extends Seeder
|
||||
'id' => 23,
|
||||
'dialog_id' => 17,
|
||||
'userid' => 2,
|
||||
'last_at' => seeders_at('2021-07-01 16:56:35'),
|
||||
'created_at' => seeders_at('2021-07-01 16:56:35'),
|
||||
'updated_at' => seeders_at('2021-07-01 16:56:35'),
|
||||
),
|
||||
@@ -210,6 +233,7 @@ class WebSocketDialogUsersTableSeeder extends Seeder
|
||||
'id' => 24,
|
||||
'dialog_id' => 17,
|
||||
'userid' => 1,
|
||||
'last_at' => seeders_at('2021-07-01 16:56:35'),
|
||||
'created_at' => seeders_at('2021-07-01 16:56:35'),
|
||||
'updated_at' => seeders_at('2021-07-01 16:56:35'),
|
||||
),
|
||||
@@ -218,6 +242,7 @@ class WebSocketDialogUsersTableSeeder extends Seeder
|
||||
'id' => 25,
|
||||
'dialog_id' => 18,
|
||||
'userid' => 2,
|
||||
'last_at' => seeders_at('2021-07-01 16:56:57'),
|
||||
'created_at' => seeders_at('2021-07-01 16:56:57'),
|
||||
'updated_at' => seeders_at('2021-07-01 16:56:57'),
|
||||
),
|
||||
@@ -226,6 +251,7 @@ class WebSocketDialogUsersTableSeeder extends Seeder
|
||||
'id' => 26,
|
||||
'dialog_id' => 18,
|
||||
'userid' => 1,
|
||||
'last_at' => seeders_at('2021-07-01 16:56:57'),
|
||||
'created_at' => seeders_at('2021-07-01 16:56:57'),
|
||||
'updated_at' => seeders_at('2021-07-01 16:56:57'),
|
||||
),
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace Database\Seeders;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\WebSocketDialog;
|
||||
use App\Models\WebSocketDialogUser;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
@@ -30,7 +31,6 @@ class WebSocketDialogsTableSeeder extends Seeder
|
||||
'type' => 'user',
|
||||
'group_type' => '',
|
||||
'name' => '',
|
||||
'last_at' => seeders_at('2021-07-01 17:12:52'),
|
||||
'created_at' => seeders_at('2021-07-01 10:46:37'),
|
||||
'updated_at' => seeders_at('2021-07-01 17:12:52'),
|
||||
'deleted_at' => NULL,
|
||||
@@ -41,7 +41,6 @@ class WebSocketDialogsTableSeeder extends Seeder
|
||||
'type' => 'group',
|
||||
'group_type' => 'project',
|
||||
'name' => '',
|
||||
'last_at' => NULL,
|
||||
'created_at' => seeders_at('2021-07-01 10:46:47'),
|
||||
'updated_at' => seeders_at('2021-07-01 17:31:03'),
|
||||
'deleted_at' => seeders_at('2021-07-01 17:31:03'),
|
||||
@@ -52,7 +51,6 @@ class WebSocketDialogsTableSeeder extends Seeder
|
||||
'type' => 'group',
|
||||
'group_type' => 'project',
|
||||
'name' => '',
|
||||
'last_at' => NULL,
|
||||
'created_at' => seeders_at('2021-07-01 10:46:47'),
|
||||
'updated_at' => seeders_at('2021-07-01 10:46:55'),
|
||||
'deleted_at' => seeders_at('2021-07-01 10:46:55'),
|
||||
@@ -63,7 +61,6 @@ class WebSocketDialogsTableSeeder extends Seeder
|
||||
'type' => 'group',
|
||||
'group_type' => 'project',
|
||||
'name' => '',
|
||||
'last_at' => NULL,
|
||||
'created_at' => seeders_at('2021-07-01 10:47:45'),
|
||||
'updated_at' => seeders_at('2021-07-01 17:30:05'),
|
||||
'deleted_at' => seeders_at('2021-07-01 17:30:05'),
|
||||
@@ -74,7 +71,6 @@ class WebSocketDialogsTableSeeder extends Seeder
|
||||
'type' => 'group',
|
||||
'group_type' => 'project',
|
||||
'name' => '',
|
||||
'last_at' => seeders_at('2021-07-01 16:31:58'),
|
||||
'created_at' => seeders_at('2021-07-01 10:47:45'),
|
||||
'updated_at' => seeders_at('2021-07-01 16:31:58'),
|
||||
'deleted_at' => NULL,
|
||||
@@ -85,7 +81,6 @@ class WebSocketDialogsTableSeeder extends Seeder
|
||||
'type' => 'group',
|
||||
'group_type' => 'project',
|
||||
'name' => '',
|
||||
'last_at' => NULL,
|
||||
'created_at' => seeders_at('2021-07-01 11:02:57'),
|
||||
'updated_at' => seeders_at('2021-07-01 17:31:28'),
|
||||
'deleted_at' => seeders_at('2021-07-01 17:31:28'),
|
||||
@@ -96,7 +91,6 @@ class WebSocketDialogsTableSeeder extends Seeder
|
||||
'type' => 'group',
|
||||
'group_type' => 'project',
|
||||
'name' => '',
|
||||
'last_at' => seeders_at('2021-07-01 16:32:50'),
|
||||
'created_at' => seeders_at('2021-07-01 11:02:57'),
|
||||
'updated_at' => seeders_at('2021-07-01 16:32:50'),
|
||||
'deleted_at' => NULL,
|
||||
@@ -107,7 +101,6 @@ class WebSocketDialogsTableSeeder extends Seeder
|
||||
'type' => 'group',
|
||||
'group_type' => 'project',
|
||||
'name' => '',
|
||||
'last_at' => NULL,
|
||||
'created_at' => seeders_at('2021-07-01 11:43:01'),
|
||||
'updated_at' => seeders_at('2021-07-01 17:31:28'),
|
||||
'deleted_at' => seeders_at('2021-07-01 17:31:28'),
|
||||
@@ -118,7 +111,6 @@ class WebSocketDialogsTableSeeder extends Seeder
|
||||
'type' => 'group',
|
||||
'group_type' => 'project',
|
||||
'name' => '',
|
||||
'last_at' => seeders_at('2021-07-01 14:02:57'),
|
||||
'created_at' => seeders_at('2021-07-01 11:43:01'),
|
||||
'updated_at' => seeders_at('2021-07-01 14:02:57'),
|
||||
'deleted_at' => NULL,
|
||||
@@ -129,7 +121,6 @@ class WebSocketDialogsTableSeeder extends Seeder
|
||||
'type' => 'group',
|
||||
'group_type' => 'project',
|
||||
'name' => '',
|
||||
'last_at' => NULL,
|
||||
'created_at' => seeders_at('2021-07-01 15:33:23'),
|
||||
'updated_at' => seeders_at('2021-07-01 17:31:28'),
|
||||
'deleted_at' => seeders_at('2021-07-01 17:31:28'),
|
||||
@@ -140,7 +131,6 @@ class WebSocketDialogsTableSeeder extends Seeder
|
||||
'type' => 'group',
|
||||
'group_type' => 'project',
|
||||
'name' => '',
|
||||
'last_at' => seeders_at('2021-07-01 16:31:40'),
|
||||
'created_at' => seeders_at('2021-07-01 15:33:23'),
|
||||
'updated_at' => seeders_at('2021-07-01 16:31:40'),
|
||||
'deleted_at' => NULL,
|
||||
@@ -151,7 +141,6 @@ class WebSocketDialogsTableSeeder extends Seeder
|
||||
'type' => 'group',
|
||||
'group_type' => 'project',
|
||||
'name' => '',
|
||||
'last_at' => NULL,
|
||||
'created_at' => seeders_at('2021-07-01 15:37:06'),
|
||||
'updated_at' => seeders_at('2021-07-01 17:31:28'),
|
||||
'deleted_at' => seeders_at('2021-07-01 17:31:28'),
|
||||
@@ -162,7 +151,6 @@ class WebSocketDialogsTableSeeder extends Seeder
|
||||
'type' => 'group',
|
||||
'group_type' => 'project',
|
||||
'name' => '',
|
||||
'last_at' => seeders_at('2021-07-01 15:44:09'),
|
||||
'created_at' => seeders_at('2021-07-01 15:37:06'),
|
||||
'updated_at' => seeders_at('2021-07-01 15:44:09'),
|
||||
'deleted_at' => NULL,
|
||||
@@ -173,7 +161,6 @@ class WebSocketDialogsTableSeeder extends Seeder
|
||||
'type' => 'group',
|
||||
'group_type' => 'project',
|
||||
'name' => '',
|
||||
'last_at' => NULL,
|
||||
'created_at' => seeders_at('2021-07-01 16:15:28'),
|
||||
'updated_at' => seeders_at('2021-07-01 17:31:28'),
|
||||
'deleted_at' => seeders_at('2021-07-01 17:31:28'),
|
||||
@@ -184,7 +171,6 @@ class WebSocketDialogsTableSeeder extends Seeder
|
||||
'type' => 'group',
|
||||
'group_type' => 'project',
|
||||
'name' => '',
|
||||
'last_at' => seeders_at('2021-07-01 16:37:59'),
|
||||
'created_at' => seeders_at('2021-07-01 16:15:28'),
|
||||
'updated_at' => seeders_at('2021-07-01 16:37:59'),
|
||||
'deleted_at' => NULL,
|
||||
@@ -195,7 +181,6 @@ class WebSocketDialogsTableSeeder extends Seeder
|
||||
'type' => 'group',
|
||||
'group_type' => 'task',
|
||||
'name' => '',
|
||||
'last_at' => seeders_at('2021-07-01 16:58:02'),
|
||||
'created_at' => seeders_at('2021-07-01 16:56:05'),
|
||||
'updated_at' => seeders_at('2021-07-01 16:58:02'),
|
||||
'deleted_at' => NULL,
|
||||
@@ -206,7 +191,6 @@ class WebSocketDialogsTableSeeder extends Seeder
|
||||
'type' => 'group',
|
||||
'group_type' => 'task',
|
||||
'name' => '',
|
||||
'last_at' => seeders_at('2021-07-01 16:57:58'),
|
||||
'created_at' => seeders_at('2021-07-01 16:56:35'),
|
||||
'updated_at' => seeders_at('2021-07-01 16:57:58'),
|
||||
'deleted_at' => NULL,
|
||||
@@ -217,7 +201,6 @@ class WebSocketDialogsTableSeeder extends Seeder
|
||||
'type' => 'group',
|
||||
'group_type' => 'task',
|
||||
'name' => '',
|
||||
'last_at' => seeders_at('2021-07-01 16:57:52'),
|
||||
'created_at' => seeders_at('2021-07-01 16:56:57'),
|
||||
'updated_at' => seeders_at('2021-07-01 16:57:52'),
|
||||
'deleted_at' => NULL,
|
||||
@@ -228,8 +211,7 @@ class WebSocketDialogsTableSeeder extends Seeder
|
||||
if ($botUser) {
|
||||
$dialog = WebSocketDialog::checkUserDialog($botUser, 1);
|
||||
if ($dialog) {
|
||||
$dialog->last_at = Carbon::now();
|
||||
$dialog->save();
|
||||
WebSocketDialogUser::whereDialogId($dialog->id)->change(['last_at' => Carbon::now()->subSecond()]);
|
||||
}
|
||||
}
|
||||
User::botGetOrCreate('ai-openai');
|
||||
|
||||
@@ -36,6 +36,7 @@ services:
|
||||
image: "nginx:alpine"
|
||||
ports:
|
||||
- "${APP_PORT}:80"
|
||||
- "${APP_SSL_PORT:-}:443"
|
||||
volumes:
|
||||
- ./docker/nginx:/etc/nginx/conf.d
|
||||
- ./public:/var/www/public
|
||||
@@ -103,7 +104,7 @@ services:
|
||||
|
||||
fileview:
|
||||
container_name: "dootask-fileview-${APP_ID}"
|
||||
image: "kuaifan/fileview:4.2.0-SNAPSHOT-RC22"
|
||||
image: "kuaifan/fileview:4.2.0-SNAPSHOT-RC25"
|
||||
environment:
|
||||
KK_CONTEXT_PATH: "/fileview"
|
||||
KK_OFFICE_PREVIEW_SWITCH_DISABLED: true
|
||||
@@ -150,7 +151,7 @@ services:
|
||||
|
||||
approve:
|
||||
container_name: "dootask-approve-${APP_ID}"
|
||||
image: "kuaifan/dooapprove:0.0.5"
|
||||
image: "kuaifan/dooapprove:0.0.10"
|
||||
environment:
|
||||
TZ: "${TIMEZONE:-PRC}"
|
||||
MYSQL_HOST: "${DB_HOST}"
|
||||
@@ -159,6 +160,8 @@ services:
|
||||
MYSQL_USERNAME: "${DB_USERNAME}"
|
||||
MYSQL_PASSWORD: "${DB_PASSWORD}"
|
||||
MYSQL_Prefix: "${DB_PREFIX}approve_"
|
||||
DEMO_DATA: true
|
||||
KEY: ${APP_KEY}
|
||||
networks:
|
||||
extnetwork:
|
||||
ipv4_address: "${APP_IPPR}.11"
|
||||
@@ -168,7 +171,7 @@ services:
|
||||
|
||||
ai:
|
||||
container_name: "dootask-ai-${APP_ID}"
|
||||
image: "kuaifan/dooai:0.0.8"
|
||||
image: "kuaifan/dooai:0.1.8"
|
||||
networks:
|
||||
extnetwork:
|
||||
ipv4_address: "${APP_IPPR}.12"
|
||||
@@ -176,7 +179,7 @@ services:
|
||||
|
||||
okr:
|
||||
container_name: "dootask-okr-${APP_ID}"
|
||||
image: "kuaifan/doookr:0.0.22"
|
||||
image: "kuaifan/doookr:0.4.2"
|
||||
environment:
|
||||
TZ: "${TIMEZONE:-PRC}"
|
||||
DOO_TASK_URL: "http://${APP_IPPR}.3"
|
||||
@@ -185,6 +188,9 @@ services:
|
||||
MYSQL_DBNAME: "${DB_DATABASE}"
|
||||
MYSQL_USERNAME: "${DB_USERNAME}"
|
||||
MYSQL_PASSWORD: "${DB_PASSWORD}"
|
||||
MYSQL_PREFIX: "${DB_PREFIX}"
|
||||
DEMO_DATA: true
|
||||
KEY: ${APP_KEY}
|
||||
networks:
|
||||
extnetwork:
|
||||
ipv4_address: "${APP_IPPR}.13"
|
||||
|
||||
@@ -12,7 +12,7 @@ default_storage_engine = InnoDB
|
||||
performance_schema_max_table_instances = 400
|
||||
table_definition_cache = 400
|
||||
key_buffer_size = 1024M
|
||||
max_allowed_packet = 10G
|
||||
max_allowed_packet = 4096M
|
||||
table_open_cache = 2048
|
||||
sort_buffer_size = 4096K
|
||||
net_buffer_length = 4K
|
||||
@@ -47,7 +47,7 @@ innodb_write_io_threads = 2
|
||||
|
||||
[mysqldump]
|
||||
quick
|
||||
max_allowed_packet = 500M
|
||||
max_allowed_packet = 4096M
|
||||
|
||||
[mysql]
|
||||
no-auto-rehash
|
||||
|
||||
1
docker/nginx/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
site
|
||||
2
docker/nginx/conf.d/.gitignore
vendored
@@ -1 +1 @@
|
||||
/*.conf
|
||||
!.gitignore
|
||||
|
||||
1
docker/nginx/site/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!.gitignore
|
||||
1
docker/office/resources/require.js
vendored
@@ -52,7 +52,6 @@
|
||||
window._disableDownload = setInterval(function() {
|
||||
var downloadBtn = document.getElementById("slot-hbtn-download")
|
||||
if(downloadBtn) {
|
||||
console.log(1)
|
||||
clearInterval(window._disableDownload);
|
||||
document.getElementById("slot-hbtn-download")?.remove()
|
||||
document.getElementById("fm-btn-download")?.remove()
|
||||
|
||||
39
electron/build.js
vendored
@@ -126,6 +126,16 @@ function genericPublish({url, key, version, output}) {
|
||||
if (err) {
|
||||
console.warn(err)
|
||||
} else {
|
||||
let uploadFileNum = 0;
|
||||
for (const filename of files) {
|
||||
const localFile = path.join(filePath, filename)
|
||||
if (fs.existsSync(localFile)) {
|
||||
const fileStat = fs.statSync(localFile)
|
||||
if (fileStat.isFile()) {
|
||||
uploadFileNum += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
const uploadOras = {}
|
||||
for (const filename of files) {
|
||||
const localFile = path.join(filePath, filename)
|
||||
@@ -135,6 +145,7 @@ function genericPublish({url, key, version, output}) {
|
||||
uploadOras[filename] = ora(`Upload [0%] ${filename}`).start()
|
||||
const formData = new FormData()
|
||||
formData.append("file", fs.createReadStream(localFile));
|
||||
formData.append("file_num", uploadFileNum);
|
||||
await axiosAutoTry({
|
||||
axios: {
|
||||
method: 'post',
|
||||
@@ -203,8 +214,6 @@ function startBuild(data) {
|
||||
fs.writeFileSync(electronDir + "/config.js", "window.systemInfo = " + JSON.stringify(systemInfo), 'utf8');
|
||||
fs.writeFileSync(nativeCachePath, utils.formatUrl(data.url));
|
||||
fs.writeFileSync(devloadCachePath, "", 'utf8');
|
||||
// default (fix "Failed to load resource: net::ERR_FILE_NOT_FOUND" report)
|
||||
fs.writeFileSync(electronDir + "/default", "default", 'utf8');
|
||||
// index.html
|
||||
let manifestFile = path.resolve(electronDir, "manifest.json");
|
||||
if (!fs.existsSync(manifestFile)) {
|
||||
@@ -220,14 +229,16 @@ function startBuild(data) {
|
||||
fs.writeFileSync(indexFile, indexString, 'utf8');
|
||||
//
|
||||
if (data.id === 'app') {
|
||||
const eeuiDir = path.resolve(__dirname, "../resources/mobile");
|
||||
const eeuiCli = "kuaifan/eeui-cli:0.0.1"
|
||||
const publicDir = path.resolve(__dirname, "../resources/mobile/src/public");
|
||||
fse.removeSync(publicDir)
|
||||
fse.copySync(electronDir, publicDir)
|
||||
if (argv[3] === "setting") {
|
||||
child_process.spawnSync("eeui", ["setting"], {stdio: "inherit", cwd: "resources/mobile"});
|
||||
child_process.spawnSync("docker", `run -it --rm -v ${eeuiDir}:/work -w /work ${eeuiCli} eeui setting`.split(" "), {stdio: "inherit", cwd: "resources/mobile"});
|
||||
}
|
||||
if (['setting', 'build'].includes(argv[3])) {
|
||||
child_process.spawnSync("eeui", ["build", "--simple"], {stdio: "inherit", cwd: "resources/mobile"});
|
||||
child_process.spawnSync("docker", `run -it --rm -v ${eeuiDir}:/work -w /work ${eeuiCli} eeui build --simple`.split(" "), {stdio: "inherit", cwd: "resources/mobile"});
|
||||
} else {
|
||||
[
|
||||
path.resolve(publicDir, "../../platforms/ios/eeuiApp/bundlejs/eeui/public"),
|
||||
@@ -262,8 +273,8 @@ function startBuild(data) {
|
||||
econfig.build.nsis.artifactName = appName + "-v${version}-${os}-${arch}.${ext}";
|
||||
// changelog
|
||||
econfig.build.releaseInfo.releaseNotes = changeLog()
|
||||
if (release) {
|
||||
econfig.build.releaseInfo.releaseNotes = econfig.build.releaseInfo.releaseNotes.replace(`## [${config.version}]`, `## [${config.version}-Release]`)
|
||||
if (!release) {
|
||||
econfig.build.releaseInfo.releaseNotes = econfig.build.releaseInfo.releaseNotes.replace(`## [${config.version}]`, `## [${config.version}-Silence]`)
|
||||
}
|
||||
// darwin notarize
|
||||
if (notarize && APPLEID && APPLEIDPASS) {
|
||||
@@ -323,18 +334,20 @@ if (["dev"].includes(argv[2])) {
|
||||
configure: {
|
||||
platform: '',
|
||||
publish: false,
|
||||
release: false,
|
||||
release: true,
|
||||
notarize: false,
|
||||
}
|
||||
}, false, false)
|
||||
} else if (["all"].includes(argv[2])) {
|
||||
} else if (["all", "win", "mac"].includes(argv[2])) {
|
||||
// 自动编译
|
||||
platforms.forEach(platform => {
|
||||
platforms.filter(p => {
|
||||
return argv[2] === "all" || p.indexOf(argv[2]) !== -1
|
||||
}).forEach(platform => {
|
||||
config.app.forEach(data => {
|
||||
data.configure = {
|
||||
platform,
|
||||
publish: true,
|
||||
release: false,
|
||||
release: true,
|
||||
notarize: false,
|
||||
}
|
||||
startBuild(data)
|
||||
@@ -375,11 +388,11 @@ if (["dev"].includes(argv[2])) {
|
||||
name: 'release',
|
||||
message: "选择是否弹出升级提示框",
|
||||
choices: [{
|
||||
name: "No",
|
||||
value: false
|
||||
}, {
|
||||
name: "Yes",
|
||||
value: true
|
||||
}, {
|
||||
name: "No",
|
||||
value: false
|
||||
}]
|
||||
},
|
||||
{
|
||||
|
||||
3
electron/electron-preload.js
vendored
@@ -72,6 +72,7 @@ contextBridge.exposeInMainWorld(
|
||||
contextBridge.exposeInMainWorld(
|
||||
'process', {
|
||||
type: process.type,
|
||||
versions: process.versions
|
||||
versions: process.versions,
|
||||
platform: process.platform,
|
||||
}
|
||||
);
|
||||
|
||||
508
electron/electron.js
vendored
@@ -1,9 +1,11 @@
|
||||
const fs = require('fs')
|
||||
const os = require("os");
|
||||
const path = require('path')
|
||||
const {app, BrowserWindow, ipcMain, dialog, clipboard, nativeImage, shell, Tray, Menu, globalShortcut, Notification} = require('electron')
|
||||
const {app, BrowserWindow, ipcMain, dialog, clipboard, nativeImage, shell, Tray, Menu, globalShortcut, Notification, BrowserView, nativeTheme} = require('electron')
|
||||
const {autoUpdater} = require("electron-updater")
|
||||
const log = require("electron-log");
|
||||
const electronConf = require('electron-config')
|
||||
const userConf = new electronConf()
|
||||
const fsProm = require('fs/promises');
|
||||
const PDFDocument = require('pdf-lib').PDFDocument;
|
||||
const Screenshots = require("electron-screenshots-tool").Screenshots;
|
||||
@@ -17,13 +19,13 @@ const spawn = require("child_process").spawn;
|
||||
const isMac = process.platform === 'darwin'
|
||||
const isWin = process.platform === 'win32'
|
||||
const allowedUrls = /^(?:https?|mailto|tel|callto):/i;
|
||||
const allowedCalls = /^(?:mailto|tel|callto):/i;
|
||||
let enableStoreBkp = true;
|
||||
let dialogOpen = false;
|
||||
let enablePlugins = false;
|
||||
|
||||
let mainWindow = null,
|
||||
mainTray = null,
|
||||
subWindow = [],
|
||||
isReady = false,
|
||||
willQuitApp = false,
|
||||
devloadUrl = "",
|
||||
@@ -32,6 +34,20 @@ let mainWindow = null,
|
||||
let screenshotObj = null,
|
||||
screenshotKey = null;
|
||||
|
||||
let childWindow = [],
|
||||
webTabWindow = null,
|
||||
webTabView = [],
|
||||
webTabHeight = 38;
|
||||
|
||||
let showState = {},
|
||||
onShowWindow = (win) => {
|
||||
if (typeof showState[win.webContents.id] === 'undefined') {
|
||||
showState[win.webContents.id] = true
|
||||
win.setBackgroundColor('rgba(255, 255, 255, 0)')
|
||||
win.show();
|
||||
}
|
||||
}
|
||||
|
||||
if (fs.existsSync(devloadCachePath)) {
|
||||
devloadUrl = fs.readFileSync(devloadCachePath, 'utf8')
|
||||
}
|
||||
@@ -43,6 +59,8 @@ function createMainWindow() {
|
||||
mainWindow = new BrowserWindow({
|
||||
width: 1280,
|
||||
height: 800,
|
||||
minWidth: 360,
|
||||
minHeight: 360,
|
||||
center: true,
|
||||
autoHideMenuBar: true,
|
||||
webPreferences: {
|
||||
@@ -56,19 +74,20 @@ function createMainWindow() {
|
||||
const originalUA = mainWindow.webContents.session.getUserAgent() || mainWindow.webContents.getUserAgent()
|
||||
mainWindow.webContents.setUserAgent(originalUA + " MainTaskWindow/" + process.platform + "/" + os.arch() + "/1.0");
|
||||
mainWindow.webContents.setWindowOpenHandler(({url}) => {
|
||||
openExternal(url)
|
||||
if (allowedCalls.test(url)) {
|
||||
return {action: 'allow'}
|
||||
}
|
||||
utils.onBeforeOpenWindow(mainWindow.webContents, url).then(() => {
|
||||
openExternal(url)
|
||||
})
|
||||
return {action: 'deny'}
|
||||
})
|
||||
electronMenu.webContentsMenu(mainWindow.webContents)
|
||||
|
||||
if (devloadUrl) {
|
||||
mainWindow.loadURL(devloadUrl).then(_ => {
|
||||
|
||||
})
|
||||
mainWindow.loadURL(devloadUrl).then(_ => { }).catch(_ => { })
|
||||
} else {
|
||||
mainWindow.loadFile('./public/index.html').then(_ => {
|
||||
|
||||
})
|
||||
mainWindow.loadFile('./public/index.html').then(_ => { }).catch(_ => { })
|
||||
}
|
||||
|
||||
mainWindow.on('page-title-updated', (event, title) => {
|
||||
@@ -96,7 +115,7 @@ function createMainWindow() {
|
||||
* 创建子窗口
|
||||
* @param args {path, hash, title, titleFixed, force, userAgent, config, webPreferences}
|
||||
*/
|
||||
function createSubWindow(args) {
|
||||
function createChildWindow(args) {
|
||||
if (!args) {
|
||||
return;
|
||||
}
|
||||
@@ -106,7 +125,7 @@ function createSubWindow(args) {
|
||||
}
|
||||
|
||||
let name = args.name || "auto_" + utils.randomString(6);
|
||||
let item = subWindow.find(item => item.name == name);
|
||||
let item = childWindow.find(item => item.name == name);
|
||||
let browser = item ? item.browser : null;
|
||||
if (browser) {
|
||||
browser.focus();
|
||||
@@ -119,7 +138,10 @@ function createSubWindow(args) {
|
||||
browser = new BrowserWindow(Object.assign({
|
||||
width: 1280,
|
||||
height: 800,
|
||||
minWidth: 360,
|
||||
minHeight: 360,
|
||||
center: true,
|
||||
show: false,
|
||||
parent: mainWindow,
|
||||
autoHideMenuBar: true,
|
||||
webPreferences: Object.assign({
|
||||
@@ -146,34 +168,49 @@ function createSubWindow(args) {
|
||||
})
|
||||
|
||||
browser.on('closed', () => {
|
||||
let index = subWindow.findIndex(item => item.name == name);
|
||||
let index = childWindow.findIndex(item => item.name == name);
|
||||
if (index > -1) {
|
||||
subWindow.splice(index, 1)
|
||||
childWindow.splice(index, 1)
|
||||
}
|
||||
})
|
||||
|
||||
subWindow.push({ name, browser })
|
||||
browser.once('ready-to-show', () => {
|
||||
onShowWindow(browser);
|
||||
})
|
||||
|
||||
browser.webContents.once('dom-ready', () => {
|
||||
onShowWindow(browser);
|
||||
})
|
||||
|
||||
childWindow.push({ name, browser })
|
||||
}
|
||||
const originalUA = browser.webContents.session.getUserAgent() || browser.webContents.getUserAgent()
|
||||
browser.webContents.setUserAgent(originalUA + " SubTaskWindow/" + process.platform + "/" + os.arch() + "/1.0" + (args.userAgent ? (" " + args.userAgent) : ""));
|
||||
browser.webContents.setWindowOpenHandler(({url}) => {
|
||||
openExternal(url)
|
||||
if (allowedCalls.test(url)) {
|
||||
return {action: 'allow'}
|
||||
}
|
||||
utils.onBeforeOpenWindow(browser.webContents, url).then(() => {
|
||||
openExternal(url)
|
||||
})
|
||||
return {action: 'deny'}
|
||||
})
|
||||
electronMenu.webContentsMenu(browser.webContents)
|
||||
|
||||
const hash = args.hash || args.path;
|
||||
if (devloadUrl) {
|
||||
browser.loadURL(devloadUrl + '#' + hash).then(_ => {
|
||||
|
||||
})
|
||||
} else {
|
||||
browser.loadFile('./public/index.html', {
|
||||
hash
|
||||
}).then(_ => {
|
||||
|
||||
})
|
||||
if (/^https?:\/\//i.test(hash)) {
|
||||
browser.loadURL(hash).then(_ => { }).catch(_ => { })
|
||||
return;
|
||||
}
|
||||
if (devloadUrl) {
|
||||
browser.loadURL(devloadUrl + '#' + hash).then(_ => { }).catch(_ => { })
|
||||
return;
|
||||
}
|
||||
browser.loadFile('./public/index.html', {
|
||||
hash
|
||||
}).then(_ => {
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -181,7 +218,7 @@ function createSubWindow(args) {
|
||||
* @param browser
|
||||
* @param args
|
||||
*/
|
||||
function updateSubWindow(browser, args) {
|
||||
function updateChildWindow(browser, args) {
|
||||
if (!args) {
|
||||
return;
|
||||
}
|
||||
@@ -193,25 +230,331 @@ function updateSubWindow(browser, args) {
|
||||
const hash = args.hash || args.path;
|
||||
if (hash) {
|
||||
if (devloadUrl) {
|
||||
browser.loadURL(devloadUrl + '#' + hash).then(_ => {
|
||||
|
||||
})
|
||||
browser.loadURL(devloadUrl + '#' + hash).then(_ => { }).catch(_ => { })
|
||||
} else {
|
||||
browser.loadFile('./public/index.html', {
|
||||
hash
|
||||
}).then(_ => {
|
||||
|
||||
})
|
||||
}).then(_ => { }).catch(_ => { })
|
||||
}
|
||||
}
|
||||
if (args.name) {
|
||||
const er = subWindow.find(item => item.browser == browser);
|
||||
const er = childWindow.find(item => item.browser == browser);
|
||||
if (er) {
|
||||
er.name = args.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建内置浏览器
|
||||
* @param args {url, ?}
|
||||
*/
|
||||
function createWebTabWindow(args) {
|
||||
if (!args) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!utils.isJson(args)) {
|
||||
args = {url: args}
|
||||
}
|
||||
|
||||
if (!allowedUrls.test(args.url)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建父级窗口
|
||||
if (!webTabWindow) {
|
||||
let config = Object.assign(args.config || {}, userConf.get('webTabWindow', {}));
|
||||
let webPreferences = args.webPreferences || {};
|
||||
const titleBarOverlay = {
|
||||
height: webTabHeight
|
||||
}
|
||||
if (nativeTheme.shouldUseDarkColors) {
|
||||
titleBarOverlay.color = '#3B3B3D'
|
||||
titleBarOverlay.symbolColor = '#C5C5C5'
|
||||
}
|
||||
webTabWindow = new BrowserWindow(Object.assign({
|
||||
x: mainWindow.getBounds().x + webTabHeight,
|
||||
y: mainWindow.getBounds().y + webTabHeight,
|
||||
width: 1280,
|
||||
height: 800,
|
||||
minWidth: 360,
|
||||
minHeight: 360,
|
||||
center: true,
|
||||
show: false,
|
||||
autoHideMenuBar: true,
|
||||
titleBarStyle: 'hidden',
|
||||
titleBarOverlay,
|
||||
webPreferences: Object.assign({
|
||||
preload: path.join(__dirname, 'electron-preload.js'),
|
||||
webSecurity: true,
|
||||
nodeIntegration: true,
|
||||
contextIsolation: true,
|
||||
nativeWindowOpen: true
|
||||
}, webPreferences),
|
||||
}, config))
|
||||
|
||||
webTabWindow.on('resize', () => {
|
||||
resizeWebTab(0)
|
||||
})
|
||||
|
||||
webTabWindow.on('enter-full-screen', () => {
|
||||
utils.onDispatchEvent(webTabWindow.webContents, {
|
||||
event: 'enter-full-screen',
|
||||
}).then(_ => { })
|
||||
})
|
||||
|
||||
webTabWindow.on('leave-full-screen', () => {
|
||||
utils.onDispatchEvent(webTabWindow.webContents, {
|
||||
event: 'leave-full-screen',
|
||||
}).then(_ => { })
|
||||
})
|
||||
|
||||
webTabWindow.on('close', event => {
|
||||
if (!willQuitApp) {
|
||||
closeWebTab(0)
|
||||
event.preventDefault()
|
||||
} else {
|
||||
userConf.set('webTabWindow', webTabWindow.getBounds())
|
||||
}
|
||||
})
|
||||
|
||||
webTabWindow.on('closed', () => {
|
||||
webTabView.forEach(({view}) => {
|
||||
try {
|
||||
view.webContents.close()
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
})
|
||||
webTabView = []
|
||||
webTabWindow = null
|
||||
})
|
||||
|
||||
webTabWindow.once('ready-to-show', () => {
|
||||
onShowWindow(webTabWindow);
|
||||
})
|
||||
|
||||
webTabWindow.webContents.once('dom-ready', () => {
|
||||
onShowWindow(webTabWindow);
|
||||
})
|
||||
|
||||
webTabWindow.webContents.on('before-input-event', (event, input) => {
|
||||
if (utils.isMetaOrControl(input) && input.key.toLowerCase() === 'r') {
|
||||
reloadWebTab(0)
|
||||
event.preventDefault()
|
||||
} else if (utils.isMetaOrControl(input) && input.shift && input.key.toLowerCase() === 'i') {
|
||||
devToolsWebTab(0)
|
||||
}
|
||||
})
|
||||
|
||||
webTabWindow.loadFile('./render/tabs/index.html', {}).then(_ => {
|
||||
|
||||
})
|
||||
}
|
||||
webTabWindow.focus();
|
||||
|
||||
// 创建子窗口
|
||||
const browserView = new BrowserView({
|
||||
useHTMLTitleAndIcon: true,
|
||||
useLoadingView: true,
|
||||
useErrorView: true,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'electron-preload.js'),
|
||||
}
|
||||
})
|
||||
if (nativeTheme.shouldUseDarkColors) {
|
||||
browserView.setBackgroundColor('#575757')
|
||||
} else {
|
||||
browserView.setBackgroundColor('#FFFFFF')
|
||||
}
|
||||
browserView.setBounds({
|
||||
x: 0,
|
||||
y: webTabHeight,
|
||||
width: webTabWindow.getContentBounds().width || 1280,
|
||||
height: (webTabWindow.getContentBounds().height || 800) - webTabHeight,
|
||||
})
|
||||
browserView.webContents.on('destroyed', () => {
|
||||
closeWebTab(browserView.webContents.id)
|
||||
})
|
||||
browserView.webContents.setWindowOpenHandler(({url}) => {
|
||||
if (allowedCalls.test(url)) {
|
||||
return {action: 'allow'}
|
||||
}
|
||||
createWebTabWindow({url})
|
||||
return {action: 'deny'}
|
||||
})
|
||||
browserView.webContents.on('page-title-updated', (event, title) => {
|
||||
utils.onDispatchEvent(webTabWindow.webContents, {
|
||||
event: 'title',
|
||||
id: browserView.webContents.id,
|
||||
title: title,
|
||||
url: browserView.webContents.getURL(),
|
||||
}).then(_ => { })
|
||||
})
|
||||
browserView.webContents.on('did-fail-load', (event, errorCode, errorDescription, validatedURL, isMainFrame) => {
|
||||
if (!errorDescription) {
|
||||
return
|
||||
}
|
||||
utils.onDispatchEvent(webTabWindow.webContents, {
|
||||
event: 'title',
|
||||
id: browserView.webContents.id,
|
||||
title: errorDescription,
|
||||
url: browserView.webContents.getURL(),
|
||||
}).then(_ => { })
|
||||
})
|
||||
browserView.webContents.on('page-favicon-updated', (event, favicons) => {
|
||||
utils.onDispatchEvent(webTabWindow.webContents, {
|
||||
event: 'favicon',
|
||||
id: browserView.webContents.id,
|
||||
favicons
|
||||
}).then(_ => { })
|
||||
})
|
||||
browserView.webContents.on('did-start-loading', _ => {
|
||||
utils.onDispatchEvent(webTabWindow.webContents, {
|
||||
event: 'start-loading',
|
||||
id: browserView.webContents.id,
|
||||
}).then(_ => { })
|
||||
})
|
||||
browserView.webContents.on('did-stop-loading', _ => {
|
||||
utils.onDispatchEvent(webTabWindow.webContents, {
|
||||
event: 'stop-loading',
|
||||
id: browserView.webContents.id,
|
||||
}).then(_ => { })
|
||||
})
|
||||
browserView.webContents.on('before-input-event', (event, input) => {
|
||||
if (utils.isMetaOrControl(input) && input.key.toLowerCase() === 'r') {
|
||||
browserView.webContents.reload()
|
||||
event.preventDefault()
|
||||
} else if (utils.isMetaOrControl(input) && input.shift && input.key.toLowerCase() === 'i') {
|
||||
browserView.webContents.toggleDevTools()
|
||||
}
|
||||
})
|
||||
browserView.webContents.loadURL(args.url).then(_ => { }).catch(_ => { })
|
||||
|
||||
webTabWindow.addBrowserView(browserView)
|
||||
webTabView.push({
|
||||
id: browserView.webContents.id,
|
||||
view: browserView
|
||||
})
|
||||
|
||||
utils.onDispatchEvent(webTabWindow.webContents, {
|
||||
event: 'create',
|
||||
id: browserView.webContents.id,
|
||||
url: args.url,
|
||||
}).then(_ => { })
|
||||
activateWebTab(browserView.webContents.id)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前内置浏览器标签
|
||||
* @returns {Electron.BrowserView|undefined}
|
||||
*/
|
||||
function currentWebTab() {
|
||||
const views = webTabWindow.getBrowserViews()
|
||||
const view = views.length ? views[views.length - 1] : undefined
|
||||
if (!view) {
|
||||
return undefined
|
||||
}
|
||||
return webTabView.find(item => item.id == view.webContents.id)
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新加载内置浏览器标签
|
||||
* @param id
|
||||
*/
|
||||
function reloadWebTab(id) {
|
||||
const item = id === 0 ? currentWebTab() : webTabView.find(item => item.id == id)
|
||||
if (!item) {
|
||||
return
|
||||
}
|
||||
item.view.webContents.reload()
|
||||
}
|
||||
|
||||
/**
|
||||
* 内置浏览器标签打开开发者工具
|
||||
* @param id
|
||||
*/
|
||||
function devToolsWebTab(id) {
|
||||
const item = id === 0 ? currentWebTab() : webTabView.find(item => item.id == id)
|
||||
if (!item) {
|
||||
return
|
||||
}
|
||||
item.view.webContents.toggleDevTools()
|
||||
}
|
||||
|
||||
/**
|
||||
* 调整内置浏览器标签尺寸
|
||||
* @param id
|
||||
*/
|
||||
function resizeWebTab(id) {
|
||||
const item = id === 0 ? currentWebTab() : webTabView.find(item => item.id == id)
|
||||
if (!item) {
|
||||
return
|
||||
}
|
||||
item.view.setBounds({
|
||||
x: 0,
|
||||
y: webTabHeight,
|
||||
width: webTabWindow.getContentBounds().width || 1280,
|
||||
height: (webTabWindow.getContentBounds().height || 800) - webTabHeight,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换内置浏览器标签
|
||||
* @param id
|
||||
*/
|
||||
function activateWebTab(id) {
|
||||
const item = id === 0 ? currentWebTab() : webTabView.find(item => item.id == id)
|
||||
if (!item) {
|
||||
return
|
||||
}
|
||||
resizeWebTab(item.id)
|
||||
webTabWindow.setTopBrowserView(item.view)
|
||||
item.view.webContents.focus()
|
||||
utils.onDispatchEvent(webTabWindow.webContents, {
|
||||
event: 'switch',
|
||||
id: item.id,
|
||||
}).then(_ => { })
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭内置浏览器标签
|
||||
* @param id
|
||||
*/
|
||||
function closeWebTab(id) {
|
||||
const item = id === 0 ? currentWebTab() : webTabView.find(item => item.id == id)
|
||||
if (!item) {
|
||||
return
|
||||
}
|
||||
if (webTabView.length === 1) {
|
||||
webTabWindow.hide()
|
||||
}
|
||||
webTabWindow.removeBrowserView(item.view)
|
||||
try {
|
||||
item.view.webContents.close()
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
|
||||
const index = webTabView.findIndex(({id}) => item.id == id)
|
||||
if (index > -1) {
|
||||
webTabView.splice(index, 1)
|
||||
}
|
||||
|
||||
utils.onDispatchEvent(webTabWindow.webContents, {
|
||||
event: 'close',
|
||||
id: item.id,
|
||||
}).then(_ => { })
|
||||
|
||||
if (webTabView.length === 0) {
|
||||
userConf.set('webTabWindow', webTabWindow.getBounds())
|
||||
webTabWindow.destroy()
|
||||
} else {
|
||||
activateWebTab(0)
|
||||
}
|
||||
}
|
||||
|
||||
const getTheLock = app.requestSingleInstanceLock()
|
||||
if (!getTheLock) {
|
||||
app.quit()
|
||||
@@ -329,8 +672,8 @@ ipcMain.on('windowQuit', (event) => {
|
||||
* 创建路由窗口
|
||||
* @param args {path, ?}
|
||||
*/
|
||||
ipcMain.on('windowRouter', (event, args) => {
|
||||
createSubWindow(args)
|
||||
ipcMain.on('openChildWindow', (event, args) => {
|
||||
createChildWindow(args)
|
||||
event.returnValue = "ok"
|
||||
})
|
||||
|
||||
@@ -338,9 +681,89 @@ ipcMain.on('windowRouter', (event, args) => {
|
||||
* 更新路由窗口
|
||||
* @param args {?name, ?path} // name: 不是要更改的窗口名,是要把窗口名改成什么, path: 地址
|
||||
*/
|
||||
ipcMain.on('updateChildWindow', (event, args) => {
|
||||
const browser = BrowserWindow.fromWebContents(event.sender);
|
||||
updateChildWindow(browser, args)
|
||||
event.returnValue = "ok"
|
||||
})
|
||||
|
||||
/**
|
||||
* 创建路由窗口(todo 已废弃)
|
||||
* @param args {path, ?}
|
||||
*/
|
||||
ipcMain.on('windowRouter', (event, args) => {
|
||||
createChildWindow(args)
|
||||
event.returnValue = "ok"
|
||||
})
|
||||
|
||||
/**
|
||||
* 更新路由窗口(todo 已废弃)
|
||||
* @param args {?name, ?path} // name: 不是要更改的窗口名,是要把窗口名改成什么, path: 地址
|
||||
*/
|
||||
ipcMain.on('updateRouter', (event, args) => {
|
||||
const browser = BrowserWindow.fromWebContents(event.sender);
|
||||
updateSubWindow(browser, args)
|
||||
updateChildWindow(browser, args)
|
||||
event.returnValue = "ok"
|
||||
})
|
||||
|
||||
/**
|
||||
* 内置浏览器 - 打开创建
|
||||
* @param args {url, ?}
|
||||
*/
|
||||
ipcMain.on('openWebTabWindow', (event, args) => {
|
||||
createWebTabWindow(args)
|
||||
event.returnValue = "ok"
|
||||
})
|
||||
|
||||
/**
|
||||
* 内置浏览器 - 激活标签
|
||||
* @param id
|
||||
*/
|
||||
ipcMain.on('webTabActivate', (event, id) => {
|
||||
activateWebTab(id)
|
||||
event.returnValue = "ok"
|
||||
})
|
||||
|
||||
/**
|
||||
* 内置浏览器 - 关闭标签
|
||||
* @param id
|
||||
*/
|
||||
ipcMain.on('webTabClose', (event, id) => {
|
||||
closeWebTab(id)
|
||||
event.returnValue = "ok"
|
||||
})
|
||||
|
||||
/**
|
||||
* 内置浏览器 - 在外部浏览器打开
|
||||
*/
|
||||
ipcMain.on('webTabExternal', (event) => {
|
||||
const item = currentWebTab()
|
||||
if (!item) {
|
||||
return
|
||||
}
|
||||
openExternal(item.view.webContents.getURL())
|
||||
event.returnValue = "ok"
|
||||
})
|
||||
|
||||
/**
|
||||
* 内置浏览器 - 打开开发者工具
|
||||
*/
|
||||
ipcMain.on('webTabOpenDevTools', (event) => {
|
||||
const item = currentWebTab()
|
||||
if (!item) {
|
||||
return
|
||||
}
|
||||
item.view.webContents.openDevTools()
|
||||
event.returnValue = "ok"
|
||||
})
|
||||
|
||||
/**
|
||||
* 内置浏览器 - 销毁所有标签及窗口
|
||||
*/
|
||||
ipcMain.on('webTabDestroyAll', (event) => {
|
||||
if (webTabWindow) {
|
||||
webTabWindow.destroy()
|
||||
}
|
||||
event.returnValue = "ok"
|
||||
})
|
||||
|
||||
@@ -377,8 +800,8 @@ ipcMain.on('windowDestroy', (event) => {
|
||||
/**
|
||||
* 关闭所有子窗口
|
||||
*/
|
||||
ipcMain.on('subWindowCloseAll', (event) => {
|
||||
subWindow.some(({browser}) => {
|
||||
ipcMain.on('childWindowCloseAll', (event) => {
|
||||
childWindow.some(({browser}) => {
|
||||
browser && browser.close()
|
||||
})
|
||||
event.returnValue = "ok"
|
||||
@@ -387,8 +810,8 @@ ipcMain.on('subWindowCloseAll', (event) => {
|
||||
/**
|
||||
* 销毁所有子窗口
|
||||
*/
|
||||
ipcMain.on('subWindowDestroyAll', (event) => {
|
||||
subWindow.some(({browser}) => {
|
||||
ipcMain.on('childWindowDestroyAll', (event) => {
|
||||
childWindow.some(({browser}) => {
|
||||
browser && browser.destroy()
|
||||
})
|
||||
event.returnValue = "ok"
|
||||
@@ -644,6 +1067,9 @@ ipcMain.on('updateCheckAndDownload', (event, args) => {
|
||||
autoUpdater.setFeedURL(args)
|
||||
}
|
||||
autoUpdater.checkForUpdates().then(info => {
|
||||
if (!info) {
|
||||
return
|
||||
}
|
||||
if (utils.compareVersion(config.version, info.updateInfo.version) >= 0) {
|
||||
return
|
||||
}
|
||||
@@ -685,7 +1111,7 @@ ipcMain.on('mainWindowActive', (event) => {
|
||||
ipcMain.on('updateQuitAndInstall', (event) => {
|
||||
event.returnValue = "ok"
|
||||
willQuitApp = true
|
||||
subWindow.some(({browser}) => {
|
||||
childWindow.some(({browser}) => {
|
||||
browser && browser.destroy()
|
||||
})
|
||||
setTimeout(_ => {
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
<!--style-->
|
||||
<link rel="stylesheet" type="text/css" href="./css/iview.css">
|
||||
<link rel="stylesheet" type="text/css" href="./css/loading.css">
|
||||
<script src="./js/loading-theme.js"></script>
|
||||
<script src="./js/jsencrypt.min.js"></script>
|
||||
<script src="./js/scroll-into-view.min.js"></script>
|
||||
<script src="./config.js"></script>
|
||||
@@ -20,7 +21,7 @@
|
||||
|
||||
|
||||
<div id="app">
|
||||
<div class="app-view-loading">
|
||||
<div class="app-view-loading no-dark-mode">
|
||||
<div>
|
||||
<div>PAGE LOADING</div>
|
||||
<span></span>
|
||||
|
||||
28
electron/package.json
Normal file → Executable file
@@ -26,26 +26,27 @@
|
||||
"url": "https://github.com/kuaifan/dootask.git"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron-forge/cli": "^6.4.2",
|
||||
"@electron-forge/maker-deb": "^6.4.2",
|
||||
"@electron-forge/maker-rpm": "^6.4.2",
|
||||
"@electron-forge/maker-squirrel": "^6.4.2",
|
||||
"@electron-forge/maker-zip": "^6.4.2",
|
||||
"dotenv": "^16.0.3",
|
||||
"electron": "^27.0.2",
|
||||
"electron-builder": "^24.6.4",
|
||||
"@electron-forge/cli": "^7.3.0",
|
||||
"@electron-forge/maker-deb": "^7.3.0",
|
||||
"@electron-forge/maker-rpm": "^7.3.0",
|
||||
"@electron-forge/maker-squirrel": "^7.3.0",
|
||||
"@electron-forge/maker-zip": "^7.3.0",
|
||||
"dotenv": "^16.3.1",
|
||||
"electron": "^29.1.0",
|
||||
"electron-builder": "^24.12.0",
|
||||
"electron-notarize": "^1.2.2",
|
||||
"form-data": "^4.0.0",
|
||||
"ora": "^4.1.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.3.4",
|
||||
"axios": "^1.6.2",
|
||||
"crc": "^3.8.0",
|
||||
"electron-log": "^5.0.0",
|
||||
"electron-screenshots-tool": "^1.1.0",
|
||||
"electron-config": "^2.0.0",
|
||||
"electron-log": "^5.1.1",
|
||||
"electron-screenshots-tool": "^1.1.2",
|
||||
"electron-squirrel-startup": "^1.0.0",
|
||||
"electron-updater": "^6.1.4",
|
||||
"fs-extra": "^10.1.0",
|
||||
"electron-updater": "^6.1.8",
|
||||
"fs-extra": "^11.2.0",
|
||||
"pdf-lib": "^1.17.1",
|
||||
"request": "^2.88.2"
|
||||
},
|
||||
@@ -67,6 +68,7 @@
|
||||
"output": "dist"
|
||||
},
|
||||
"files": [
|
||||
"render/**/*",
|
||||
"public/**/*",
|
||||
"electron-menu.js",
|
||||
"electron-preload.js",
|
||||
|
||||
269
electron/render/tabs/assets/css/style.css
vendored
Normal file
@@ -0,0 +1,269 @@
|
||||
:root {
|
||||
--tab-font-family: -apple-system, 'Segoe UI', roboto, oxygen-sans, ubuntu, cantarell, 'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol';
|
||||
--tab-font-size: 12px;
|
||||
--tab-transition: background-color 200ms ease-out, color 200ms ease-out;
|
||||
--tab-cursor: pointer; /* 设置鼠标指针为手型 */
|
||||
--tab-color: #7f8792;
|
||||
--tab-background: #EFF0F4;
|
||||
--tab-active-color: #222529;
|
||||
--tab-active-background: #FFFFFF;
|
||||
--tab-close-color: #9DA3AC;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: 'Roboto', sans-serif;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.nav {
|
||||
font-family: var(--tab-font-family);
|
||||
font-feature-settings: 'clig', 'kern';
|
||||
display: flex;
|
||||
width: 100%;
|
||||
cursor: default;
|
||||
background-color: var(--tab-background);
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
.nav ul {
|
||||
display: flex;
|
||||
height: 30px;
|
||||
margin: 8px 46px 0 0;
|
||||
user-select: none;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.nav ul::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav ul li {
|
||||
display: inline-flex;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
padding: 7px 8px;
|
||||
margin: 0 8px 0 0;
|
||||
min-width: 100px;
|
||||
max-width: 240px;
|
||||
scroll-margin: 12px;
|
||||
font-size: var(--tab-font-size);
|
||||
color: var(--tab-color);
|
||||
cursor: var(--tab-cursor);
|
||||
transition: var(--tab-transition);
|
||||
-webkit-app-region: none;
|
||||
}
|
||||
|
||||
.nav ul li:first-child {
|
||||
margin-left: 8px;
|
||||
border-left: none;
|
||||
}
|
||||
|
||||
|
||||
.nav ul li.active {
|
||||
color: var(--tab-active-color);
|
||||
background: var(--tab-active-background);
|
||||
border-radius: 6px 6px 0 0;
|
||||
}
|
||||
|
||||
.nav ul li.active::before {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: -6px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background-image: url(../image/select_left.png);
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
content: '';
|
||||
}
|
||||
|
||||
.nav ul li.active::after {
|
||||
position: absolute;
|
||||
right: -6px;
|
||||
bottom: 0;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background-image: url(../image/select_right.png);
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
content: '';
|
||||
}
|
||||
|
||||
.nav ul li.active .tab-icon.background {
|
||||
background-image: url(../image/link_normal_selected_icon.png);
|
||||
}
|
||||
|
||||
|
||||
.nav ul li:not(.active)::after {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
width: 1px;
|
||||
height: 16px;
|
||||
background: rgba(0, 0, 0, 0.08);
|
||||
content: '';
|
||||
}
|
||||
|
||||
.nav ul li:not(.active):last-child::after {
|
||||
content: none;
|
||||
}
|
||||
|
||||
/* 浏览器打开 */
|
||||
.browser {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 38px;
|
||||
padding: 0 14px;
|
||||
cursor: pointer;
|
||||
-webkit-app-region: none;
|
||||
}
|
||||
.browser span {
|
||||
display: inline-block;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background-size: cover;
|
||||
background-image: url(../image/link_normal_selected_icon.png);
|
||||
}
|
||||
|
||||
/* 图标 */
|
||||
.tab-icon {
|
||||
display: inline-block;
|
||||
flex-shrink: 0;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.tab-icon.background {
|
||||
background-image: url(../image/link_normal_icon.png);
|
||||
}
|
||||
|
||||
.tab-icon.loading {
|
||||
background-image: none !important;
|
||||
}
|
||||
|
||||
.tab-icon .tab-icon-loading {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: 2px solid #eeeeee;
|
||||
border-bottom-color: #84C56A;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
animation: spin 0.75s linear infinite;
|
||||
}
|
||||
|
||||
.tab-icon:not(.loading) .tab-icon-loading {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-icon img {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% {
|
||||
transform: scale(0.8) rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: scale(0.8) rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* 标题 */
|
||||
.tab-title {
|
||||
display: inline-block;
|
||||
flex: 1;
|
||||
margin-right: 8px;
|
||||
margin-left: 6px;
|
||||
overflow: hidden;
|
||||
line-height: 150%;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 关闭 */
|
||||
.tab-close {
|
||||
display: inline-block;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
margin-right: 2px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.tab-close::after,
|
||||
.tab-close::before {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 50%;
|
||||
transform: translate(50%, -50%) scale(0.9) rotate(45deg);
|
||||
content: "";
|
||||
width: 2px;
|
||||
height: 11px;
|
||||
border-radius: 3px;
|
||||
background-color: var(--tab-close-color);
|
||||
}
|
||||
|
||||
.tab-close::before {
|
||||
transform: translate(50%, -50%) scale(0.9) rotate(-45deg);
|
||||
}
|
||||
|
||||
/* 不同平台样式 */
|
||||
body.win32 .nav ul {
|
||||
margin-left: 8px;
|
||||
margin-right: 186px;
|
||||
}
|
||||
body.win32 .browser {
|
||||
right: 140px;
|
||||
}
|
||||
body.darwin .nav ul {
|
||||
margin-left: 76px;
|
||||
}
|
||||
body.darwin.full-screen .nav ul {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
/* 暗黑模式 */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--tab-color: #C5C5C5;
|
||||
--tab-background: #3B3B3D;
|
||||
--tab-active-color: #E1E1E1;
|
||||
--tab-active-background: #575757;
|
||||
--tab-close-color: #E3E3E3;
|
||||
}
|
||||
.nav ul li.active::before {
|
||||
background-image: url(../image/dark/select_left.png);
|
||||
}
|
||||
|
||||
.nav ul li.active::after {
|
||||
background-image: url(../image/dark/select_right.png);
|
||||
}
|
||||
|
||||
.nav ul li.active .tab-icon.background {
|
||||
background-image: url(../image/dark/link_normal_selected_icon.png);
|
||||
}
|
||||
|
||||
.browser span {
|
||||
background-image: url(../image/dark/link_normal_selected_icon.png);
|
||||
}
|
||||
|
||||
.tab-icon.background {
|
||||
background-image: url(../image/dark/link_normal_icon.png);
|
||||
}
|
||||
}
|
||||
BIN
electron/render/tabs/assets/image/dark/link_normal_icon.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
BIN
electron/render/tabs/assets/image/dark/select_left.png
Normal file
|
After Width: | Height: | Size: 205 B |
BIN
electron/render/tabs/assets/image/dark/select_right.png
Normal file
|
After Width: | Height: | Size: 219 B |
BIN
electron/render/tabs/assets/image/link_normal_icon.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
electron/render/tabs/assets/image/link_normal_selected_icon.png
Normal file
|
After Width: | Height: | Size: 914 B |
BIN
electron/render/tabs/assets/image/select_left.png
Normal file
|
After Width: | Height: | Size: 235 B |
BIN
electron/render/tabs/assets/image/select_right.png
Normal file
|
After Width: | Height: | Size: 236 B |
14
electron/render/tabs/assets/js/vue.global.min.js
vendored
Normal file
176
electron/render/tabs/index.html
Normal file
@@ -0,0 +1,176 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Untitled</title>
|
||||
<link rel="stylesheet" href="./assets/css/style.css">
|
||||
<script src="./assets/js/vue.global.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app" class="app">
|
||||
<div class="nav">
|
||||
<ul>
|
||||
<li v-for="item in tabs" :data-id="item.id" :class="{active: activeId === item.id}" @click="onSwitch(item)">
|
||||
<div v-if="item.state === 'loading'" class="tab-icon loading">
|
||||
<div class="tab-icon-loading"></div>
|
||||
</div>
|
||||
<div v-else class="tab-icon background" :style="iconStyle(item)"></div>
|
||||
<div class="tab-title" :title="item.title">{{tabTitle(item)}}</div>
|
||||
<div class="tab-close" @click.stop="onClose(item)"></div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="tabs.length > 0" class="browser" @click="onBrowser">
|
||||
<span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const App = {
|
||||
data() {
|
||||
return {
|
||||
activeId: 0,
|
||||
tabs: [],
|
||||
|
||||
stopTimer: null,
|
||||
}
|
||||
},
|
||||
beforeCreate() {
|
||||
document.body.classList.add(window.process.platform)
|
||||
},
|
||||
mounted() {
|
||||
window.__onDispatchEvent = (detail) => {
|
||||
const {id, event} = detail
|
||||
switch (event) {
|
||||
case 'create':
|
||||
this.tabs.push(Object.assign({
|
||||
id,
|
||||
title: '',
|
||||
url: '',
|
||||
icon: '',
|
||||
state: 'loading'
|
||||
}, detail))
|
||||
break
|
||||
|
||||
case 'close':
|
||||
const closeIndex = this.tabs.findIndex(item => item.id === id)
|
||||
if (closeIndex > -1) {
|
||||
this.tabs.splice(closeIndex, 1)
|
||||
}
|
||||
break
|
||||
|
||||
case 'switch':
|
||||
this.activeId = id
|
||||
this.scrollTabActive()
|
||||
break
|
||||
|
||||
case 'title':
|
||||
if (["HitoseaTask", "DooTask", "about:blank"].includes(detail.title)) {
|
||||
return
|
||||
}
|
||||
const titleItem = this.tabs.find(item => item.id === id)
|
||||
if (titleItem) {
|
||||
titleItem.title = detail.title
|
||||
titleItem.url = detail.url
|
||||
}
|
||||
break
|
||||
|
||||
case 'favicon':
|
||||
const faviconItem = this.tabs.find(item => item.id === id)
|
||||
if (faviconItem) {
|
||||
faviconItem.icon = detail.favicons[detail.favicons.length - 1]
|
||||
}
|
||||
break
|
||||
|
||||
case 'start-loading':
|
||||
const startItem = this.tabs.find(item => item.id === id)
|
||||
if (startItem) {
|
||||
this.stopTimer && clearTimeout(this.stopTimer)
|
||||
startItem.state = 'loading'
|
||||
}
|
||||
break
|
||||
|
||||
case 'stop-loading':
|
||||
this.stopTimer = setTimeout(_ => {
|
||||
const stopItem = this.tabs.find(item => item.id === id)
|
||||
if (stopItem) {
|
||||
stopItem.state = 'loaded'
|
||||
}
|
||||
}, 300)
|
||||
break
|
||||
|
||||
case 'enter-full-screen':
|
||||
document.body.classList.add('full-screen')
|
||||
break
|
||||
|
||||
case 'leave-full-screen':
|
||||
document.body.classList.remove('full-screen')
|
||||
break
|
||||
}
|
||||
}
|
||||
window.__openDevTools = () => {
|
||||
this.sendMessage('webTabOpenDevTools')
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
pageTitle() {
|
||||
const activeItem = this.tabs.find(item => item.id === this.activeId)
|
||||
return activeItem ? activeItem.title : 'Untitled'
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
pageTitle(title) {
|
||||
document.title = title;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onSwitch(item) {
|
||||
this.sendMessage('webTabActivate', item.id)
|
||||
},
|
||||
|
||||
onClose(item) {
|
||||
this.sendMessage('webTabClose', item.id);
|
||||
},
|
||||
|
||||
onBrowser() {
|
||||
this.sendMessage('webTabExternal')
|
||||
},
|
||||
|
||||
iconStyle(item) {
|
||||
return item.icon ? `background-image: url(${item.icon})` : ''
|
||||
},
|
||||
|
||||
tabTitle(item) {
|
||||
if (item.title) {
|
||||
return item.title
|
||||
}
|
||||
if (item.state === 'loading') {
|
||||
return 'Loading...'
|
||||
}
|
||||
if (item.url) {
|
||||
return `${item.url}`.replace(/^https*:\/\//, '')
|
||||
}
|
||||
},
|
||||
|
||||
scrollTabActive() {
|
||||
setTimeout(() => {
|
||||
try {
|
||||
const child = document.querySelector(`.nav ul li[data-id=${this.activeId}]`)
|
||||
if (child) {
|
||||
child.scrollIntoView({behavior: 'smooth', block: 'nearest'})
|
||||
}
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
}, 0)
|
||||
},
|
||||
|
||||
sendMessage(event, args) {
|
||||
electron?.sendMessage(event, args)
|
||||
}
|
||||
},
|
||||
}
|
||||
Vue.createApp(App).mount('#app')
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
49
electron/utils.js
vendored
@@ -313,6 +313,42 @@ module.exports = {
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 新窗口打开事件
|
||||
* @param webContents
|
||||
* @param url
|
||||
* @returns {Promise<unknown>}
|
||||
*/
|
||||
onBeforeOpenWindow(webContents, url) {
|
||||
return new Promise(resolve => {
|
||||
const dataStr = JSON.stringify({url: url})
|
||||
webContents.executeJavaScript(`if(typeof window.__onBeforeOpenWindow === 'function'){window.__onBeforeOpenWindow(${dataStr})}`, true).then(options => {
|
||||
if (options !== true) {
|
||||
resolve()
|
||||
}
|
||||
}).catch(_ => {
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 分发事件
|
||||
* @param webContents
|
||||
* @param data
|
||||
* @returns {Promise<unknown>}
|
||||
*/
|
||||
onDispatchEvent(webContents, data) {
|
||||
return new Promise(resolve => {
|
||||
const dataStr = JSON.stringify(data)
|
||||
webContents.executeJavaScript(`window.__onDispatchEvent(${dataStr})`, true).then(options => {
|
||||
resolve(options)
|
||||
}).catch(_ => {
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 版本比较
|
||||
* @param version1
|
||||
@@ -385,5 +421,18 @@ module.exports = {
|
||||
}
|
||||
callback({responseHeaders: details.responseHeaders});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* win mac meta control
|
||||
* @param input
|
||||
* @returns {boolean | Point | HTMLElement}
|
||||
*/
|
||||
isMetaOrControl(input) {
|
||||
if (process.platform === 'win32') {
|
||||
return input.control
|
||||
} else {
|
||||
return input.meta
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -479,4 +479,15 @@ Api接口文档
|
||||
文件总大小已超过1GB,请分批下载
|
||||
保存任务详情至文件失败
|
||||
保存任务详情至文件失败,请重试
|
||||
移动成功
|
||||
移动成功
|
||||
|
||||
不能重复投票
|
||||
|
||||
系统机器人不存在
|
||||
|
||||
个人会话禁言
|
||||
个人群组禁言
|
||||
|
||||
点击下载
|
||||
|
||||
置顶会话无法隐藏
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
如果必填,发送聊天前必须设置昵称、电话。
|
||||
开启同步本地帐号登录后将同步到 LDAP 服务器
|
||||
必填:发送聊天内容前必须设置昵称、电话。
|
||||
进入路由器终端执行以下命令即可完成安装:
|
||||
进入路由器终端执行以下命令即可完成安装
|
||||
设备连接上指定路由器(WiFi)后自动签到。
|
||||
切换语言需要刷新后生效,是否确定刷新?
|
||||
详细描述,选填...(点击右键使用工具栏)
|
||||
@@ -58,7 +58,7 @@
|
||||
密码错误,请输入正确密码
|
||||
开放:所有人都可以发言。
|
||||
强大易用的协同创作云文档
|
||||
注销前,请确认一下事项:
|
||||
注销前,请确认一下事项
|
||||
签到前后时间收到消息通知
|
||||
签到提醒对象:3天内有签到的成员(法定工作日)
|
||||
该状态下任务自动标记完成
|
||||
@@ -146,7 +146,7 @@
|
||||
选择转发最近聊天
|
||||
邮箱、昵称、职位
|
||||
请输入会议频道ID
|
||||
第二次任务提醒:
|
||||
第二次任务提醒
|
||||
请输入职位/职称
|
||||
不在成员列表内
|
||||
仅支持网页版。
|
||||
@@ -364,7 +364,7 @@ html转markdown
|
||||
验证码错误
|
||||
会议频道ID
|
||||
SMTP服务器
|
||||
任务提醒:
|
||||
任务提醒
|
||||
职位/职称
|
||||
验收/测试
|
||||
上传图片
|
||||
@@ -461,7 +461,7 @@ SMTP服务器
|
||||
导出成员
|
||||
尚未签到
|
||||
工作报告
|
||||
开启后:
|
||||
开启后
|
||||
开启推送
|
||||
开启通知
|
||||
开始会议
|
||||
@@ -642,7 +642,6 @@ LDAP 端口
|
||||
MD编辑器
|
||||
Word 文档
|
||||
导出XLSX
|
||||
APP推送
|
||||
MAC地址
|
||||
扫一扫
|
||||
上个月
|
||||
@@ -1054,7 +1053,8 @@ ID、任务名...
|
||||
语音
|
||||
群头像
|
||||
修改头像
|
||||
没有任何与(*)相关的会话
|
||||
没有任何联系人
|
||||
没有任何与(*)相关的结果
|
||||
未读消息(*)条
|
||||
帐号相关
|
||||
项目相关
|
||||
@@ -1106,7 +1106,8 @@ html转markdown
|
||||
是否将消息中的网络图片保存到本地服务器。
|
||||
保存网络图片
|
||||
无声发送
|
||||
Markdown 格式发送
|
||||
MD 格式发送
|
||||
正常发送
|
||||
编辑消息
|
||||
发送失败
|
||||
再次编辑
|
||||
@@ -1250,8 +1251,9 @@ Markdown 格式发送
|
||||
退出
|
||||
会议组件加载失败!
|
||||
|
||||
OKR管理
|
||||
OKR结果分析
|
||||
OKR 管理
|
||||
OKR 结果
|
||||
OKR 结果分析
|
||||
计划时间冲突提示
|
||||
忽略并继续
|
||||
你确定要清除缓存吗?
|
||||
@@ -1271,10 +1273,10 @@ OKR结果分析
|
||||
审批类型
|
||||
导出类型
|
||||
未完成
|
||||
AI机器人
|
||||
AI 机器人
|
||||
任务相关
|
||||
请填写名称!
|
||||
访问OpenAI网站查看:
|
||||
访问OpenAI网站查看
|
||||
使用代理
|
||||
支持 http 或 socks 代理
|
||||
例如:http://proxy.com 或 socks5://proxy.com
|
||||
@@ -1354,21 +1356,25 @@ AI机器人
|
||||
|
||||
应用
|
||||
机器人设置
|
||||
去聊天
|
||||
开始聊天
|
||||
返回
|
||||
会议设置
|
||||
我是一个人工智能助手,为用户提供问题解答和指导。我没有具体的身份,只是一个程序。您有什么问题可以问我哦?
|
||||
我是由Google开发的生成式人工智能聊天机器人。
|
||||
它基于同名的Gemini系列大型语言模型。
|
||||
是应对OpenAI公司开发的ChatGPT聊天机器人的崛起而开发的。
|
||||
我是Claude,一个由Anthropic公司创造出来的AI助手机器人。我的工作是帮助人类,与人对话并给出解答。
|
||||
我是文心一言,英文名是ERNIE Bot。我能够与人对话互动,回答问题,协助创作,高效便捷地帮助人们获取信息、知识和灵感。
|
||||
我是达摩院自主研发的超大规模语言模型,能够回答问题、创作文字,还能表达观点、撰写代码。
|
||||
机器人暂未开启
|
||||
创建一个全新的会议视频会议,与会者可以在实时中进行面对面的视听交流。通过视频会议平台,参与者可以分享屏幕、共享文档,并与其他与会人员进行讨论和协。
|
||||
创建一个全新的会议视频会议,与会者可以在实时中进行面对面的视听交流。
|
||||
通过视频会议平台,参与者可以分享屏幕、共享文档,并与其他与会人员进行讨论和协。
|
||||
加入视频会议,参与已经创建的会议,在会议过程中与其他参会人员进行远程实时视听交流和协作。
|
||||
新会议
|
||||
新建会议
|
||||
LDAP设置
|
||||
LDAP 设置
|
||||
邮件管理
|
||||
APP推送
|
||||
APP 推送
|
||||
流程设置
|
||||
邮件
|
||||
签到
|
||||
@@ -1390,8 +1396,8 @@ APP推送
|
||||
包含消息发送的文件
|
||||
特殊设置
|
||||
打包列表
|
||||
使用独立的发送按钮
|
||||
开启后,键盘上的发送按钮会被替换成换行
|
||||
发送按钮
|
||||
开启后,发送消息时键盘上的发送按钮会被替换成换行
|
||||
仅我的
|
||||
未领任务提醒
|
||||
开启后每天按设定的提醒时间在项目群聊中发送未领取任务通知。
|
||||
@@ -1400,3 +1406,153 @@ APP推送
|
||||
请输入时长
|
||||
必须大于0
|
||||
忍心拒绝
|
||||
|
||||
邮件通知
|
||||
移动任务
|
||||
请输入备注
|
||||
任务延期
|
||||
修改时间
|
||||
请确认扫码的服务器与当前服务器一致
|
||||
二维码服务器
|
||||
当前服务器
|
||||
你确定取消待办吗?
|
||||
取消成功
|
||||
|
||||
请等待打包完成
|
||||
选择一个项目查看更多任务
|
||||
首页
|
||||
无相关数据
|
||||
当前环境
|
||||
|
||||
权限设置
|
||||
任务列权限
|
||||
添加列
|
||||
修改列
|
||||
删除列
|
||||
排序列
|
||||
任务权限
|
||||
修改任务
|
||||
修改状态
|
||||
移动任务
|
||||
任务协助人
|
||||
搜索项目名称
|
||||
服务器版本过低,请升级服务器。
|
||||
|
||||
不显示原发送者信息
|
||||
转发给
|
||||
留言
|
||||
多选
|
||||
@我的
|
||||
移动前
|
||||
移动后
|
||||
状态
|
||||
协助人
|
||||
未变更移动项
|
||||
接龙
|
||||
参与接龙
|
||||
发起接龙
|
||||
由
|
||||
发起接龙,参与接龙目前共(*)人
|
||||
请输入接龙主题
|
||||
请输入接龙内容
|
||||
可填写接龙格式
|
||||
重复内容将不再计入接龙结果
|
||||
返回编辑
|
||||
继续发送
|
||||
例
|
||||
接龙结果
|
||||
选择群组发起接龙
|
||||
来自
|
||||
发起投票
|
||||
投票结果
|
||||
发起
|
||||
请输入投票主题
|
||||
请输入选项内容
|
||||
允许多选
|
||||
匿名投票
|
||||
投票
|
||||
匿名
|
||||
实名
|
||||
单选
|
||||
多选
|
||||
请选择后投票
|
||||
立即投票
|
||||
票
|
||||
再次发送
|
||||
再次发送投票?
|
||||
结束投票
|
||||
确定结束投票?
|
||||
已发送
|
||||
选择群组发起投票
|
||||
以下为新消息
|
||||
|
||||
|
||||
@我
|
||||
签到打卡
|
||||
在线会议
|
||||
群接龙
|
||||
群投票
|
||||
LDAP
|
||||
License Key
|
||||
你确定要打包下载【(*)】文件夹吗?
|
||||
正在打包,请留意系统消息。
|
||||
发起,参与接龙目前共(*)人
|
||||
|
||||
选择会员
|
||||
请选择移动后状态
|
||||
移动成功
|
||||
置顶人员
|
||||
置顶了
|
||||
你确定取消置顶吗?
|
||||
|
||||
打包下载(*)
|
||||
|
||||
数据已超过(*)条,是否继续加载?
|
||||
版本过低
|
||||
服务器((*))接口版本过低,部分功能可能无法正常使用。
|
||||
|
||||
|
||||
禁言
|
||||
私聊禁言
|
||||
群聊禁言
|
||||
默认不限制
|
||||
开放:所有人都可以在全员群组发言。
|
||||
开放:所有人都可以相互发起个人聊天。
|
||||
开放:允许个人群组聊天发言。
|
||||
禁言:除管理员外所有人都禁止在全员群组发言。
|
||||
禁言:除管理员外所有人都禁止发起个人聊天。
|
||||
除管理员外禁止个人群组聊天发言。
|
||||
注意,仅禁止个人群组,其他类型的群组不禁止,比如:部门群聊、项目群聊等系统群聊。
|
||||
|
||||
浏览器打开
|
||||
|
||||
离最新版本只有一步之遥了!重新启动应用即可完成更新。
|
||||
重新启动
|
||||
|
||||
欢迎使用本软件!
|
||||
在您使用本软件前,请您认真阅读并了解相应的
|
||||
以了解我们的服务内容和您相关个人信息的处理规则。
|
||||
我们将严格的按照隐私服务协议为您提供服务,保护您的个人信息。
|
||||
服务器((*))版本过低
|
||||
|
||||
不显示该会话
|
||||
昨天
|
||||
|
||||
文件已存在
|
||||
文件(*)已存在,是否替换?
|
||||
保留两者
|
||||
替换
|
||||
|
||||
端到端加密
|
||||
关闭端到端加密传输数据。
|
||||
等(*)个文件
|
||||
你确定要删除【(*)】等2个文件吗?
|
||||
审批详情
|
||||
天前
|
||||
月前
|
||||
请输入用户名
|
||||
是否拨打电话给(*)?
|
||||
|
||||
你确定要取消发送吗?
|
||||
消息已发送,不可取消
|
||||
取消发送失败
|
||||
|
||||
18
package.json
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "DooTask",
|
||||
"version": "0.32.9",
|
||||
"version": "0.36.26",
|
||||
"description": "DooTask is task management system.",
|
||||
"scripts": {
|
||||
"start": "./cmd dev",
|
||||
"build": "./cmd prod",
|
||||
"version": "node ./version.js",
|
||||
"version": "node ./bin/version.js",
|
||||
"translate": "./cmd translate"
|
||||
},
|
||||
"author": {
|
||||
@@ -26,7 +26,7 @@
|
||||
"css-loader": "^6.7.2",
|
||||
"dexie": "^3.2.3",
|
||||
"echarts": "^5.2.2",
|
||||
"element-sea": "^2.15.10-6",
|
||||
"element-sea": "^2.15.10-7",
|
||||
"file-loader": "^6.2.0",
|
||||
"highlight.js": "^11.7.0",
|
||||
"inquirer": "^8.2.0",
|
||||
@@ -41,21 +41,21 @@
|
||||
"markdown-it": "^13.0.1",
|
||||
"markdown-it-link-attributes": "^4.0.1",
|
||||
"moment": "^2.29.1",
|
||||
"node-sass": "^6.0.1",
|
||||
"node-sass": "^9.0.0",
|
||||
"notification-koro1": "^1.1.1",
|
||||
"openpgp_hi": "^5.7.0-1",
|
||||
"photoswipe": "^5.2.8",
|
||||
"postcss": "^8.4.5",
|
||||
"quill": "^1.3.7",
|
||||
"quill-mention-hi": "^3.1.0-1",
|
||||
"quill-mention-hi": "^3.1.0-4",
|
||||
"resolve-url-loader": "^4.0.0",
|
||||
"sass": "~1.32.13",
|
||||
"sass-loader": "^12.6.0",
|
||||
"sass": "^1.71.1",
|
||||
"sass-loader": "^14.1.1",
|
||||
"stylus": "^0.59.0",
|
||||
"stylus-loader": "^7.1.0",
|
||||
"tinymce": "^5.10.3",
|
||||
"tui-calendar-hi": "^1.15.1-5",
|
||||
"view-design-hi": "^4.7.0-48",
|
||||
"view-design-hi": "^4.7.0-50",
|
||||
"vite": "^2.9.15",
|
||||
"vite-plugin-file-copy": "^1.0.0",
|
||||
"vite-plugin-require": "^1.1.10",
|
||||
@@ -68,7 +68,7 @@
|
||||
"vue-resize-observer": "^2.0.16",
|
||||
"vue-router": "^3.6.5",
|
||||
"vue-template-compiler": "~2.6.14",
|
||||
"vue-virtual-scroll-list-hi": "^2.3.5-3",
|
||||
"vue-virtual-scroll-list-hi": "^2.3.5-10",
|
||||
"vuedraggable": "^2.24.3",
|
||||
"vuex": "^3.6.2"
|
||||
},
|
||||
|
||||