200 Commits
2.0 ... master

Author SHA1 Message Date
dependabot[bot]
38d52c4609 Bump js-yaml from 3.14.1 to 3.14.2 (#875)
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 3.14.1 to 3.14.2.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/3.14.1...3.14.2)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 3.14.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-11-24 08:47:33 +08:00
dependabot[bot]
2b3f31a204 Bump cipher-base from 1.0.4 to 1.0.6 (#872)
Bumps [cipher-base](https://github.com/crypto-browserify/cipher-base) from 1.0.4 to 1.0.6.
- [Changelog](https://github.com/browserify/cipher-base/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crypto-browserify/cipher-base/compare/v1.0.4...v1.0.6)

---
updated-dependencies:
- dependency-name: cipher-base
  dependency-version: 1.0.6
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-22 07:54:10 +08:00
dependabot[bot]
94332ee597 Bump sha.js from 2.4.11 to 2.4.12 (#873)
Bumps [sha.js](https://github.com/crypto-browserify/sha.js) from 2.4.11 to 2.4.12.
- [Changelog](https://github.com/browserify/sha.js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crypto-browserify/sha.js/compare/v2.4.11...v2.4.12)

---
updated-dependencies:
- dependency-name: sha.js
  dependency-version: 2.4.12
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-22 07:53:58 +08:00
dependabot[bot]
aa6dd044ba Bump form-data from 4.0.1 to 4.0.4 (#870)
Bumps [form-data](https://github.com/form-data/form-data) from 4.0.1 to 4.0.4.
- [Release notes](https://github.com/form-data/form-data/releases)
- [Changelog](https://github.com/form-data/form-data/blob/master/CHANGELOG.md)
- [Commits](https://github.com/form-data/form-data/compare/v4.0.1...v4.0.4)

---
updated-dependencies:
- dependency-name: form-data
  dependency-version: 4.0.4
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-23 08:25:56 +08:00
dependabot[bot]
aa9aa31c42 Bump on-headers and compression (#868)
Bumps [on-headers](https://github.com/jshttp/on-headers) and [compression](https://github.com/expressjs/compression). These dependencies needed to be updated together.

Updates `on-headers` from 1.0.2 to 1.1.0
- [Release notes](https://github.com/jshttp/on-headers/releases)
- [Changelog](https://github.com/jshttp/on-headers/blob/master/HISTORY.md)
- [Commits](https://github.com/jshttp/on-headers/compare/v1.0.2...v1.1.0)

Updates `compression` from 1.7.4 to 1.8.1
- [Release notes](https://github.com/expressjs/compression/releases)
- [Changelog](https://github.com/expressjs/compression/blob/master/HISTORY.md)
- [Commits](https://github.com/expressjs/compression/compare/1.7.4...v1.8.1)

---
updated-dependencies:
- dependency-name: on-headers
  dependency-version: 1.1.0
  dependency-type: indirect
- dependency-name: compression
  dependency-version: 1.8.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-21 11:09:24 +08:00
dependabot[bot]
bbdb1fb39d Bump brace-expansion from 1.1.11 to 1.1.12 (#866)
Bumps [brace-expansion](https://github.com/juliangruber/brace-expansion) from 1.1.11 to 1.1.12.
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/1.1.11...v1.1.12)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 1.1.12
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-02 10:00:46 +08:00
dependabot[bot]
b3f3594cd5 Bump pbkdf2 from 3.1.2 to 3.1.3 (#864)
Bumps [pbkdf2](https://github.com/crypto-browserify/pbkdf2) from 3.1.2 to 3.1.3.
- [Changelog](https://github.com/browserify/pbkdf2/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crypto-browserify/pbkdf2/compare/v3.1.2...v3.1.3)

---
updated-dependencies:
- dependency-name: pbkdf2
  dependency-version: 3.1.3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-29 09:51:03 +08:00
dependabot[bot]
c0504fe62f Bump http-proxy-middleware from 2.0.4 to 2.0.9 (#862) 2025-05-29 17:25:24 +08:00
dependabot[bot]
0bc23c796d Bump axios from 0.28.0 to 1.8.2 (#859) 2025-05-29 17:25:04 +08:00
zhendery
011b291e05 fix(水印): 添加水印后编码图片时漏掉了保存质量参数 (#856)
Co-authored-by: zhendery <zhendery@qq.com>
2025-02-15 02:36:56 +08:00
zhendery
effe173d9c fix(svg): svg也需要“缩略图”,复制一份svg到本地缩略图目录,否则从远程服务器拉取可能会比较慢。 (#852) 2025-01-08 06:33:12 +08:00
zhendery
bdd440d38f fix(图片上传): (小概率)修复图片“格式转换”时不同的图使用同一个临时文件导致变成同一张图的问题 (#851) 2025-01-08 06:32:50 +08:00
zhendery
fe5e4d9a67 feat: 添加svg矢量图的支持 (#833) 2024-12-28 20:47:52 +08:00
熊孝兵
66d7a0dce1 Update README.md 2024-12-28 20:46:24 +08:00
dependabot[bot]
4f13d7d5a1 Bump ip and webpack-dev-server (#848)
Removes [ip](https://github.com/indutny/node-ip). It's no longer used after updating ancestor dependency [webpack-dev-server](https://github.com/webpack/webpack-dev-server). These dependencies need to be updated together.


Removes `ip`

Updates `webpack-dev-server` from 4.7.4 to 4.15.2
- [Release notes](https://github.com/webpack/webpack-dev-server/releases)
- [Changelog](https://github.com/webpack/webpack-dev-server/blob/v4.15.2/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-dev-server/compare/v4.7.4...v4.15.2)

---
updated-dependencies:
- dependency-name: ip
  dependency-type: indirect
- dependency-name: webpack-dev-server
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-28 20:25:19 +08:00
dependabot[bot]
093ef71c2c Bump aws/aws-sdk-php from 3.261.13 to 3.336.6 (#849)
Bumps [aws/aws-sdk-php](https://github.com/aws/aws-sdk-php) from 3.261.13 to 3.336.6.
- [Release notes](https://github.com/aws/aws-sdk-php/releases)
- [Commits](https://github.com/aws/aws-sdk-php/compare/3.261.13...3.336.6)

---
updated-dependencies:
- dependency-name: aws/aws-sdk-php
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-28 20:24:54 +08:00
dependabot[bot]
4a87a90d5b Bump browserify-sign from 4.2.1 to 4.2.3 (#847)
Bumps [browserify-sign](https://github.com/crypto-browserify/browserify-sign) from 4.2.1 to 4.2.3.
- [Changelog](https://github.com/browserify/browserify-sign/blob/main/CHANGELOG.md)
- [Commits](https://github.com/crypto-browserify/browserify-sign/compare/v4.2.1...v4.2.3)

---
updated-dependencies:
- dependency-name: browserify-sign
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-28 20:24:29 +08:00
dependabot[bot]
17cb901046 Bump @babel/traverse from 7.17.3 to 7.26.4 (#846)
Bumps [@babel/traverse](https://github.com/babel/babel/tree/HEAD/packages/babel-traverse) from 7.17.3 to 7.26.4.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.26.4/packages/babel-traverse)

---
updated-dependencies:
- dependency-name: "@babel/traverse"
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-28 20:24:16 +08:00
dependabot[bot]
c0ba47d47d Bump axios from 0.25.0 to 0.28.0 (#845)
Bumps [axios](https://github.com/axios/axios) from 0.25.0 to 0.28.0.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v0.28.0/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v0.25.0...v0.28.0)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-28 20:24:06 +08:00
dependabot[bot]
1d0cb15854 Bump follow-redirects from 1.14.9 to 1.15.9 (#844)
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.14.9 to 1.15.9.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.14.9...v1.15.9)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-28 20:23:51 +08:00
dependabot[bot]
465ff63230 Bump ws from 8.5.0 to 8.18.0 (#843)
Bumps [ws](https://github.com/websockets/ws) from 8.5.0 to 8.18.0.
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.5.0...8.18.0)

---
updated-dependencies:
- dependency-name: ws
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-28 20:23:37 +08:00
dependabot[bot]
04a421d3eb Bump webpack-dev-middleware from 5.3.1 to 5.3.4 (#842)
Bumps [webpack-dev-middleware](https://github.com/webpack/webpack-dev-middleware) from 5.3.1 to 5.3.4.
- [Release notes](https://github.com/webpack/webpack-dev-middleware/releases)
- [Changelog](https://github.com/webpack/webpack-dev-middleware/blob/v5.3.4/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-dev-middleware/compare/v5.3.1...v5.3.4)

---
updated-dependencies:
- dependency-name: webpack-dev-middleware
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-28 20:23:26 +08:00
dependabot[bot]
2642719769 Bump webpack from 5.76.1 to 5.97.1 (#841)
Bumps [webpack](https://github.com/webpack/webpack) from 5.76.1 to 5.97.1.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.76.1...v5.97.1)

---
updated-dependencies:
- dependency-name: webpack
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-28 20:23:14 +08:00
dependabot[bot]
4de97fd91e Bump phpseclib/phpseclib from 3.0.19 to 3.0.43 (#840)
Bumps [phpseclib/phpseclib](https://github.com/phpseclib/phpseclib) from 3.0.19 to 3.0.43.
- [Release notes](https://github.com/phpseclib/phpseclib/releases)
- [Changelog](https://github.com/phpseclib/phpseclib/blob/master/CHANGELOG.md)
- [Commits](https://github.com/phpseclib/phpseclib/compare/3.0.19...3.0.43)

---
updated-dependencies:
- dependency-name: phpseclib/phpseclib
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-28 20:19:17 +08:00
dependabot[bot]
ee93913aac Bump path-to-regexp from 1.8.0 to 1.9.0 (#839)
Bumps [path-to-regexp](https://github.com/pillarjs/path-to-regexp) from 1.8.0 to 1.9.0.
- [Release notes](https://github.com/pillarjs/path-to-regexp/releases)
- [Changelog](https://github.com/pillarjs/path-to-regexp/blob/master/History.md)
- [Commits](https://github.com/pillarjs/path-to-regexp/compare/v1.8.0...v1.9.0)

---
updated-dependencies:
- dependency-name: path-to-regexp
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-28 20:18:59 +08:00
dependabot[bot]
e54c4e551c Bump league/commonmark from 2.3.9 to 2.6.0 (#834)
Bumps [league/commonmark](https://github.com/thephpleague/commonmark) from 2.3.9 to 2.6.0.
- [Release notes](https://github.com/thephpleague/commonmark/releases)
- [Changelog](https://github.com/thephpleague/commonmark/blob/2.6/CHANGELOG.md)
- [Commits](https://github.com/thephpleague/commonmark/compare/2.3.9...2.6.0)

---
updated-dependencies:
- dependency-name: league/commonmark
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-28 20:18:45 +08:00
dependabot[bot]
743409726f Bump send and express (#835)
Bumps [send](https://github.com/pillarjs/send) and [express](https://github.com/expressjs/express). These dependencies needed to be updated together.

Updates `send` from 0.17.2 to 0.19.0
- [Release notes](https://github.com/pillarjs/send/releases)
- [Changelog](https://github.com/pillarjs/send/blob/master/HISTORY.md)
- [Commits](https://github.com/pillarjs/send/compare/0.17.2...0.19.0)

Updates `express` from 4.17.3 to 4.21.2
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/4.21.2/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.17.3...4.21.2)

---
updated-dependencies:
- dependency-name: send
  dependency-type: indirect
- dependency-name: express
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-28 20:18:07 +08:00
dependabot[bot]
2fa82cc231 Bump elliptic from 6.5.4 to 6.6.1 (#836)
Bumps [elliptic](https://github.com/indutny/elliptic) from 6.5.4 to 6.6.1.
- [Commits](https://github.com/indutny/elliptic/compare/v6.5.4...v6.6.1)

---
updated-dependencies:
- dependency-name: elliptic
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-28 20:17:56 +08:00
dependabot[bot]
dd4c40d846 Bump cookie and express (#837)
Bumps [cookie](https://github.com/jshttp/cookie) and [express](https://github.com/expressjs/express). These dependencies needed to be updated together.

Updates `cookie` from 0.4.2 to 0.7.1
- [Release notes](https://github.com/jshttp/cookie/releases)
- [Commits](https://github.com/jshttp/cookie/compare/v0.4.2...v0.7.1)

Updates `express` from 4.17.3 to 4.21.2
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/4.21.2/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.17.3...4.21.2)

---
updated-dependencies:
- dependency-name: cookie
  dependency-type: indirect
- dependency-name: express
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-28 20:17:45 +08:00
dependabot[bot]
8acf78dac4 Bump laravel/framework from 9.52.4 to 9.52.17 (#838)
Bumps [laravel/framework](https://github.com/laravel/framework) from 9.52.4 to 9.52.17.
- [Release notes](https://github.com/laravel/framework/releases)
- [Changelog](https://github.com/laravel/framework/blob/11.x/CHANGELOG.md)
- [Commits](https://github.com/laravel/framework/compare/v9.52.4...v9.52.17)

---
updated-dependencies:
- dependency-name: laravel/framework
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-28 20:17:30 +08:00
dependabot[bot]
eb7c051512 Bump postcss from 8.4.12 to 8.4.31 (#723)
Bumps [postcss](https://github.com/postcss/postcss) from 8.4.12 to 8.4.31.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.4.12...8.4.31)

---
updated-dependencies:
- dependency-name: postcss
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-28 20:14:05 +08:00
dependabot[bot]
5ec90a08a1 Bump semver from 5.7.1 to 5.7.2 (#694)
Bumps [semver](https://github.com/npm/node-semver) from 5.7.1 to 5.7.2.
- [Release notes](https://github.com/npm/node-semver/releases)
- [Changelog](https://github.com/npm/node-semver/blob/v5.7.2/CHANGELOG.md)
- [Commits](https://github.com/npm/node-semver/compare/v5.7.1...v5.7.2)

---
updated-dependencies:
- dependency-name: semver
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-28 20:13:52 +08:00
dependabot[bot]
911275c13b Bump guzzlehttp/psr7 from 2.4.4 to 2.5.0 (#651)
Bumps [guzzlehttp/psr7](https://github.com/guzzle/psr7) from 2.4.4 to 2.5.0.
- [Release notes](https://github.com/guzzle/psr7/releases)
- [Changelog](https://github.com/guzzle/psr7/blob/2.5/CHANGELOG.md)
- [Commits](https://github.com/guzzle/psr7/compare/2.4.4...2.5.0)

---
updated-dependencies:
- dependency-name: guzzlehttp/psr7
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-16 17:55:13 +08:00
dependabot[bot]
704a15896d Bump laminas/laminas-diactoros from 2.24.0 to 2.25.2 (#657)
Bumps [laminas/laminas-diactoros](https://github.com/laminas/laminas-diactoros) from 2.24.0 to 2.25.2.
- [Release notes](https://github.com/laminas/laminas-diactoros/releases)
- [Commits](https://github.com/laminas/laminas-diactoros/compare/2.24.0...2.25.2)

---
updated-dependencies:
- dependency-name: laminas/laminas-diactoros
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-05-16 17:55:03 +08:00
熊孝兵
c80b92b346 后台图片管理支持通过图片路径名称搜索。Fixed #640 2023-03-30 09:49:01 +08:00
熊孝兵
67fc980968 Merge remote-tracking branch 'origin/master' 2023-03-29 11:11:45 +08:00
熊孝兵
05af7dffb8 后台图片管理支持通过图片路径名称搜索。Fixed #640 2023-03-29 11:11:37 +08:00
dependabot[bot]
ee068513c6 Bump webpack from 5.70.0 to 5.76.1 (#633)
Bumps [webpack](https://github.com/webpack/webpack) from 5.70.0 to 5.76.1.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v5.70.0...v5.76.1)

---
updated-dependencies:
- dependency-name: webpack
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-29 11:10:27 +08:00
熊孝兵
fbb022c877 改进 WebDav 以适配多种认证方式。Fixes #497, #520 2023-03-17 08:46:05 +08:00
dependabot[bot]
2b90d5eb75 Bump symfony/http-kernel from 6.0.11 to 6.0.20 (#608)
Bumps [symfony/http-kernel](https://github.com/symfony/http-kernel) from 6.0.11 to 6.0.20.
- [Release notes](https://github.com/symfony/http-kernel/releases)
- [Changelog](https://github.com/symfony/http-kernel/blob/6.2/CHANGELOG.md)
- [Commits](https://github.com/symfony/http-kernel/compare/v6.0.11...v6.0.20)

---
updated-dependencies:
- dependency-name: symfony/http-kernel
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-02-02 14:33:12 +08:00
dependabot[bot]
edb36f8a92 Bump json5 from 1.0.1 to 1.0.2 (#591)
Bumps [json5](https://github.com/json5/json5) from 1.0.1 to 1.0.2.
- [Release notes](https://github.com/json5/json5/releases)
- [Changelog](https://github.com/json5/json5/blob/main/CHANGELOG.md)
- [Commits](https://github.com/json5/json5/compare/v1.0.1...v1.0.2)

---
updated-dependencies:
- dependency-name: json5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-18 14:26:33 +08:00
熊孝兵
a5bbb26ec8 Update README.md 2022-12-07 08:06:09 +08:00
dependabot[bot]
9394e590a8 Bump loader-utils from 1.4.1 to 1.4.2 (#564)
Bumps [loader-utils](https://github.com/webpack/loader-utils) from 1.4.1 to 1.4.2.
- [Release notes](https://github.com/webpack/loader-utils/releases)
- [Changelog](https://github.com/webpack/loader-utils/blob/v1.4.2/CHANGELOG.md)
- [Commits](https://github.com/webpack/loader-utils/compare/v1.4.1...v1.4.2)

---
updated-dependencies:
- dependency-name: loader-utils
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-18 09:26:51 +08:00
dependabot[bot]
0cb84a9662 Bump loader-utils from 1.4.0 to 1.4.1 (#561)
Bumps [loader-utils](https://github.com/webpack/loader-utils) from 1.4.0 to 1.4.1.
- [Release notes](https://github.com/webpack/loader-utils/releases)
- [Changelog](https://github.com/webpack/loader-utils/blob/v1.4.1/CHANGELOG.md)
- [Commits](https://github.com/webpack/loader-utils/compare/v1.4.0...v1.4.1)

---
updated-dependencies:
- dependency-name: loader-utils
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-11-09 10:45:22 +08:00
Wisp X
bb37a11bfb 降低缩略图质量 2022-10-21 10:25:47 +08:00
Jerry
d5230acdb3 修复前端图片水印路径提示错误问题 (#546)
* 修复前端水印上传目录提示错误的问题
e.g. 本应放在 /storage/app/public 目录下
     但前端提示放在 /public 目录下

* 修复编辑角色组中图片水印存储位置提示不正确bug
2022-10-14 09:10:48 +08:00
Wisp X
c66e58353a 修复图片无法重命名的 bug (fixed #536) 2022-10-09 17:46:51 +08:00
Wisp X
6661f35a15 图片处理质量默认值改为 75% 2022-10-09 10:18:52 +08:00
Jerry
5a8eb833be 修复前端水印上传目录提示错误的问题 (#540)
e.g. 本应放在 /storage/app/public 目录下
     但前端提示放在 /public 目录下
2022-09-28 08:41:10 +08:00
Wisp X
1925a1ae7a minio 策略增加 bucket_endpoint 选项配置 2022-09-20 09:01:49 +08:00
Frank Qing
bb9ef6e267 Support minio https url (#534) 2022-09-20 08:23:02 +08:00
Wisp X
923f567e0a 升级拓展 2022-08-17 13:36:19 +08:00
Wisp X
62809f5300 改进升级方式 2022-08-17 13:06:14 +08:00
Wisp X
c46207c278 支持使用角色组控制上传图片的质量于格式 (Closed #415) 2022-08-17 11:35:05 +08:00
Wisp X
289ecffbb1 上传时及时销毁 InterventionImage 实例以节省内存。 2022-08-17 10:48:54 +08:00
Wisp X
2fdb10a60f 改进阿里云 oss client 连接方式 2022-08-17 10:37:53 +08:00
Wisp X
b2b1693a2f 公告支持手动打开 (Closed #504) 2022-08-17 10:28:28 +08:00
Wisp X
b6f5f2405b 修复角色组设置水印模式,无法选择「动态生成」的 bug (Fixed #502) 2022-08-17 10:09:52 +08:00
Wisp X
f49f52dc34 修改接口文档文案 2022-08-17 10:04:21 +08:00
Wisp X
cd1436add2 update packages 2022-08-17 10:00:07 +08:00
Wisp X
aefbbda2bf Merge remote-tracking branch 'origin/master' 2022-08-05 17:49:49 +08:00
Wisp X
768ca9f24a 修复使用原文件名规则时返回了错误链接的 bug.(Fixed #496) 2022-08-05 17:35:10 +08:00
dependabot[bot]
fb9c390bc7 Bump terser from 4.8.0 to 4.8.1 (#488)
Bumps [terser](https://github.com/terser/terser) from 4.8.0 to 4.8.1.
- [Release notes](https://github.com/terser/terser/releases)
- [Changelog](https://github.com/terser/terser/blob/master/CHANGELOG.md)
- [Commits](https://github.com/terser/terser/commits)

---
updated-dependencies:
- dependency-name: terser
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-07-21 08:10:56 +08:00
Wisp X
a4a60f13a0 修复文件名含有特殊字符导致排版异常的 bug.(Fixed #471) 2022-07-11 14:13:46 +08:00
TeacherDu
5071c3a904 fix a bug (#474)
* fix a bug

https://github.com/lsky-org/lsky-pro/issues/473

* Update index.blade.php

Co-authored-by: Wisp X <wisp-x@qq.com>
2022-07-11 08:06:10 +08:00
dependabot[bot]
2c52c87c95 Bump guzzlehttp/guzzle from 7.4.4 to 7.4.5 (#453)
Bumps [guzzlehttp/guzzle](https://github.com/guzzle/guzzle) from 7.4.4 to 7.4.5.
- [Release notes](https://github.com/guzzle/guzzle/releases)
- [Changelog](https://github.com/guzzle/guzzle/blob/master/CHANGELOG.md)
- [Commits](https://github.com/guzzle/guzzle/compare/7.4.4...7.4.5)

---
updated-dependencies:
- dependency-name: guzzlehttp/guzzle
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-06-22 08:31:33 +08:00
Wisp X
3161394a83 改进 dropdown 组件 2022-06-15 10:32:15 +08:00
Wisp X
8f6da67f4f 改进 dropdown 组件 2022-06-15 10:30:53 +08:00
Wisp X
4d3bfed846 Merge remote-tracking branch 'origin/master' 2022-06-15 10:19:00 +08:00
Wisp X
2086daf1ae 改进公告弹窗 Closed #447 2022-06-15 10:17:55 +08:00
dependabot[bot]
a8ab0c6680 Bump guzzlehttp/guzzle from 7.4.3 to 7.4.4 (#445)
Bumps [guzzlehttp/guzzle](https://github.com/guzzle/guzzle) from 7.4.3 to 7.4.4.
- [Release notes](https://github.com/guzzle/guzzle/releases)
- [Changelog](https://github.com/guzzle/guzzle/blob/master/CHANGELOG.md)
- [Commits](https://github.com/guzzle/guzzle/compare/7.4.3...7.4.4)

---
updated-dependencies:
- dependency-name: guzzlehttp/guzzle
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-06-10 14:53:04 +08:00
Wisp X
8b5bf74aa2 修复新用户注册后 IP 为空的 bug. 2022-06-02 08:10:24 +08:00
Wisp X
c0513f85a1 支持设置图片 url 额外参数 Closed #435 2022-05-30 11:14:32 +08:00
dependabot[bot]
8f0db089af Bump guzzlehttp/guzzle from 7.4.2 to 7.4.3 (#434)
Bumps [guzzlehttp/guzzle](https://github.com/guzzle/guzzle) from 7.4.2 to 7.4.3.
- [Release notes](https://github.com/guzzle/guzzle/releases)
- [Changelog](https://github.com/guzzle/guzzle/blob/master/CHANGELOG.md)
- [Commits](https://github.com/guzzle/guzzle/compare/7.4.2...7.4.3)

---
updated-dependencies:
- dependency-name: guzzlehttp/guzzle
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-05-26 06:49:04 +08:00
Wisp X
d705542f44 fix a bug 2022-05-23 17:38:05 +08:00
Wisp X
4266fd60b7 水印支持切换为覆盖原图或动态生成 2022-05-23 17:28:45 +08:00
Wisp X
315b5b8296 改进样式 2022-05-20 11:28:42 +08:00
Wisp X
10699d69f6 fix a bug 2022-05-20 09:09:40 +08:00
Wisp X
58da33d3c9 取消索引创建迁移文件 2022-05-17 16:42:25 +08:00
Wisp X
20871efb5c 改进 2022-05-11 15:27:57 +08:00
Wisp X
912e3747ee fix a bug 2022-05-10 13:22:55 +08:00
Wisp X
fba0d77e7b fix a bug 2022-05-07 09:56:55 +08:00
Wisp X
10dea5cffd fix a bug 2022-05-06 09:30:54 +08:00
Wisp X
f7983a2f21 修复PC端开启平板模式后无法响应触摸事件的问题 Fixed #386 2022-04-29 11:16:50 +08:00
Wisp X
8ab428c593 改进样式 2022-04-29 09:47:17 +08:00
Wisp X
fb5f83f43c push version 2022-04-27 17:34:10 +08:00
Wisp X
73750890d2 build style 2022-04-27 17:32:27 +08:00
Wisp X
3ad7e711be build style 2022-04-27 17:01:50 +08:00
Wisp X
b6fd41841b 更新版本号 2022-04-27 16:59:33 +08:00
Wisp X
b61db2b31a fix a bug 2022-04-27 16:54:45 +08:00
Wisp X
cff42566c8 fix a bug 2022-04-27 16:30:44 +08:00
Wisp X
e44fabbd31 给图片表增加索引 2022-04-27 15:58:58 +08:00
Wisp X
3cdf495bef 给图片表增加索引 2022-04-27 15:53:52 +08:00
Wisp X
6291acbcee 给图片表增加索引 2022-04-27 15:51:53 +08:00
Wisp X
1434d6fa1c 改进 2022-04-27 15:34:41 +08:00
Wisp X
73c55b1a77 修复图片量太大导致内存溢出的 bug 2022-04-27 14:52:48 +08:00
TeacherDu
8780a21fe7 gravatar国内的节点 (#399) 2022-04-24 08:07:58 +08:00
Wisp X
3acabd7b38 fix a bug. 2022-04-21 14:14:34 +08:00
Wisp X
1f826fd652 默认信任所有代理 2022-04-19 17:10:11 +08:00
Wisp X
8d266b3d5a update packages. 2022-04-19 16:42:08 +08:00
Wisp X
02b9695882 修复时间不正确的 bug 2022-04-19 11:13:03 +08:00
Wisp X
5bba6d0d52 移除 alpinejs 控制台警告 2022-04-12 14:37:23 +08:00
Wisp X
52f9c0cbe8 update packages 2022-04-07 14:36:08 +08:00
Wisp X
d2b4910196 删除文件时同步删除缓存 2022-04-07 08:27:52 +08:00
Wisp X
9a5cbb2ea7 fix a bug 2022-04-06 08:45:54 +08:00
Wisp X
b9a3ea2c44 fix a bug 2022-04-06 08:19:51 +08:00
Wisp X
0d826042ad 更新版本号 2022-04-01 17:12:26 +08:00
Wisp X
f9a84c149c 增加一些 tips 2022-04-01 16:44:40 +08:00
Wisp X
798e72f634 适配路径支持设置为空 2022-04-01 16:20:23 +08:00
Wisp X
dec4e6fdb7 修复通过请求删除接口无法删除物理文件的 bug. Closed #366 2022-04-01 16:08:37 +08:00
Wisp X
3cecc20f44 改进首页底部在手机端的样式 2022-03-31 08:30:37 +08:00
Wisp X
86d67450ea 修复站点名称过长导致手机端侧边栏无法收起的 bug 2022-03-30 17:10:43 +08:00
Wisp X
aa9887715a 修复我的图片中添加/修改项目时回车出错的 bug. Closed 363 2022-03-30 16:26:11 +08:00
Wisp X
fc1c2095a6 up 2022-03-30 08:01:15 +08:00
Q-Y-H
a0d216fb2d 路径命名可为空 (#364) 2022-03-30 07:58:23 +08:00
Wisp X
b8471f2ca2 fix a bug 2022-03-25 08:48:02 +08:00
Wisp X
b7601272dd fix a bug 2022-03-25 08:41:06 +08:00
Wisp X
184dd51122 update README.md 2022-03-24 15:15:33 +08:00
Wisp X
3df02550c4 update README.md 2022-03-24 10:32:46 +08:00
Wisp X
f9b046a32f 图片审核跳过 ico 格式的图片 2022-03-24 10:25:34 +08:00
Wisp X
01c850739a update packages 2022-03-24 10:13:51 +08:00
Wisp X
43bb7792ac 增加 nsfwjs 图片审核驱动 Closed #347 2022-03-24 10:07:25 +08:00
Wisp X
6c31ace31a update packages 2022-03-24 07:55:51 +08:00
Wisp X
e9050f4b9b fix a bug 2022-03-23 17:40:06 +08:00
Wisp X
a2a45e72ba fix a bug 2022-03-23 17:31:03 +08:00
Wisp X
317c2ea369 Merge branch 'dev' 2022-03-23 17:28:14 +08:00
Wisp X
df2ed46ab2 fix 2022-03-23 17:27:51 +08:00
Wisp X
979b628f59 fix 2022-03-23 17:15:24 +08:00
Wisp X
71924b0397 支持腾讯云图片审核 2022-03-23 17:00:47 +08:00
Wisp X
b33c309a56 鉴黄跳过 psd、tif 格式的图片 2022-03-23 15:55:46 +08:00
Wisp X
ae785cc56f 支持设置粘贴图片后的动作 2022-03-23 15:45:36 +08:00
Wisp X
66423b1d18 上传时预览图片支持点击查看大图 2022-03-23 15:02:48 +08:00
Wisp X
ab21fb203f 改进升级方式 2022-03-23 13:09:15 +08:00
Wisp X
31503d340a 改进升级方式 2022-03-23 12:26:01 +08:00
Wisp X
165635bce9 改进升级方式 2022-03-23 12:25:42 +08:00
Wisp X
45a639fe3e Merge branch 'dev' 2022-03-22 17:35:32 +08:00
Wisp X
b22a334b86 format code 2022-03-22 17:35:26 +08:00
Wisp X
4be12d828d fix a bug 2022-03-22 17:34:53 +08:00
Wisp X
683a0a4888 修复翻页后搜索条件丢失的 bug Closed #343 2022-03-22 17:34:08 +08:00
Wisp X
769eb4bbd5 改进 2022-03-22 14:17:21 +08:00
Wisp X
10921b34fc update README.md 2022-03-22 14:10:47 +08:00
Wisp X
ede1ec5344 图片管理列表,psd、tiff 格式的图片预览时使用缩略图 #339 2022-03-22 08:13:41 +08:00
Wisp X
6f6403165c 图片管理列表,psd、tiff 格式的图片预览时使用缩略图 #339 2022-03-22 08:13:12 +08:00
Pumpkin
43b54a05ca 为底部ICP备案号链接添加target属性“_blank” (#340) 2022-03-22 06:27:33 +08:00
Wisp X
b1b5bd0782 增加在线更新超时时间 2022-03-21 14:24:05 +08:00
Wisp X
b2a4c5b971 增加在线更新超时时间 2022-03-21 14:23:19 +08:00
Wisp X
b86d5053f2 upgrade packages 2022-03-21 13:11:27 +08:00
Wisp X
aa4fd1c80a update version 2022-03-21 13:07:35 +08:00
Wisp X
ed48d6ca75 fix a bug 2022-03-21 11:35:47 +08:00
Wisp X
97a8c74e22 rollback 2022-03-21 10:02:53 +08:00
Wisp X
8d79647974 s3 支持设置 endpoint 2022-03-21 09:54:01 +08:00
Wisp X
07cd115048 增加安装要求 2022-03-21 09:40:20 +08:00
Wisp X
e227e5acd6 上传时设置文件以及文件夹可见性 2022-03-21 09:38:34 +08:00
Wisp X
2fb3eba233 上传时设置文件以及文件夹可见性 2022-03-21 09:36:02 +08:00
Wisp X
dde0e988f8 Merge branch 'master' into dev 2022-03-21 08:11:55 +08:00
梦彗業
32a9c2e85c 为底部ICP备案号添加工信部跳转 (#334) 2022-03-21 08:09:10 +08:00
Wisp X
5bd4e81ec5 在线更新后清除缓存 2022-03-18 15:40:20 +08:00
Wisp X
d428d535f5 删除本地策略时同时删除符号连接 2022-03-18 15:13:18 +08:00
Wisp X
3b1d566973 水印合成跳过 gif、ico 格式图片 2022-03-18 09:14:17 +08:00
Wisp X
659afb6a50 ico 格式图片在开启原图保护的情况下直接输出,不经过 InterventionImage 处理 2022-03-18 09:11:45 +08:00
Wisp X
59c490b5d3 修复 ico 以及部分带有动画的 webp 格式图片无法上传的 bug.
获取图片宽高使用 getimagesize 而非 imagick 以获取更好的兼容性和性能
2022-03-18 08:17:01 +08:00
Wisp X
f1af71d3f3 粘贴上传文件名改为时间戳 Closed #314 2022-03-17 09:14:30 +08:00
Wisp X
2511fd6abb 删除文件图片时同时删除缩略图 2022-03-16 21:11:02 +08:00
Wisp X
b2663b8ec0 修复我的图片页面删除图片后列表为空的bug 2022-03-16 17:55:59 +08:00
Wisp X
eee6346bec 修复管理后台无法冻结用户的bug Fixed #328 2022-03-16 11:13:59 +08:00
Wisp X
6edee6b783 update configs 2022-03-16 11:10:02 +08:00
Wisp X
e526d0b4c8 update composer.json 2022-03-16 10:10:03 +08:00
Wisp X
37c6ccd29d compatible swoole 2022-03-16 10:00:58 +08:00
Wisp X
b0c6a6defa add swoole options 2022-03-16 09:30:28 +08:00
Wisp X
3badd7f7a3 add extension laravel/octane 2022-03-16 09:01:56 +08:00
Wisp X
96ba030b03 修复细节错误 2022-03-15 08:58:42 +08:00
Wisp X
54d421f93b 修复文字水印字体颜色无法修改的bug 2022-03-15 08:55:44 +08:00
Wisp X
9495179f96 fix a bug 2022-03-15 08:45:12 +08:00
Wisp X
32f629bac9 改进 2022-03-15 08:42:11 +08:00
Wisp X
a9650b6780 Minio 增加区域配置 2022-03-15 08:03:55 +08:00
Wisp X
08e016e8b0 修复打开弹框时公告被同时打开的bug 2022-03-14 14:05:50 +08:00
Wisp X
bcffb4c8fc fix a bug 2022-03-14 11:44:54 +08:00
Wisp X
985af20b5b remove thumbnail route 2022-03-14 11:38:08 +08:00
Wisp X
0c8e4f20c3 角色组增加 tips 2022-03-14 10:47:04 +08:00
Wisp X
86cc72f27a fix a bug 2022-03-14 10:40:34 +08:00
Wisp X
bf26cb6699 增加生成缩略图命令 2022-03-14 10:35:41 +08:00
Wisp X
928727c149 修复关闭游客上传后公告无法弹出的问题 2022-03-14 10:17:15 +08:00
Wisp X
db9d878fe2 更改缩略图生成逻辑 2022-03-14 10:00:07 +08:00
Wisp X
8663fb4134 修复图片不属于任何策略时无法查看详情的bug 2022-03-14 08:12:30 +08:00
Wisp X
7464decb8e fix a bug 2022-03-13 20:02:26 +08:00
Wisp X
7d5df4cfba remove polyfilljs 2022-03-13 19:58:54 +08:00
Wisp X
03f0544ff4 修复删除策略或删除组导致500错误的bug 2022-03-13 18:34:11 +08:00
Wisp X
ca5a215fc9 修复 psd、tif 格式图片无法上传的bug 2022-03-13 17:29:13 +08:00
Wisp X
4ba44d7a16 add polyfill 2022-03-13 16:08:32 +08:00
Wisp X
476d797191 修复首次进入首页上传时提示未知策略的bug 2022-03-13 15:50:00 +08:00
Wisp X
1714e7c876 修复图片宽高过小导致我的图片页面展示异常的bug 2022-03-13 15:08:53 +08:00
Wisp X
a3acf85199 修复管理员修改用户密码将明文入库的bug 2022-03-13 15:05:37 +08:00
Wisp X
6d0631b494 upgrade packages 2022-03-13 13:23:17 +08:00
Wisp X
d465172509 修复备案号不显示的 bug 2022-03-13 13:20:32 +08:00
Wisp X
f2089c96ca fix a bug 2022-03-13 13:17:36 +08:00
Wisp X
e9e2a1eb0e 修复无法上传大写拓展名的文件 2022-03-13 13:11:15 +08:00
Wisp X
51b2682dd0 登录页面增加注册超链接 2022-03-13 12:24:58 +08:00
Wisp X
d0bd143e17 修复后台图片管理中图片大小显示错误的bug 2022-03-13 11:52:02 +08:00
Wisp X
aab42fb630 忽略默认符号连接 2022-03-13 09:54:40 +08:00
Wisp X
2389f7b39b 修复相册图片不能加载更多的bug 2022-03-13 09:47:12 +08:00
101 changed files with 6974 additions and 4415 deletions

1
.gitignore vendored
View File

@@ -17,3 +17,4 @@ npm-debug.log
yarn-error.log yarn-error.log
/.idea /.idea
/.vscode /.vscode
/public/i

View File

@@ -12,12 +12,18 @@
[![Last commit](https://img.shields.io/github/last-commit/lsky-org/lsky-pro/dev)](https://github.com/lsky-org/lsky-pro/commits/dev) [![Last commit](https://img.shields.io/github/last-commit/lsky-org/lsky-pro/dev)](https://github.com/lsky-org/lsky-pro/commits/dev)
[![License](https://img.shields.io/badge/license-GPL_V3.0-yellowgreen.svg)](https://github.com/lsky-org/lsky-pro/blob/master/LICENSE) [![License](https://img.shields.io/badge/license-GPL_V3.0-yellowgreen.svg)](https://github.com/lsky-org/lsky-pro/blob/master/LICENSE)
[官网](https://www.lsky.pro) &nbsp; [官网](https://www.lsky.pro) &middot;
[文档](https://docs.lsky.pro) &nbsp; [文档](https://docs.lsky.pro) &middot;
[演示](https://pic.iqy.ink) &nbsp; [社区](https://github.com/lsky-org/lsky-pro/discussions) &middot;
[演示](https://pic.vv1234.cn) &middot;
[Telegram 群组](https://t.me/lsky_pro) [Telegram 群组](https://t.me/lsky_pro)
> 正式版本请点击 [这里](https://github.com/lsky-org/lsky-pro/releases) 下载,发现 bug 请提交 [issues](https://github.com/lsky-org/lsky-pro/issues) (提问前建议阅读[提问的智慧](https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/main/README-zh_CN.md)) > [!WARNING]
> 开源版本已停止维护,不再进行新特性更新和 bug 修复。
> master 分支为未安装三方拓展的版本,通常包含了最新未发布版本的一些实验性新特性和修复补丁,正式版本请点击 [这里](https://github.com/lsky-org/lsky-pro/releases) 下载。
> 发现 bug 请提交 [issues](https://github.com/lsky-org/lsky-pro/issues) (提问前建议阅读[提问的智慧](https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/main/README-zh_CN.md))
> 有任何想法、建议、或分享,请移步 [社区](https://github.com/lsky-org/lsky-pro/discussions)
![看不见图片请使用科学上网](https://user-images.githubusercontent.com/22728201/157242302-bfbd04a0-fb30-4241-800e-cc2b1dad9b19.png) ![看不见图片请使用科学上网](https://user-images.githubusercontent.com/22728201/157242302-bfbd04a0-fb30-4241-800e-cc2b1dad9b19.png)
![看不见图片请使用科学上网](https://user-images.githubusercontent.com/22728201/157242314-5716d578-fee5-4083-8d91-0d98cb2545d9.png) ![看不见图片请使用科学上网](https://user-images.githubusercontent.com/22728201/157242314-5716d578-fee5-4083-8d91-0d98cb2545d9.png)
@@ -51,6 +57,7 @@
- exec、shell_exec 函数 - exec、shell_exec 函数
- readlink、symlink 函数 - readlink、symlink 函数
- putenv、getenv 函数 - putenv、getenv 函数
- chmod、chown、fileperms 函数
### 😋 鸣谢 ### 😋 鸣谢
- [Laravel](https://laravel.com) - [Laravel](https://laravel.com)

View File

@@ -0,0 +1,58 @@
<?php
namespace App\Console\Commands;
use App\Models\Image;
use App\Services\ImageService;
use Illuminate\Console\Command;
use League\Flysystem\FilesystemException;
use Symfony\Component\Console\Helper\ProgressBar;
class MakeThumbnails extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'lsky:thumbnails';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Make images thumbnails.';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$progress = new ProgressBar($this->output, Image::query()->count());
$progress->setMessage('获取图片处理中...');
$progress->start();
$service = new ImageService();
/** @var Image $image */
foreach (Image::query()->whereNotNull('strategy_id')->cursor() as $image) {
try {
$service->makeThumbnail(
image: $image,
data: $image->filesystem()->read($image->pathname),
force: true,
);
$progress->advance();
} catch (\Throwable $e) {
$this->error("缩略图生成失败, {$e->getMessage()}");
}
}
$progress->finish();
return 0;
}
}

View File

@@ -54,4 +54,10 @@ final class GroupConfigKey
/** @var string 图片缓存时间 */ /** @var string 图片缓存时间 */
const ImageCacheTtl = 'image_cache_ttl'; const ImageCacheTtl = 'image_cache_ttl';
/** @var string 图片保存格式 */
const ImageSaveFormat = 'image_save_format';
/** @var string 图片保存质量 */
const ImageSaveQuality = 'image_save_quality';
} }

View File

@@ -0,0 +1,9 @@
<?php
namespace App\Enums;
final class PastedAction
{
const Upload = 2; // 直接上传
const Waiting = 1; // 等待上传
}

View File

@@ -0,0 +1,15 @@
<?php
namespace App\Enums\Scan;
final class NsfwJsOption
{
/** @var string 接口地址 */
const ApiUrl = 'api_url';
/** @var string 表单属性名称 */
const AttrName = 'attr_name';
/** @var string 阈值 */
const Threshold = 'threshold';
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Enums\Scan;
final class TencentOption
{
/** @var string SecretId */
const SecretId = 'secret_id';
/** @var string SecretKey */
const SecretKey = 'secret_key';
/** @var string Region */
const Region = 'region';
/** @var string Endpoint */
const Endpoint = 'endpoint';
/** @var string 业务场景 */
const BizType = 'biz_type';
}

View File

@@ -16,6 +16,12 @@ final class MinioOption
/** @var string Endpoint */ /** @var string Endpoint */
const Endpoint = 'endpoint'; const Endpoint = 'endpoint';
/** @var string 区域 */
const Region = 'region';
/** @var string Bucket */ /** @var string Bucket */
const Bucket = 'bucket'; const Bucket = 'bucket';
/** @var string BucketEndpoint */
const BucketEndpoint = 'bucket_endpoint';
} }

View File

@@ -13,6 +13,9 @@ final class S3Option
/** @var string SecretAccessKey */ /** @var string SecretAccessKey */
const SecretAccessKey = 'secret_access_key'; const SecretAccessKey = 'secret_access_key';
/** @var string Endpoint */
const Endpoint = 'endpoint';
/** @var string 区域 */ /** @var string 区域 */
const Region = 'region'; const Region = 'region';

View File

@@ -15,4 +15,10 @@ final class WebDavOption
/** @var string 密码 */ /** @var string 密码 */
const Password = 'password'; const Password = 'password';
/** @var string 认证方式 */
const AuthType = 'auth_type';
/** @var string 地址前缀 */
const Prefix = 'prefix';
} }

View File

@@ -13,6 +13,9 @@ final class UserConfigKey
/** @var string 默认权限 */ /** @var string 默认权限 */
const DefaultPermission = 'default_permission'; const DefaultPermission = 'default_permission';
/** @var string 图片粘贴后动作 */
const PastedAction = 'pasted_action';
/** @var string 上传是否自动清除预览 */ /** @var string 上传是否自动清除预览 */
const IsAutoClearPreview = 'is_auto_clear_preview'; const IsAutoClearPreview = 'is_auto_clear_preview';
} }

View File

@@ -0,0 +1,9 @@
<?php
namespace App\Enums\Watermark;
final class Mode
{
const Overlay = 1; // 覆盖原图
const Dynamic = 2; // 动态生成
}

View File

@@ -32,7 +32,7 @@ class ConsoleController extends Controller
$images = Image::query() $images = Image::query()
->whereBetween('created_at', [$start->format($format), $end->format($format)]) ->whereBetween('created_at', [$start->format($format), $end->format($format)])
->get() ->get(['user_id', 'created_at'])
->transform(function (Image $image) { ->transform(function (Image $image) {
$image['date'] = $image->created_at->format('Y-m-d'); $image['date'] = $image->created_at->format('Y-m-d');
return $image; return $image;

View File

@@ -17,7 +17,7 @@ use Illuminate\View\View;
class GroupController extends Controller class GroupController extends Controller
{ {
public function __construct() private function share()
{ {
\Illuminate\Support\Facades\View::share([ \Illuminate\Support\Facades\View::share([
'default' => Group::getDefaultConfigs(), 'default' => Group::getDefaultConfigs(),
@@ -28,20 +28,31 @@ class GroupController extends Controller
public function index(Request $request): View public function index(Request $request): View
{ {
$groups = Group::query()->when($request->query('keywords'), function (Builder $builder, $keywords) { $keywords = $request->query('keywords');
$groups = Group::query()->when($keywords, function (Builder $builder, $keywords) {
$builder->where('name', 'like', "%{$keywords}%"); $builder->where('name', 'like', "%{$keywords}%");
})->withCount('users')->withCount('strategies')->latest()->paginate(); })->withCount('users')->withCount('strategies')->latest()->paginate();
$groups->appends(compact('keywords'));
$this->share();
return view('admin.group.index', compact('groups')); return view('admin.group.index', compact('groups'));
} }
public function add(): View public function add(): View
{ {
$this->share();
return view('admin.group.add'); return view('admin.group.add');
} }
public function edit(Request $request): View public function edit(Request $request): View
{ {
$group = Group::query()->findOrFail($request->route('id')); $group = Group::query()->findOrFail($request->route('id'));
$this->share();
return view('admin.group.edit', compact('group')); return view('admin.group.edit', compact('group'));
} }
@@ -53,6 +64,8 @@ class GroupController extends Controller
$group->save(); $group->save();
}); });
$this->share();
return $this->success('创建成功'); return $this->success('创建成功');
} }

View File

@@ -17,9 +17,10 @@ class ImageController extends Controller
{ {
public function index(Request $request): View public function index(Request $request): View
{ {
$keywords = $request->query('keywords');
$images = Image::query()->with(['user' => function (BelongsTo $belongsTo) { $images = Image::query()->with(['user' => function (BelongsTo $belongsTo) {
$belongsTo->withSum('images', 'size'); $belongsTo->withSum('images', 'size');
}, 'album', 'group', 'strategy'])->when($request->input('keywords'), function (Builder $builder, $keywords) { }, 'album', 'group', 'strategy'])->when($keywords, function (Builder $builder, $keywords) {
$words = []; $words = [];
$qualifiers = [ $qualifiers = [
'name:', 'album:', 'group:', 'strategy:', 'email:', 'extension:', 'md5:', 'sha1:', 'ip:', 'is:', 'order:', 'name:', 'album:', 'group:', 'strategy:', 'email:', 'extension:', 'md5:', 'sha1:', 'ip:', 'is:', 'order:',
@@ -38,8 +39,8 @@ class ImageController extends Controller
'is:guest' => $builder->whereNull('user_id'), 'is:guest' => $builder->whereNull('user_id'),
'is:adminer' => $builder->whereHas('user', fn (Builder $builder) => $builder->where('is_adminer', 1)), 'is:adminer' => $builder->whereHas('user', fn (Builder $builder) => $builder->where('is_adminer', 1)),
'order:earliest' => $builder->orderBy('created_at'), 'order:earliest' => $builder->orderBy('created_at'),
'order:utmost' => $builder->orderBy('size'), 'order:utmost' => $builder->orderByDesc('size'),
'order:least' => $builder->orderByDesc('size'), 'order:least' => $builder->orderBy('size'),
default => 0, default => 0,
}; };
@@ -63,16 +64,20 @@ class ImageController extends Controller
}); });
foreach ($words as $word) { foreach ($words as $word) {
$builder->where('origin_name', 'like', "%{$word}%")->orWhere('alias_name', 'like', "%{$word}%"); $builder->where('name', 'like', "%{$word}%")
->orWhere('origin_name', 'like', "%{$word}%")
->orWhere('alias_name', 'like', "%{$word}%");
} }
})->latest()->paginate(40); })->latest()->paginate(40);
$images->getCollection()->each(function (Image $image) { $images->getCollection()->each(function (Image $image) {
$image->append('url', 'pathname'); $image->append('url', 'pathname', 'thumb_url');
$image->album?->setVisible(['name']); $image->album?->setVisible(['name']);
$image->group?->setVisible(['name']); $image->group?->setVisible(['name']);
$image->strategy?->setVisible(['name']); $image->strategy?->setVisible(['name']);
}); });
$images->appends(compact('keywords'));
return view('admin.image.index', compact('images')); return view('admin.image.index', compact('images'));
} }

View File

@@ -15,9 +15,13 @@ class StrategyController extends Controller
{ {
public function index(Request $request): View public function index(Request $request): View
{ {
$strategies = Strategy::query()->when($request->query('keywords'), function (Builder $builder, $keywords) { $keywords = $request->query('keywords');
$strategies = Strategy::query()->when($keywords, function (Builder $builder, $keywords) {
$builder->where('name', 'like', "%{$keywords}%")->orWhere('intro', 'like', "%{$keywords}%"); $builder->where('name', 'like', "%{$keywords}%")->orWhere('intro', 'like', "%{$keywords}%");
})->withCount('images')->withSum('images', 'size')->latest()->paginate(); })->withCount('images')->withSum('images', 'size')->latest()->paginate();
$strategies->appends(compact('keywords'));
return view('admin.strategy.index', compact('strategies')); return view('admin.strategy.index', compact('strategies'));
} }
@@ -59,8 +63,12 @@ class StrategyController extends Controller
public function delete(Request $request): Response public function delete(Request $request): Response
{ {
if ($group = Strategy::query()->find($request->route('id'))) { /** @var Strategy $strategy */
$group->delete(); if ($strategy = Strategy::query()->find($request->route('id'))) {
DB::transaction(function () use ($strategy) {
$strategy->images()->update(['strategy_id' => null]);
$strategy->delete();
});
} }
return $this->success('删除成功'); return $this->success('删除成功');
} }

View File

@@ -19,15 +19,19 @@ class UserController extends Controller
public function index(Request $request): View public function index(Request $request): View
{ {
$status = $request->query('status'); $status = $request->query('status');
$keywords = $request->query('keywords');
$users = User::query()->when($status > -1, function (Builder $builder) use ($status) { $users = User::query()->when($status > -1, function (Builder $builder) use ($status) {
$builder->where('status', $status); $builder->where('status', $status);
})->when($request->query('keywords'), function (Builder $builder, $keywords) { })->when($keywords, function (Builder $builder, $keywords) {
$builder->where('name', 'like', "%{$keywords}%")->orWhere('email', 'like', "%{$keywords}%"); $builder->where('name', 'like', "%{$keywords}%")->orWhere('email', 'like', "%{$keywords}%");
})->with('group')->withSum('images', 'size')->latest()->paginate(); })->with('group')->withSum('images', 'size')->latest()->paginate();
$users->getCollection()->each(function (User $user) { $users->getCollection()->each(function (User $user) {
$user->group->setVisible(['name']); $user->group->setVisible(['name']);
}); });
$statuses = [-1 => '全部', 1 => '正常', 0 => '冻结']; $statuses = [-1 => '全部', 1 => '正常', 0 => '冻结'];
$users->appends(compact('status', 'keywords'));
return view('admin.user.index', compact('users', 'statuses')); return view('admin.user.index', compact('users', 'statuses'));
} }
@@ -43,9 +47,7 @@ class UserController extends Controller
$user = User::query()->findOrFail($request->route('id')); $user = User::query()->findOrFail($request->route('id'));
$validated = $request->validated(); $validated = $request->validated();
if (empty($validated['password'])) { if (! empty($validated['password'])) {
unset($validated['password']);
} else {
$user->forceFill([ $user->forceFill([
'password' => Hash::make($validated['password']), 'password' => Hash::make($validated['password']),
'remember_token' => Str::random(60), 'remember_token' => Str::random(60),
@@ -54,6 +56,7 @@ class UserController extends Controller
event(new PasswordReset($user)); event(new PasswordReset($user));
} }
unset($validated['password']);
$user->fill($validated); $user->fill($validated);
$user->group_id = $validated['group_id']; $user->group_id = $validated['group_id'];

View File

@@ -7,6 +7,7 @@ use App\Http\Controllers\Controller;
use App\Models\Image; use App\Models\Image;
use App\Models\User; use App\Models\User;
use App\Services\ImageService; use App\Services\ImageService;
use App\Services\UserService;
use App\Utils; use App\Utils;
use Illuminate\Auth\AuthenticationException; use Illuminate\Auth\AuthenticationException;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@@ -76,7 +77,7 @@ class ImageController extends Controller
{ {
/** @var User $user */ /** @var User $user */
$user = Auth::user(); $user = Auth::user();
$user->images()->where('key', $request->route('key'))->delete(); (new UserService())->deleteImages([$request->route('key')], $user, 'key');
return $this->success('删除成功'); return $this->success('删除成功');
} }
} }

View File

@@ -15,13 +15,6 @@ use Illuminate\Validation\Rules;
class RegisteredUserController extends Controller class RegisteredUserController extends Controller
{ {
public function __construct()
{
if (! Utils::config(ConfigKey::IsEnableRegistration)) {
abort(404);
}
}
/** /**
* Display the registration view. * Display the registration view.
* *
@@ -52,6 +45,7 @@ class RegisteredUserController extends Controller
'name' => $request->name, 'name' => $request->name,
'email' => $request->email, 'email' => $request->email,
'password' => Hash::make($request->password), 'password' => Hash::make($request->password),
'registered_ip' => $request->ip(),
]); ]);
if (Utils::config(ConfigKey::IsUserNeedVerify)) { if (Utils::config(ConfigKey::IsUserNeedVerify)) {

View File

@@ -2,20 +2,11 @@
namespace App\Http\Controllers\Common; namespace App\Http\Controllers\Common;
use App\Enums\ConfigKey;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Utils;
use Illuminate\View\View; use Illuminate\View\View;
class ApiController extends Controller class ApiController extends Controller
{ {
public function __construct()
{
if (! Utils::config(ConfigKey::IsEnableApi)) {
abort(404);
}
}
public function index(): View public function index(): View
{ {
return view('common.api'); return view('common.api');

View File

@@ -2,22 +2,13 @@
namespace App\Http\Controllers\Common; namespace App\Http\Controllers\Common;
use App\Enums\ConfigKey;
use App\Enums\ImagePermission; use App\Enums\ImagePermission;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\Image; use App\Models\Image;
use App\Utils;
use Illuminate\View\View; use Illuminate\View\View;
class GalleryController extends Controller class GalleryController extends Controller
{ {
public function __construct()
{
if (! Utils::config(ConfigKey::IsEnableGallery)) {
abort(404);
}
}
public function index(): View public function index(): View
{ {
$images = Image::query() $images = Image::query()

View File

@@ -4,6 +4,7 @@ namespace App\Http\Controllers;
use App\Enums\GroupConfigKey; use App\Enums\GroupConfigKey;
use App\Enums\UserStatus; use App\Enums\UserStatus;
use App\Enums\Watermark\Mode;
use App\Exceptions\UploadException; use App\Exceptions\UploadException;
use App\Http\Result; use App\Http\Result;
use App\Models\Group; use App\Models\Group;
@@ -22,6 +23,7 @@ use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Hash;
use Illuminate\View\View; use Illuminate\View\View;
use Intervention\Image\Facades\Image as InterventionImage;
use League\Flysystem\FilesystemException; use League\Flysystem\FilesystemException;
use Symfony\Component\Console\Output\BufferedOutput; use Symfony\Component\Console\Output\BufferedOutput;
use Symfony\Component\HttpFoundation\StreamedResponse; use Symfony\Component\HttpFoundation\StreamedResponse;
@@ -67,6 +69,10 @@ class Controller extends BaseController
'name' => 'exec、shell_exec 函数', 'name' => 'exec、shell_exec 函数',
'intro' => '执行外部命令', 'intro' => '执行外部命令',
'result' => function_exists('exec') && function_exists('shell_exec'), 'result' => function_exists('exec') && function_exists('shell_exec'),
])->push([
'name' => 'chmod、chown、fileperms 函数',
'intro' => '设置和获取文件、文件夹权限函数',
'result' => function_exists('chmod') && function_exists('chown') && function_exists('fileperms'),
])->push([ ])->push([
'name' => 'PHP >= 8.0.2', 'name' => 'PHP >= 8.0.2',
'intro' => '最低要求 PHP 8.0.2 版本', 'intro' => '最低要求 PHP 8.0.2 版本',
@@ -143,7 +149,7 @@ class Controller extends BaseController
->where('key', $request->route('key')) ->where('key', $request->route('key'))
->where('extension', strtolower($request->route('extension'))) ->where('extension', strtolower($request->route('extension')))
->firstOr(fn() => abort(404)); ->firstOr(fn() => abort(404));
if (! $image->group->configs->get(GroupConfigKey::IsEnableOriginalProtection)) { if (! $image->group?->configs->get(GroupConfigKey::IsEnableOriginalProtection)) {
abort(404); abort(404);
} }
try { try {
@@ -153,12 +159,18 @@ class Controller extends BaseController
$contents = Cache::get($cacheKey); $contents = Cache::get($cacheKey);
} else { } else {
$contents = $image->filesystem()->read($image->pathname); $contents = $image->filesystem()->read($image->pathname);
// 是否启用了水印功能跳过gif图片 $configs = collect($image->group?->configs->get(GroupConfigKey::WatermarkConfigs));
if ($image->group->configs->get(GroupConfigKey::IsEnableWatermark) && $image->mimetype !== 'image/gif') {
$configs = $image->group->configs->get(GroupConfigKey::WatermarkConfigs); // 是否启用了水印功能跳过gif和ico图片
$contents = (string)$service->stickWatermark($contents, collect($configs))->encode(); if (
$image->group?->configs->get(GroupConfigKey::IsEnableWatermark) &&
$configs->get('mode', Mode::Overlay) == Mode::Dynamic &&
! in_array($image->extension, ['ico', 'gif', 'svg'])
) {
$quality = $image->group?->configs->get(GroupConfigKey::ImageSaveQuality, 75);
$contents = $service->stickWatermark($contents, $configs)->encode($image->extension, $quality)->getEncoded();
} }
$cacheTtl = (int)$image->group->configs->get(GroupConfigKey::ImageCacheTtl, 0); $cacheTtl = (int)$image->group?->configs->get(GroupConfigKey::ImageCacheTtl, 0);
// 是否启用了缓存 // 是否启用了缓存
if ($cacheTtl) { if ($cacheTtl) {
Cache::remember($cacheKey, $cacheTtl, fn () => $contents); Cache::remember($cacheKey, $cacheTtl, fn () => $contents);
@@ -167,51 +179,27 @@ class Controller extends BaseController
} }
} }
} catch (FilesystemException $e) { } catch (FilesystemException $e) {
Utils::e($e, '图片输出时出现异常');
abort(404); abort(404);
} }
return \response()->stream(function () use ($contents) { $mimetype = $image->mimetype;
echo $contents;
}, headers: ['Content-type' => $image->mimetype]);
}
public function thumbnail(Request $request): StreamedResponse // ico svg 图片直接输出,不经过 InterventionImage 处理
{ if (in_array($image->extension, ['ico', 'svg'])) {
/** @var Image $image */ goto out;
$image = Image::query()
->where('key', $request->route('key'))
->where('extension', strtolower($request->route('extension')))
->firstOr(fn() => abort(404));
try {
$cacheKey = "image_thumb_{$image->key}";
if (Cache::has($cacheKey)) {
$contents = Cache::get($cacheKey);
} else {
$stream = $image->filesystem()->readStream($image->pathname);
$img = \Intervention\Image\Facades\Image::make($stream);
$width = $w = $image->width;
$height = $h = $image->height;
$max = 400; // 最大宽高
if ($w > $max && $h > $max) {
$scale = min($max / $w, $max / $h);
$width = (int)($w * $scale);
$height = (int)($h * $scale);
}
$contents = $img->fit($width, $height, fn($constraint) => $constraint->upsize())->encode();
Cache::rememberForever($cacheKey, fn () => (string)$contents);
}
} catch (FilesystemException $e) {
abort(404);
} }
// 浏览器无法预览的图片,改为 png 格式输出
if (in_array($image->extension, ['psd', 'tif', 'bmp'])) {
$mimetype = 'image/png';
$contents = InterventionImage::make($contents)->encode('png')->getEncoded();
}
out:
return \response()->stream(function () use ($contents) { return \response()->stream(function () use ($contents) {
echo $contents; echo $contents;
}, headers: ['Content-type' => $image->mimetype]); }, headers: ['Content-type' => $mimetype]);
} }
} }

View File

@@ -29,6 +29,10 @@ class ImageController extends Controller
$images = $user->images()->filter($request)->with('group', 'strategy')->paginate(40); $images = $user->images()->filter($request)->with('group', 'strategy')->paginate(40);
$images->getCollection()->each(function (Image $image) { $images->getCollection()->each(function (Image $image) {
// 图片宽高过小会导致前端排版异常
$image->width = max($image->width, 200);
$image->height = max($image->height, 200);
$image->human_date = $image->created_at->diffForHumans(); $image->human_date = $image->created_at->diffForHumans();
$image->date = $image->created_at->format('Y-m-d H:i:s'); $image->date = $image->created_at->format('Y-m-d H:i:s');
$image->append(['url', 'thumb_url', 'filename', 'links'])->setVisible([ $image->append(['url', 'thumb_url', 'filename', 'links'])->setVisible([
@@ -46,7 +50,7 @@ class ImageController extends Controller
if (!$image = $user->images()->find($request->route('id'))) { if (!$image = $user->images()->find($request->route('id'))) {
return $this->fail('未找到该图片'); return $this->fail('未找到该图片');
} }
$image->strategy->setVisible(['name']); $image->strategy?->setVisible(['name']);
$image->album?->setVisible(['name']); $image->album?->setVisible(['name']);
$image->append(['url', 'thumb_url', 'filename', 'links'])->setVisible([ $image->append(['url', 'thumb_url', 'filename', 'links'])->setVisible([
'id', 'filename', 'origin_name', 'url', 'thumb_url', 'width', 'height', 'size', 'mimetype', 'md5', 'sha1', 'id', 'filename', 'origin_name', 'url', 'thumb_url', 'width', 'height', 'size', 'mimetype', 'md5', 'sha1',

View File

@@ -15,7 +15,10 @@ class CheckIsEnableApi
public function handle(Request $request, Closure $next) public function handle(Request $request, Closure $next)
{ {
if (! Utils::config(ConfigKey::IsEnableApi)) { if (! Utils::config(ConfigKey::IsEnableApi)) {
return $this->fail('管理员未启用 API')->setStatusCode(403); if ($request->expectsJson()) {
return $this->fail('管理员未启用 API')->setStatusCode(403);
}
abort(404);
} }
return $next($request); return $next($request);

View File

@@ -0,0 +1,26 @@
<?php
namespace App\Http\Middleware;
use App\Enums\ConfigKey;
use App\Http\Result;
use App\Utils;
use Closure;
use Illuminate\Http\Request;
class CheckIsEnableGallery
{
use Result;
public function handle(Request $request, Closure $next)
{
if (! Utils::config(ConfigKey::IsEnableGallery)) {
if ($request->expectsJson()) {
return $this->fail('管理员未启用画廊功能')->setStatusCode(403);
}
abort(404);
}
return $next($request);
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\Http\Middleware;
use App\Enums\ConfigKey;
use App\Http\Result;
use App\Utils;
use Closure;
use Illuminate\Http\Request;
class CheckIsEnableRegistration
{
use Result;
public function handle(Request $request, Closure $next)
{
if (! Utils::config(ConfigKey::IsEnableRegistration)) {
if ($request->expectsJson()) {
return $this->fail('站点管理员关闭了注册功能')->setStatusCode(403);
}
abort(404);
}
return $next($request);
}
}

View File

@@ -12,7 +12,7 @@ class TrustProxies extends Middleware
* *
* @var array<int, string>|string|null * @var array<int, string>|string|null
*/ */
protected $proxies; protected $proxies = '*';
/** /**
* The headers that should be used to detect proxies. * The headers that should be used to detect proxies.

View File

@@ -34,26 +34,39 @@ class GroupRequest extends FormRequest
'configs.limit_per_day' => 'required|integer', 'configs.limit_per_day' => 'required|integer',
'configs.limit_per_week' => 'required|integer', 'configs.limit_per_week' => 'required|integer',
'configs.limit_per_month' => 'required|integer', 'configs.limit_per_month' => 'required|integer',
'configs.image_save_quality' => 'required|min:1|max:100',
'configs.image_save_format' => '',
'configs.path_naming_rule' => 'max:400', 'configs.path_naming_rule' => 'max:400',
'configs.file_naming_rule' => 'max:400', 'configs.file_naming_rule' => 'max:400',
'configs.accepted_file_suffixes' => 'required|array|in:jpeg,jpg,png,gif,tif,bmp,ico,psd,webp', 'configs.accepted_file_suffixes' => 'required|array|in:jpeg,jpg,png,gif,tif,bmp,ico,psd,webp,svg',
'configs.is_enable_scan' => 'boolean', 'configs.is_enable_scan' => 'boolean',
'configs.scanned_action' => [ 'configs.scanned_action' => [
'exclude_if:configs.is_enable_scan,false', 'exclude_if:configs.is_enable_scan,false',
'in:mark,delete', 'in:mark,delete',
], ],
'configs.scan_configs.driver' => ['exclude_if:configs.is_enable_scan,false', 'in:aliyun'], 'configs.scan_configs.driver' => ['exclude_if:configs.is_enable_scan,false', 'in:tencent,aliyun,nsfwjs'],
'configs.scan_configs.drivers.tencent.endpoint' => [$requiredIfReview('tencent')],
'configs.scan_configs.drivers.tencent.secret_id' => [$requiredIfReview('tencent')],
'configs.scan_configs.drivers.tencent.secret_key' => [$requiredIfReview('tencent')],
'configs.scan_configs.drivers.tencent.region' => [$requiredIfReview('tencent')],
'configs.scan_configs.drivers.tencent.biz_type' => '',
'configs.scan_configs.drivers.aliyun.access_key_id' => [$requiredIfReview('aliyun')], 'configs.scan_configs.drivers.aliyun.access_key_id' => [$requiredIfReview('aliyun')],
'configs.scan_configs.drivers.aliyun.access_key_secret' => [$requiredIfReview('aliyun')], 'configs.scan_configs.drivers.aliyun.access_key_secret' => [$requiredIfReview('aliyun')],
'configs.scan_configs.drivers.aliyun.biz_type' => [$requiredIfReview('aliyun')],
'configs.scan_configs.drivers.aliyun.region_id' => [$requiredIfReview('aliyun')], 'configs.scan_configs.drivers.aliyun.region_id' => [$requiredIfReview('aliyun')],
'configs.scan_configs.drivers.aliyun.biz_type' => '',
'configs.scan_configs.drivers.aliyun.scenes' => [$requiredIfReview('aliyun'), 'array'], 'configs.scan_configs.drivers.aliyun.scenes' => [$requiredIfReview('aliyun'), 'array'],
'configs.scan_configs.drivers.nsfwjs.api_url' => [$requiredIfReview('nsfwjs')],
'configs.scan_configs.drivers.nsfwjs.attr_name' => [$requiredIfReview('nsfwjs'), 'nullable'],
'configs.scan_configs.drivers.nsfwjs.threshold' => [$requiredIfReview('nsfwjs'), 'nullable', 'integer', 'between:1,100'],
'configs.is_enable_original_protection' => 'boolean', 'configs.is_enable_original_protection' => 'boolean',
'configs.image_cache_ttl' => 'nullable|numeric', 'configs.image_cache_ttl' => 'nullable|numeric',
'configs.is_enable_watermark' => 'boolean', 'configs.is_enable_watermark' => 'boolean',
'configs.watermark_configs.mode' => ['in:1,2'],
'configs.watermark_configs.driver' => ['exclude_if:configs.is_enable_watermark,false', 'in:font,image'], 'configs.watermark_configs.driver' => ['exclude_if:configs.is_enable_watermark,false', 'in:font,image'],
'configs.watermark_configs.drivers.font.font' => [ 'configs.watermark_configs.drivers.font.font' => [
$requiredIfWatermark('font'), $requiredIfWatermark('font'),
@@ -65,6 +78,7 @@ class GroupRequest extends FormRequest
], ],
'configs.watermark_configs.drivers.font.position' => [$requiredIfWatermark('font')], 'configs.watermark_configs.drivers.font.position' => [$requiredIfWatermark('font')],
'configs.watermark_configs.drivers.font.text' => [$requiredIfWatermark('font')], 'configs.watermark_configs.drivers.font.text' => [$requiredIfWatermark('font')],
'configs.watermark_configs.drivers.font.color' => [$requiredIfWatermark('font')],
'configs.watermark_configs.drivers.font.size' => [$requiredIfWatermark('font'), 'nullable', 'integer'], 'configs.watermark_configs.drivers.font.size' => [$requiredIfWatermark('font'), 'nullable', 'integer'],
'configs.watermark_configs.drivers.font.angle' => [$requiredIfWatermark('font'), 'nullable', 'integer'], 'configs.watermark_configs.drivers.font.angle' => [$requiredIfWatermark('font'), 'nullable', 'integer'],
'configs.watermark_configs.drivers.font.x' => [$requiredIfWatermark('font'), 'nullable', 'integer'], 'configs.watermark_configs.drivers.font.x' => [$requiredIfWatermark('font'), 'nullable', 'integer'],
@@ -104,16 +118,26 @@ class GroupRequest extends FormRequest
'configs.limit_per_month' => '每月上传限制', 'configs.limit_per_month' => '每月上传限制',
'configs.path_naming_rule' => '路径命名规则', 'configs.path_naming_rule' => '路径命名规则',
'configs.file_naming_rule' => '文件命名规则', 'configs.file_naming_rule' => '文件命名规则',
'configs.image_save_quality' => '图片保存质量',
'configs.image_save_format' => '图片保存格式',
'configs.accepted_file_suffixes' => '允许上传的文件后缀', 'configs.accepted_file_suffixes' => '允许上传的文件后缀',
'configs.is_enable_scan' => '是否启用图片审核', 'configs.is_enable_scan' => '是否启用图片审核',
'configs.scanned_action' => '图片审核动作', 'configs.scanned_action' => '图片审核动作',
'configs.scan_configs.driver' => '图片审核驱动', 'configs.scan_configs.driver' => '图片审核驱动',
'configs.scan_configs.drivers.tencent.endpoint' => 'Endpoint',
'configs.scan_configs.drivers.tencent.secret_id' => 'SecretId',
'configs.scan_configs.drivers.tencent.secret_key' => 'SecretKey',
'configs.scan_configs.drivers.tencent.region' => '地域节点',
'configs.scan_configs.drivers.tencent.biz_type' => '业务场景',
'configs.scan_configs.drivers.aliyun.access_key_id' => 'AccessKeyId', 'configs.scan_configs.drivers.aliyun.access_key_id' => 'AccessKeyId',
'configs.scan_configs.drivers.aliyun.access_key_secret' => 'AccessKeySecret', 'configs.scan_configs.drivers.aliyun.access_key_secret' => 'AccessKeySecret',
'configs.scan_configs.drivers.aliyun.biz_type' => '场景名称',
'configs.scan_configs.drivers.aliyun.region_id' => '地域节点', 'configs.scan_configs.drivers.aliyun.region_id' => '地域节点',
'configs.scan_configs.drivers.aliyun.biz_type' => '场景名称',
'configs.scan_configs.drivers.aliyun.scenes' => '审核场景', 'configs.scan_configs.drivers.aliyun.scenes' => '审核场景',
'configs.scan_configs.drivers.nsfwjs.api_url' => '接口地址',
'configs.scan_configs.drivers.nsfwjs.attr_name' => '表单名称',
'configs.scan_configs.drivers.nsfwjs.threshold' => '阈值',
'configs.is_enable_original_protection' => '是否启用原图保护功能', 'configs.is_enable_original_protection' => '是否启用原图保护功能',
'configs.image_cache_ttl' => '图片缓存时间', 'configs.image_cache_ttl' => '图片缓存时间',
@@ -123,6 +147,7 @@ class GroupRequest extends FormRequest
'configs.watermark_configs.drivers.font.font' => '字体文件', 'configs.watermark_configs.drivers.font.font' => '字体文件',
'configs.watermark_configs.drivers.font.position' => '水印位置', 'configs.watermark_configs.drivers.font.position' => '水印位置',
'configs.watermark_configs.drivers.font.text' => '水印文字', 'configs.watermark_configs.drivers.font.text' => '水印文字',
'configs.watermark_configs.drivers.font.color' => '字体颜色',
'configs.watermark_configs.drivers.font.size' => '水印文字大小', 'configs.watermark_configs.drivers.font.size' => '水印文字大小',
'configs.watermark_configs.drivers.font.angle' => '水印旋转角度', 'configs.watermark_configs.drivers.font.angle' => '水印旋转角度',
'configs.watermark_configs.drivers.font.x' => '水印X轴偏移量', 'configs.watermark_configs.drivers.font.x' => '水印X轴偏移量',

View File

@@ -18,13 +18,13 @@ class StrategyRequest extends FormRequest
{ {
$checkUrl = function ($attribute, $value, $fail) { $checkUrl = function ($attribute, $value, $fail) {
if ($this->input('key') == StrategyKey::Local) { if ($this->input('key') == StrategyKey::Local) {
$folders = ['fonts', 'css', 'js']; $folders = [config('app.thumbnail_path'), 'fonts', 'css', 'js'];
$symlink = Strategy::getRootPath($value); $symlink = Strategy::getRootPath($value);
if (! $symlink) { if (! $symlink) {
return $fail('访问域名缺少根路径'); return $fail('访问域名缺少根路径');
} }
if (in_array($symlink, $folders)) { if (in_array($symlink, $folders)) {
return $fail('系统保留路径'); return $fail('系统保留路径'. $symlink);
} }
if (false !== strpbrk($symlink, "\\/?%*:|\"<>")) { if (false !== strpbrk($symlink, "\\/?%*:|\"<>")) {
return $fail('根路径名称不符合规则'); return $fail('根路径名称不符合规则');
@@ -54,6 +54,7 @@ class StrategyRequest extends FormRequest
'intro' => 'max:2000', 'intro' => 'max:2000',
'key' => 'required|integer', 'key' => 'required|integer',
'configs.url' => ['required', 'url'], 'configs.url' => ['required', 'url'],
'configs.queries' => '',
]; ];
return array_merge($array, match((int)$this->input('key')) { return array_merge($array, match((int)$this->input('key')) {
@@ -73,6 +74,7 @@ class StrategyRequest extends FormRequest
StrategyKey::S3 => [ StrategyKey::S3 => [
'configs.access_key_id' => 'required', 'configs.access_key_id' => 'required',
'configs.secret_access_key' => 'required', 'configs.secret_access_key' => 'required',
'configs.endpoint' => '',
'configs.region' => '', 'configs.region' => '',
'configs.bucket' => 'required', 'configs.bucket' => 'required',
], ],
@@ -122,12 +124,16 @@ class StrategyRequest extends FormRequest
'configs.base_uri' => 'required', 'configs.base_uri' => 'required',
'configs.username' => '', 'configs.username' => '',
'configs.password' => '', 'configs.password' => '',
'configs.auth_type' => '',
'configs.prefix' => '',
], ],
StrategyKey::Minio => [ StrategyKey::Minio => [
'configs.access_key' => 'required', 'configs.access_key' => 'required',
'configs.secret_key' => 'required', 'configs.secret_key' => 'required',
'configs.endpoint' => 'required', 'configs.endpoint' => '',
'configs.region' => '',
'configs.bucket' => 'required', 'configs.bucket' => 'required',
'configs.bucket_endpoint' => '',
], ],
}); });
} }
@@ -139,6 +145,7 @@ class StrategyRequest extends FormRequest
'intro' => '简介', 'intro' => '简介',
'key' => '策略', 'key' => '策略',
'configs.url' => '访问网址', 'configs.url' => '访问网址',
'configs.queries' => 'Url 额外参数',
]; ];
return array_merge($array, match((int)$this->input('key')) { return array_merge($array, match((int)$this->input('key')) {
@@ -148,6 +155,7 @@ class StrategyRequest extends FormRequest
StrategyKey::S3 => [ StrategyKey::S3 => [
'configs.access_key_id' => 'AccessKeyId', 'configs.access_key_id' => 'AccessKeyId',
'configs.secret_access_key' => 'SecretAccessKey', 'configs.secret_access_key' => 'SecretAccessKey',
'configs.endpoint' => '连接地址',
'configs.region' => '区域', 'configs.region' => '区域',
'configs.bucket' => '储存桶名称', 'configs.bucket' => '储存桶名称',
], ],
@@ -194,15 +202,19 @@ class StrategyRequest extends FormRequest
'configs.passive' => '被动模式', 'configs.passive' => '被动模式',
], ],
StrategyKey::Webdav => [ StrategyKey::Webdav => [
'configs.base_uri' => 'required', 'configs.base_uri' => '连接地址',
'configs.username' => 'required', 'configs.username' => '用户名',
'configs.password' => 'required', 'configs.password' => '密码',
'configs.auth_type' => '认证方式',
'configs.prefix' => '前缀',
], ],
StrategyKey::Minio => [ StrategyKey::Minio => [
'configs.access_key' => 'AccessKey', 'configs.access_key' => 'AccessKey',
'configs.secret_key' => 'SecretKey', 'configs.secret_key' => 'SecretKey',
'configs.endpoint' => '连接地址', 'configs.endpoint' => '连接地址',
'configs.region' => '区域',
'configs.bucket' => 'Bucket 名称', 'configs.bucket' => 'Bucket 名称',
'configs.bucket_endpoint' => 'BucketEndpoint',
], ],
}); });
} }

View File

@@ -12,7 +12,7 @@ class AlbumRequest extends FormRequest
public function rules() public function rules()
{ {
return [ return [
'name' => 'required|max:60', 'name' => 'required|max:60|alpha_dash',
'intro' => 'max:600' 'intro' => 'max:600'
]; ];
} }
@@ -22,6 +22,7 @@ class AlbumRequest extends FormRequest
return [ return [
'name.required' => '名称不能为空', 'name.required' => '名称不能为空',
'name.max' => '名称字符过长,最大不能超过 60', 'name.max' => '名称字符过长,最大不能超过 60',
'name.alpha_dash' => '名称只能是字母、数字,短破折号(-和下划线_',
'intro.max' => '简介字符过长,最大不能超过 600' 'intro.max' => '简介字符过长,最大不能超过 600'
]; ];
} }

View File

@@ -34,7 +34,7 @@ class ImageRenameRequest extends FormRequest
'id.numeric' => '图片选择异常', 'id.numeric' => '图片选择异常',
'name.required' => '请输入名称', 'name.required' => '请输入名称',
'name.max' => '名称长度不能超过 50 个字符', 'name.max' => '名称长度不能超过 50 个字符',
'name.string' => '名称格式不正确' 'name.string' => '名称格式不正确',
]; ];
} }
} }

View File

@@ -19,6 +19,7 @@ class UserSettingRequest extends FormRequest
'configs.default_album' => 'required|numeric', 'configs.default_album' => 'required|numeric',
'configs.default_strategy' => 'required|numeric', 'configs.default_strategy' => 'required|numeric',
'configs.default_permission' => 'required|in:1,0', 'configs.default_permission' => 'required|in:1,0',
'configs.pasted_action' => 'required|in:1,2',
'configs.is_auto_clear_preview' => 'nullable|boolean' 'configs.is_auto_clear_preview' => 'nullable|boolean'
]; ];
} }
@@ -29,7 +30,7 @@ class UserSettingRequest extends FormRequest
'name.required' => '昵称不能为空', 'name.required' => '昵称不能为空',
'name.between' => '昵称必须在 2-20 个字符之间', 'name.between' => '昵称必须在 2-20 个字符之间',
'url.url' => '个人主页地址格式不正确', 'url.url' => '个人主页地址格式不正确',
'password.between' => '昵称必须在 6-32 个字符之间', 'password.between' => '密码必须在 6-32 个字符之间',
'configs.array' => '配置值不正确', 'configs.array' => '配置值不正确',
'configs.default_album.required' => '默认相册选择错误', 'configs.default_album.required' => '默认相册选择错误',
'configs.default_album.numeric' => '默认相册选择错误', 'configs.default_album.numeric' => '默认相册选择错误',
@@ -37,6 +38,8 @@ class UserSettingRequest extends FormRequest
'configs.default_strategy.numeric' => '默认策略选择错误', 'configs.default_strategy.numeric' => '默认策略选择错误',
'configs.default_permission.required' => '权限值选择错误', 'configs.default_permission.required' => '权限值选择错误',
'configs.default_permission.in' => '权限值不正确', 'configs.default_permission.in' => '权限值不正确',
'configs.pasted_action.required' => '粘贴动作值选择错误',
'configs.pasted_action.in' => '粘贴动作值不正确',
'configs.is_auto_clear_preview.boolean' => '是否自动清除预览选择错误' 'configs.is_auto_clear_preview.boolean' => '是否自动清除预览选择错误'
]; ];
} }

View File

@@ -5,7 +5,6 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Http\Request; use Illuminate\Http\Request;

View File

@@ -3,7 +3,6 @@
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
/** /**
* @property string $name * @property string $name

View File

@@ -3,10 +3,8 @@
namespace App\Models; namespace App\Models;
use App\Utils; use App\Utils;
use Carbon\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
@@ -83,7 +81,7 @@ class Group extends Model
*/ */
public static function getDefaultConfigs(): Collection public static function getDefaultConfigs(): Collection
{ {
return collect(config('convention.app.group')); return collect(config('convention.group'));
} }
public function users(): HasMany public function users(): HasMany

View File

@@ -6,11 +6,10 @@ use App\Enums\GroupConfigKey;
use App\Enums\ImagePermission; use App\Enums\ImagePermission;
use App\Services\ImageService; use App\Services\ImageService;
use App\Utils; use App\Utils;
use Carbon\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
@@ -93,6 +92,8 @@ class Image extends Model
'permission' => 'integer', 'permission' => 'integer',
]; ];
protected ?Filesystem $filesystem = null;
protected static function booted() protected static function booted()
{ {
static::creating(function (self $image) { static::creating(function (self $image) {
@@ -110,10 +111,12 @@ class Image extends Model
->exists() ->exists()
) { ) {
// 删除本地缓存文件 // 删除本地缓存文件
Cache::forget("image_thumb_{$image->key}");
try { try {
// 删除物理文件 // 删除物理文件
$image->filesystem()->delete($image->pathname); $image->filesystem()->delete($image->pathname);
@unlink(public_path($image->getThumbnailPathname()));
// 删除缓存
Cache::forget("image_{$image->key}");
} catch (\Throwable $e) { } catch (\Throwable $e) {
Utils::e($e, '删除物理文件时发生异常'); Utils::e($e, '删除物理文件时发生异常');
} }
@@ -162,25 +165,36 @@ class Image extends Model
public function pathname(): Attribute public function pathname(): Attribute
{ {
return new Attribute(fn() => "{$this->path}/{$this->name}"); $path = $this->path ? "{$this->path}/" : '';
return new Attribute(fn() => "{$path}{$this->name}");
} }
public function url(): Attribute public function url(): Attribute
{ {
return new Attribute(function () { return new Attribute(function () {
// 是否启用原图保护功能 // 是否启用原图保护功能
if ($this->group->configs->get(GroupConfigKey::IsEnableOriginalProtection)) { if ($this->group?->configs->get(GroupConfigKey::IsEnableOriginalProtection)) {
return asset("{$this->key}.{$this->extension}"); $url = asset("{$this->key}.{$this->extension}");
} else { } else {
return rtrim($this->strategy->configs->get('url'), '/').'/'.$this->pathname; $url = rtrim($this->strategy?->configs->get('url'), '/').'/'.ltrim($this->pathname, '/');
} }
// 拼接图片 url
return $url.($this->strategy?->configs->get('queries') ?: '');
}); });
} }
public function thumbUrl(): Attribute public function thumbUrl(): Attribute
{ {
return new Attribute(function () { return new Attribute(function () {
return asset("{$this->key}.{$this->extension}!thumbnail"); $pathname = $this->getThumbnailPathname();
// 没有缩略图则返回原图
if (! file_exists(public_path($pathname))) {
return $this->url;
}
return asset($pathname);
}); });
} }
@@ -198,7 +212,10 @@ class Image extends Model
public function filesystem(): Filesystem public function filesystem(): Filesystem
{ {
return new Filesystem((new ImageService())->getAdapter($this->strategy)); if (is_null($this->filesystem)) {
$this->filesystem = new Filesystem((new ImageService())->getAdapter($this->strategy));
}
return $this->filesystem;
} }
public function user(): BelongsTo public function user(): BelongsTo
@@ -221,6 +238,11 @@ class Image extends Model
return $this->belongsTo(Strategy::class, 'strategy_id', 'id'); return $this->belongsTo(Strategy::class, 'strategy_id', 'id');
} }
public function getThumbnailPathname(): string
{
return trim(config('app.thumbnail_path'), '/')."/{$this->md5}.". ($this->extension === 'svg' ? 'svg' : "png");
}
private function generateKey($length = 6): string private function generateKey($length = 6): string
{ {
$key = Str::random($length); $key = Str::random($length);

13
app/Models/Model.php Normal file
View File

@@ -0,0 +1,13 @@
<?php
namespace App\Models;
use Carbon\CarbonInterface;
abstract class Model extends \Illuminate\Database\Eloquent\Model
{
protected function serializeDate(\DateTimeInterface $date): string
{
return $date->format(CarbonInterface::DEFAULT_TO_STRING_FORMAT);
}
}

View File

@@ -3,14 +3,14 @@
namespace App\Models; namespace App\Models;
use App\Enums\StrategyKey; use App\Enums\StrategyKey;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Filesystem\Filesystem; use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Carbon;
use Sabre\DAV\Client;
/** /**
* @property int $id * @property int $id
@@ -57,6 +57,13 @@ class Strategy extends Model
StrategyKey::Minio => 'Minio', StrategyKey::Minio => 'Minio',
]; ];
const WEBDAV_AUTH_TYPES = [
'' => 'Auto',
Client::AUTH_BASIC => 'Basic',
Client::AUTH_DIGEST => 'Digest',
Client::AUTH_NTLM => 'Ntlm',
];
protected static function booted() protected static function booted()
{ {
static::saving(function (self $strategy) { static::saving(function (self $strategy) {
@@ -78,6 +85,14 @@ class Strategy extends Model
} }
} }
}); });
static::deleted(function (self $strategy) {
// 如果是本地策略,删除的时候同时删除符号连接
if ($strategy->key === StrategyKey::Local) {
$symlink = self::getRootPath($strategy->configs['url']);
@unlink(public_path($symlink));
}
});
} }
public static function getRootPath($url): string public static function getRootPath($url): string

View File

@@ -3,10 +3,8 @@
namespace App\Models; namespace App\Models;
use App\Enums\ConfigKey; use App\Enums\ConfigKey;
use App\Enums\ImagePermission;
use App\Enums\UserConfigKey;
use App\Utils; use App\Utils;
use Carbon\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
@@ -26,6 +24,7 @@ use Laravel\Sanctum\HasApiTokens;
* @property string $remember_token * @property string $remember_token
* @property boolean $is_adminer * @property boolean $is_adminer
* @property float $capacity * @property float $capacity
* @property float $use_capacity
* @property string $url * @property string $url
* @property Collection $configs * @property Collection $configs
* @property int $image_num * @property int $image_num
@@ -58,6 +57,7 @@ class User extends Authenticatable implements MustVerifyEmail
'configs', 'configs',
'configs->default_strategy', 'configs->default_strategy',
'registered_ip', 'registered_ip',
'status',
]; ];
/** /**
@@ -99,13 +99,7 @@ class User extends Authenticatable implements MustVerifyEmail
$user->group_id = Group::query()->where('is_default', true)->value('id'); $user->group_id = Group::query()->where('is_default', true)->value('id');
// 初始容量 // 初始容量
$user->capacity = Utils::config(ConfigKey::UserInitialCapacity); $user->capacity = Utils::config(ConfigKey::UserInitialCapacity);
$user->configs = collect(config('convention.user'))->merge($user->configs ?: []);
$user->configs = collect([
UserConfigKey::DefaultAlbum => 0,
UserConfigKey::DefaultStrategy => 0,
UserConfigKey::DefaultPermission => ImagePermission::Private,
UserConfigKey::IsAutoClearPreview => false,
])->merge($user->configs ?: []);
}); });
} }
@@ -114,6 +108,11 @@ class User extends Authenticatable implements MustVerifyEmail
return new Attribute(fn () => Utils::getAvatar($this->email)); return new Attribute(fn () => Utils::getAvatar($this->email));
} }
public function useCapacity(): Attribute
{
return new Attribute(fn () => $this->images()->sum('size'));
}
public function group(): BelongsTo public function group(): BelongsTo
{ {
return $this->belongsTo(Group::class, 'group_id', 'id'); return $this->belongsTo(Group::class, 'group_id', 'id');

View File

@@ -46,7 +46,10 @@ class AppServiceProvider extends ServiceProvider
View::composer('*', function (\Illuminate\View\View $view) { View::composer('*', function (\Illuminate\View\View $view) {
/** @var Group $group */ /** @var Group $group */
$group = Auth::check() ? Auth::user()->group : Group::query()->where('is_guest', true)->first(); $group = Auth::check() ? Auth::user()->group : Group::query()->where('is_guest', true)->first();
$view->with('_group', $group); $view->with([
'_group' => $group,
'_is_notice' => strip_tags(Utils::config(ConfigKey::SiteNotice)),
]);
}); });
} }
} }

View File

@@ -8,6 +8,8 @@ use App\Enums\ConfigKey;
use App\Enums\GroupConfigKey; use App\Enums\GroupConfigKey;
use App\Enums\ImagePermission; use App\Enums\ImagePermission;
use App\Enums\Scan\AliyunOption; use App\Enums\Scan\AliyunOption;
use App\Enums\Scan\NsfwJsOption;
use App\Enums\Scan\TencentOption;
use App\Enums\Strategy\CosOption; use App\Enums\Strategy\CosOption;
use App\Enums\Strategy\FtpOption; use App\Enums\Strategy\FtpOption;
use App\Enums\Strategy\KodoOption; use App\Enums\Strategy\KodoOption;
@@ -23,6 +25,7 @@ use App\Enums\UserConfigKey;
use App\Enums\UserStatus; use App\Enums\UserStatus;
use App\Enums\Watermark\FontOption; use App\Enums\Watermark\FontOption;
use App\Enums\Watermark\ImageOption; use App\Enums\Watermark\ImageOption;
use App\Enums\Watermark\Mode;
use App\Exceptions\UploadException; use App\Exceptions\UploadException;
use App\Models\Group; use App\Models\Group;
use App\Models\Image; use App\Models\Image;
@@ -30,14 +33,14 @@ use App\Models\Strategy;
use App\Models\User; use App\Models\User;
use App\Utils; use App\Utils;
use Aws\S3\S3Client; use Aws\S3\S3Client;
use Carbon\Carbon; use Illuminate\Support\Carbon;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Http\UploadedFile; use Illuminate\Http\UploadedFile;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Intervention\Image\Facades\Image as InterventionImage; use Intervention\Image\Facades\Image as InterventionImage;
use Intervention\Image\Imagick\Font; use Intervention\Image\Imagick\Font;
@@ -49,17 +52,20 @@ use League\Flysystem\FilesystemException;
use League\Flysystem\Ftp\FtpAdapter; use League\Flysystem\Ftp\FtpAdapter;
use League\Flysystem\Ftp\FtpConnectionOptions; use League\Flysystem\Ftp\FtpConnectionOptions;
use League\Flysystem\Local\LocalFilesystemAdapter; use League\Flysystem\Local\LocalFilesystemAdapter;
use League\Flysystem\PhpseclibV2\SftpAdapter; use League\Flysystem\PhpseclibV3\SftpAdapter;
use League\Flysystem\PhpseclibV2\SftpConnectionProvider; use League\Flysystem\PhpseclibV3\SftpConnectionProvider;
use League\Flysystem\UnixVisibility\PortableVisibilityConverter;
use League\Flysystem\Visibility;
use League\Flysystem\WebDAV\WebDAVAdapter; use League\Flysystem\WebDAV\WebDAVAdapter;
use OSS\OssClient;
use Overtrue\Flysystem\Cos\CosAdapter; use Overtrue\Flysystem\Cos\CosAdapter;
use Overtrue\Flysystem\Qiniu\QiniuAdapter; use Overtrue\Flysystem\Qiniu\QiniuAdapter;
use Sabre\DAV\Client; use Sabre\DAV\Client;
use TencentCloud\Common\Credential;
use TencentCloud\Common\Profile\ClientProfile;
use TencentCloud\Common\Profile\HttpProfile;
use TencentCloud\Ims\V20201229\ImsClient;
use TencentCloud\Ims\V20201229\Models\ImageModerationRequest;
use WispX\Flysystem\Upyun\UpyunAdapter; use WispX\Flysystem\Upyun\UpyunAdapter;
use Zing\Flysystem\Oss\OssAdapter; use Zing\Flysystem\Oss\OssAdapter;
use OSS\OssClient;
class ImageService class ImageService
{ {
@@ -76,8 +82,6 @@ class ImageService
throw new UploadException('管理员关闭了游客上传'); throw new UploadException('管理员关闭了游客上传');
} }
$img = InterventionImage::make($file);
$image = new Image(); $image = new Image();
/** @var User|null $user */ /** @var User|null $user */
$user = $request->user(); $user = $request->user();
@@ -92,7 +96,9 @@ class ImageService
throw new UploadException('没有可用的储存,请联系管理员。'); throw new UploadException('没有可用的储存,请联系管理员。');
} }
if (!in_array($file->getClientOriginalExtension(), $configs->get(GroupConfigKey::AcceptedFileSuffixes))) { $extension = strtolower($file->getClientOriginalExtension());
if (! in_array($extension, $configs->get(GroupConfigKey::AcceptedFileSuffixes))) {
throw new UploadException('不支持的文件类型'); throw new UploadException('不支持的文件类型');
} }
@@ -145,27 +151,58 @@ class ImageService
// 上传频率限制 // 上传频率限制
$this->rateLimiter($configs, $request); $this->rateLimiter($configs, $request);
// 图片处理,跳过 ico gif svg
if (! in_array($extension, ['ico', 'gif', 'svg'])) {
// 图片保存质量与格式
$quality = $configs->get(GroupConfigKey::ImageSaveQuality, 75);
$format = $configs->get(GroupConfigKey::ImageSaveFormat);
if ($quality < 100 || $format) {
// 获取拓展名,判断是否需要转换
$format = $format ?: $extension;
$filename = Str::replaceLast($extension, $format, $file->getClientOriginalName());
$handleImage = InterventionImage::make($file)->save('tmp_' . md5_file($file->getRealPath()), $quality);
$file = new UploadedFile($handleImage->basePath(), $filename, $handleImage->mime());
// 重新设置拓展名
$extension = $format;
$handleImage->destroy();
}
// 是否启用水印,覆盖原图片
if (
$configs->get(GroupConfigKey::IsEnableWatermark) &&
collect($configs->get(GroupConfigKey::WatermarkConfigs))->get('mode', Mode::Overlay) == Mode::Overlay
) {
$watermarkImage = $this->stickWatermark($file, collect($configs->get(GroupConfigKey::WatermarkConfigs)));
$watermarkImage->save();
$file = new UploadedFile($watermarkImage->basePath(), $file->getClientOriginalName(), $file->getMimeType());
$watermarkImage->destroy();
}
}
$filename = $this->replacePathname( $filename = $this->replacePathname(
$configs->get(GroupConfigKey::PathNamingRule).'/'.$configs->get(GroupConfigKey::FileNamingRule), $file, $configs->get(GroupConfigKey::PathNamingRule).'/'.$configs->get(GroupConfigKey::FileNamingRule), $file,
); );
$pathname = $filename.".{$file->getClientOriginalExtension()}"; $pathname = $filename.".{$extension}";
[$width, $height] = @getimagesize($file->getRealPath()) ?: [400, 400];
$image->fill([ $image->fill([
'md5' => md5_file($file->getRealPath()), 'md5' => md5_file($file->getRealPath()),
'sha1' => sha1_file($file->getRealPath()), 'sha1' => sha1_file($file->getRealPath()),
'path' => dirname($pathname), 'path' => $configs->get(GroupConfigKey::PathNamingRule) ? dirname($pathname) : '',
'name' => basename($pathname), 'name' => basename($pathname),
'origin_name' => $file->getClientOriginalName(), 'origin_name' => $file->getClientOriginalName(),
'size' => $file->getSize() / 1024, 'size' => $file->getSize() / 1024,
'mimetype' => $file->getMimeType(), 'mimetype' => $file->getMimeType(),
'extension' => strtolower($file->getClientOriginalExtension()), 'extension' => strtolower($extension),
'width' => $img->width(), 'width' => $width,
'height' => $img->height(), 'height' => $height,
'is_unhealthy' => false, 'is_unhealthy' => false,
'uploaded_ip' => $request->ip(), 'uploaded_ip' => $request->ip(),
]); ]);
$filesystem = new Filesystem($this->getAdapter($strategy)); $filesystem = new Filesystem($this->getAdapter($strategy));
// 检测该策略是否存在该图片,有则只创建记录不保存文件 // 检测该策略是否存在该图片,有则只创建记录不保存文件
/** @var Image $existing */ /** @var Image $existing */
$existing = Image::query()->when($image->strategy_id, function (Builder $builder, $id) { $existing = Image::query()->when($image->strategy_id, function (Builder $builder, $id) {
@@ -179,7 +216,7 @@ class ImageService
Utils::e($e, '保存图片时出现异常'); Utils::e($e, '保存图片时出现异常');
throw new UploadException(config('app.debug', false) ? $e->getMessage() : '图片上传失败'); throw new UploadException(config('app.debug', false) ? $e->getMessage() : '图片上传失败');
} }
@fclose($handle); if (is_resource($handle)) @fclose($handle);
} else { } else {
$image->fill($existing->only('path', 'name')); $image->fill($existing->only('path', 'name'));
} }
@@ -208,10 +245,15 @@ class ImageService
throw new UploadException('图片记录保存失败'); throw new UploadException('图片记录保存失败');
} }
// 图片检测 // 图片检测,跳过 tif、ico、psd、svg 格式
if ($configs->get(GroupConfigKey::IsEnableScan)) { if ($configs->get(GroupConfigKey::IsEnableScan) && ! in_array($extension, ['psd', 'ico', 'tif', 'svg'])) {
$scanConfigs = $configs->get(GroupConfigKey::ScanConfigs); $scanConfigs = $configs->get(GroupConfigKey::ScanConfigs);
if ($this->scan($scanConfigs['driver'], collect($scanConfigs['drivers'][$scanConfigs['driver']]), $image)) { if ($this->scan(
driver: $scanConfigs['driver'],
configs: collect($scanConfigs['drivers'][$scanConfigs['driver']]),
image: $image,
file: $file,
)) {
// 标记 or 删除 // 标记 or 删除
if ($configs->get(GroupConfigKey::ScannedAction) === 'delete') { if ($configs->get(GroupConfigKey::ScannedAction) === 'delete') {
$image->delete(); $image->delete();
@@ -224,6 +266,11 @@ class ImageService
} }
} }
$this->makeThumbnail($image, $file);
// 上传完成后删除临时文件
unlink($file->getPathname());
return $image; return $image;
} }
@@ -240,17 +287,17 @@ class ImageService
'key' => $configs->get(S3Option::AccessKeyId), 'key' => $configs->get(S3Option::AccessKeyId),
'secret' => $configs->get(S3Option::SecretAccessKey) 'secret' => $configs->get(S3Option::SecretAccessKey)
], ],
'endpoint' => $configs->get(S3Option::Endpoint),
'region' => $configs->get(S3Option::Region), 'region' => $configs->get(S3Option::Region),
'version' => '2006-03-01', 'version' => '2006-03-01',
]), ]),
bucket: $configs->get(S3Option::Bucket), bucket: $configs->get(S3Option::Bucket),
visibility: new \League\Flysystem\AwsS3V3\PortableVisibilityConverter(Visibility::PUBLIC),
), ),
StrategyKey::Oss => new OssAdapter( StrategyKey::Oss => new OssAdapter(
client: new OssClient( client: new OssClient($configs->get(
accessKeyId: $configs->get(OssOption::AccessKeyId), OssOption::AccessKeyId),
accessKeySecret: $configs->get(OssOption::AccessKeySecret), $configs->get(OssOption::AccessKeySecret),
endpoint: $configs->get(OssOption::Endpoint), $configs->get(OssOption::Endpoint),
), ),
bucket: $configs->get(OssOption::Bucket), bucket: $configs->get(OssOption::Bucket),
), ),
@@ -280,16 +327,6 @@ class ImageService
useAgent: (bool)$configs->get(SftpOption::UseAgent) useAgent: (bool)$configs->get(SftpOption::UseAgent)
), ),
root: $configs->get(SftpOption::Root), root: $configs->get(SftpOption::Root),
visibilityConverter: PortableVisibilityConverter::fromArray([
'file' => [
'public' => 0640,
'private' => 0604,
],
'dir' => [
'public' => 0740,
'private' => 7604,
],
])
), ),
StrategyKey::Ftp => new FtpAdapter( StrategyKey::Ftp => new FtpAdapter(
connectionOptions: FtpConnectionOptions::fromArray([ connectionOptions: FtpConnectionOptions::fromArray([
@@ -303,11 +340,12 @@ class ImageService
'timeout' => 30, 'timeout' => 30,
]), ]),
), ),
StrategyKey::Webdav => new WebDAVAdapter(new Client([ StrategyKey::Webdav => new WebDAVAdapter(new Client(([
'baseUri' => $configs->get(WebDavOption::BaseUri), 'baseUri' => $configs->get(WebDavOption::BaseUri),
'userName' => $configs->get(WebDavOption::Username), 'userName' => $configs->get(WebDavOption::Username),
'password' => $configs->get(WebDavOption::Password) 'password' => $configs->get(WebDavOption::Password),
])), 'authType' => (int)$configs->get(WebDavOption::AuthType),
])), $configs->get(WebDavOption::Prefix) ?: ''),
StrategyKey::Minio => new AwsS3V3Adapter( StrategyKey::Minio => new AwsS3V3Adapter(
client: new S3Client([ client: new S3Client([
'credentials' => [ 'credentials' => [
@@ -315,11 +353,11 @@ class ImageService
'secret' => $configs->get(MinioOption::SecretKey) 'secret' => $configs->get(MinioOption::SecretKey)
], ],
'endpoint' => $configs->get(MinioOption::Endpoint), 'endpoint' => $configs->get(MinioOption::Endpoint),
'region' => '', 'region' => $configs->get(MinioOption::Region),
'version' => '2006-03-01', 'version' => '2006-03-01',
'bucket_endpoint' => (bool)$configs->get(MinioOption::BucketEndpoint),
]), ]),
bucket: $configs->get(MinioOption::Bucket), bucket: $configs->get(MinioOption::Bucket),
visibility: new \League\Flysystem\AwsS3V3\PortableVisibilityConverter(Visibility::PUBLIC),
), ),
}; };
} }
@@ -337,7 +375,7 @@ class ImageService
'hours' => ['key' => GroupConfigKey::LimitPerHour, 'str' => '小时'], 'hours' => ['key' => GroupConfigKey::LimitPerHour, 'str' => '小时'],
'days' => ['key' => GroupConfigKey::LimitPerDay, 'str' => '天'], 'days' => ['key' => GroupConfigKey::LimitPerDay, 'str' => '天'],
'weeks' => ['key' => GroupConfigKey::LimitPerWeek, 'str' => '周'], 'weeks' => ['key' => GroupConfigKey::LimitPerWeek, 'str' => '周'],
'months' => ['key' => GroupConfigKey::LimitPerWeek, 'str' => '月'], 'months' => ['key' => GroupConfigKey::LimitPerMonth, 'str' => '月'],
]; ];
foreach ($array as $key => $item) { foreach ($array as $key => $item) {
@@ -362,14 +400,47 @@ class ImageService
* @param $driver * @param $driver
* @param Collection $configs * @param Collection $configs
* @param Image $image * @param Image $image
* @param UploadedFile $file
* @return bool true=违规 * @return bool true=违规
* @throws UploadException * @throws UploadException
*/ */
public function scan($driver, Collection $configs, Image $image): bool public function scan($driver, Collection $configs, Image $image, UploadedFile $file): bool
{ {
$flag = false; $flag = false;
try { try {
if ($driver === 'tencent') {
// 图片大小不得超过 5mb
if ($file->getSize() >= 5242880) {
return false;
}
$cred = new Credential($configs->get(TencentOption::SecretId), $configs->get(TencentOption::SecretKey));
$httpProfile = new HttpProfile();
$httpProfile->setEndpoint($configs->get(TencentOption::Endpoint));
$clientProfile = new ClientProfile();
$clientProfile->setHttpProfile($httpProfile);
$client = new ImsClient($cred, $configs->get(TencentOption::Region), $clientProfile);
$req = new ImageModerationRequest();
$params = [
"FileContent" => base64_encode($file->getContent()),
];
if ($configs->get(TencentOption::BizType)) {
$params['BizType'] = $configs->get(TencentOption::BizType);
}
$req->fromJsonString(json_encode($params));
$resp = $client->ImageModeration($req);
if ($resp->getSuggestion() === 'Block') {
$flag = true;
}
}
if ($driver === 'aliyun') { if ($driver === 'aliyun') {
// 20 mb以内、宽高不超过 30000px
if ($file->getSize() >= 20971520 || $image->width >= 30000 || $image->height >= 30000) {
return false;
}
AlibabaCloud::accessKeyClient( AlibabaCloud::accessKeyClient(
$configs->get(AliyunOption::AccessKeyId), $configs->get(AliyunOption::AccessKeyId),
$configs->get(AliyunOption::AccessKeySecret), $configs->get(AliyunOption::AccessKeySecret),
@@ -393,6 +464,29 @@ class ImageService
} }
} }
} }
if ($driver === 'nsfwjs') {
// 不支持 bmp 格式
if ($image->extension === 'bmp') {
return false;
}
$response = Http::timeout(180)->withOptions(['timeout' => 180])->attach(
$configs->get(NsfwJsOption::AttrName, 'image'), $file->getContent(), $file->getClientOriginalName(),
)->post($configs->get(NsfwJsOption::ApiUrl));
$ratio = $configs->get(NsfwJsOption::Threshold, 60) / 100;
if ($response->json('hentai', 0.00) >= $ratio) {
$flag = true;
}
if ($response->json('porn', 0.00) >= $ratio) {
$flag = true;
}
if ($response->json('sexy', 0.00) >= $ratio) {
$flag = true;
}
}
} catch (\Throwable $e) { } catch (\Throwable $e) {
throw new UploadException('Scan: '.$e->getMessage()); throw new UploadException('Scan: '.$e->getMessage());
} }
@@ -443,6 +537,52 @@ class ImageService
return $image; return $image;
} }
/**
* 生成缩略图,缩略图目录必须在 public 目录下,且该目录必须存在
*
* @param mixed $image 图片数据
* @param mixed $data 物理图片数据
* @param int $max 最大宽高
* @param bool $force 是否强制覆盖
* @return void
*/
public function makeThumbnail(Image $image, mixed $data, int $max = 400, bool $force = false): void
{
$pathname = public_path($image->getThumbnailPathname());
if (! file_exists($pathname) || $force) {
try {
// 创建文件夹
if (! is_dir(dirname($pathname))) {
@mkdir(dirname($pathname));
}
// 生成缩略图svg等格式本身体积足够小且网页原生支持(比生成的png缩略图还小),不用生成缩略图,直接复制文件
if($image->extension ==='svg') {
copy($data->getPathname(), $pathname);
}else{
@ini_set('memory_limit', '512M');
$img = InterventionImage::make($data);
$width = $w = $image->width;
$height = $h = $image->height;
if ($w > $max && $h > $max) {
$scale = min($max / $w, $max / $h);
$width = (int)($w * $scale);
$height = (int)($h * $scale);
}
$img->fit($width, $height, fn($constraint) => $constraint->upsize())->encode('png', 60)->save($pathname);
$img->destroy();
}
} catch (\Throwable $e) {
Utils::e($e, '生成缩略图时出现异常');
}
}
}
/** /**
* 获取水印画布 * 获取水印画布
* *
@@ -504,7 +644,7 @@ class ImageService
'{md5-16}' => substr(md5(microtime().Str::random()), 0, 16), '{md5-16}' => substr(md5(microtime().Str::random()), 0, 16),
'{str-random-16}' => Str::random(), '{str-random-16}' => Str::random(),
'{str-random-10}' => Str::random(10), '{str-random-10}' => Str::random(10),
'{filename}' => rtrim($file->getClientOriginalName(), '.'.$file->getClientOriginalExtension()), '{filename}' => Str::replaceLast('.'.$file->getClientOriginalExtension(), '', $file->getClientOriginalName()),
'{uid}' => Auth::check() ? Auth::id() : 0, '{uid}' => Auth::check() ? Auth::id() : 0,
]; ];
return str_replace(array_keys($array), array_values($array), $pathname); return str_replace(array_keys($array), array_values($array), $pathname);

View File

@@ -10,14 +10,13 @@ use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use League\Flysystem\Filesystem; use League\Flysystem\Filesystem;
use League\Flysystem\FilesystemException; use League\Flysystem\FilesystemException;
use League\Flysystem\Local\LocalFilesystemAdapter; use League\Flysystem\Local\LocalFilesystemAdapter;
class UpgradeService class UpgradeService
{ {
const ApiUrl = 'https://api.lsky.pro/v1'; const ApiUrl = 'https://api.lsky.pro/v2';
/** @var array|array[] 所有版本 */ /** @var array|array[] 所有版本 */
protected array $versions = []; protected array $versions = [];
@@ -34,7 +33,7 @@ class UpgradeService
public function __construct(protected string $version) public function __construct(protected string $version)
{ {
$this->http = Http::baseUrl(self::ApiUrl)->withOptions(['timeout' => 30])->timeout(30); $this->http = Http::baseUrl(self::ApiUrl)->withOptions(['timeout' => 1800])->timeout(1800);
$this->filesystem = new Filesystem(new LocalFilesystemAdapter(base_path())); $this->filesystem = new Filesystem(new LocalFilesystemAdapter(base_path()));
} }
@@ -55,7 +54,7 @@ class UpgradeService
public function getVersions(): Collection public function getVersions(): Collection
{ {
if (! $this->versions) { if (! $this->versions) {
$response = $this->http->timeout(30)->get('/versions'); $response = $this->http->get('/versions');
if (! $response->successful()) { if (! $response->successful()) {
throw new \Exception('无法请求升级服务器'); throw new \Exception('无法请求升级服务器');
} }
@@ -82,17 +81,23 @@ class UpgradeService
$this->setProgress('准备升级...'); $this->setProgress('准备升级...');
@ini_set('memory_limit', '1G'); @ini_set('memory_limit', '1G');
@ini_set('max_execution_time', '86400');
// 获取差异信息 // 获取差异信息
$response = $this->http->timeout(30)->get('/diff/'.urlencode(Utils::config(ConfigKey::AppVersion))); $response = $this->http->get('/diff/'.urlencode(Utils::config(ConfigKey::AppVersion)));
if (! $response->successful()) { if ($response->failed()) {
throw new \Exception('无法请求升级服务器'); throw new \Exception('无法请求升级服务器');
} }
$files = $response->json(); $result = $response->json();
$files = $result['files'];
$this->setProgress('下载补丁包...'); $this->setProgress('下载补丁包...');
foreach ($files as $file) { foreach ($files as $file) {
if ($file['action'] === 'deleted') continue; if ($file['action'] === 'deleted') continue;
$this->filesystem->write($this->temp.'/'.$file['pathname'], base64_decode($file['content'])); $res = $this->http->baseUrl($result['download_url'])->get($file['pathname']);
if ($res->failed()) {
throw new \Exception("补丁文件 {$file['pathname']} 下载失败。");
}
$this->filesystem->write($this->temp.'/'.$file['pathname'], $res->body());
// 校验文件 // 校验文件
if ($file['md5'] !== md5_file(base_path($this->temp).'/'.$file['pathname'])) { if ($file['md5'] !== md5_file(base_path($this->temp).'/'.$file['pathname'])) {
throw new \Exception("补丁文件 {$file['pathname']} 校验失败。"); throw new \Exception("补丁文件 {$file['pathname']} 校验失败。");
@@ -110,9 +115,9 @@ class UpgradeService
$version = $this->getVersions()->first()['name']; $version = $this->getVersions()->first()['name'];
Config::query()->where('name', ConfigKey::AppVersion)->update(['value' => $version]); Config::query()->where('name', ConfigKey::AppVersion)->update(['value' => $version]);
// 执行数据库迁移 // 执行数据库迁移
Artisan::call('migrate'); Artisan::call('migrate', ['--seed' => true]);
// 清除配置缓存 // 清除缓存
Cache::forget('configs'); Artisan::call('optimize:clear');
Artisan::call('package:discover'); Artisan::call('package:discover');
} catch (\Throwable $e) { } catch (\Throwable $e) {
Utils::e($e, '升级失败'); Utils::e($e, '升级失败');

View File

@@ -16,14 +16,15 @@ class UserService
* *
* @param array $keys * @param array $keys
* @param User|null $user 传入用户数据则会根据用户id过滤 * @param User|null $user 传入用户数据则会根据用户id过滤
* @param string $field
* @return int * @return int
*/ */
public function deleteImages(array $keys, ?User $user = null): int public function deleteImages(array $keys, ?User $user = null, string $field = 'id'): int
{ {
$count = 0; $count = 0;
$model = Image::with('user', 'strategy', 'album')->when(! is_null($user), function (Builder $builder) use ($user) { $model = Image::with('user', 'strategy', 'album')->when(! is_null($user), function (Builder $builder) use ($user) {
$builder->where('user_id', $user->id); $builder->where('user_id', $user->id);
})->whereIn('id', $keys); })->whereIn($field, $keys);
DB::transaction(function () use ($model, $keys, &$count) { DB::transaction(function () use ($model, $keys, &$count) {
/** @var Image $image */ /** @var Image $image */

View File

@@ -32,7 +32,7 @@ class Utils
*/ */
public static function getAvatar($email, int $s = 96, string $d = 'mp', string $r = 'g'): string public static function getAvatar($email, int $s = 96, string $d = 'mp', string $r = 'g'): string
{ {
$url = 'https://gravatar.cat.net/avatar/'; $url = 'https://cravatar.cn/avatar/';
$url .= md5(strtolower(trim($email))); $url .= md5(strtolower(trim($email)));
$url .= "?s=$s&d=$d&r=$r"; $url .= "?s=$s&d=$d&r=$r";
return $url; return $url;
@@ -147,7 +147,7 @@ class Utils
{ {
foreach ($array as &$value) { foreach ($array as &$value) {
if (is_array($value)) { if (is_array($value)) {
$value = self::filter($value); $value = self::filter($value, $callback, $mode);
} }
} }
return array_filter($array, $callback, $mode); return array_filter($array, $callback, $mode);

View File

@@ -12,16 +12,19 @@
"fruitcake/laravel-cors": "^2.0.5", "fruitcake/laravel-cors": "^2.0.5",
"guzzlehttp/guzzle": "^7.2", "guzzlehttp/guzzle": "^7.2",
"intervention/image": "^2.7", "intervention/image": "^2.7",
"intervention/imagecache": "^2.5",
"laravel/breeze": "^1.8", "laravel/breeze": "^1.8",
"laravel/framework": "^9.0", "laravel/framework": "^9.0",
"laravel/octane": "^1.2",
"laravel/sanctum": "^2.14", "laravel/sanctum": "^2.14",
"laravel/tinker": "^2.7", "laravel/tinker": "^2.7",
"league/flysystem-aws-s3-v3": "^3.0", "league/flysystem-aws-s3-v3": "^3.0",
"league/flysystem-ftp": "^3.0", "league/flysystem-ftp": "^3.0",
"league/flysystem-sftp": "^3.0", "league/flysystem-sftp-v3": "^3.0",
"league/flysystem-webdav": "^3.0", "league/flysystem-webdav": "^3.0",
"overtrue/flysystem-cos": "^5.0", "overtrue/flysystem-cos": "^5.0",
"overtrue/flysystem-qiniu": "^3.0", "overtrue/flysystem-qiniu": "^3.0",
"tencentcloud/ims": "^3.0",
"wispx/flysystem-upyun": "^1.0", "wispx/flysystem-upyun": "^1.0",
"zing/flysystem-oss": "^2.1" "zing/flysystem-oss": "^2.1"
}, },

3779
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -194,4 +194,11 @@ return [
// ... // ...
])->toArray(), ])->toArray(),
/*
|--------------------------------------------------------------------------
| Lsky configs
|--------------------------------------------------------------------------
*/
'thumbnail_path' => env('THUMBNAIL_PATH', 'thumbnails')
]; ];

View File

@@ -4,15 +4,21 @@
use App\Enums\ConfigKey; use App\Enums\ConfigKey;
use App\Enums\GroupConfigKey; use App\Enums\GroupConfigKey;
use App\Enums\ImagePermission;
use App\Enums\Mail\SmtpOption; use App\Enums\Mail\SmtpOption;
use App\Enums\PastedAction;
use App\Enums\Scan\AliyunOption; use App\Enums\Scan\AliyunOption;
use App\Enums\Scan\NsfwJsOption;
use App\Enums\Scan\TencentOption;
use App\Enums\UserConfigKey;
use App\Enums\Watermark\FontOption; use App\Enums\Watermark\FontOption;
use App\Enums\Watermark\ImageOption; use App\Enums\Watermark\ImageOption;
use App\Enums\Watermark\Mode;
return [ return [
'app' => [ 'app' => [
ConfigKey::AppName => 'Lsky Pro', ConfigKey::AppName => 'Lsky Pro',
ConfigKey::AppVersion => 'V 2.0', ConfigKey::AppVersion => 'V 2.1',
ConfigKey::SiteKeywords => 'Lsky Pro,lsky,兰空图床', ConfigKey::SiteKeywords => 'Lsky Pro,lsky,兰空图床',
ConfigKey::SiteDescription => 'Lsky Pro, Your photo album on the cloud.', ConfigKey::SiteDescription => 'Lsky Pro, Your photo album on the cloud.',
ConfigKey::SiteNotice => '', ConfigKey::SiteNotice => '',
@@ -37,59 +43,81 @@ return [
] ]
], ],
], ],
ConfigKey::Group => [
GroupConfigKey::MaximumFileSize => 5120,
GroupConfigKey::ConcurrentUploadNum => 3,
GroupConfigKey::IsEnableScan => 0,
GroupConfigKey::IsEnableWatermark => 0,
GroupConfigKey::IsEnableOriginalProtection => 0,
GroupConfigKey::ScannedAction => 'mark', // in mark or delete
GroupConfigKey::ScanConfigs => [
'driver' => 'aliyun',
'drivers' => [
'aliyun' => [
AliyunOption::AccessKeyId => '',
AliyunOption::AccessKeySecret => '',
AliyunOption::RegionId => '',
AliyunOption::Scenes => ['porn'],
AliyunOption::BizType => '',
],
],
],
GroupConfigKey::WatermarkConfigs => [
'driver' => 'font',
'drivers' => [
'font' => [
FontOption::Text => 'Lsky Pro',
FontOption::Position => 'bottom-right',
FontOption::Angle => 0,
FontOption::Size => 50,
FontOption::Font => '',
FontOption::Color => '#000000',
FontOption::X => 10,
FontOption::Y => 10,
],
'image' => [
ImageOption::Image => '',
ImageOption::Position => 'bottom-right',
ImageOption::Opacity => 100,
ImageOption::Rotate => 0,
ImageOption::Width => 0,
ImageOption::Height => 0,
ImageOption::X => 10,
ImageOption::Y => 10,
]
],
],
GroupConfigKey::LimitPerMinute => 20,
GroupConfigKey::LimitPerHour => 100,
GroupConfigKey::LimitPerDay => 300,
GroupConfigKey::LimitPerWeek => 600,
GroupConfigKey::LimitPerMonth => 999,
GroupConfigKey::AcceptedFileSuffixes => ['jpeg', 'jpg', 'png', 'gif', 'tif', 'bmp', 'ico', 'psd', 'webp'],
GroupConfigKey::PathNamingRule => '{Y}/{m}/{d}',
GroupConfigKey::FileNamingRule => '{uniqid}',
GroupConfigKey::ImageCacheTtl => 2626560,
],
], ],
'group' => [
GroupConfigKey::MaximumFileSize => 5120,
GroupConfigKey::ConcurrentUploadNum => 3,
GroupConfigKey::IsEnableScan => 0,
GroupConfigKey::IsEnableWatermark => 0,
GroupConfigKey::IsEnableOriginalProtection => 0,
GroupConfigKey::ScannedAction => 'mark', // in mark or delete
GroupConfigKey::ScanConfigs => [
'driver' => 'tencent',
'drivers' => [
'tencent' => [
TencentOption::Endpoint => 'ims.tencentcloudapi.com',
TencentOption::SecretId => '',
TencentOption::SecretKey => '',
TencentOption::Region => '',
TencentOption::BizType => ''
],
'aliyun' => [
AliyunOption::AccessKeyId => '',
AliyunOption::AccessKeySecret => '',
AliyunOption::RegionId => '',
AliyunOption::Scenes => ['porn'],
AliyunOption::BizType => '',
],
'nsfwjs' => [
NsfwJsOption::ApiUrl => '',
NsfwJsOption::AttrName => 'image',
NsfwJsOption::Threshold => 60,
]
],
],
GroupConfigKey::WatermarkConfigs => [
'mode' => Mode::Overlay,
'driver' => 'font',
'drivers' => [
'font' => [
FontOption::Text => 'Lsky Pro',
FontOption::Position => 'bottom-right',
FontOption::Angle => 0,
FontOption::Size => 50,
FontOption::Font => '',
FontOption::Color => '#000000',
FontOption::X => 10,
FontOption::Y => 10,
],
'image' => [
ImageOption::Image => '',
ImageOption::Position => 'bottom-right',
ImageOption::Opacity => 100,
ImageOption::Rotate => 0,
ImageOption::Width => 0,
ImageOption::Height => 0,
ImageOption::X => 10,
ImageOption::Y => 10,
]
],
],
GroupConfigKey::LimitPerMinute => 20,
GroupConfigKey::LimitPerHour => 100,
GroupConfigKey::LimitPerDay => 300,
GroupConfigKey::LimitPerWeek => 600,
GroupConfigKey::LimitPerMonth => 999,
GroupConfigKey::AcceptedFileSuffixes => ['jpeg', 'jpg', 'png', 'gif', 'tif', 'bmp', 'ico', 'psd', 'webp', 'svg'],
GroupConfigKey::ImageSaveFormat => '',
GroupConfigKey::ImageSaveQuality => 75,
GroupConfigKey::PathNamingRule => '{Y}/{m}/{d}',
GroupConfigKey::FileNamingRule => '{uniqid}',
GroupConfigKey::ImageCacheTtl => 2626560,
],
'user' => [
UserConfigKey::DefaultAlbum => 0,
UserConfigKey::DefaultStrategy => 0,
UserConfigKey::DefaultPermission => ImagePermission::Private,
UserConfigKey::PastedAction => PastedAction::Waiting,
UserConfigKey::IsAutoClearPreview => false,
]
]; ];

238
config/octane.php Normal file
View File

@@ -0,0 +1,238 @@
<?php
use Laravel\Octane\Contracts\OperationTerminated;
use Laravel\Octane\Events\RequestHandled;
use Laravel\Octane\Events\RequestReceived;
use Laravel\Octane\Events\RequestTerminated;
use Laravel\Octane\Events\TaskReceived;
use Laravel\Octane\Events\TaskTerminated;
use Laravel\Octane\Events\TickReceived;
use Laravel\Octane\Events\TickTerminated;
use Laravel\Octane\Events\WorkerErrorOccurred;
use Laravel\Octane\Events\WorkerStarting;
use Laravel\Octane\Events\WorkerStopping;
use Laravel\Octane\Listeners\CollectGarbage;
use Laravel\Octane\Listeners\DisconnectFromDatabases;
use Laravel\Octane\Listeners\EnsureUploadedFilesAreValid;
use Laravel\Octane\Listeners\EnsureUploadedFilesCanBeMoved;
use Laravel\Octane\Listeners\FlushTemporaryContainerInstances;
use Laravel\Octane\Listeners\FlushUploadedFiles;
use Laravel\Octane\Listeners\ReportException;
use Laravel\Octane\Listeners\StopWorkerIfNecessary;
use Laravel\Octane\Octane;
use Swoole\Constant;
return [
/*
|--------------------------------------------------------------------------
| Octane Server
|--------------------------------------------------------------------------
|
| This value determines the default "server" that will be used by Octane
| when starting, restarting, or stopping your server via the CLI. You
| are free to change this to the supported server of your choosing.
|
| Supported: "roadrunner", "swoole"
|
*/
'server' => env('OCTANE_SERVER', 'roadrunner'),
/*
|--------------------------------------------------------------------------
| Force HTTPS
|--------------------------------------------------------------------------
|
| When this configuration value is set to "true", Octane will inform the
| framework that all absolute links must be generated using the HTTPS
| protocol. Otherwise your links may be generated using plain HTTP.
|
*/
'https' => env('OCTANE_HTTPS', false),
/*
|--------------------------------------------------------------------------
| Octane Listeners
|--------------------------------------------------------------------------
|
| All of the event listeners for Octane's events are defined below. These
| listeners are responsible for resetting your application's state for
| the next request. You may even add your own listeners to the list.
|
*/
'listeners' => [
WorkerStarting::class => [
EnsureUploadedFilesAreValid::class,
EnsureUploadedFilesCanBeMoved::class,
],
RequestReceived::class => [
...Octane::prepareApplicationForNextOperation(),
...Octane::prepareApplicationForNextRequest(),
//
],
RequestHandled::class => [
//
],
RequestTerminated::class => [
// FlushUploadedFiles::class,
],
TaskReceived::class => [
...Octane::prepareApplicationForNextOperation(),
//
],
TaskTerminated::class => [
//
],
TickReceived::class => [
...Octane::prepareApplicationForNextOperation(),
//
],
TickTerminated::class => [
//
],
OperationTerminated::class => [
FlushTemporaryContainerInstances::class,
// DisconnectFromDatabases::class,
// CollectGarbage::class,
],
WorkerErrorOccurred::class => [
ReportException::class,
StopWorkerIfNecessary::class,
],
WorkerStopping::class => [
//
],
],
/*
|--------------------------------------------------------------------------
| Warm / Flush Bindings
|--------------------------------------------------------------------------
|
| The bindings listed below will either be pre-warmed when a worker boots
| or they will be flushed before every new request. Flushing a binding
| will force the container to resolve that binding again when asked.
|
*/
'warm' => [
...Octane::defaultServicesToWarm(),
],
'flush' => [
//
],
/*
|--------------------------------------------------------------------------
| Octane Cache Table
|--------------------------------------------------------------------------
|
| While using Swoole, you may leverage the Octane cache, which is powered
| by a Swoole table. You may set the maximum number of rows as well as
| the number of bytes per row using the configuration options below.
|
*/
'cache' => [
'rows' => 1000,
'bytes' => 10000,
],
/*
|--------------------------------------------------------------------------
| Octane Swoole Tables
|--------------------------------------------------------------------------
|
| While using Swoole, you may define additional tables as required by the
| application. These tables can be used to store data that needs to be
| quickly accessed by other workers on the particular Swoole server.
|
*/
'tables' => [
'example:1000' => [
'name' => 'string:1000',
'votes' => 'int',
],
],
/*
|--------------------------------------------------------------------------
| File Watching
|--------------------------------------------------------------------------
|
| The following list of files and directories will be watched when using
| the --watch option offered by Octane. If any of the directories and
| files are changed, Octane will automatically reload your workers.
|
*/
'watch' => [
'app',
'bootstrap',
'config',
'database',
'public/**/*.php',
'resources/**/*.php',
'routes',
'composer.lock',
'.env',
],
/*
|--------------------------------------------------------------------------
| Garbage Collection Threshold
|--------------------------------------------------------------------------
|
| When executing long-lived PHP scripts such as Octane, memory can build
| up before being cleared by PHP. You can force Octane to run garbage
| collection if your application consumes this amount of megabytes.
|
*/
'garbage' => 50,
/*
|--------------------------------------------------------------------------
| Maximum Execution Time
|--------------------------------------------------------------------------
|
| The following setting configures the maximum execution time for requests
| being handled by Octane. You may set this value to 0 to indicate that
| there isn't a specific time limit on Octane request execution time.
|
*/
'max_execution_time' => 0,
/*
|--------------------------------------------------------------------------
| Swoole
|--------------------------------------------------------------------------
|
| The Swoole options
|
*/
'swoole' => [
'options' => [
'max_request' => env('SWOOLE_MAX_REQUEST', 100000),
'package_max_length' => env('SWOOLE_PACKAGE_MAX_LENGTH', 50) * 1024 * 1024,
'buffer_output_size' => env('SWOOLE_BUFFER_OUTPUT_SIZE', 50) * 1024 * 1024,
]
]
];

View File

@@ -2,6 +2,7 @@
namespace Database\Seeders; namespace Database\Seeders;
use App\Models\Config;
use Illuminate\Database\Seeder; use Illuminate\Database\Seeder;
class DatabaseSeeder extends Seeder class DatabaseSeeder extends Seeder
@@ -13,6 +14,10 @@ class DatabaseSeeder extends Seeder
*/ */
public function run() public function run()
{ {
// $this->call([]); // 初始化系统默认配置
foreach (config('convention.app') as $key => $value) {
$content = is_array($value) ? json_encode($value, JSON_UNESCAPED_UNICODE) : $value;
Config::query()->firstOrCreate(['name' => $key], ['value' => $content]);
}
} }
} }

View File

@@ -2,7 +2,6 @@
namespace Database\Seeders; namespace Database\Seeders;
use App\Enums\ConfigKey;
use App\Enums\StrategyKey; use App\Enums\StrategyKey;
use App\Models\Group; use App\Models\Group;
use Illuminate\Database\Seeder; use Illuminate\Database\Seeder;
@@ -19,9 +18,7 @@ class InstallSeeder extends Seeder
public function run() public function run()
{ {
$date = Carbon::now()->format('Y-m-d H:i:s'); $date = Carbon::now()->format('Y-m-d H:i:s');
$array = collect(config('convention.app'))->except([ $array = collect(config('convention.app'))->transform(function ($value, $key) use ($date) {
ConfigKey::Group,
])->transform(function ($value, $key) use ($date) {
return [ return [
'name' => $key, 'name' => $key,
'value' => is_array($value) ? json_encode($value, JSON_UNESCAPED_UNICODE) : $value, 'value' => is_array($value) ? json_encode($value, JSON_UNESCAPED_UNICODE) : $value,
@@ -37,7 +34,7 @@ class InstallSeeder extends Seeder
'name' => '系统默认组&游客组', 'name' => '系统默认组&游客组',
'is_default' => true, 'is_default' => true,
'is_guest' => true, 'is_guest' => true,
'configs' => config('convention.app.group'), 'configs' => config('convention.group'),
]); ]);
// 创建默认策略 // 创建默认策略
$group->strategies()->create([ $group->strategies()->create([

5317
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -14,7 +14,7 @@
"@tailwindcss/forms": "^0.4.0", "@tailwindcss/forms": "^0.4.0",
"alpinejs": "^3.4.2", "alpinejs": "^3.4.2",
"autoprefixer": "^10.1.0", "autoprefixer": "^10.1.0",
"axios": "^0.25", "axios": "^1.8",
"blueimp-canvas-to-blob": "^3.29.0", "blueimp-canvas-to-blob": "^3.29.0",
"blueimp-file-upload": "^10.32.0", "blueimp-file-upload": "^10.32.0",
"blueimp-load-image": "^5.16.0", "blueimp-load-image": "^5.16.0",
@@ -33,7 +33,7 @@
"less-loader": "^10.2.0", "less-loader": "^10.2.0",
"lodash": "^4.17.19", "lodash": "^4.17.19",
"masonry-layout": "^4.2.2", "masonry-layout": "^4.2.2",
"postcss": "^8.2.1", "postcss": "^8.4.31",
"postcss-import": "^14.0.1", "postcss-import": "^14.0.1",
"resolve-url-loader": "^4.0.0", "resolve-url-loader": "^4.0.0",
"sweetalert2": "^11.3.3", "sweetalert2": "^11.3.3",

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -20,5 +20,6 @@
"/js/echarts/echarts.min.js": "/js/echarts/echarts.min.js", "/js/echarts/echarts.min.js": "/js/echarts/echarts.min.js",
"/js/masonry/masonry.pkgd.min.js": "/js/masonry/masonry.pkgd.min.js", "/js/masonry/masonry.pkgd.min.js": "/js/masonry/masonry.pkgd.min.js",
"/js/imagesloaded/imagesloaded.pkgd.min.js": "/js/imagesloaded/imagesloaded.pkgd.min.js", "/js/imagesloaded/imagesloaded.pkgd.min.js": "/js/imagesloaded/imagesloaded.pkgd.min.js",
"/css/markdown-css/github-markdown.css": "/css/markdown-css/github-markdown.css" "/css/markdown-css/github-markdown.css": "/css/markdown-css/github-markdown.css",
"/css/markdown-css/github-markdown-light.css": "/css/markdown-css/github-markdown-light.css"
} }

2
public/thumbnails/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
*
!.gitignore

View File

@@ -1,5 +1,7 @@
@import '~toastr'; @import '~toastr';
[x-cloak] { display: none !important; }
.scrollbar-none::-webkit-scrollbar { .scrollbar-none::-webkit-scrollbar {
display: none; display: none;
} }

View File

@@ -2,10 +2,10 @@ require('./bootstrap');
import Alpine from 'alpinejs'; import Alpine from 'alpinejs';
import Sidebar from './stores/sidebar'; import Sidebar from './stores/sidebar';
import Modal from './stores/modal' import Modal from './stores/modal';
Alpine.store('sidebar', Sidebar); Alpine.store('sidebar', Sidebar);
Alpine.store('modal', Modal) Alpine.store('modal', Modal);
window.Alpine = Alpine; window.Alpine = Alpine;
@@ -56,7 +56,7 @@ window.utils = {
|| navigator.userAgent.match(/BlackBerry/i) || navigator.userAgent.match(/BlackBerry/i)
|| navigator.userAgent.match(/Windows Phone/i) || navigator.userAgent.match(/Windows Phone/i)
) { ) {
return true; return window.screen.width < 768;
} }
return false; return false;
}, },
@@ -84,6 +84,7 @@ window.utils = {
}, },
beforeSend() { beforeSend() {
props.loading = true; props.loading = true;
props.finished = false;
$btn.text(loadingText).addClass('disabled') $btn.text(loadingText).addClass('disabled')
}, },
success(response) { success(response) {

View File

@@ -1,8 +1,41 @@
export default { export default {
open: false, state: {},
loading: false,
toggle() { open(id) {
this.open = ! this.open; this.setState(id, {open: true});
},
close(id) {
this.setState(id, {open: false});
},
isOpen(id) {
return this.getState(id).open;
},
toggle(id) {
let state = this.getState(id);
return this.setState(id, {open: state.open = ! state.open});
},
isLoading(id) {
return this.getState(id).loading ? true : false;
},
setLoading(id, loading) {
this.setState(id, loading);
},
setState(id, data) {
if (this.state[id] === undefined) {
this.state[id] = {};
}
for (let dataKey in data) {
this.state[id][dataKey] = data[dataKey];
}
},
getState(id) {
return this.state[id] || {};
} }
}; };

View File

@@ -10,7 +10,7 @@
<div class="my-6 md:my-9"> <div class="my-6 md:my-9">
<p class="mb-3 font-semibold text-lg text-gray-700">概览</p> <p class="mb-3 font-semibold text-lg text-gray-700">概览</p>
<div class="relative grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8"> <div class="relative grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
<div class="flex justify-between rounded-md bg-white p-3 overflow-hidden"> <div class="flex justify-between rounded-md bg-white p-3 overflow-hidden shadow-custom">
<div class="flex flex-col justify-between space-y-2 w-[80%]"> <div class="flex flex-col justify-between space-y-2 w-[80%]">
<p class="font-bold text-2xl text-red-700 truncate"> <p class="font-bold text-2xl text-red-700 truncate">
{{ \App\Utils::shortenNumber(\App\Models\Image::query()->count()) }} {{ \App\Utils::shortenNumber(\App\Models\Image::query()->count()) }}
@@ -19,7 +19,7 @@
</div> </div>
<i class="fas fa-images text-red-600 text-2xl"></i> <i class="fas fa-images text-red-600 text-2xl"></i>
</div> </div>
<div class="flex justify-between rounded-md bg-white p-3 overflow-hidden"> <div class="flex justify-between rounded-md bg-white p-3 overflow-hidden shadow-custom">
<div class="flex flex-col justify-between space-y-2 w-[80%]"> <div class="flex flex-col justify-between space-y-2 w-[80%]">
<p class="font-bold text-2xl text-lime-700 truncate"> <p class="font-bold text-2xl text-lime-700 truncate">
{{ \App\Utils::shortenNumber(\App\Models\Album::query()->count()) }} {{ \App\Utils::shortenNumber(\App\Models\Album::query()->count()) }}
@@ -28,7 +28,7 @@
</div> </div>
<i class="fas fa-tags text-lime-600 text-2xl"></i> <i class="fas fa-tags text-lime-600 text-2xl"></i>
</div> </div>
<div class="flex justify-between rounded-md bg-white p-3 overflow-hidden"> <div class="flex justify-between rounded-md bg-white p-3 overflow-hidden shadow-custom">
<div class="flex flex-col justify-between space-y-2 w-[80%]"> <div class="flex flex-col justify-between space-y-2 w-[80%]">
<p class="font-bold text-2xl text-blue-700 truncate"> <p class="font-bold text-2xl text-blue-700 truncate">
{{ \App\Utils::shortenNumber(\App\Models\User::query()->count()) }} {{ \App\Utils::shortenNumber(\App\Models\User::query()->count()) }}
@@ -37,7 +37,7 @@
</div> </div>
<i class="fas fa-users text-blue-600 text-2xl"></i> <i class="fas fa-users text-blue-600 text-2xl"></i>
</div> </div>
<div class="flex justify-between rounded-md bg-white p-3 overflow-hidden"> <div class="flex justify-between rounded-md bg-white p-3 overflow-hidden shadow-custom">
<div class="flex flex-col justify-between space-y-2 w-[80%]"> <div class="flex flex-col justify-between space-y-2 w-[80%]">
<p class="font-bold text-2xl text-cyan-700 truncate"> <p class="font-bold text-2xl text-cyan-700 truncate">
{{ \App\Utils::formatSize(\App\Models\Image::query()->sum('size') * 1024) }} {{ \App\Utils::formatSize(\App\Models\Image::query()->sum('size') * 1024) }}
@@ -47,28 +47,28 @@
<i class="fas fa-server text-cyan-600 text-2xl"></i> <i class="fas fa-server text-cyan-600 text-2xl"></i>
</div> </div>
<div class="flex justify-between rounded-md bg-white p-3 overflow-hidden"> <div class="flex justify-between rounded-md bg-white p-3 overflow-hidden shadow-custom">
<div class="flex flex-col justify-between space-y-2 w-[80%]"> <div class="flex flex-col justify-between space-y-2 w-[80%]">
<p class="font-bold text-2xl text-zinc-700 truncate">{{ \App\Utils::shortenNumber($numbers['today']) }}</p> <p class="font-bold text-2xl text-zinc-700 truncate">{{ \App\Utils::shortenNumber($numbers['today']) }}</p>
<p class="text-md text-gray-600">今日上传</p> <p class="text-md text-gray-600">今日上传</p>
</div> </div>
<i class="fas fa-upload text-zinc-600 text-2xl"></i> <i class="fas fa-upload text-zinc-600 text-2xl"></i>
</div> </div>
<div class="flex justify-between rounded-md bg-white p-3 overflow-hidden"> <div class="flex justify-between rounded-md bg-white p-3 overflow-hidden shadow-custom">
<div class="flex flex-col justify-between space-y-2 w-[80%]"> <div class="flex flex-col justify-between space-y-2 w-[80%]">
<p class="font-bold text-2xl text-zinc-700 truncate">{{ \App\Utils::shortenNumber($numbers['yesterday']) }}</p> <p class="font-bold text-2xl text-zinc-700 truncate">{{ \App\Utils::shortenNumber($numbers['yesterday']) }}</p>
<p class="text-md text-gray-600">昨日上传</p> <p class="text-md text-gray-600">昨日上传</p>
</div> </div>
<i class="fas fa-upload text-zinc-600 text-2xl"></i> <i class="fas fa-upload text-zinc-600 text-2xl"></i>
</div> </div>
<div class="flex justify-between rounded-md bg-white p-3 overflow-hidden"> <div class="flex justify-between rounded-md bg-white p-3 overflow-hidden shadow-custom">
<div class="flex flex-col justify-between space-y-2 w-[80%]"> <div class="flex flex-col justify-between space-y-2 w-[80%]">
<p class="font-bold text-2xl text-zinc-700 truncate">{{ \App\Utils::shortenNumber($numbers['week']) }}</p> <p class="font-bold text-2xl text-zinc-700 truncate">{{ \App\Utils::shortenNumber($numbers['week']) }}</p>
<p class="text-md text-gray-600">本周上传</p> <p class="text-md text-gray-600">本周上传</p>
</div> </div>
<i class="fas fa-upload text-zinc-600 text-2xl"></i> <i class="fas fa-upload text-zinc-600 text-2xl"></i>
</div> </div>
<div class="flex justify-between rounded-md bg-white p-3 overflow-hidden"> <div class="flex justify-between rounded-md bg-white p-3 overflow-hidden shadow-custom">
<div class="flex flex-col justify-between space-y-2 w-[80%]"> <div class="flex flex-col justify-between space-y-2 w-[80%]">
<p class="font-bold text-2xl text-zinc-700 truncate">{{ \App\Utils::shortenNumber($numbers['month']) }}</p> <p class="font-bold text-2xl text-zinc-700 truncate">{{ \App\Utils::shortenNumber($numbers['month']) }}</p>
<p class="text-md text-gray-600">本月上传</p> <p class="text-md text-gray-600">本月上传</p>
@@ -78,12 +78,12 @@
</div> </div>
<p class="mb-3 font-semibold text-lg text-gray-700">趋势</p> <p class="mb-3 font-semibold text-lg text-gray-700">趋势</p>
<div class="relative p-4 rounded-md bg-white h-80 mb-8" id="chart"> <div class="relative p-4 rounded-md bg-white h-80 mb-8 shadow-custom" id="chart">
<canvas></canvas> <canvas></canvas>
</div> </div>
<p class="mb-3 font-semibold text-lg text-gray-700">系统情况</p> <p class="mb-3 font-semibold text-lg text-gray-700">系统情况</p>
<div class="relative rounded-md bg-white mb-8 overflow-hidden"> <div class="relative rounded-md bg-white mb-8 overflow-hidden shadow-custom">
<dl> <dl>
<div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6"> <div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-medium text-gray-500">操作系统</dt> <dt class="text-sm font-medium text-gray-500">操作系统</dt>
@@ -119,7 +119,7 @@
</div> </div>
<p class="mb-3 font-semibold text-lg text-gray-700">软件信息</p> <p class="mb-3 font-semibold text-lg text-gray-700">软件信息</p>
<div class="relative rounded-md bg-white mb-8 overflow-hidden"> <div class="relative rounded-md bg-white mb-8 overflow-hidden shadow-custom">
<dl> <dl>
<div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6"> <div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-medium text-gray-500">软件版本</dt> <dt class="text-sm font-medium text-gray-500">软件版本</dt>

View File

@@ -24,7 +24,7 @@
</li> </li>
</ul> </ul>
<form action="{{ route('admin.group.create') }}" method="POST"> <form action="{{ route('admin.group.create') }}" method="POST">
<div class="overflow-hidden rounded-md rounded-l-none"> <div class="overflow-hidden rounded-md rounded-l-none shadow-custom">
<div class="px-4 py-5 bg-white sm:p-6"> <div class="px-4 py-5 bg-white sm:p-6">
<div data-tab="basic" class="grid grid-cols-6 gap-6"> <div data-tab="basic" class="grid grid-cols-6 gap-6">
<div class="col-span-6"> <div class="col-span-6">
@@ -68,7 +68,7 @@
</div> </div>
<div class="col-span-6 sm:col-span-3"> <div class="col-span-6 sm:col-span-3">
<label for="path_naming_rule" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>路径命名规则</label> <label for="path_naming_rule" class="block text-sm font-medium text-gray-700">路径命名规则</label>
<x-input type="text" name="configs[path_naming_rule]" id="path_naming_rule" autocomplete="path_naming_rule" placeholder="请输入路径命名规则" value="{{ $default->get('path_naming_rule') }}" /> <x-input type="text" name="configs[path_naming_rule]" id="path_naming_rule" autocomplete="path_naming_rule" placeholder="请输入路径命名规则" value="{{ $default->get('path_naming_rule') }}" />
<a href="javascript:void(0)" class="mt-1 text-sm text-indigo-600" id="rename-rules"><i class="fas fa-pencil-alt text-xs"></i> 命名规则对照表</a> <a href="javascript:void(0)" class="mt-1 text-sm text-indigo-600" id="rename-rules"><i class="fas fa-pencil-alt text-xs"></i> 命名规则对照表</a>
@include('admin.group.rules') @include('admin.group.rules')
@@ -79,6 +79,21 @@
<x-input type="text" name="configs[file_naming_rule]" id="file_naming_rule" autocomplete="file_naming_rule" placeholder="请输入文件命名规则" value="{{ $default->get('file_naming_rule') }}" /> <x-input type="text" name="configs[file_naming_rule]" id="file_naming_rule" autocomplete="file_naming_rule" placeholder="请输入文件命名规则" value="{{ $default->get('file_naming_rule') }}" />
</div> </div>
<div class="col-span-6 sm:col-span-3">
<label for="image_save_quality" class="block text-sm font-medium text-gray-700">图片保存质量</label>
<x-input type="number" name="configs[image_save_quality]" id="image_save_quality" autocomplete="path_naming_rule" placeholder="请输入图片保存质量" value="{{ $default->get('image_save_quality', 100) }}" />
</div>
<div class="col-span-6 sm:col-span-3">
<label for="image_save_format" class="block text-sm font-medium text-gray-700">图片转换格式</label>
<x-select id="configs[image_save_format]" name="configs[image_save_format]" autocomplete="image_save_format">
<option value="">不转换格式</option>
@foreach($default->get('accepted_file_suffixes') as $extension)
<option value="{{ strtolower($extension) }}">{{ strtoupper($extension) }}</option>
@endforeach
</x-select>
</div>
<div class="col-span-6"> <div class="col-span-6">
<x-fieldset title="是否默认" faq="设置默认后,新用户注册以后将会属于该默认角色组,且默认组只能有一个。"> <x-fieldset title="是否默认" faq="设置默认后,新用户注册以后将会属于该默认角色组,且默认组只能有一个。">
<x-switch id="is_default" name="is_default" value="1"></x-switch> <x-switch id="is_default" name="is_default" value="1"></x-switch>
@@ -116,9 +131,35 @@
</div> </div>
<div class="col-span-6 mb-4"> <div class="col-span-6 mb-4">
<x-fieldset title="审核驱动"> <x-fieldset title="审核驱动">
<x-fieldset-radio id="configs[scan_configs][driver]" name="configs[scan_configs][driver]" data-select="scan" value="aliyun" checked>阿里</x-fieldset-radio> <x-fieldset-radio id="configs[scan_configs][driver]_tencent" name="configs[scan_configs][driver]" data-select="scan" value="tencent" checked>腾讯</x-fieldset-radio>
<x-fieldset-radio id="configs[scan_configs][driver]_aliyun" name="configs[scan_configs][driver]" data-select="scan" value="aliyun">阿里云</x-fieldset-radio>
<x-fieldset-radio id="configs[scan_configs][driver]_nsfwjs" name="configs[scan_configs][driver]" data-select="scan" value="nsfwjs">NsfwJs</x-fieldset-radio>
</x-fieldset> </x-fieldset>
</div> </div>
<div class="hidden mb-4" data-scan-driver="tencent">
<div class="col-span-6 sm:col-span-3 mb-4">
<label for="configs[scan_configs][drivers][tencent][endpoint]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>Endpoint</label>
<x-input type="text" name="configs[scan_configs][drivers][tencent][endpoint]" id="configs[scan_configs][drivers][tencent][endpoint]" autocomplete="endpoint" placeholder="请输入 Endpoint" value="ims.tencentcloudapi.com" />
</div>
<div class="col-span-6 sm:col-span-3 mb-4">
<label for="configs[scan_configs][drivers][tencent][secret_id]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>SecretId</label>
<x-input type="text" name="configs[scan_configs][drivers][tencent][secret_id]" id="configs[scan_configs][drivers][tencent][secret_id]" autocomplete="secret_id" placeholder="请输入 SecretId" />
</div>
<div class="col-span-6 sm:col-span-3 mb-4">
<label for="configs[scan_configs][drivers][tencent][secret_key]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>SecretKey</label>
<x-input type="password" name="configs[scan_configs][drivers][tencent][secret_key]" id="configs[scan_configs][drivers][tencent][secret_key]" autocomplete="secret_key" placeholder="请输入 SecretKey" />
</div>
<div class="col-span-6 sm:col-span-3 mb-4">
<label for="configs[scan_configs][drivers][tencent][region]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>地域</label>
<x-input type="text" name="configs[scan_configs][drivers][tencent][region]" id="configs[scan_configs][drivers][tencent][region]" autocomplete="region" placeholder="请输入地域节点例如ap-beijing" />
</div>
<div class="col-span-6 sm:col-span-3 mb-4">
<label for="configs[scan_configs][drivers][tencent][biz_type]" class="block text-sm font-medium text-gray-700">场景名称</label>
<x-input type="text" name="configs[scan_configs][drivers][tencent][biz_type]" id="configs[scan_configs][drivers][tencent][biz_type]" autocomplete="biz_type" placeholder="业务场景名称,可为空" />
</div>
</div>
<div class="hidden mb-4" data-scan-driver="aliyun"> <div class="hidden mb-4" data-scan-driver="aliyun">
<div class="col-span-6 sm:col-span-3 mb-4"> <div class="col-span-6 sm:col-span-3 mb-4">
<label for="configs[scan_configs][drivers][aliyun][access_key_id]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>AccessKeyId</label> <label for="configs[scan_configs][drivers][aliyun][access_key_id]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>AccessKeyId</label>
@@ -128,14 +169,14 @@
<label for="configs[scan_configs][drivers][aliyun][access_key_secret]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>AccessKeySecret</label> <label for="configs[scan_configs][drivers][aliyun][access_key_secret]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>AccessKeySecret</label>
<x-input type="password" name="configs[scan_configs][drivers][aliyun][access_key_secret]" id="configs[scan_configs][drivers][aliyun][access_key_secret]" autocomplete="access_key_id" placeholder="请输入 AccessKeySecret" /> <x-input type="password" name="configs[scan_configs][drivers][aliyun][access_key_secret]" id="configs[scan_configs][drivers][aliyun][access_key_secret]" autocomplete="access_key_id" placeholder="请输入 AccessKeySecret" />
</div> </div>
<div class="col-span-6 sm:col-span-3 mb-4">
<label for="configs[scan_configs][drivers][aliyun][biz_type]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>场景名称</label>
<x-input type="text" name="configs[scan_configs][drivers][aliyun][biz_type]" id="configs[scan_configs][drivers][aliyun][biz_type]" autocomplete="biz_type" placeholder="请输入业务场景名称" />
</div>
<div class="col-span-6 sm:col-span-3 mb-4"> <div class="col-span-6 sm:col-span-3 mb-4">
<label for="configs[scan_configs][drivers][aliyun][region_id]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>地域节点</label> <label for="configs[scan_configs][drivers][aliyun][region_id]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>地域节点</label>
<x-input type="text" name="configs[scan_configs][drivers][aliyun][region_id]" id="configs[scan_configs][drivers][aliyun][region_id]" autocomplete="region_id" placeholder="请输入地域节点例如cn-shanghai" /> <x-input type="text" name="configs[scan_configs][drivers][aliyun][region_id]" id="configs[scan_configs][drivers][aliyun][region_id]" autocomplete="region_id" placeholder="请输入地域节点例如cn-shanghai" />
</div> </div>
<div class="col-span-6 sm:col-span-3 mb-4">
<label for="configs[scan_configs][drivers][aliyun][biz_type]" class="block text-sm font-medium text-gray-700">场景名称</label>
<x-input type="text" name="configs[scan_configs][drivers][aliyun][biz_type]" id="configs[scan_configs][drivers][aliyun][biz_type]" autocomplete="biz_type" placeholder="请输入业务场景名称" />
</div>
<div class="col-span-6 sm:col-span-3 mb-4"> <div class="col-span-6 sm:col-span-3 mb-4">
<x-fieldset title="审核场景"> <x-fieldset title="审核场景">
@foreach($scenes as $key => $scene) @foreach($scenes as $key => $scene)
@@ -144,6 +185,22 @@
</x-fieldset> </x-fieldset>
</div> </div>
</div> </div>
<div class="hidden mb-4" data-scan-driver="nsfwjs">
<div class="col-span-6 sm:col-span-3 mb-4">
<label for="configs[scan_configs][drivers][nsfwjs][api_url]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>接口地址</label>
<x-input type="url" name="configs[scan_configs][drivers][nsfwjs][api_url]" id="configs[scan_configs][drivers][nsfwjs][api_url]" autocomplete="api_url" placeholder="请输入接口地址http(s)://domain.com/classify" />
</div>
<div class="col-span-6 sm:col-span-3 mb-4">
<label for="configs[scan_configs][drivers][nsfwjs][attr_name]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>属性名称</label>
<x-input type="text" name="configs[scan_configs][drivers][nsfwjs][attr_name]" id="configs[scan_configs][drivers][nsfwjs][attr_name]" autocomplete="attr_name" placeholder="接口的表单文件属性名" value="image" />
</div>
<div class="col-span-6 sm:col-span-3 mb-4">
<label for="configs[scan_configs][drivers][nsfwjs][threshold]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>阈值</label>
<x-input type="number" name="configs[scan_configs][drivers][nsfwjs][threshold]" id="configs[scan_configs][drivers][nsfwjs][threshold]" autocomplete="threshold" placeholder="取值 1-100" value="60" />
<small class="text-gray-500"><i class="fas fa-exclamation-circle"></i> 阈值是指图片违规程度上限,取值 1-100 之间,数值越低审核越严格</small>
</div>
</div>
</div> </div>
<div data-tab="protection" class="hidden grid grid-cols-6 gap-6"> <div data-tab="protection" class="hidden grid grid-cols-6 gap-6">
@@ -162,9 +219,15 @@
<div data-tab="watermark" class="hidden grid grid-cols-6 gap-6"> <div data-tab="watermark" class="hidden grid grid-cols-6 gap-6">
<p class="mb-3 text-red-600 text-sm"><i class="fas fa-exclamation"></i> 开启水印功能前请注意考虑图片版权问题。</p> <p class="mb-3 text-red-600 text-sm"><i class="fas fa-exclamation"></i> 开启水印功能前请注意考虑图片版权问题。</p>
<div class="col-span-6 mb-4"> <div class="col-span-6 mb-4">
<x-fieldset title="开启水印" faq="请注意,水印功能仅在开启了「原图保护」功能的情况下生效。"> <x-fieldset title="开启水印" faq="请注意,水印模式为动态生成时,仅在开启了「原图保护」功能的情况下生效。">
<x-switch id="configs[is_enable_watermark]" name="configs[is_enable_watermark]" value="1"></x-switch> <x-switch id="configs[is_enable_watermark]" name="configs[is_enable_watermark]" value="1"></x-switch>
</x-fieldset> </x-fieldset>
<div class="col-span-6 mt-4 mb-4">
<x-fieldset title="水印模式">
<x-fieldset-radio id="configs[watermark_configs][mode]_overlay" name="configs[watermark_configs][mode]" value="{{ \App\Enums\Watermark\Mode::Overlay }}" checked>覆盖原图</x-fieldset-radio>
<x-fieldset-radio id="configs[watermark_configs][mode]_dynamic" name="configs[watermark_configs][mode]" value="{{ \App\Enums\Watermark\Mode::Dynamic }}">动态生成</x-fieldset-radio>
</x-fieldset>
</div>
<div class="col-span-6 mt-4 mb-4"> <div class="col-span-6 mt-4 mb-4">
<x-fieldset title="水印类型"> <x-fieldset title="水印类型">
<x-fieldset-radio id="configs[watermark_configs][driver]_font" name="configs[watermark_configs][driver]" data-select="watermark" value="font" checked>文字水印</x-fieldset-radio> <x-fieldset-radio id="configs[watermark_configs][driver]_font" name="configs[watermark_configs][driver]" data-select="watermark" value="font" checked>文字水印</x-fieldset-radio>
@@ -214,7 +277,7 @@
<div class="col-span-6 sm:col-span-3 mb-4"> <div class="col-span-6 sm:col-span-3 mb-4">
<label for="configs[watermark_configs][drivers][image][image]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>水印图片</label> <label for="configs[watermark_configs][drivers][image][image]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>水印图片</label>
<x-input type="text" name="configs[watermark_configs][drivers][image][image]" id="configs[watermark_configs][drivers][image][image]" autocomplete="image" placeholder="请输入水印路径例如images/lsky.png" /> <x-input type="text" name="configs[watermark_configs][drivers][image][image]" id="configs[watermark_configs][drivers][image][image]" autocomplete="image" placeholder="请输入水印路径例如images/lsky.png" />
<small class="text-yellow-500">请将水印图片放置 {{ public_path() }} 目录下</small> <small class="text-yellow-500">请将水印图片放置 {{ storage_path('app/public') }} 目录下</small>
</div> </div>
<div class="col-span-6 sm:col-span-3 mb-4"> <div class="col-span-6 sm:col-span-3 mb-4">
<label for="configs[watermark_configs][drivers][image][position]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>水印位置</label> <label for="configs[watermark_configs][drivers][image][position]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>水印位置</label>

View File

@@ -20,7 +20,7 @@
</li> </li>
</ul> </ul>
<form action="{{ route('admin.group.update', ['id' => $group->id]) }}" method="POST"> <form action="{{ route('admin.group.update', ['id' => $group->id]) }}" method="POST">
<div class="overflow-hidden rounded-md rounded-l-none"> <div class="overflow-hidden rounded-md rounded-l-none shadow-custom">
<div class="px-4 py-5 bg-white sm:p-6"> <div class="px-4 py-5 bg-white sm:p-6">
<div data-tab="basic" class="grid grid-cols-6 gap-6"> <div data-tab="basic" class="grid grid-cols-6 gap-6">
<div class="col-span-6"> <div class="col-span-6">
@@ -30,49 +30,64 @@
<div class="col-span-6"> <div class="col-span-6">
<label for="maximum_file_size" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>最大文件大小(KB)</label> <label for="maximum_file_size" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>最大文件大小(KB)</label>
<x-input type="number" name="configs[maximum_file_size]" id="maximum_file_size" autocomplete="maximum_file_size" placeholder="请输入上传文件的最大限制单位kb" value="{{ $group->configs['maximum_file_size'] }}" /> <x-input type="number" name="configs[maximum_file_size]" id="maximum_file_size" autocomplete="maximum_file_size" placeholder="请输入上传文件的最大限制单位kb" value="{{ $group->configs->get('maximum_file_size') }}" />
</div> </div>
<div class="col-span-6 sm:col-span-3"> <div class="col-span-6 sm:col-span-3">
<label for="concurrent_upload_num" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>并发上传限制</label> <label for="concurrent_upload_num" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>并发上传限制</label>
<x-input type="number" name="configs[concurrent_upload_num]" id="concurrent_upload_num" autocomplete="concurrent_upload_num" placeholder="请输入并发上传数量" value="{{ $group->configs['concurrent_upload_num'] }}" /> <x-input type="number" name="configs[concurrent_upload_num]" id="concurrent_upload_num" autocomplete="concurrent_upload_num" placeholder="请输入并发上传数量" value="{{ $group->configs->get('concurrent_upload_num') }}" />
</div> </div>
<div class="col-span-6 sm:col-span-3"> <div class="col-span-6 sm:col-span-3">
<label for="limit_per_minute" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>每分钟上传限制</label> <label for="limit_per_minute" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>每分钟上传限制</label>
<x-input type="number" name="configs[limit_per_minute]" id="limit_per_minute" autocomplete="limit_per_minute" placeholder="请输入每分钟可以上传的图片数量" value="{{ $group->configs['limit_per_minute'] }}" /> <x-input type="number" name="configs[limit_per_minute]" id="limit_per_minute" autocomplete="limit_per_minute" placeholder="请输入每分钟可以上传的图片数量" value="{{ $group->configs->get('limit_per_minute') }}" />
</div> </div>
<div class="col-span-6 sm:col-span-3"> <div class="col-span-6 sm:col-span-3">
<label for="limit_per_hour" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>每小时上传限制</label> <label for="limit_per_hour" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>每小时上传限制</label>
<x-input type="number" name="configs[limit_per_hour]" id="limit_per_hour" autocomplete="limit_per_hour" placeholder="请输入每小时可以上传的图片数量" value="{{ $group->configs['limit_per_hour'] }}" /> <x-input type="number" name="configs[limit_per_hour]" id="limit_per_hour" autocomplete="limit_per_hour" placeholder="请输入每小时可以上传的图片数量" value="{{ $group->configs->get('limit_per_hour') }}" />
</div> </div>
<div class="col-span-6 sm:col-span-3"> <div class="col-span-6 sm:col-span-3">
<label for="limit_per_day" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>每天上传限制</label> <label for="limit_per_day" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>每天上传限制</label>
<x-input type="number" name="configs[limit_per_day]" id="limit_per_day" autocomplete="limit_per_day" placeholder="请输入每天可以上传的图片数量" value="{{ $group->configs['limit_per_day'] }}" /> <x-input type="number" name="configs[limit_per_day]" id="limit_per_day" autocomplete="limit_per_day" placeholder="请输入每天可以上传的图片数量" value="{{ $group->configs->get('limit_per_day') }}" />
</div> </div>
<div class="col-span-6 sm:col-span-3"> <div class="col-span-6 sm:col-span-3">
<label for="limit_per_week" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>每周上传限制</label> <label for="limit_per_week" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>每周上传限制</label>
<x-input type="number" name="configs[limit_per_week]" id="limit_per_week" autocomplete="limit_per_week" placeholder="请输入每周可以上传的图片数量" value="{{ $group->configs['limit_per_week'] }}" /> <x-input type="number" name="configs[limit_per_week]" id="limit_per_week" autocomplete="limit_per_week" placeholder="请输入每周可以上传的图片数量" value="{{ $group->configs->get('limit_per_week') }}" />
</div> </div>
<div class="col-span-6 sm:col-span-3"> <div class="col-span-6 sm:col-span-3">
<label for="limit_per_month" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>每月上传限制</label> <label for="limit_per_month" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>每月上传限制</label>
<x-input type="number" name="configs[limit_per_month]" id="limit_per_month" autocomplete="limit_per_month" placeholder="请输入每月可以上传的图片数量" value="{{ $group->configs['limit_per_month'] }}" /> <x-input type="number" name="configs[limit_per_month]" id="limit_per_month" autocomplete="limit_per_month" placeholder="请输入每月可以上传的图片数量" value="{{ $group->configs->get('limit_per_month') }}" />
</div> </div>
<div class="col-span-6 sm:col-span-3"> <div class="col-span-6 sm:col-span-3">
<label for="path_naming_rule" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>路径命名规则</label> <label for="path_naming_rule" class="block text-sm font-medium text-gray-700">路径命名规则</label>
<x-input type="text" name="configs[path_naming_rule]" id="path_naming_rule" autocomplete="path_naming_rule" placeholder="请输入路径命名规则" value="{{ $group->configs['path_naming_rule'] }}" /> <x-input type="text" name="configs[path_naming_rule]" id="path_naming_rule" autocomplete="path_naming_rule" placeholder="请输入路径命名规则" value="{{ $group->configs->get('path_naming_rule') }}" />
<a href="javascript:void(0)" class="mt-1 text-sm text-indigo-600" id="rename-rules"><i class="fas fa-pencil-alt text-xs"></i> 命名规则对照表</a> <a href="javascript:void(0)" class="mt-1 text-sm text-indigo-600" id="rename-rules"><i class="fas fa-pencil-alt text-xs"></i> 命名规则对照表</a>
@include('admin.group.rules') @include('admin.group.rules')
</div> </div>
<div class="col-span-6 sm:col-span-3"> <div class="col-span-6 sm:col-span-3">
<label for="file_naming_rule" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>文件命名规则</label> <label for="file_naming_rule" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>文件命名规则</label>
<x-input type="text" name="configs[file_naming_rule]" id="file_naming_rule" autocomplete="file_naming_rule" placeholder="请输入文件命名规则" value="{{ $group->configs['file_naming_rule'] }}" /> <x-input type="text" name="configs[file_naming_rule]" id="file_naming_rule" autocomplete="file_naming_rule" placeholder="请输入文件命名规则" value="{{ $group->configs->get('file_naming_rule') }}" />
</div>
<div class="col-span-6 sm:col-span-3">
<label for="image_save_quality" class="block text-sm font-medium text-gray-700">图片保存质量</label>
<x-input type="number" name="configs[image_save_quality]" id="image_save_quality" autocomplete="path_naming_rule" placeholder="请输入图片保存质量" value="{{ $group->configs->get('image_save_quality', 100) }}" />
</div>
<div class="col-span-6 sm:col-span-3">
<label for="image_save_format" class="block text-sm font-medium text-gray-700">图片转换格式</label>
<x-select id="configs[image_save_format]" name="configs[image_save_format]" autocomplete="image_save_format">
<option value="">不转换格式</option>
@foreach($default->get('accepted_file_suffixes') as $extension)
<option value="{{ strtolower($extension) }}" @selected($group->configs->get('image_save_format') === $extension)>{{ strtoupper($extension) }}</option>
@endforeach
</x-select>
</div> </div>
<div class="col-span-6"> <div class="col-span-6">
@@ -90,7 +105,7 @@
<div class="col-span-6"> <div class="col-span-6">
<x-fieldset title="允许上传的图片类型"> <x-fieldset title="允许上传的图片类型">
@foreach($default['accepted_file_suffixes'] as $extension) @foreach($default['accepted_file_suffixes'] as $extension)
<x-fieldset-checkbox id="configs[accepted_file_suffixes]_{{ $extension }}" name="configs[accepted_file_suffixes][]" value="{{ $extension }}" :checked="in_array($extension, $group->configs['accepted_file_suffixes'])"> <x-fieldset-checkbox id="configs[accepted_file_suffixes]_{{ $extension }}" name="configs[accepted_file_suffixes][]" value="{{ $extension }}" :checked="in_array($extension, $group->configs->get('accepted_file_suffixes'))">
{{ strtoupper($extension) }} {{ strtoupper($extension) }}
</x-fieldset-checkbox> </x-fieldset-checkbox>
@endforeach @endforeach
@@ -101,57 +116,99 @@
<div data-tab="review" class="hidden grid grid-cols-6 gap-6"> <div data-tab="review" class="hidden grid grid-cols-6 gap-6">
<div class="col-span-6 mb-4"> <div class="col-span-6 mb-4">
<x-fieldset title="图片审核" faq="设置上传是否需要应用第三方审查,违规的图片会被标记为不健康的图片,或直接被删除。"> <x-fieldset title="图片审核" faq="设置上传是否需要应用第三方审查,违规的图片会被标记为不健康的图片,或直接被删除。">
<x-switch id="configs[is_enable_scan]" name="configs[is_enable_scan]" value="1" :checked="(bool)$group->configs['is_enable_scan']"></x-switch> <x-switch id="configs[is_enable_scan]" name="configs[is_enable_scan]" value="1" :checked="(bool)$group->configs->get('is_enable_scan')"></x-switch>
</x-fieldset> </x-fieldset>
</div> </div>
<div class="col-span-6 mb-4"> <div class="col-span-6 mb-4">
<x-fieldset title="审核动作"> <x-fieldset title="审核动作">
<x-fieldset-radio id="configs[scanned_action]_mark" name="configs[scanned_action]" value="mark" :checked="$group->configs['scanned_action'] === 'mark'">标记为不健康</x-fieldset-radio> <x-fieldset-radio id="configs[scanned_action]_mark" name="configs[scanned_action]" value="mark" :checked="$group->configs->get('scanned_action') === 'mark'">标记为不健康</x-fieldset-radio>
<x-fieldset-radio id="configs[scanned_action]_delete" name="configs[scanned_action]" value="delete" :checked="$group->configs['scanned_action'] === 'delete'">直接删除</x-fieldset-radio> <x-fieldset-radio id="configs[scanned_action]_delete" name="configs[scanned_action]" value="delete" :checked="$group->configs->get('scanned_action') === 'delete'">直接删除</x-fieldset-radio>
</x-fieldset> </x-fieldset>
</div> </div>
<div class="col-span-6 mb-4"> <div class="col-span-6 mb-4">
<x-fieldset title="审核驱动"> <x-fieldset title="审核驱动">
<x-fieldset-radio id="configs[scan_configs][driver]" name="configs[scan_configs][driver]" data-select="scan" value="aliyun" :checked="$group->configs['scan_configs']['driver'] === 'aliyun'">阿里</x-fieldset-radio> <x-fieldset-radio id="configs[scan_configs][driver]_tencent" name="configs[scan_configs][driver]" data-select="scan" value="tencent" :checked="($group->configs['scan_configs']['driver'] ?? '') === 'tencent'">腾讯</x-fieldset-radio>
<x-fieldset-radio id="configs[scan_configs][driver]_aliyun" name="configs[scan_configs][driver]" data-select="scan" value="aliyun" :checked="($group->configs['scan_configs']['driver'] ?? '') === 'aliyun'">阿里云</x-fieldset-radio>
<x-fieldset-radio id="configs[scan_configs][driver]_nsfwjs" name="configs[scan_configs][driver]" data-select="scan" value="nsfwjs" :checked="($group->configs['scan_configs']['driver'] ?? '') === 'nsfwjs'">NsfwJs</x-fieldset-radio>
</x-fieldset> </x-fieldset>
</div> </div>
<div class="hidden mb-4" data-scan-driver="tencent">
<div class="col-span-6 sm:col-span-3 mb-4">
<label for="configs[scan_configs][drivers][tencent][endpoint]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>Endpoint</label>
<x-input type="text" name="configs[scan_configs][drivers][tencent][endpoint]" id="configs[scan_configs][drivers][tencent][endpoint]" autocomplete="endpoint" placeholder="请输入 Endpoint" value="{{ $group->configs['scan_configs']['drivers']['tencent']['endpoint'] ?? '' }}" />
</div>
<div class="col-span-6 sm:col-span-3 mb-4">
<label for="configs[scan_configs][drivers][tencent][secret_id]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>SecretId</label>
<x-input type="text" name="configs[scan_configs][drivers][tencent][secret_id]" id="configs[scan_configs][drivers][tencent][secret_id]" autocomplete="secret_id" placeholder="请输入 SecretId" value="{{ $group->configs['scan_configs']['drivers']['tencent']['secret_id'] ?? '' }}" />
</div>
<div class="col-span-6 sm:col-span-3 mb-4">
<label for="configs[scan_configs][drivers][tencent][secret_key]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>SecretKey</label>
<x-input type="password" name="configs[scan_configs][drivers][tencent][secret_key]" id="configs[scan_configs][drivers][tencent][secret_key]" autocomplete="secret_key" placeholder="请输入 SecretKey" value="{{ $group->configs['scan_configs']['drivers']['tencent']['secret_key'] ?? '' }}" />
</div>
<div class="col-span-6 sm:col-span-3 mb-4">
<label for="configs[scan_configs][drivers][tencent][region]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>地域</label>
<x-input type="text" name="configs[scan_configs][drivers][tencent][region]" id="configs[scan_configs][drivers][tencent][region]" autocomplete="region" placeholder="请输入地域节点例如ap-beijing" value="{{ $group->configs['scan_configs']['drivers']['tencent']['region'] ?? '' }}" />
</div>
<div class="col-span-6 sm:col-span-3 mb-4">
<label for="configs[scan_configs][drivers][tencent][biz_type]" class="block text-sm font-medium text-gray-700">场景名称</label>
<x-input type="text" name="configs[scan_configs][drivers][tencent][biz_type]" id="configs[scan_configs][drivers][tencent][biz_type]" autocomplete="biz_type" placeholder="业务场景名称,可为空" value="{{ $group->configs['scan_configs']['drivers']['tencent']['biz_type'] ?? '' }}" />
</div>
</div>
<div class="hidden mb-4" data-scan-driver="aliyun"> <div class="hidden mb-4" data-scan-driver="aliyun">
<div class="col-span-6 sm:col-span-3 mb-4"> <div class="col-span-6 sm:col-span-3 mb-4">
<label for="configs[scan_configs][drivers][aliyun][access_key_id]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>AccessKeyId</label> <label for="configs[scan_configs][drivers][aliyun][access_key_id]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>AccessKeyId</label>
<x-input type="text" name="configs[scan_configs][drivers][aliyun][access_key_id]" id="configs[scan_configs][drivers][aliyun][access_key_id]" autocomplete="access_key_id" placeholder="请输入 AccessKeyId" value="{{ $group->configs['scan_configs']['drivers']['aliyun']['access_key_id'] }}" /> <x-input type="text" name="configs[scan_configs][drivers][aliyun][access_key_id]" id="configs[scan_configs][drivers][aliyun][access_key_id]" autocomplete="access_key_id" placeholder="请输入 AccessKeyId" value="{{ $group->configs['scan_configs']['drivers']['aliyun']['access_key_id'] ?? '' }}" />
</div> </div>
<div class="col-span-6 sm:col-span-3 mb-4"> <div class="col-span-6 sm:col-span-3 mb-4">
<label for="configs[scan_configs][drivers][aliyun][access_key_secret]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>AccessKeySecret</label> <label for="configs[scan_configs][drivers][aliyun][access_key_secret]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>AccessKeySecret</label>
<x-input type="password" name="configs[scan_configs][drivers][aliyun][access_key_secret]" id="configs[scan_configs][drivers][aliyun][access_key_secret]" autocomplete="access_key_id" placeholder="请输入 AccessKeySecret" value="{{ $group->configs['scan_configs']['drivers']['aliyun']['access_key_secret'] }}" /> <x-input type="password" name="configs[scan_configs][drivers][aliyun][access_key_secret]" id="configs[scan_configs][drivers][aliyun][access_key_secret]" autocomplete="access_key_id" placeholder="请输入 AccessKeySecret" value="{{ $group->configs['scan_configs']['drivers']['aliyun']['access_key_secret'] ?? '' }}" />
</div>
<div class="col-span-6 sm:col-span-3 mb-4">
<label for="configs[scan_configs][drivers][aliyun][biz_type]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>场景名称</label>
<x-input type="text" name="configs[scan_configs][drivers][aliyun][biz_type]" id="configs[scan_configs][drivers][aliyun][biz_type]" autocomplete="biz_type" placeholder="请输入业务场景名称" value="{{ $group->configs['scan_configs']['drivers']['aliyun']['biz_type'] }}" />
</div> </div>
<div class="col-span-6 sm:col-span-3 mb-4"> <div class="col-span-6 sm:col-span-3 mb-4">
<label for="configs[scan_configs][drivers][aliyun][region_id]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>地域节点</label> <label for="configs[scan_configs][drivers][aliyun][region_id]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>地域节点</label>
<x-input type="text" name="configs[scan_configs][drivers][aliyun][region_id]" id="configs[scan_configs][drivers][aliyun][region_id]" autocomplete="region_id" placeholder="请输入地域节点例如cn-shanghai" value="{{ $group->configs['scan_configs']['drivers']['aliyun']['region_id'] }}" /> <x-input type="text" name="configs[scan_configs][drivers][aliyun][region_id]" id="configs[scan_configs][drivers][aliyun][region_id]" autocomplete="region_id" placeholder="请输入地域节点例如cn-shanghai" value="{{ $group->configs['scan_configs']['drivers']['aliyun']['region_id'] ?? '' }}" />
</div>
<div class="col-span-6 sm:col-span-3 mb-4">
<label for="configs[scan_configs][drivers][aliyun][biz_type]" class="block text-sm font-medium text-gray-700">场景名称</label>
<x-input type="text" name="configs[scan_configs][drivers][aliyun][biz_type]" id="configs[scan_configs][drivers][aliyun][biz_type]" autocomplete="biz_type" placeholder="请输入业务场景名称" value="{{ $group->configs['scan_configs']['drivers']['aliyun']['biz_type'] ?? '' }}" />
</div> </div>
<div class="col-span-6 sm:col-span-3 mb-4"> <div class="col-span-6 sm:col-span-3 mb-4">
<x-fieldset title="审核场景"> <x-fieldset title="审核场景">
@foreach($scenes as $key => $scene) @foreach($scenes as $key => $scene)
<x-fieldset-checkbox id="configs[scan_configs][drivers][aliyun][scenes][]_{{ $key }}" name="configs[scan_configs][drivers][aliyun][scenes][]" value="{{ $key }}" :checked="in_array($key, $group->configs['scan_configs']['drivers']['aliyun']['scenes'])">{{ $scene }}</x-fieldset-checkbox> <x-fieldset-checkbox id="configs[scan_configs][drivers][aliyun][scenes][]_{{ $key }}" name="configs[scan_configs][drivers][aliyun][scenes][]" value="{{ $key }}" :checked="in_array($key, ($group->configs['scan_configs']['drivers']['aliyun']['scenes'] ?? []))">{{ $scene }}</x-fieldset-checkbox>
@endforeach @endforeach
</x-fieldset> </x-fieldset>
</div> </div>
</div> </div>
<div class="hidden mb-4" data-scan-driver="nsfwjs">
<div class="col-span-6 sm:col-span-3 mb-4">
<label for="configs[scan_configs][drivers][nsfwjs][api_url]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>接口地址</label>
<x-input type="url" name="configs[scan_configs][drivers][nsfwjs][api_url]" id="configs[scan_configs][drivers][nsfwjs][api_url]" autocomplete="api_url" placeholder="请输入接口地址http(s)://domain.com/classify" value="{{ $group->configs['scan_configs']['drivers']['nsfwjs']['api_url'] ?? '' }}" />
</div>
<div class="col-span-6 sm:col-span-3 mb-4">
<label for="configs[scan_configs][drivers][nsfwjs][attr_name]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>属性名称</label>
<x-input type="text" name="configs[scan_configs][drivers][nsfwjs][attr_name]" id="configs[scan_configs][drivers][nsfwjs][attr_name]" autocomplete="attr_name" placeholder="接口的表单文件属性名" value="{{ $group->configs['scan_configs']['drivers']['nsfwjs']['attr_name'] ?? '' }}" />
</div>
<div class="col-span-6 sm:col-span-3 mb-4">
<label for="configs[scan_configs][drivers][nsfwjs][threshold]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>阈值</label>
<x-input type="number" name="configs[scan_configs][drivers][nsfwjs][threshold]" id="configs[scan_configs][drivers][nsfwjs][threshold]" autocomplete="threshold" placeholder="取值 1-100" value="{{ $group->configs['scan_configs']['drivers']['nsfwjs']['threshold'] ?? '' }}" />
<small class="text-gray-500"><i class="fas fa-exclamation-circle"></i> 阈值是指图片违规程度上限,取值 1-100 之间,数值越低审核越严格</small>
</div>
</div>
</div> </div>
<div data-tab="protection" class="hidden grid grid-cols-6 gap-6"> <div data-tab="protection" class="hidden grid grid-cols-6 gap-6">
<div class="col-span-6 mb-4"> <div class="col-span-6 mb-4">
<x-fieldset title="原图保护" faq="设置该角色组下的用户上传的图片是否应用原图保护功能,开启后图片<b>不返回直链</b>"> <x-fieldset title="原图保护" faq="设置该角色组下的用户上传的图片是否应用原图保护功能,开启后图片<b>不返回直链</b>">
<x-switch id="configs[is_enable_original_protection]" name="configs[is_enable_original_protection]" value="1" :checked="(bool)$group->configs['is_enable_original_protection']"></x-switch> <x-switch id="configs[is_enable_original_protection]" name="configs[is_enable_original_protection]" value="1" :checked="(bool)$group->configs->get('is_enable_original_protection')"></x-switch>
</x-fieldset> </x-fieldset>
</div> </div>
<div class="col-span-6 mb-4"> <div class="col-span-6 mb-4">
<label for="configs[image_cache_ttl]" class="block text-sm font-medium text-gray-700">图片缓存时间()</label> <label for="configs[image_cache_ttl]" class="block text-sm font-medium text-gray-700">图片缓存时间()</label>
<x-input type="number" name="configs[image_cache_ttl]" id="configs[image_cache_ttl]" autocomplete="image_cache_ttl" placeholder="请输入受保护图片的缓存时间不填或填0表示不缓存" value="{{ $group->configs['image_cache_ttl'] }}" /> <x-input type="number" name="configs[image_cache_ttl]" id="configs[image_cache_ttl]" autocomplete="image_cache_ttl" placeholder="请输入受保护图片的缓存时间不填或填0表示不缓存" value="{{ $group->configs->get('image_cache_ttl') }}" />
</div> </div>
<a href="javascript:void(0)" id="clear-cache" class="text-sm text-red-500"> <a href="javascript:void(0)" id="clear-cache" class="text-sm text-red-500">
@@ -162,91 +219,97 @@
<div data-tab="watermark" class="hidden grid grid-cols-6 gap-6"> <div data-tab="watermark" class="hidden grid grid-cols-6 gap-6">
<p class="mb-3 text-red-600 text-sm"><i class="fas fa-exclamation"></i> 开启水印功能前请注意考虑图片版权问题。</p> <p class="mb-3 text-red-600 text-sm"><i class="fas fa-exclamation"></i> 开启水印功能前请注意考虑图片版权问题。</p>
<div class="col-span-6 mb-4"> <div class="col-span-6 mb-4">
<x-fieldset title="开启水印" faq="请注意,水印功能仅在开启了「原图保护」功能的情况下生效。"> <x-fieldset title="开启水印" faq="请注意,水印模式为动态生成时,仅在开启了「原图保护」功能的情况下生效。">
<x-switch id="configs[is_enable_watermark]" name="configs[is_enable_watermark]" value="1" :checked="(bool)$group->configs['is_enable_watermark']"></x-switch> <x-switch id="configs[is_enable_watermark]" name="configs[is_enable_watermark]" value="1" :checked="(bool)$group->configs->get('is_enable_watermark')"></x-switch>
</x-fieldset> </x-fieldset>
<div class="col-span-6 mt-4 mb-4">
<x-fieldset title="水印模式">
<x-fieldset-radio id="configs[watermark_configs][mode]_overlay" name="configs[watermark_configs][mode]" value="{{ \App\Enums\Watermark\Mode::Overlay }}" :checked="($group->configs['watermark_configs']['mode'] ?? '') == \App\Enums\Watermark\Mode::Overlay">覆盖原图</x-fieldset-radio>
<x-fieldset-radio id="configs[watermark_configs][mode]_dynamic" name="configs[watermark_configs][mode]" value="{{ \App\Enums\Watermark\Mode::Dynamic }}" :checked="($group->configs['watermark_configs']['mode'] ?? '') == \App\Enums\Watermark\Mode::Dynamic">动态生成</x-fieldset-radio>
</x-fieldset>
</div>
<div class="col-span-6 mt-4 mb-4"> <div class="col-span-6 mt-4 mb-4">
<x-fieldset title="水印类型"> <x-fieldset title="水印类型">
<x-fieldset-radio id="configs[watermark_configs][driver]_font" name="configs[watermark_configs][driver]" data-select="watermark" value="font" :checked="$group->configs['watermark_configs']['driver'] === 'font'">文字水印</x-fieldset-radio> <x-fieldset-radio id="configs[watermark_configs][driver]_font" name="configs[watermark_configs][driver]" data-select="watermark" value="font" :checked="($group->configs['watermark_configs']['driver'] ?? '') === 'font'">文字水印</x-fieldset-radio>
<x-fieldset-radio id="configs[watermark_configs][driver]_image" name="configs[watermark_configs][driver]" data-select="watermark" value="image" :checked="$group->configs['watermark_configs']['driver'] === 'image'">图片水印</x-fieldset-radio> <x-fieldset-radio id="configs[watermark_configs][driver]_image" name="configs[watermark_configs][driver]" data-select="watermark" value="image" :checked="($group->configs['watermark_configs']['driver'] ?? '') === 'image'">图片水印</x-fieldset-radio>
</x-fieldset> </x-fieldset>
</div> </div>
<div class="mb-4 hidden" data-watermark-driver="font"> <div class="mb-4 hidden" data-watermark-driver="font">
<div class="col-span-6 sm:col-span-3 mb-4"> <div class="col-span-6 sm:col-span-3 mb-4">
<label for="configs[watermark_configs][drivers][font][font]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>字体文件</label> <label for="configs[watermark_configs][drivers][font][font]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>字体文件</label>
<x-input type="text" name="configs[watermark_configs][drivers][font][font]" id="configs[watermark_configs][drivers][font][font]" autocomplete="text" placeholder="请输入字体文件路径例如fonts/lsky.ttf" value="{{ $group->configs['watermark_configs']['drivers']['font']['font'] }}" /> <x-input type="text" name="configs[watermark_configs][drivers][font][font]" id="configs[watermark_configs][drivers][font][font]" autocomplete="text" placeholder="请输入字体文件路径例如fonts/lsky.ttf" value="{{ $group->configs['watermark_configs']['drivers']['font']['font'] ?? '' }}" />
<small class="text-yellow-500">请将字体文件放置 {{ storage_path('app/public') }} 目录下</small> <small class="text-yellow-500">请将字体文件放置 {{ storage_path('app/public') }} 目录下</small>
</div> </div>
<div class="col-span-6 sm:col-span-3 mb-4"> <div class="col-span-6 sm:col-span-3 mb-4">
<label for="configs[watermark_configs][drivers][font][position]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>水印位置</label> <label for="configs[watermark_configs][drivers][font][position]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>水印位置</label>
<x-select id="configs[watermark_configs][drivers][font][position]" name="configs[watermark_configs][drivers][font][position]" autocomplete="position"> <x-select id="configs[watermark_configs][drivers][font][position]" name="configs[watermark_configs][drivers][font][position]" autocomplete="position">
@foreach($positions as $key => $position) @foreach($positions as $key => $position)
<option value="{{ $key }}" {{ $group->configs['watermark_configs']['drivers']['font']['position'] === $key ? 'selected' : '' }}>{{ $position }}</option> <option value="{{ $key }}" {{ ($group->configs['watermark_configs']['drivers']['font']['position'] ?? '') === $key ? 'selected' : '' }}>{{ $position }}</option>
@endforeach @endforeach
</x-select> </x-select>
</div> </div>
<div class="col-span-6 sm:col-span-3 mb-4"> <div class="col-span-6 sm:col-span-3 mb-4">
<label for="configs[watermark_configs][drivers][font][text]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>水印文字</label> <label for="configs[watermark_configs][drivers][font][text]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>水印文字</label>
<x-input type="text" name="configs[watermark_configs][drivers][font][text]" id="configs[watermark_configs][drivers][font][text]" autocomplete="text" placeholder="请输入水印文字" value="{{ $group->configs['watermark_configs']['drivers']['font']['text'] }}" /> <x-input type="text" name="configs[watermark_configs][drivers][font][text]" id="configs[watermark_configs][drivers][font][text]" autocomplete="text" placeholder="请输入水印文字" value="{{ $group->configs['watermark_configs']['drivers']['font']['text'] ?? '' }}" />
</div> </div>
<div class="col-span-6 sm:col-span-3 mb-4"> <div class="col-span-6 sm:col-span-3 mb-4">
<label for="configs[watermark_configs][drivers][font][color]" class="block text-sm font-medium text-gray-700">字体颜色</label> <label for="configs[watermark_configs][drivers][font][color]" class="block text-sm font-medium text-gray-700">字体颜色</label>
<x-input type="text" name="configs[watermark_configs][drivers][font][color]" id="configs[watermark_configs][drivers][font][color]" autocomplete="color" placeholder="请输入字体颜色,例如:#ffffff" value="{{ $group->configs['watermark_configs']['drivers']['font']['color'] }}" /> <x-input type="text" name="configs[watermark_configs][drivers][font][color]" id="configs[watermark_configs][drivers][font][color]" autocomplete="color" placeholder="请输入字体颜色,例如:#ffffff" value="{{ $group->configs['watermark_configs']['drivers']['font']['color'] ?? '' }}" />
</div> </div>
<div class="col-span-6 sm:col-span-3 mb-4"> <div class="col-span-6 sm:col-span-3 mb-4">
<label for="configs[watermark_configs][drivers][font][size]" class="block text-sm font-medium text-gray-700">字体大小</label> <label for="configs[watermark_configs][drivers][font][size]" class="block text-sm font-medium text-gray-700">字体大小</label>
<x-input type="number" name="configs[watermark_configs][drivers][font][size]" id="configs[watermark_configs][drivers][font][size]" autocomplete="size" placeholder="请输入字体大小,默认 14" value="{{ $group->configs['watermark_configs']['drivers']['font']['size'] }}" /> <x-input type="number" name="configs[watermark_configs][drivers][font][size]" id="configs[watermark_configs][drivers][font][size]" autocomplete="size" placeholder="请输入字体大小,默认 14" value="{{ $group->configs['watermark_configs']['drivers']['font']['size'] ?? '' }}" />
</div> </div>
<div class="col-span-6 sm:col-span-3 mb-4"> <div class="col-span-6 sm:col-span-3 mb-4">
<label for="configs[watermark_configs][drivers][font][angle]" class="block text-sm font-medium text-gray-700">旋转角度</label> <label for="configs[watermark_configs][drivers][font][angle]" class="block text-sm font-medium text-gray-700">旋转角度</label>
<x-input type="number" name="configs[watermark_configs][drivers][font][angle]" id="configs[watermark_configs][drivers][font][angle]" autocomplete="angle" placeholder="请输入旋转角度,默认 0可以为" value="{{ $group->configs['watermark_configs']['drivers']['font']['angle'] }}" /> <x-input type="number" name="configs[watermark_configs][drivers][font][angle]" id="configs[watermark_configs][drivers][font][angle]" autocomplete="angle" placeholder="请输入旋转角度,默认 0可以为" value="{{ $group->configs['watermark_configs']['drivers']['font']['angle'] ?? '' }}" />
</div> </div>
<div class="col-span-6 sm:col-span-3 mb-4"> <div class="col-span-6 sm:col-span-3 mb-4">
<label for="configs[watermark_configs][drivers][font][x]" class="block text-sm font-medium text-gray-700">X轴偏移量</label> <label for="configs[watermark_configs][drivers][font][x]" class="block text-sm font-medium text-gray-700">X轴偏移量</label>
<x-input type="number" name="configs[watermark_configs][drivers][font][x]" id="configs[watermark_configs][drivers][font][x]" autocomplete="x" placeholder="X轴偏移量" value="{{ $group->configs['watermark_configs']['drivers']['font']['x'] }}" /> <x-input type="number" name="configs[watermark_configs][drivers][font][x]" id="configs[watermark_configs][drivers][font][x]" autocomplete="x" placeholder="X轴偏移量" value="{{ $group->configs['watermark_configs']['drivers']['font']['x'] ?? '' }}" />
</div> </div>
<div class="col-span-6 sm:col-span-3 mb-4"> <div class="col-span-6 sm:col-span-3 mb-4">
<label for="configs[watermark_configs][drivers][font][y]" class="block text-sm font-medium text-gray-700">Y轴偏移量</label> <label for="configs[watermark_configs][drivers][font][y]" class="block text-sm font-medium text-gray-700">Y轴偏移量</label>
<x-input type="number" name="configs[watermark_configs][drivers][font][y]" id="configs[watermark_configs][drivers][font][y]" autocomplete="y" placeholder="Y轴偏移量" value="{{ $group->configs['watermark_configs']['drivers']['font']['y'] }}" /> <x-input type="number" name="configs[watermark_configs][drivers][font][y]" id="configs[watermark_configs][drivers][font][y]" autocomplete="y" placeholder="Y轴偏移量" value="{{ $group->configs['watermark_configs']['drivers']['font']['y'] ?? '' }}" />
</div> </div>
</div> </div>
<div class="mb-4 hidden" data-watermark-driver="image"> <div class="mb-4 hidden" data-watermark-driver="image">
<div class="col-span-6 sm:col-span-3 mb-4"> <div class="col-span-6 sm:col-span-3 mb-4">
<label for="configs[watermark_configs][drivers][image][image]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>水印图片</label> <label for="configs[watermark_configs][drivers][image][image]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>水印图片</label>
<x-input type="text" name="configs[watermark_configs][drivers][image][image]" id="configs[watermark_configs][drivers][image][image]" autocomplete="image" placeholder="请输入水印路径例如images/lsky.png" value="{{ $group->configs['watermark_configs']['drivers']['image']['image'] }}" /> <x-input type="text" name="configs[watermark_configs][drivers][image][image]" id="configs[watermark_configs][drivers][image][image]" autocomplete="image" placeholder="请输入水印路径例如images/lsky.png" value="{{ $group->configs['watermark_configs']['drivers']['image']['image'] ?? '' }}" />
<small class="text-yellow-500">请将水印图片放置 {{ public_path() }} 目录下</small> <small class="text-yellow-500">请将水印图片放置 {{ storage_path('app/public') }} 目录下</small>
</div> </div>
<div class="col-span-6 sm:col-span-3 mb-4"> <div class="col-span-6 sm:col-span-3 mb-4">
<label for="configs[watermark_configs][drivers][image][position]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>水印位置</label> <label for="configs[watermark_configs][drivers][image][position]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>水印位置</label>
<x-select id="configs[watermark_configs][drivers][image][position]" name="configs[watermark_configs][drivers][image][position]" autocomplete="position"> <x-select id="configs[watermark_configs][drivers][image][position]" name="configs[watermark_configs][drivers][image][position]" autocomplete="position">
@foreach($positions as $key => $position) @foreach($positions as $key => $position)
<option value="{{ $key }}" {{ $group->configs['watermark_configs']['drivers']['image']['position'] === $key ? 'selected' : '' }}>{{ $position }}</option> <option value="{{ $key }}" {{ ($group->configs['watermark_configs']['drivers']['image']['position'] ?? '') === $key ? 'selected' : '' }}>{{ $position }}</option>
@endforeach @endforeach
</x-select> </x-select>
</div> </div>
<div class="col-span-6 sm:col-span-3 mb-4"> <div class="col-span-6 sm:col-span-3 mb-4">
<label for="configs[watermark_configs][drivers][image][width]" class="block text-sm font-medium text-gray-700">图片宽度</label> <label for="configs[watermark_configs][drivers][image][width]" class="block text-sm font-medium text-gray-700">图片宽度</label>
<x-input type="number" name="configs[watermark_configs][drivers][image][width]" id="configs[watermark_configs][drivers][image][width]" autocomplete="width" placeholder="请输入水印图片宽度" value="{{ $group->configs['watermark_configs']['drivers']['image']['width'] }}" /> <x-input type="number" name="configs[watermark_configs][drivers][image][width]" id="configs[watermark_configs][drivers][image][width]" autocomplete="width" placeholder="请输入水印图片宽度" value="{{ $group->configs['watermark_configs']['drivers']['image']['width'] ?? '' }}" />
</div> </div>
<div class="col-span-6 sm:col-span-3 mb-4"> <div class="col-span-6 sm:col-span-3 mb-4">
<label for="configs[watermark_configs][drivers][image][height]" class="block text-sm font-medium text-gray-700">图片高度</label> <label for="configs[watermark_configs][drivers][image][height]" class="block text-sm font-medium text-gray-700">图片高度</label>
<x-input type="number" name="configs[watermark_configs][drivers][image][height]" id="configs[watermark_configs][drivers][image][height]" autocomplete="height" placeholder="请输入水印图片高度" value="{{ $group->configs['watermark_configs']['drivers']['image']['height'] }}" /> <x-input type="number" name="configs[watermark_configs][drivers][image][height]" id="configs[watermark_configs][drivers][image][height]" autocomplete="height" placeholder="请输入水印图片高度" value="{{ $group->configs['watermark_configs']['drivers']['image']['height'] ?? '' }}" />
</div> </div>
<div class="col-span-6 sm:col-span-3 mb-4"> <div class="col-span-6 sm:col-span-3 mb-4">
<label for="configs[watermark_configs][drivers][image][opacity]" class="block text-sm font-medium text-gray-700">不透明度</label> <label for="configs[watermark_configs][drivers][image][opacity]" class="block text-sm font-medium text-gray-700">不透明度</label>
<x-input type="number" name="configs[watermark_configs][drivers][image][opacity]" id="configs[watermark_configs][drivers][image][opacity]" autocomplete="opacity" placeholder="请输入不透明度,取值 0 - 100" value="{{ $group->configs['watermark_configs']['drivers']['image']['opacity'] }}" /> <x-input type="number" name="configs[watermark_configs][drivers][image][opacity]" id="configs[watermark_configs][drivers][image][opacity]" autocomplete="opacity" placeholder="请输入不透明度,取值 0 - 100" value="{{ $group->configs['watermark_configs']['drivers']['image']['opacity'] ?? '' }}" />
</div> </div>
<div class="col-span-6 sm:col-span-3 mb-4"> <div class="col-span-6 sm:col-span-3 mb-4">
<label for="configs[watermark_configs][drivers][image][rotate]" class="block text-sm font-medium text-gray-700">旋转角度</label> <label for="configs[watermark_configs][drivers][image][rotate]" class="block text-sm font-medium text-gray-700">旋转角度</label>
<x-input type="number" name="configs[watermark_configs][drivers][image][rotate]" id="configs[watermark_configs][drivers][image][rotate]" autocomplete="rotate" placeholder="请输入旋转角度,默认 0" value="{{ $group->configs['watermark_configs']['drivers']['image']['rotate'] }}" /> <x-input type="number" name="configs[watermark_configs][drivers][image][rotate]" id="configs[watermark_configs][drivers][image][rotate]" autocomplete="rotate" placeholder="请输入旋转角度,默认 0" value="{{ $group->configs['watermark_configs']['drivers']['image']['rotate'] ?? '' }}" />
</div> </div>
<div class="col-span-6 sm:col-span-3 mb-4"> <div class="col-span-6 sm:col-span-3 mb-4">
<label for="configs[watermark_configs][drivers][image][x]" class="block text-sm font-medium text-gray-700">X轴偏移量</label> <label for="configs[watermark_configs][drivers][image][x]" class="block text-sm font-medium text-gray-700">X轴偏移量</label>
<x-input type="number" name="configs[watermark_configs][drivers][image][x]" id="configs[watermark_configs][drivers][image][x]" autocomplete="x" placeholder="X轴偏移量" value="{{ $group->configs['watermark_configs']['drivers']['image']['x'] }}" /> <x-input type="number" name="configs[watermark_configs][drivers][image][x]" id="configs[watermark_configs][drivers][image][x]" autocomplete="x" placeholder="X轴偏移量" value="{{ $group->configs['watermark_configs']['drivers']['image']['x'] ?? '' }}" />
</div> </div>
<div class="col-span-6 sm:col-span-3 mb-4"> <div class="col-span-6 sm:col-span-3 mb-4">
<label for="configs[watermark_configs][drivers][image][y]" class="block text-sm font-medium text-gray-700">Y轴偏移量</label> <label for="configs[watermark_configs][drivers][image][y]" class="block text-sm font-medium text-gray-700">Y轴偏移量</label>
<x-input type="number" name="configs[watermark_configs][drivers][image][y]" id="configs[watermark_configs][drivers][image][y]" autocomplete="y" placeholder="Y轴偏移量" value="{{ $group->configs['watermark_configs']['drivers']['image']['y'] }}" /> <x-input type="number" name="configs[watermark_configs][drivers][image][y]" id="configs[watermark_configs][drivers][image][y]" autocomplete="y" placeholder="Y轴偏移量" value="{{ $group->configs['watermark_configs']['drivers']['image']['y'] ?? '' }}" />
</div> </div>
</div> </div>
</div> </div>

View File

@@ -25,18 +25,18 @@
</span> </span>
</td> </td>
<td class="px-6 py-4 whitespace-nowrap"> <td class="px-6 py-4 whitespace-nowrap">
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full {{ $group->configs['is_enable_scan'] ? 'text-green-500' : 'text-rose-500' }}"> <span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full {{ $group->configs->get('is_enable_scan') ? 'text-green-500' : 'text-rose-500' }}">
<i class="text-lg fas fa-{{ $group->configs['is_enable_scan'] ? 'check-circle' : 'times-circle' }}"></i> <i class="text-lg fas fa-{{ $group->configs->get('is_enable_scan') ? 'check-circle' : 'times-circle' }}"></i>
</span> </span>
</td> </td>
<td class="px-6 py-4 whitespace-nowrap"> <td class="px-6 py-4 whitespace-nowrap">
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full {{ $group->configs['is_enable_original_protection'] ? 'text-green-500' : 'text-rose-500' }}"> <span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full {{ $group->configs->get('is_enable_original_protection') ? 'text-green-500' : 'text-rose-500' }}">
<i class="text-lg fas fa-{{ $group->configs['is_enable_original_protection'] ? 'check-circle' : 'times-circle' }}"></i> <i class="text-lg fas fa-{{ $group->configs->get('is_enable_original_protection') ? 'check-circle' : 'times-circle' }}"></i>
</span> </span>
</td> </td>
<td class="px-6 py-4 whitespace-nowrap"> <td class="px-6 py-4 whitespace-nowrap">
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full {{ $group->configs['is_enable_watermark'] ? 'text-green-500' : 'text-rose-500' }}"> <span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full {{ $group->configs->get('is_enable_watermark') ? 'text-green-500' : 'text-rose-500' }}">
<i class="text-lg fas fa-{{ $group->configs['is_enable_watermark'] ? 'check-circle' : 'times-circle' }}"></i> <i class="text-lg fas fa-{{ $group->configs->get('is_enable_watermark') ? 'check-circle' : 'times-circle' }}"></i>
</span> </span>
</td> </td>
<td class="px-6 py-4 whitespace-nowrap">{{ $group->users_count }}</td> <td class="px-6 py-4 whitespace-nowrap">{{ $group->users_count }}</td>

View File

@@ -1,4 +1,4 @@
<x-modal> <x-modal id="rules-modal">
<div id="modal-content"> <div id="modal-content">
<table class="min-w-full divide-y divide-gray-200"> <table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50"> <thead class="bg-gray-50">
@@ -68,7 +68,7 @@
@push('scripts') @push('scripts')
<script> <script>
$('#rename-rules').click(function () { $('#rename-rules').click(function () {
Alpine.store('modal').open = true; Alpine.store('modal').open('rules-modal')
}) })
</script> </script>
@endpush @endpush

View File

@@ -6,3 +6,6 @@
<p class="bg-yellow-500 p-2 mb-2 rounded text-sm text-white"> <p class="bg-yellow-500 p-2 mb-2 rounded text-sm text-white">
<i class="fas fa-exclamation-circle"></i> 系统运行环境允许上传大小的最大值为 {{ ini_get('upload_max_filesize') }},最大 POST 数据大小为 {{ ini_get('post_max_size') }},上传文件大小不得超过这两项配置值。 <i class="fas fa-exclamation-circle"></i> 系统运行环境允许上传大小的最大值为 {{ ini_get('upload_max_filesize') }},最大 POST 数据大小为 {{ ini_get('post_max_size') }},上传文件大小不得超过这两项配置值。
</p> </p>
<p class="bg-yellow-500 p-2 mb-2 rounded text-sm text-white">
<i class="fas fa-exclamation-circle"></i> 原图保护以及水印功能,原理是使用 PHP 接管图片请求,动态处理后缓存之后通过载入缓存到内存中输出图片,对服务器有着较高的要求,请谨慎使用。如果你使用第三方储存,兰空图床更推荐你使用第三方储存的图片处理规则。
</p>

View File

@@ -53,7 +53,7 @@
@endif @endif
</div> </div>
<x-modal> <x-modal id="content-modal">
<div id="modal-content"></div> <div id="modal-content"></div>
</x-modal> </x-modal>
@@ -194,8 +194,8 @@
</dl> </dl>
<dl> <dl>
<div class="bg-gray-50 px-2 py-3 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6"> <div class="bg-gray-50 px-2 py-3 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6">
<dt class="text-sm font-medium text-gray-500">剩余容量</dt> <dt class="text-sm font-medium text-gray-500">已用容量</dt>
<dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2 truncate">__surplus_capacity__</dd> <dd class="mt-1 text-sm text-gray-900 sm:mt-0 sm:col-span-2 truncate">__used_capacity__</dd>
</div> </div>
</dl> </dl>
<dl> <dl>
@@ -333,7 +333,7 @@
if (result.isConfirmed) { if (result.isConfirmed) {
axios.delete(`/admin/images/${id}`).then(response => { axios.delete(`/admin/images/${id}`).then(response => {
if (response.data.status) { if (response.data.status) {
modal = false; modal.close('content-modal')
toastr.success(response.data.message); toastr.success(response.data.message);
setTimeout(function () { setTimeout(function () {
history.go(0); history.go(0);
@@ -348,23 +348,24 @@
$('#grammar').click(function () { $('#grammar').click(function () {
$('#modal-content').html($('#search-grammar-tpl').html()); $('#modal-content').html($('#search-grammar-tpl').html());
modal.open = true; modal.open('content-modal')
}); });
$('.item').click(function () { $('.item').click(function () {
let image = $(this).data('json'); let image = $(this).data('json');
let previewUrl = ['psd', 'tif'].indexOf(image.extension) === -1 ? image.url : image.thumb_url;
let html = $('#image-tpl').html() let html = $('#image-tpl').html()
.replace(/__id__/g, image.id) .replace(/__id__/g, image.id)
.replace(/__url__/g, image.url) .replace(/__url__/g, previewUrl)
.replace(/__user_name__/g, image.user ? image.user.name+'('+image.user.email+')' : '游客') .replace(/__user_name__/g, image.user ? image.user.name+'('+image.user.email+')' : '游客')
.replace(/__user_email__/g, image.user ? image.user.email : '-') .replace(/__user_email__/g, image.user ? image.user.email : '-')
.replace(/__album_name__/g, image.album ? image.album.name : '-') .replace(/__album_name__/g, image.album ? image.album.name : '-')
.replace(/__group_name__/g, image.group ? image.group.name : '-') .replace(/__group_name__/g, image.group ? image.group.name : '-')
.replace(/__strategy_name__/g, image.strategy.name || '-') .replace(/__strategy_name__/g, image.strategy ? image.strategy.name : '-')
.replace(/__name__/g, image.name) .replace(/__name__/g, image.name)
.replace(/__origin_name__/g, image.origin_name) .replace(/__origin_name__/g, image.origin_name)
.replace(/__pathname__/g, image.pathname) .replace(/__pathname__/g, image.pathname)
.replace(/__size__/g, utils.formatSize(image.size)) .replace(/__size__/g, utils.formatSize(image.size * 1024))
.replace(/__mimetype__/g, image.mimetype) .replace(/__mimetype__/g, image.mimetype)
.replace(/__md5__/g, image.md5) .replace(/__md5__/g, image.md5)
.replace(/__sha1__/g, image.sha1) .replace(/__sha1__/g, image.sha1)
@@ -377,7 +378,7 @@
$('#modal-content').html(html); $('#modal-content').html(html);
modal.open = true; modal.open('content-modal')
}); });
$('.item-user').click(function (e) { $('.item-user').click(function (e) {
@@ -388,7 +389,7 @@
.replace(/__name__/g, user.name) .replace(/__name__/g, user.name)
.replace(/__email__/g, user.email) .replace(/__email__/g, user.email)
.replace(/__capacity__/g, utils.formatSize(user.capacity * 1024)) .replace(/__capacity__/g, utils.formatSize(user.capacity * 1024))
.replace(/__surplus_capacity__/g, utils.formatSize(user.images_sum_size * 1024)) .replace(/__used_capacity__/g, utils.formatSize(user.images_sum_size * 1024))
.replace(/__image_num__/g, user.image_num) .replace(/__image_num__/g, user.image_num)
.replace(/__album_num__/g, user.album_num) .replace(/__album_num__/g, user.album_num)
.replace(/__registered_ip__/g, user.registered_ip || '-') .replace(/__registered_ip__/g, user.registered_ip || '-')
@@ -398,7 +399,7 @@
$('#modal-content').html(html); $('#modal-content').html(html);
modal.open = true; modal.open('content-modal')
}); });
$('.item .delete').click(function (e) { $('.item .delete').click(function (e) {

View File

@@ -1,33 +1,33 @@
@section('title', '系统设置') @section('title', '系统设置')
@push('styles') @push('styles')
<link rel="stylesheet" href="{{ asset('css/markdown-css/github-markdown.css') }}"> <link rel="stylesheet" href="{{ asset('css/markdown-css/github-markdown-light.css') }}">
@endpush @endpush
<x-app-layout> <x-app-layout>
<div class="my-6 md:my-9"> <div class="my-6 md:my-9">
<p class="mb-3 font-semibold text-lg text-gray-700">通用</p> <p class="mb-3 font-semibold text-lg text-gray-700">通用</p>
<form action="{{ route('admin.settings.save') }}"> <form action="{{ route('admin.settings.save') }}">
<div class="relative p-4 rounded-md bg-white mb-8 space-y-4"> <div class="relative p-4 rounded-md bg-white mb-8 space-y-4 shadow-custom">
<div> <div>
<label for="app_name" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>应用名称</label> <label for="app_name" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>应用名称</label>
<x-input type="text" name="app_name" id="app_name" value="{{ $configs['app_name'] }}" placeholder="请输入应用名称"/> <x-input type="text" name="app_name" id="app_name" value="{{ $configs->get('app_name') }}" placeholder="请输入应用名称"/>
</div> </div>
<div> <div>
<label for="site_keywords" class="block text-sm font-medium text-gray-700">网站关键字</label> <label for="site_keywords" class="block text-sm font-medium text-gray-700">网站关键字</label>
<x-textarea type="text" name="site_keywords" id="site_keywords" placeholder="请输入网站关键字">{{ $configs['site_keywords'] }}</x-textarea> <x-textarea type="text" name="site_keywords" id="site_keywords" placeholder="请输入网站关键字">{{ $configs->get('site_keywords') }}</x-textarea>
</div> </div>
<div> <div>
<label for="site_description" class="block text-sm font-medium text-gray-700">网站描述</label> <label for="site_description" class="block text-sm font-medium text-gray-700">网站描述</label>
<x-textarea type="text" name="site_description" id="site_description" placeholder="请输入网站描述">{{ $configs['site_description'] }}</x-textarea> <x-textarea type="text" name="site_description" id="site_description" placeholder="请输入网站描述">{{ $configs->get('site_description') }}</x-textarea>
</div> </div>
<div> <div>
<label for="icp_no" class="block text-sm font-medium text-gray-700">备案号</label> <label for="icp_no" class="block text-sm font-medium text-gray-700">备案号</label>
<x-input type="text" name="icp_no" id="icp_no" value="{{ $configs['icp_no'] }}" placeholder="请输入备案号"/> <x-input type="text" name="icp_no" id="icp_no" value="{{ $configs->get('icp_no') }}" placeholder="请输入备案号"/>
</div> </div>
<div> <div>
<label for="site_notice" class="block text-sm font-medium text-gray-700">网站公告</label> <label for="site_notice" class="block text-sm font-medium text-gray-700">网站公告</label>
<x-textarea type="text" name="site_notice" id="site_notice" placeholder="首页弹出公告,支持 Markdown不设置请留空。" rows="7">{{ $configs['site_notice'] }}</x-textarea> <x-textarea type="text" name="site_notice" id="site_notice" placeholder="首页弹出公告,支持 Markdown不设置请留空。" rows="7">{{ $configs->get('site_notice') }}</x-textarea>
</div> </div>
<div class="text-right"> <div class="text-right">
@@ -38,21 +38,21 @@
<p class="mb-3 font-semibold text-lg text-gray-700">控制</p> <p class="mb-3 font-semibold text-lg text-gray-700">控制</p>
<form action="{{ route('admin.settings.save') }}"> <form action="{{ route('admin.settings.save') }}">
<div class="relative p-4 rounded-md bg-white mb-8 space-y-4"> <div class="relative p-4 rounded-md bg-white mb-8 space-y-4 shadow-custom">
<x-fieldset title="是否启用注册" faq="启用或关闭系统注册功能"> <x-fieldset title="是否启用注册" faq="启用或关闭系统注册功能">
<x-switch name="is_enable_registration" value="1" :checked="(bool) $configs['is_enable_registration']" /> <x-switch name="is_enable_registration" value="1" :checked="(bool) $configs->get('is_enable_registration')" />
</x-fieldset> </x-fieldset>
<x-fieldset title="是否启用画廊" faq="启用或关闭画廊功能,画廊只有已登录的用户可见,画廊中的图片均为所有用户公开的图片。"> <x-fieldset title="是否启用画廊" faq="启用或关闭画廊功能,画廊只有已登录的用户可见,画廊中的图片均为所有用户公开的图片。">
<x-switch name="is_enable_gallery" value="1" :checked="(bool) $configs['is_enable_gallery']" /> <x-switch name="is_enable_gallery" value="1" :checked="(bool) $configs->get('is_enable_gallery')" />
</x-fieldset> </x-fieldset>
<x-fieldset title="是否启用接口" faq="启用或关闭接口功能,关闭后将无法通过接口上传图片、管理图片等操作。"> <x-fieldset title="是否启用接口" faq="启用或关闭接口功能,关闭后将无法通过接口上传图片、管理图片等操作。">
<x-switch name="is_enable_api" value="1" :checked="(bool) $configs['is_enable_api']" /> <x-switch name="is_enable_api" value="1" :checked="(bool) $configs->get('is_enable_api')" />
</x-fieldset> </x-fieldset>
<x-fieldset title="是否允许游客上传" faq="启用或关闭游客上传功能,游客上传受「系统默认组」控制。"> <x-fieldset title="是否允许游客上传" faq="启用或关闭游客上传功能,游客上传受「系统默认组」控制。">
<x-switch name="is_allow_guest_upload" value="1" :checked="(bool) $configs['is_allow_guest_upload']" /> <x-switch name="is_allow_guest_upload" value="1" :checked="(bool) $configs->get('is_allow_guest_upload')" />
</x-fieldset> </x-fieldset>
<x-fieldset title="账号验证" faq="是否强制用户验证邮箱,开启后用户必须经过验证邮箱后才能上传图片,请确保邮件配置正常。"> <x-fieldset title="账号验证" faq="是否强制用户验证邮箱,开启后用户必须经过验证邮箱后才能上传图片,请确保邮件配置正常。">
<x-switch name="is_user_need_verify" value="1" :checked="(bool) $configs['is_user_need_verify']" /> <x-switch name="is_user_need_verify" value="1" :checked="(bool) $configs->get('is_user_need_verify')" />
</x-fieldset> </x-fieldset>
<div class="text-right"> <div class="text-right">
<x-button type="submit">保存更改</x-button> <x-button type="submit">保存更改</x-button>
@@ -62,10 +62,10 @@
<p class="mb-3 font-semibold text-lg text-gray-700">用户</p> <p class="mb-3 font-semibold text-lg text-gray-700">用户</p>
<form action="{{ route('admin.settings.save') }}"> <form action="{{ route('admin.settings.save') }}">
<div class="relative p-4 rounded-md bg-white mb-8 space-y-4"> <div class="relative p-4 rounded-md bg-white mb-8 space-y-4 shadow-custom">
<div> <div>
<label for="user_initial_capacity" class="block text-sm font-medium text-gray-700">用户初始容量(kb)</label> <label for="user_initial_capacity" class="block text-sm font-medium text-gray-700">用户初始容量(kb)</label>
<x-input type="number" name="user_initial_capacity" id="user_initial_capacity" step="0.01" value="{{ $configs['user_initial_capacity'] }}" placeholder="请输入用户初始容量(kb)"/> <x-input type="number" name="user_initial_capacity" id="user_initial_capacity" step="0.01" value="{{ $configs->get('user_initial_capacity') }}" placeholder="请输入用户初始容量(kb)"/>
</div> </div>
<div class="text-right"> <div class="text-right">
@@ -75,7 +75,7 @@
</form> </form>
<p class="mb-3 font-semibold text-lg text-gray-700">邮件配置</p> <p class="mb-3 font-semibold text-lg text-gray-700">邮件配置</p>
<div class="relative p-4 rounded-md bg-white mb-8 space-y-4"> <div class="relative p-4 rounded-md bg-white mb-8 space-y-4 shadow-custom">
<x-fieldset title="发信驱动"> <x-fieldset title="发信驱动">
<x-fieldset-radio id="mail[default]" name="mail[default]" data-select="mailer" value="smtp" checked>SMTP</x-fieldset-radio> <x-fieldset-radio id="mail[default]" name="mail[default]" data-select="mailer" value="smtp" checked>SMTP</x-fieldset-radio>
</x-fieldset> </x-fieldset>
@@ -84,27 +84,27 @@
<form action="{{ route('admin.settings.save') }}" class="space-y-4"> <form action="{{ route('admin.settings.save') }}" class="space-y-4">
<div> <div>
<label for="mail[mailers][smtp][host]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>主机地址</label> <label for="mail[mailers][smtp][host]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>主机地址</label>
<x-input type="text" name="mail[mailers][smtp][host]" id="mail[mailers][smtp][host]" value="{{ $configs['mail']['mailers']['smtp']['host'] }}" placeholder="请输入 SMTP 主机地址"/> <x-input type="text" name="mail[mailers][smtp][host]" id="mail[mailers][smtp][host]" value="{{ $configs['mail']['mailers']['smtp']['host'] ?? '' }}" placeholder="请输入 SMTP 主机地址"/>
</div> </div>
<div> <div>
<label for="mail[mailers][smtp][port]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>连接端口</label> <label for="mail[mailers][smtp][port]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>连接端口</label>
<x-input type="number" name="mail[mailers][smtp][port]" id="mail[mailers][smtp][port]" value="{{ $configs['mail']['mailers']['smtp']['port'] }}" placeholder="请输入 SMTP 主机连接端口"/> <x-input type="number" name="mail[mailers][smtp][port]" id="mail[mailers][smtp][port]" value="{{ $configs['mail']['mailers']['smtp']['port'] ?? 587 }}" placeholder="请输入 SMTP 主机连接端口"/>
</div> </div>
<div> <div>
<label for="mail[mailers][smtp][username]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>用户名</label> <label for="mail[mailers][smtp][username]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>用户名</label>
<x-input type="text" name="mail[mailers][smtp][username]" id="mail[mailers][smtp][username]" value="{{ $configs['mail']['mailers']['smtp']['username'] }}" placeholder="请输入用户名"/> <x-input type="text" name="mail[mailers][smtp][username]" id="mail[mailers][smtp][username]" value="{{ $configs['mail']['mailers']['smtp']['username'] ?? '' }}" placeholder="请输入用户名"/>
</div> </div>
<div> <div>
<label for="mail[mailers][smtp][password]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>密码</label> <label for="mail[mailers][smtp][password]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>密码</label>
<x-input type="password" name="mail[mailers][smtp][password]" id="mail[mailers][smtp][password]" value="{{ $configs['mail']['mailers']['smtp']['password'] }}" placeholder="请输入密码"/> <x-input type="password" name="mail[mailers][smtp][password]" id="mail[mailers][smtp][password]" value="{{ $configs['mail']['mailers']['smtp']['password'] ?? '' }}" placeholder="请输入密码"/>
</div> </div>
<div> <div>
<label for="mail[mailers][smtp][encryption]" class="block text-sm font-medium text-gray-700">加密方式</label> <label for="mail[mailers][smtp][encryption]" class="block text-sm font-medium text-gray-700">加密方式</label>
<x-input type="text" name="mail[mailers][smtp][encryption]" id="mail[mailers][smtp][encryption]" value="{{ $configs['mail']['mailers']['smtp']['encryption'] }}" placeholder="请输入加密方式(ssl, tls)"/> <x-input type="text" name="mail[mailers][smtp][encryption]" id="mail[mailers][smtp][encryption]" value="{{ $configs['mail']['mailers']['smtp']['encryption'] ?? '' }}" placeholder="请输入加密方式(ssl, tls)"/>
</div> </div>
<div> <div>
<label for="mail[mailers][smtp][timeout]" class="block text-sm font-medium text-gray-700">连接超时时间()</label> <label for="mail[mailers][smtp][timeout]" class="block text-sm font-medium text-gray-700">连接超时时间()</label>
<x-input type="number" name="mail[mailers][smtp][timeout]" id="mail[mailers][smtp][timeout]" value="{{ $configs['mail']['mailers']['smtp']['timeout'] }}" placeholder="请输入连接超时时间(秒)"/> <x-input type="number" name="mail[mailers][smtp][timeout]" id="mail[mailers][smtp][timeout]" value="{{ $configs['mail']['mailers']['smtp']['timeout'] ?? 10 }}" placeholder="请输入连接超时时间(秒)"/>
</div> </div>
<div> <div>
<label for="mail[mailers][smtp][from_address]" class="block text-sm font-medium text-gray-700">发件人地址</label> <label for="mail[mailers][smtp][from_address]" class="block text-sm font-medium text-gray-700">发件人地址</label>
@@ -127,7 +127,7 @@
</div> </div>
<p class="mb-3 font-semibold text-lg text-gray-700">系统升级</p> <p class="mb-3 font-semibold text-lg text-gray-700">系统升级</p>
<div class="relative p-4 rounded-md bg-white mb-8"> <div class="relative p-4 rounded-md bg-white mb-8 shadow-custom">
<p id="check-update" class="text-gray-600 text-center p-4" style="display: none"> <p id="check-update" class="text-gray-600 text-center p-4" style="display: none">
<i class="fas fa-cog animate-spin"></i> 正在检查更新... <i class="fas fa-cog animate-spin"></i> 正在检查更新...
</p> </p>

View File

@@ -7,7 +7,7 @@
<div class="md:mt-0 md:col-span-2"> <div class="md:mt-0 md:col-span-2">
<form action="{{ route('admin.strategy.create') }}" method="POST"> <form action="{{ route('admin.strategy.create') }}" method="POST">
<div class="overflow-hidden rounded-md"> <div class="overflow-hidden rounded-md">
<div class="px-4 py-5 bg-white sm:p-6 space-y-4"> <div class="px-4 py-5 bg-white sm:p-6 space-y-4 shadow-custom">
<div class="col-span-6"> <div class="col-span-6">
<label class="block"> <label class="block">
<span class="text-gray-700">选择角色组</span> <span class="text-gray-700">选择角色组</span>
@@ -46,6 +46,10 @@
本地储存的访问网址必须有根路径例如https://www.lsky.pro/uploads 中的 uploads 就是根路径,且根路径不能和其他策略重复。修改根路径直接影响已经上传并已使用的链接的访问。 本地储存的访问网址必须有根路径例如https://www.lsky.pro/uploads 中的 uploads 就是根路径,且根路径不能和其他策略重复。修改根路径直接影响已经上传并已使用的链接的访问。
</small> </small>
</div> </div>
<div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[queries]" class="block text-sm font-medium text-gray-700">URL Queries</label>
<x-input type="text" name="configs[queries]" id="configs[queries]" placeholder="请输入 url 额外参数" />
</div>
<div class="col-span-6"> <div class="col-span-6">
<div class="col-span-6 sm:col-span-3 mb-4"> <div class="col-span-6 sm:col-span-3 mb-4">
<label for="configs[root]" class="block text-sm font-medium text-gray-700">储存路径</label> <label for="configs[root]" class="block text-sm font-medium text-gray-700">储存路径</label>
@@ -60,6 +64,10 @@
<label for="configs[url]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>访问域名</label> <label for="configs[url]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>访问域名</label>
<x-input type="url" name="configs[url]" id="configs[url]" placeholder="请输入图片访问域名 http(s)://" /> <x-input type="url" name="configs[url]" id="configs[url]" placeholder="请输入图片访问域名 http(s)://" />
</div> </div>
<div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[queries]" class="block text-sm font-medium text-gray-700">URL Queries</label>
<x-input type="text" name="configs[queries]" id="configs[queries]" placeholder="请输入 url 额外参数" />
</div>
<div class="col-span-3 sm:col-span-2 mb-4"> <div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[access_key_id]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>AccessKeyId</label> <label for="configs[access_key_id]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>AccessKeyId</label>
<x-input type="text" name="configs[access_key_id]" id="configs[access_key_id]" placeholder="请输入 AccessKeyId" /> <x-input type="text" name="configs[access_key_id]" id="configs[access_key_id]" placeholder="请输入 AccessKeyId" />
@@ -68,6 +76,10 @@
<label for="configs[secret_access_key]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>SecretAccessKey</label> <label for="configs[secret_access_key]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>SecretAccessKey</label>
<x-input type="password" name="configs[secret_access_key]" id="configs[secret_access_key]" placeholder="请输入 SecretAccessKey" /> <x-input type="password" name="configs[secret_access_key]" id="configs[secret_access_key]" placeholder="请输入 SecretAccessKey" />
</div> </div>
<div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[endpoint]" class="block text-sm font-medium text-gray-700">连接地址</label>
<x-input type="url" name="configs[endpoint]" id="configs[endpoint]" placeholder="请输入连接地址" />
</div>
<div class="col-span-3 sm:col-span-2 mb-4"> <div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[region]" class="block text-sm font-medium text-gray-700">区域(region)</label> <label for="configs[region]" class="block text-sm font-medium text-gray-700">区域(region)</label>
<x-input type="text" name="configs[region]" id="configs[region]" placeholder="请输入区域例如us-east-1" /> <x-input type="text" name="configs[region]" id="configs[region]" placeholder="请输入区域例如us-east-1" />
@@ -83,6 +95,10 @@
<label for="configs[url]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>访问域名</label> <label for="configs[url]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>访问域名</label>
<x-input type="url" name="configs[url]" id="configs[url]" placeholder="请输入图片访问域名 http(s)://" /> <x-input type="url" name="configs[url]" id="configs[url]" placeholder="请输入图片访问域名 http(s)://" />
</div> </div>
<div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[queries]" class="block text-sm font-medium text-gray-700">URL Queries</label>
<x-input type="text" name="configs[queries]" id="configs[queries]" placeholder="请输入 url 额外参数" />
</div>
<div class="col-span-3 sm:col-span-2 mb-4"> <div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[access_key_id]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>AccessKeyId</label> <label for="configs[access_key_id]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>AccessKeyId</label>
<x-input type="text" name="configs[access_key_id]" id="configs[access_key_id]" placeholder="请输入 AccessKeyId" /> <x-input type="text" name="configs[access_key_id]" id="configs[access_key_id]" placeholder="请输入 AccessKeyId" />
@@ -106,6 +122,10 @@
<label for="configs[url]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>访问域名</label> <label for="configs[url]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>访问域名</label>
<x-input type="url" name="configs[url]" id="configs[url]" placeholder="请输入图片访问域名 http(s)://" /> <x-input type="url" name="configs[url]" id="configs[url]" placeholder="请输入图片访问域名 http(s)://" />
</div> </div>
<div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[queries]" class="block text-sm font-medium text-gray-700">URL Queries</label>
<x-input type="text" name="configs[queries]" id="configs[queries]" placeholder="请输入 url 额外参数" />
</div>
<div class="col-span-3 sm:col-span-2 mb-4"> <div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[app_id]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>AppId</label> <label for="configs[app_id]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>AppId</label>
<x-input type="text" name="configs[app_id]" id="configs[app_id]" placeholder="请输入 AppId" /> <x-input type="text" name="configs[app_id]" id="configs[app_id]" placeholder="请输入 AppId" />
@@ -125,6 +145,7 @@
<div class="col-span-3 sm:col-span-2 mb-4"> <div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[bucket]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>储存桶名称</label> <label for="configs[bucket]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>储存桶名称</label>
<x-input type="text" name="configs[bucket]" id="configs[bucket]" placeholder="请输入储存桶名称" /> <x-input type="text" name="configs[bucket]" id="configs[bucket]" placeholder="请输入储存桶名称" />
<small class="text-gray-500"><i class="fas fa-exclamation-circle"></i> 腾讯云储存桶名称由 名称+appid 组合例如test-125146xxxx此处应该填写 test</small>
</div> </div>
</div> </div>
@@ -133,6 +154,10 @@
<label for="configs[url]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>访问域名</label> <label for="configs[url]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>访问域名</label>
<x-input type="url" name="configs[url]" id="configs[url]" placeholder="请输入图片访问域名 http(s)://" /> <x-input type="url" name="configs[url]" id="configs[url]" placeholder="请输入图片访问域名 http(s)://" />
</div> </div>
<div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[queries]" class="block text-sm font-medium text-gray-700">URL Queries</label>
<x-input type="text" name="configs[queries]" id="configs[queries]" placeholder="请输入 url 额外参数" />
</div>
<div class="col-span-3 sm:col-span-2 mb-4"> <div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[access_key]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>AccessKey</label> <label for="configs[access_key]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>AccessKey</label>
<x-input type="text" name="configs[access_key]" id="configs[access_key]" placeholder="请输入 AccessKey" /> <x-input type="text" name="configs[access_key]" id="configs[access_key]" placeholder="请输入 AccessKey" />
@@ -152,6 +177,10 @@
<label for="configs[url]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>访问域名</label> <label for="configs[url]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>访问域名</label>
<x-input type="url" name="configs[url]" id="configs[url]" placeholder="请输入图片访问域名 http(s)://" /> <x-input type="url" name="configs[url]" id="configs[url]" placeholder="请输入图片访问域名 http(s)://" />
</div> </div>
<div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[queries]" class="block text-sm font-medium text-gray-700">URL Queries</label>
<x-input type="text" name="configs[queries]" id="configs[queries]" placeholder="请输入 url 额外参数" />
</div>
<div class="col-span-3 sm:col-span-2 mb-4"> <div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[service]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>服务名称</label> <label for="configs[service]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>服务名称</label>
<x-input type="text" name="configs[service]" id="configs[service]" placeholder="请输入服务名称" /> <x-input type="text" name="configs[service]" id="configs[service]" placeholder="请输入服务名称" />
@@ -171,11 +200,13 @@
<label for="configs[url]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>访问域名</label> <label for="configs[url]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>访问域名</label>
<x-input type="url" name="configs[url]" id="configs[url]" placeholder="请输入图片访问域名 http(s)://" /> <x-input type="url" name="configs[url]" id="configs[url]" placeholder="请输入图片访问域名 http(s)://" />
</div> </div>
<div class="col-span-6"> <div class="col-span-3 sm:col-span-2 mb-4">
<div class="col-span-6 sm:col-span-3 mb-4"> <label for="configs[queries]" class="block text-sm font-medium text-gray-700">URL Queries</label>
<label for="configs[root]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>根目录</label> <x-input type="text" name="configs[queries]" id="configs[queries]" placeholder="请输入 url 额外参数" />
<x-input type="text" name="configs[root]" id="configs[root]" autocomplete="text" placeholder="请输入根目录路径" value="/" /> </div>
</div> <div class="col-span-6 sm:col-span-3 mb-4">
<label for="configs[root]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>根目录</label>
<x-input type="text" name="configs[root]" id="configs[root]" autocomplete="text" placeholder="请输入根目录路径" value="/" />
</div> </div>
<div class="col-span-3 sm:col-span-2 mb-4"> <div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[host]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>主机地址</label> <label for="configs[host]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>主机地址</label>
@@ -217,11 +248,13 @@
<label for="configs[url]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>访问域名</label> <label for="configs[url]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>访问域名</label>
<x-input type="url" name="configs[url]" id="configs[url]" placeholder="请输入图片访问域名 http(s)://" /> <x-input type="url" name="configs[url]" id="configs[url]" placeholder="请输入图片访问域名 http(s)://" />
</div> </div>
<div class="col-span-6"> <div class="col-span-3 sm:col-span-2 mb-4">
<div class="col-span-6 sm:col-span-3 mb-4"> <label for="configs[queries]" class="block text-sm font-medium text-gray-700">URL Queries</label>
<label for="configs[root]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>根目录</label> <x-input type="text" name="configs[queries]" id="configs[queries]" placeholder="请输入 url 额外参数" />
<x-input type="text" name="configs[root]" id="configs[root]" autocomplete="text" placeholder="请输入根目录路径" value="/" /> </div>
</div> <div class="col-span-6 sm:col-span-3 mb-4">
<label for="configs[root]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>根目录</label>
<x-input type="text" name="configs[root]" id="configs[root]" autocomplete="text" placeholder="请输入根目录路径" value="/" />
</div> </div>
<div class="col-span-3 sm:col-span-2 mb-4"> <div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[host]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>主机地址</label> <label for="configs[host]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>主机地址</label>
@@ -255,10 +288,26 @@
<label for="configs[url]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>访问域名</label> <label for="configs[url]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>访问域名</label>
<x-input type="url" name="configs[url]" id="configs[url]" placeholder="请输入图片访问域名 http(s)://" /> <x-input type="url" name="configs[url]" id="configs[url]" placeholder="请输入图片访问域名 http(s)://" />
</div> </div>
<div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[queries]" class="block text-sm font-medium text-gray-700">URL Queries</label>
<x-input type="text" name="configs[queries]" id="configs[queries]" placeholder="请输入 url 额外参数" />
</div>
<div class="col-span-3 sm:col-span-2 mb-4"> <div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[base_uri]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>连接地址</label> <label for="configs[base_uri]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>连接地址</label>
<x-input type="url" name="configs[base_uri]" id="configs[base_uri]" placeholder="请输入连接地址" /> <x-input type="url" name="configs[base_uri]" id="configs[base_uri]" placeholder="请输入连接地址" />
</div> </div>
<div class="col-span-3 sm:col-span-2 mb-4">
<label for="webdav-configs[auth_type]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>认证方式</label>
<x-select id="webdav-auth-type" name="configs[auth_type]" select2>
@foreach(\App\Models\Strategy::WEBDAV_AUTH_TYPES as $key => $type)
<option value="{{ $key }}" {{ $loop->first ? 'selected' : '' }}>{{ $type }}</option>
@endforeach
</x-select>
</div>
<div class="col-span-3 sm:col-span-2 mb-4">
<label for="webdav-configs[prefix]" class="block text-sm font-medium text-gray-700">路径前缀</label>
<x-input type="text" name="configs[prefix]" id="webdav-configs[prefix]" placeholder="请输入路径前缀"></x-input>
</div>
<div class="col-span-3 sm:col-span-2 mb-4"> <div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[username]" class="block text-sm font-medium text-gray-700">用户名</label> <label for="configs[username]" class="block text-sm font-medium text-gray-700">用户名</label>
<x-input type="text" name="configs[username]" id="configs[username]" placeholder="请输入用户名" /> <x-input type="text" name="configs[username]" id="configs[username]" placeholder="请输入用户名" />
@@ -275,8 +324,8 @@
<x-input type="url" name="configs[url]" id="configs[url]" placeholder="请输入图片访问域名 http(s)://" /> <x-input type="url" name="configs[url]" id="configs[url]" placeholder="请输入图片访问域名 http(s)://" />
</div> </div>
<div class="col-span-3 sm:col-span-2 mb-4"> <div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[endpoint]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>连接地址</label> <label for="configs[queries]" class="block text-sm font-medium text-gray-700">URL Queries</label>
<x-input type="url" name="configs[endpoint]" id="configs[endpoint]" placeholder="请输入连接地址" /> <x-input type="text" name="configs[queries]" id="configs[queries]" placeholder="请输入 url 额外参数" />
</div> </div>
<div class="col-span-3 sm:col-span-2 mb-4"> <div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[access_key]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>AccessKey</label> <label for="configs[access_key]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>AccessKey</label>
@@ -286,10 +335,23 @@
<label for="configs[secret_key]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>SecretKey</label> <label for="configs[secret_key]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>SecretKey</label>
<x-input type="password" name="configs[secret_key]" id="configs[secret_key]" placeholder="请输入 SecretKey" /> <x-input type="password" name="configs[secret_key]" id="configs[secret_key]" placeholder="请输入 SecretKey" />
</div> </div>
<div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[endpoint]" class="block text-sm font-medium text-gray-700">连接地址</label>
<x-input type="url" name="configs[endpoint]" id="configs[endpoint]" placeholder="请输入连接地址" />
</div>
<div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[region]" class="block text-sm font-medium text-gray-700">区域(region)</label>
<x-input type="text" name="configs[region]" id="configs[region]" placeholder="请输入区域例如us-east-1" />
</div>
<div class="col-span-3 sm:col-span-2 mb-4"> <div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[bucket]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>Bucket 名称</label> <label for="configs[bucket]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>Bucket 名称</label>
<x-input type="text" name="configs[bucket]" id="configs[bucket]" placeholder="请输入 Bucket 名称" /> <x-input type="text" name="configs[bucket]" id="configs[bucket]" placeholder="请输入 Bucket 名称" />
</div> </div>
<div class="col-span-6">
<label for="configs[bucket_endpoint]" class="block text-sm font-medium mb-2 text-gray-700">BucketEndpoint</label>
<x-switch id="configs[bucket_endpoint]" name="configs[bucket_endpoint]" value="1"></x-switch>
<p><small class="text-gray-500"><i class="fas fa-exclamation-circle"></i> 开启此选项后将会直接以「连接地址」作为数据交互传输域名,否则可能会以桶名称拼接域名(例如http://桶名称.连接地址.com)</small></p>
</div>
</div> </div>
</div> </div>
<div class="px-4 py-3 bg-gray-50 text-right sm:px-6"> <div class="px-4 py-3 bg-gray-50 text-right sm:px-6">

View File

@@ -7,7 +7,7 @@
<div class="md:mt-0 md:col-span-2"> <div class="md:mt-0 md:col-span-2">
<form action="{{ route('admin.strategy.update', ['id' => $strategy->id]) }}" method="POST"> <form action="{{ route('admin.strategy.update', ['id' => $strategy->id]) }}" method="POST">
<div class="overflow-hidden rounded-md"> <div class="overflow-hidden rounded-md">
<div class="px-4 py-5 bg-white sm:p-6 space-y-4"> <div class="px-4 py-5 bg-white sm:p-6 space-y-4 shadow-custom">
<div class="col-span-6"> <div class="col-span-6">
<label class="block"> <label class="block">
@@ -45,15 +45,19 @@
<div class="col-span-6 mb-4" data-driver="{{ \App\Enums\StrategyKey::Local }}"> <div class="col-span-6 mb-4" data-driver="{{ \App\Enums\StrategyKey::Local }}">
<div class="col-span-3 sm:col-span-2 mb-4"> <div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[url]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>访问网址</label> <label for="configs[url]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>访问网址</label>
<x-input type="text" name="configs[url]" id="configs[url]" placeholder="请输入图片访问域名,需要加 http(s)://" value="{{ $strategy->configs['url'] }}" /> <x-input type="text" name="configs[url]" id="configs[url]" placeholder="请输入图片访问域名,需要加 http(s)://" value="{{ $strategy->configs->get('url') }}" />
<small class="text-orange-500"><i class="fas fa-exclamation"></i> <small class="text-orange-500"><i class="fas fa-exclamation"></i>
本地储存的访问网址必须有根路径例如https://www.lsky.pro/uploads 中的 uploads 就是根路径,且根路径不能和其他策略重复。修改根路径直接影响已经上传并已使用的链接的访问。 本地储存的访问网址必须有根路径例如https://www.lsky.pro/uploads 中的 uploads 就是根路径,且根路径不能和其他策略重复。修改根路径直接影响已经上传并已使用的链接的访问。
</small> </small>
</div> </div>
<div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[queries]" class="block text-sm font-medium text-gray-700">URL Queries</label>
<x-input type="text" name="configs[queries]" id="configs[queries]" placeholder="请输入 url 额外参数" value="{{ $strategy->configs->get('queries') }}" />
</div>
<div class="col-span-6"> <div class="col-span-6">
<div class="col-span-6 sm:col-span-3 mb-4"> <div class="col-span-6 sm:col-span-3 mb-4">
<label for="configs[root]" class="block text-sm font-medium text-gray-700">储存路径</label> <label for="configs[root]" class="block text-sm font-medium text-gray-700">储存路径</label>
<x-input type="text" name="configs[root]" id="configs[root]" autocomplete="text" placeholder="图片保存位置,默认:{{ config('filesystems.disks.uploads.root') }}" value="{{ $strategy->configs['root'] }}" /> <x-input type="text" name="configs[root]" id="configs[root]" autocomplete="text" placeholder="图片保存位置,默认:{{ config('filesystems.disks.uploads.root') }}" value="{{ $strategy->configs->get('root') }}" />
<small class="text-orange-500"><i class="fas fa-exclamation"></i> 储存路径为绝对路径,设置错误或没有读写权限可能会导致图片保存失败。如果储存路径与其他策略相同,那么请注意使用角色组的路径命名规则、文件命名规则来区分不同文件夹,否则可能会因为名称重复而导致图片物理文件被覆盖。</small> <small class="text-orange-500"><i class="fas fa-exclamation"></i> 储存路径为绝对路径,设置错误或没有读写权限可能会导致图片保存失败。如果储存路径与其他策略相同,那么请注意使用角色组的路径命名规则、文件命名规则来区分不同文件夹,否则可能会因为名称重复而导致图片物理文件被覆盖。</small>
</div> </div>
</div> </div>
@@ -64,23 +68,31 @@
<div class="col-span-6 mb-4" data-driver="{{ \App\Enums\StrategyKey::S3 }}"> <div class="col-span-6 mb-4" data-driver="{{ \App\Enums\StrategyKey::S3 }}">
<div class="col-span-3 sm:col-span-2 mb-4"> <div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[url]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>访问域名</label> <label for="configs[url]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>访问域名</label>
<x-input type="url" name="configs[url]" id="configs[url]" placeholder="请输入图片访问域名 http(s)://" value="{{ $strategy->configs['url'] }}" /> <x-input type="url" name="configs[url]" id="configs[url]" placeholder="请输入图片访问域名 http(s)://" value="{{ $strategy->configs->get('url') }}" />
</div>
<div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[queries]" class="block text-sm font-medium text-gray-700">URL Queries</label>
<x-input type="text" name="configs[queries]" id="configs[queries]" placeholder="请输入 url 额外参数" value="{{ $strategy->configs->get('queries') }}" />
</div> </div>
<div class="col-span-3 sm:col-span-2 mb-4"> <div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[access_key_id]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>AccessKeyId</label> <label for="configs[access_key_id]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>AccessKeyId</label>
<x-input type="text" name="configs[access_key_id]" id="configs[access_key_id]" placeholder="请输入 AccessKeyId" value="{{ $strategy->configs['access_key_id'] }}" /> <x-input type="text" name="configs[access_key_id]" id="configs[access_key_id]" placeholder="请输入 AccessKeyId" value="{{ $strategy->configs->get('access_key_id') }}" />
</div> </div>
<div class="col-span-3 sm:col-span-2 mb-4"> <div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[secret_access_key]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>SecretAccessKey</label> <label for="configs[secret_access_key]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>SecretAccessKey</label>
<x-input type="password" name="configs[secret_access_key]" id="configs[secret_access_key]" placeholder="请输入 SecretAccessKey" value="{{ $strategy->configs['secret_access_key'] }}" /> <x-input type="password" name="configs[secret_access_key]" id="configs[secret_access_key]" placeholder="请输入 SecretAccessKey" value="{{ $strategy->configs->get('secret_access_key') }}" />
</div>
<div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[endpoint]" class="block text-sm font-medium text-gray-700">连接地址</label>
<x-input type="url" name="configs[endpoint]" id="configs[endpoint]" placeholder="请输入连接地址" value="{{ $strategy->configs->get('endpoint') }}" />
</div> </div>
<div class="col-span-3 sm:col-span-2 mb-4"> <div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[region]" class="block text-sm font-medium text-gray-700">区域(region)</label> <label for="configs[region]" class="block text-sm font-medium text-gray-700">区域(region)</label>
<x-input type="text" name="configs[region]" id="configs[region]" placeholder="请输入区域例如us-east-1" value="{{ $strategy->configs['region'] }}" /> <x-input type="text" name="configs[region]" id="configs[region]" placeholder="请输入区域例如us-east-1" value="{{ $strategy->configs->get('region') }}" />
</div> </div>
<div class="col-span-3 sm:col-span-2 mb-4"> <div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[bucket]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>储存桶名称</label> <label for="configs[bucket]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>储存桶名称</label>
<x-input type="text" name="configs[bucket]" id="configs[bucket]" placeholder="请输入 Bucket 名称" value="{{ $strategy->configs['bucket'] }}" /> <x-input type="text" name="configs[bucket]" id="configs[bucket]" placeholder="请输入 Bucket 名称" value="{{ $strategy->configs->get('bucket') }}" />
</div> </div>
</div> </div>
@endif @endif
@@ -89,23 +101,27 @@
<div class="col-span-6 mb-4" data-driver="{{ \App\Enums\StrategyKey::Oss }}"> <div class="col-span-6 mb-4" data-driver="{{ \App\Enums\StrategyKey::Oss }}">
<div class="col-span-3 sm:col-span-2 mb-4"> <div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[url]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>访问域名</label> <label for="configs[url]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>访问域名</label>
<x-input type="url" name="configs[url]" id="configs[url]" placeholder="请输入图片访问域名 http(s)://" value="{{ $strategy->configs['url'] }}" /> <x-input type="url" name="configs[url]" id="configs[url]" placeholder="请输入图片访问域名 http(s)://" value="{{ $strategy->configs->get('url') }}" />
</div>
<div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[queries]" class="block text-sm font-medium text-gray-700">URL Queries</label>
<x-input type="text" name="configs[queries]" id="configs[queries]" placeholder="请输入 url 额外参数" value="{{ $strategy->configs->get('queries') }}" />
</div> </div>
<div class="col-span-3 sm:col-span-2 mb-4"> <div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[access_key_id]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>AccessKeyId</label> <label for="configs[access_key_id]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>AccessKeyId</label>
<x-input type="text" name="configs[access_key_id]" id="configs[access_key_id]" placeholder="请输入 AccessKeyId" value="{{ $strategy->configs['access_key_id'] }}" /> <x-input type="text" name="configs[access_key_id]" id="configs[access_key_id]" placeholder="请输入 AccessKeyId" value="{{ $strategy->configs->get('access_key_id') }}" />
</div> </div>
<div class="col-span-3 sm:col-span-2 mb-4"> <div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[access_key_secret]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>AccessKeySecret</label> <label for="configs[access_key_secret]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>AccessKeySecret</label>
<x-input type="password" name="configs[access_key_secret]" id="configs[access_key_secret]" placeholder="请输入 AccessKeySecret" value="{{ $strategy->configs['access_key_secret'] }}" /> <x-input type="password" name="configs[access_key_secret]" id="configs[access_key_secret]" placeholder="请输入 AccessKeySecret" value="{{ $strategy->configs->get('access_key_secret') }}" />
</div> </div>
<div class="col-span-3 sm:col-span-2 mb-4"> <div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[endpoint]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>地域节点(Endpoint)</label> <label for="configs[endpoint]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>地域节点(Endpoint)</label>
<x-input type="text" name="configs[endpoint]" id="configs[endpoint]" placeholder="请输入所属地域节点例如oss-cn-beijing.aliyuncs.com" value="{{ $strategy->configs['endpoint'] }}" /> <x-input type="text" name="configs[endpoint]" id="configs[endpoint]" placeholder="请输入所属地域节点例如oss-cn-beijing.aliyuncs.com" value="{{ $strategy->configs->get('endpoint') }}" />
</div> </div>
<div class="col-span-3 sm:col-span-2 mb-4"> <div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[bucket]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>Bucket 名称</label> <label for="configs[bucket]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>Bucket 名称</label>
<x-input type="text" name="configs[bucket]" id="configs[bucket]" placeholder="请输入 Bucket 名称" value="{{ $strategy->configs['bucket'] }}" /> <x-input type="text" name="configs[bucket]" id="configs[bucket]" placeholder="请输入 Bucket 名称" value="{{ $strategy->configs->get('bucket') }}" />
</div> </div>
</div> </div>
@endif @endif
@@ -114,27 +130,32 @@
<div class="col-span-6 mb-4" data-driver="{{ \App\Enums\StrategyKey::Cos }}"> <div class="col-span-6 mb-4" data-driver="{{ \App\Enums\StrategyKey::Cos }}">
<div class="col-span-3 sm:col-span-2 mb-4"> <div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[url]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>访问域名</label> <label for="configs[url]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>访问域名</label>
<x-input type="url" name="configs[url]" id="configs[url]" placeholder="请输入图片访问域名 http(s)://" value="{{ $strategy->configs['url'] }}" /> <x-input type="url" name="configs[url]" id="configs[url]" placeholder="请输入图片访问域名 http(s)://" value="{{ $strategy->configs->get('url') }}" />
</div>
<div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[queries]" class="block text-sm font-medium text-gray-700">URL Queries</label>
<x-input type="text" name="configs[queries]" id="configs[queries]" placeholder="请输入 url 额外参数" value="{{ $strategy->configs->get('queries') }}" />
</div> </div>
<div class="col-span-3 sm:col-span-2 mb-4"> <div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[app_id]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>AppId</label> <label for="configs[app_id]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>AppId</label>
<x-input type="text" name="configs[app_id]" id="configs[app_id]" placeholder="请输入 AppId" value="{{ $strategy->configs['app_id'] }}" /> <x-input type="text" name="configs[app_id]" id="configs[app_id]" placeholder="请输入 AppId" value="{{ $strategy->configs->get('app_id') }}" />
</div> </div>
<div class="col-span-3 sm:col-span-2 mb-4"> <div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[secret_id]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>SecretId</label> <label for="configs[secret_id]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>SecretId</label>
<x-input type="text" name="configs[secret_id]" id="configs[secret_id]" placeholder="请输入 SecretId" value="{{ $strategy->configs['secret_id'] }}" /> <x-input type="text" name="configs[secret_id]" id="configs[secret_id]" placeholder="请输入 SecretId" value="{{ $strategy->configs->get('secret_id') }}" />
</div> </div>
<div class="col-span-3 sm:col-span-2 mb-4"> <div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[secret_key]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>SecretKey</label> <label for="configs[secret_key]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>SecretKey</label>
<x-input type="password" name="configs[secret_key]" id="configs[secret_key]" placeholder="请输入 SecretKey" value="{{ $strategy->configs['secret_key'] }}" /> <x-input type="password" name="configs[secret_key]" id="configs[secret_key]" placeholder="请输入 SecretKey" value="{{ $strategy->configs->get('secret_key') }}" />
</div> </div>
<div class="col-span-3 sm:col-span-2 mb-4"> <div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[region]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>所属地域</label> <label for="configs[region]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>所属地域</label>
<x-input type="text" name="configs[region]" id="configs[region]" placeholder="请输入所属地域例如ap-chengdu" value="{{ $strategy->configs['region'] }}" /> <x-input type="text" name="configs[region]" id="configs[region]" placeholder="请输入所属地域例如ap-chengdu" value="{{ $strategy->configs->get('region') }}" />
</div> </div>
<div class="col-span-3 sm:col-span-2 mb-4"> <div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[bucket]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>储存桶名称</label> <label for="configs[bucket]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>储存桶名称</label>
<x-input type="text" name="configs[bucket]" id="configs[bucket]" placeholder="请输入储存桶名称" value="{{ $strategy->configs['bucket'] }}" /> <x-input type="text" name="configs[bucket]" id="configs[bucket]" placeholder="请输入储存桶名称" value="{{ $strategy->configs->get('bucket') }}" />
<small class="text-gray-500"><i class="fas fa-exclamation-circle"></i> 腾讯云储存桶名称由 名称+appid 组合例如test-125146xxxx此处应该填写 test</>
</div> </div>
</div> </div>
@endif @endif
@@ -143,19 +164,23 @@
<div class="col-span-6 mb-4" data-driver="{{ \App\Enums\StrategyKey::Kodo }}"> <div class="col-span-6 mb-4" data-driver="{{ \App\Enums\StrategyKey::Kodo }}">
<div class="col-span-3 sm:col-span-2 mb-4"> <div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[url]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>访问域名</label> <label for="configs[url]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>访问域名</label>
<x-input type="url" name="configs[url]" id="configs[url]" placeholder="请输入图片访问域名 http(s)://" value="{{ $strategy->configs['url'] }}" /> <x-input type="url" name="configs[url]" id="configs[url]" placeholder="请输入图片访问域名 http(s)://" value="{{ $strategy->configs->get('url') }}" />
</div>
<div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[queries]" class="block text-sm font-medium text-gray-700">URL Queries</label>
<x-input type="text" name="configs[queries]" id="configs[queries]" placeholder="请输入 url 额外参数" value="{{ $strategy->configs->get('queries') }}" />
</div> </div>
<div class="col-span-3 sm:col-span-2 mb-4"> <div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[access_key]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>AccessKey</label> <label for="configs[access_key]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>AccessKey</label>
<x-input type="text" name="configs[access_key]" id="configs[access_key]" placeholder="请输入 AccessKey" value="{{ $strategy->configs['access_key'] }}" /> <x-input type="text" name="configs[access_key]" id="configs[access_key]" placeholder="请输入 AccessKey" value="{{ $strategy->configs->get('access_key') }}" />
</div> </div>
<div class="col-span-3 sm:col-span-2 mb-4"> <div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[secret_key]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>SecretKey</label> <label for="configs[secret_key]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>SecretKey</label>
<x-input type="password" name="configs[secret_key]" id="configs[secret_key]" placeholder="请输入 SecretKey" value="{{ $strategy->configs['secret_key'] }}" /> <x-input type="password" name="configs[secret_key]" id="configs[secret_key]" placeholder="请输入 SecretKey" value="{{ $strategy->configs->get('secret_key') }}" />
</div> </div>
<div class="col-span-3 sm:col-span-2 mb-4"> <div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[bucket]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>Bucket</label> <label for="configs[bucket]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>Bucket</label>
<x-input type="text" name="configs[bucket]" id="configs[bucket]" placeholder="请输入 Bucket" value="{{ $strategy->configs['bucket'] }}" /> <x-input type="text" name="configs[bucket]" id="configs[bucket]" placeholder="请输入 Bucket" value="{{ $strategy->configs->get('bucket') }}" />
</div> </div>
</div> </div>
@endif @endif
@@ -164,19 +189,23 @@
<div class="col-span-6 mb-4" data-driver="{{ \App\Enums\StrategyKey::Uss }}"> <div class="col-span-6 mb-4" data-driver="{{ \App\Enums\StrategyKey::Uss }}">
<div class="col-span-3 sm:col-span-2 mb-4"> <div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[url]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>访问域名</label> <label for="configs[url]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>访问域名</label>
<x-input type="url" name="configs[url]" id="configs[url]" placeholder="请输入图片访问域名 http(s)://" value="{{ $strategy->configs['url'] }}" /> <x-input type="url" name="configs[url]" id="configs[url]" placeholder="请输入图片访问域名 http(s)://" value="{{ $strategy->configs->get('url') }}" />
</div>
<div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[queries]" class="block text-sm font-medium text-gray-700">URL Queries</label>
<x-input type="text" name="configs[queries]" id="configs[queries]" placeholder="请输入 url 额外参数" value="{{ $strategy->configs->get('queries') }}" />
</div> </div>
<div class="col-span-3 sm:col-span-2 mb-4"> <div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[service]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>服务名称</label> <label for="configs[service]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>服务名称</label>
<x-input type="text" name="configs[service]" id="configs[service]" placeholder="请输入服务名称" value="{{ $strategy->configs['service'] }}" /> <x-input type="text" name="configs[service]" id="configs[service]" placeholder="请输入服务名称" value="{{ $strategy->configs->get('service') }}" />
</div> </div>
<div class="col-span-3 sm:col-span-2 mb-4"> <div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[operator]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>操作员名称</label> <label for="configs[operator]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>操作员名称</label>
<x-input type="text" name="configs[operator]" id="configs[operator]" placeholder="请输入操作员名称" value="{{ $strategy->configs['operator'] }}" /> <x-input type="text" name="configs[operator]" id="configs[operator]" placeholder="请输入操作员名称" value="{{ $strategy->configs->get('operator') }}" />
</div> </div>
<div class="col-span-3 sm:col-span-2 mb-4"> <div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[password]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>操作员密码</label> <label for="configs[password]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>操作员密码</label>
<x-input type="password" name="configs[password]" id="configs[password]" placeholder="请输入操作员密码" value="{{ $strategy->configs['password'] }}" /> <x-input type="password" name="configs[password]" id="configs[password]" placeholder="请输入操作员密码" value="{{ $strategy->configs->get('password') }}" />
</div> </div>
</div> </div>
@endif @endif
@@ -185,41 +214,43 @@
<div class="col-span-6 mb-4" data-driver="{{ \App\Enums\StrategyKey::Sftp }}"> <div class="col-span-6 mb-4" data-driver="{{ \App\Enums\StrategyKey::Sftp }}">
<div class="col-span-3 sm:col-span-2 mb-4"> <div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[url]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>访问域名</label> <label for="configs[url]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>访问域名</label>
<x-input type="url" name="configs[url]" id="configs[url]" placeholder="请输入图片访问域名 http(s)://" value="{{ $strategy->configs['url'] }}" /> <x-input type="url" name="configs[url]" id="configs[url]" placeholder="请输入图片访问域名 http(s)://" value="{{ $strategy->configs->get('url') }}" />
</div> </div>
<div class="col-span-6"> <div class="col-span-3 sm:col-span-2 mb-4">
<div class="col-span-6 sm:col-span-3 mb-4"> <label for="configs[queries]" class="block text-sm font-medium text-gray-700">URL Queries</label>
<label for="configs[root]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>根目录</label> <x-input type="text" name="configs[queries]" id="configs[queries]" placeholder="请输入 url 额外参数" value="{{ $strategy->configs->get('queries') }}" />
<x-input type="text" name="configs[root]" id="configs[root]" autocomplete="text" placeholder="请输入根目录路径" value="{{ $strategy->configs['root'] }}" /> </div>
</div> <div class="col-span-6 sm:col-span-3 mb-4">
<label for="configs[root]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>根目录</label>
<x-input type="text" name="configs[root]" id="configs[root]" autocomplete="text" placeholder="请输入根目录路径" value="{{ $strategy->configs->get('root') }}" />
</div> </div>
<div class="col-span-3 sm:col-span-2 mb-4"> <div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[host]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>主机地址</label> <label for="configs[host]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>主机地址</label>
<x-input type="text" name="configs[host]" id="configs[host]" placeholder="请输入主机地址例如127.0.0.1" value="{{ $strategy->configs['host'] }}" /> <x-input type="text" name="configs[host]" id="configs[host]" placeholder="请输入主机地址例如127.0.0.1" value="{{ $strategy->configs->get('host') }}" />
</div> </div>
<div class="col-span-3 sm:col-span-2 mb-4"> <div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[port]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>连接端口</label> <label for="configs[port]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>连接端口</label>
<x-input type="number" name="configs[port]" id="configs[port]" placeholder="请输入连接端口" value="{{ $strategy->configs['port'] }}" /> <x-input type="number" name="configs[port]" id="configs[port]" placeholder="请输入连接端口" value="{{ $strategy->configs->get('port') }}" />
</div> </div>
<div class="col-span-3 sm:col-span-2 mb-4"> <div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[username]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>用户名</label> <label for="configs[username]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>用户名</label>
<x-input type="text" name="configs[username]" id="configs[username]" placeholder="请输入用户名" value="{{ $strategy->configs['username'] }}" /> <x-input type="text" name="configs[username]" id="configs[username]" placeholder="请输入用户名" value="{{ $strategy->configs->get('username') }}" />
</div> </div>
<div class="col-span-3 sm:col-span-2 mb-4"> <div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[password]" class="block text-sm font-medium text-gray-700"><span class="text-yellow-500">*</span>密码</label> <label for="configs[password]" class="block text-sm font-medium text-gray-700"><span class="text-yellow-500">*</span>密码</label>
<x-input type="password" name="configs[password]" id="configs[password]" placeholder="如果使用私钥连接,可为空" value="{{ $strategy->configs['password'] }}" /> <x-input type="password" name="configs[password]" id="configs[password]" placeholder="如果使用私钥连接,可为空" value="{{ $strategy->configs->get('password') }}" />
</div> </div>
<div class="col-span-3 sm:col-span-2 mb-4"> <div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[private_key]" class="block text-sm font-medium text-gray-700"><span class="text-yellow-500">*</span>私钥</label> <label for="configs[private_key]" class="block text-sm font-medium text-gray-700"><span class="text-yellow-500">*</span>私钥</label>
<x-textarea name="configs[private_key]" id="configs[private_key]" placeholder="输入私钥文本内容,如果使用密码连接,可为空">{{ $strategy->configs['private_key'] }}</x-textarea> <x-textarea name="configs[private_key]" id="configs[private_key]" placeholder="输入私钥文本内容,如果使用密码连接,可为空">{{ $strategy->configs->get('private_key') }}</x-textarea>
</div> </div>
<div class="col-span-3 sm:col-span-2 mb-4"> <div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[passphrase]" class="block text-sm font-medium text-gray-700">私钥口令</label> <label for="configs[passphrase]" class="block text-sm font-medium text-gray-700">私钥口令</label>
<x-input type="password" name="configs[passphrase]" id="configs[passphrase]" placeholder="如果未设置私钥或私钥未设置口令,可为空" value="{{ $strategy->configs['passphrase'] }}" /> <x-input type="password" name="configs[passphrase]" id="configs[passphrase]" placeholder="如果未设置私钥或私钥未设置口令,可为空" value="{{ $strategy->configs->get('passphrase') }}" />
</div> </div>
<div class="col-span-6"> <div class="col-span-6">
<label for="configs[use_agent]" class="block text-sm font-medium mb-2 text-gray-700">是否使用代理</label> <label for="configs[use_agent]" class="block text-sm font-medium mb-2 text-gray-700">是否使用代理</label>
<x-switch id="configs[use_agent]" name="configs[use_agent]" value="1" :checked="(bool)$strategy->configs['use_agent']"></x-switch> <x-switch id="configs[use_agent]" name="configs[use_agent]" value="1" :checked="(bool)$strategy->configs->get('use_agent')"></x-switch>
</div> </div>
</div> </div>
@endif @endif
@@ -233,38 +264,40 @@
@endif @endif
<div class="col-span-3 sm:col-span-2 mb-4"> <div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[url]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>访问域名</label> <label for="configs[url]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>访问域名</label>
<x-input type="url" name="configs[url]" id="configs[url]" placeholder="请输入图片访问域名 http(s)://" value="{{ $strategy->configs['url'] }}" /> <x-input type="url" name="configs[url]" id="configs[url]" placeholder="请输入图片访问域名 http(s)://" value="{{ $strategy->configs->get('url') }}" />
</div> </div>
<div class="col-span-6"> <div class="col-span-3 sm:col-span-2 mb-4">
<div class="col-span-6 sm:col-span-3 mb-4"> <label for="configs[queries]" class="block text-sm font-medium text-gray-700">URL Queries</label>
<label for="configs[root]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>根目录</label> <x-input type="text" name="configs[queries]" id="configs[queries]" placeholder="请输入 url 额外参数" value="{{ $strategy->configs->get('queries') }}" />
<x-input type="text" name="configs[root]" id="configs[root]" autocomplete="text" placeholder="请输入根目录路径" value="{{ $strategy->configs['root'] }}" /> </div>
</div> <div class="col-span-6 sm:col-span-3 mb-4">
<label for="configs[root]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>根目录</label>
<x-input type="text" name="configs[root]" id="configs[root]" autocomplete="text" placeholder="请输入根目录路径" value="{{ $strategy->configs->get('root') }}" />
</div> </div>
<div class="col-span-3 sm:col-span-2 mb-4"> <div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[host]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>主机地址</label> <label for="configs[host]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>主机地址</label>
<x-input type="text" name="configs[host]" id="configs[host]" placeholder="请输入主机地址例如127.0.0.1" value="{{ $strategy->configs['host'] }}" /> <x-input type="text" name="configs[host]" id="configs[host]" placeholder="请输入主机地址例如127.0.0.1" value="{{ $strategy->configs->get('host') }}" />
</div> </div>
<div class="col-span-3 sm:col-span-2 mb-4"> <div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[port]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>连接端口</label> <label for="configs[port]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>连接端口</label>
<x-input type="number" name="configs[port]" id="configs[port]" placeholder="请输入连接端口 21/20" value="20" value="{{ $strategy->configs['port'] }}" /> <x-input type="number" name="configs[port]" id="configs[port]" placeholder="请输入连接端口 21/20" value="20" value="{{ $strategy->configs->get('port') }}" />
<small class="text-gray-500"><i class="fas fa-exclamation-circle"></i> 通常情况下FTP 的被动模式连接端口为 21,主动模式为 20</small> <small class="text-gray-500"><i class="fas fa-exclamation-circle"></i> 通常情况下FTP 的被动模式连接端口为 21,主动模式为 20</small>
</div> </div>
<div class="col-span-3 sm:col-span-2 mb-4"> <div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[username]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>用户名</label> <label for="configs[username]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>用户名</label>
<x-input type="text" name="configs[username]" id="configs[username]" placeholder="请输入用户名" value="{{ $strategy->configs['username'] }}" /> <x-input type="text" name="configs[username]" id="configs[username]" placeholder="请输入用户名" value="{{ $strategy->configs->get('username') }}" />
</div> </div>
<div class="col-span-3 sm:col-span-2 mb-4"> <div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[password]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>密码</label> <label for="configs[password]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>密码</label>
<x-input type="password" name="configs[password]" id="configs[password]" placeholder="请输入密码" value="{{ $strategy->configs['password'] }}" /> <x-input type="password" name="configs[password]" id="configs[password]" placeholder="请输入密码" value="{{ $strategy->configs->get('password') }}" />
</div> </div>
<div class="col-span-6"> <div class="col-span-6">
<label for="configs[ssl]" class="block text-sm font-medium mb-2 text-gray-700">加密连接</label> <label for="configs[ssl]" class="block text-sm font-medium mb-2 text-gray-700">加密连接</label>
<x-switch id="configs[ssl]" name="configs[ssl]" value="1" :checked="(bool)$strategy->configs['ssl']"></x-switch> <x-switch id="configs[ssl]" name="configs[ssl]" value="1" :checked="(bool)$strategy->configs->get('ssl')"></x-switch>
</div> </div>
<div class="col-span-6"> <div class="col-span-6">
<label for="configs[passive]" class="block text-sm font-medium my-2 text-gray-700">被动模式</label> <label for="configs[passive]" class="block text-sm font-medium my-2 text-gray-700">被动模式</label>
<x-switch id="configs[passive]" name="configs[passive]" value="1" :checked="(bool)$strategy->configs['passive']"></x-switch> <x-switch id="configs[passive]" name="configs[passive]" value="1" :checked="(bool)$strategy->configs->get('passive')"></x-switch>
</div> </div>
</div> </div>
@endif @endif
@@ -273,19 +306,35 @@
<div class="col-span-6 mb-4" data-driver="{{ \App\Enums\StrategyKey::Webdav }}"> <div class="col-span-6 mb-4" data-driver="{{ \App\Enums\StrategyKey::Webdav }}">
<div class="col-span-3 sm:col-span-2 mb-4"> <div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[url]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>访问域名</label> <label for="configs[url]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>访问域名</label>
<x-input type="url" name="configs[url]" id="configs[url]" placeholder="请输入图片访问域名 http(s)://" value="{{ $strategy->configs['url'] }}" /> <x-input type="url" name="configs[url]" id="configs[url]" placeholder="请输入图片访问域名 http(s)://" value="{{ $strategy->configs->get('url') }}" />
</div>
<div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[queries]" class="block text-sm font-medium text-gray-700">URL Queries</label>
<x-input type="text" name="configs[queries]" id="configs[queries]" placeholder="请输入 url 额外参数" value="{{ $strategy->configs->get('queries') }}" />
</div> </div>
<div class="col-span-3 sm:col-span-2 mb-4"> <div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[base_uri]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>连接地址</label> <label for="configs[base_uri]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>连接地址</label>
<x-input type="url" name="configs[base_uri]" id="configs[base_uri]" placeholder="请输入连接地址" value="{{ $strategy->configs['base_uri'] }}" /> <x-input type="url" name="configs[base_uri]" id="configs[base_uri]" placeholder="请输入连接地址" value="{{ $strategy->configs->get('base_uri') }}" />
</div>
<div class="col-span-3 sm:col-span-2 mb-4">
<label for="webdav-configs[auth_type]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>认证方式</label>
<x-select id="webdav-auth-type" name="configs[auth_type]" select2>
@foreach(\App\Models\Strategy::WEBDAV_AUTH_TYPES as $key => $type)
<option value="{{ $key }}" {{ $key == $strategy->configs->get('auth_type') ? 'selected' : '' }}>{{ $type }}</option>
@endforeach
</x-select>
</div>
<div class="col-span-3 sm:col-span-2 mb-4">
<label for="webdav-configs[prefix]" class="block text-sm font-medium text-gray-700">路径前缀</label>
<x-input type="text" name="configs[prefix]" id="webdav-configs[prefix]" placeholder="请输入路径前缀" value="{{ $strategy->configs->get('prefix') }}"></x-input>
</div> </div>
<div class="col-span-3 sm:col-span-2 mb-4"> <div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[username]" class="block text-sm font-medium text-gray-700">用户名</label> <label for="configs[username]" class="block text-sm font-medium text-gray-700">用户名</label>
<x-input type="text" name="configs[username]" id="configs[username]" placeholder="请输入用户名" value="{{ $strategy->configs['username'] }}" /> <x-input type="text" name="configs[username]" id="configs[username]" placeholder="请输入用户名" value="{{ $strategy->configs->get('username') }}" />
</div> </div>
<div class="col-span-3 sm:col-span-2 mb-4"> <div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[password]" class="block text-sm font-medium text-gray-700">密码</label> <label for="configs[password]" class="block text-sm font-medium text-gray-700">密码</label>
<x-input type="password" name="configs[password]" id="configs[password]" placeholder="请输入密码" value="{{ $strategy->configs['password'] }}" /> <x-input type="password" name="configs[password]" id="configs[password]" placeholder="请输入密码" value="{{ $strategy->configs->get('password') }}" />
</div> </div>
</div> </div>
@endif @endif
@@ -294,23 +343,36 @@
<div class="col-span-6 mb-4" data-driver="{{ \App\Enums\StrategyKey::Minio }}"> <div class="col-span-6 mb-4" data-driver="{{ \App\Enums\StrategyKey::Minio }}">
<div class="col-span-3 sm:col-span-2 mb-4"> <div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[url]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>访问域名</label> <label for="configs[url]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>访问域名</label>
<x-input type="url" name="configs[url]" id="configs[url]" placeholder="请输入图片访问域名 http(s)://" value="{{ $strategy->configs['url'] }}" /> <x-input type="url" name="configs[url]" id="configs[url]" placeholder="请输入图片访问域名 http(s)://" value="{{ $strategy->configs->get('url') }}" />
</div> </div>
<div class="col-span-3 sm:col-span-2 mb-4"> <div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[endpoint]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>连接地址</label> <label for="configs[queries]" class="block text-sm font-medium text-gray-700">URL Queries</label>
<x-input type="url" name="configs[endpoint]" id="configs[endpoint]" placeholder="请输入连接地址" value="{{ $strategy->configs['endpoint'] }}" /> <x-input type="text" name="configs[queries]" id="configs[queries]" placeholder="请输入 url 额外参数" value="{{ $strategy->configs->get('queries') }}" />
</div> </div>
<div class="col-span-3 sm:col-span-2 mb-4"> <div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[access_key]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>AccessKey</label> <label for="configs[access_key]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>AccessKey</label>
<x-input type="text" name="configs[access_key]" id="configs[access_key]" placeholder="请输入 AccessKey" value="{{ $strategy->configs['access_key'] }}" /> <x-input type="text" name="configs[access_key]" id="configs[access_key]" placeholder="请输入 AccessKey" value="{{ $strategy->configs->get('access_key') }}" />
</div> </div>
<div class="col-span-3 sm:col-span-2 mb-4"> <div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[secret_key]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>SecretKey</label> <label for="configs[secret_key]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>SecretKey</label>
<x-input type="password" name="configs[secret_key]" id="configs[secret_key]" placeholder="请输入 SecretKey" value="{{ $strategy->configs['secret_key'] }}" /> <x-input type="password" name="configs[secret_key]" id="configs[secret_key]" placeholder="请输入 SecretKey" value="{{ $strategy->configs->get('secret_key') }}" />
</div>
<div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[endpoint]" class="block text-sm font-medium text-gray-700">连接地址</label>
<x-input type="url" name="configs[endpoint]" id="configs[endpoint]" placeholder="请输入连接地址" value="{{ $strategy->configs->get('endpoint') }}" />
</div>
<div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[region]" class="block text-sm font-medium text-gray-700">区域(region)</label>
<x-input type="text" name="configs[region]" id="configs[region]" placeholder="请输入区域例如us-east-1" value="{{ $strategy->configs->get('region') }}" />
</div> </div>
<div class="col-span-3 sm:col-span-2 mb-4"> <div class="col-span-3 sm:col-span-2 mb-4">
<label for="configs[bucket]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>储存桶名称</label> <label for="configs[bucket]" class="block text-sm font-medium text-gray-700"><span class="text-red-600">*</span>储存桶名称</label>
<x-input type="text" name="configs[bucket]" id="configs[bucket]" placeholder="请输入 Bucket 名称" value="{{ $strategy->configs['bucket'] }}" /> <x-input type="text" name="configs[bucket]" id="configs[bucket]" placeholder="请输入 Bucket 名称" value="{{ $strategy->configs->get('bucket') }}" />
</div>
<div class="col-span-6">
<label for="configs[bucket_endpoint]" class="block text-sm font-medium mb-2 text-gray-700">BucketEndpoint</label>
<x-switch id="configs[bucket_endpoint]" name="configs[bucket_endpoint]" value="1" :checked="(bool)$strategy->configs->get('bucket_endpoint')"></x-switch>
<p><small class="text-gray-500"><i class="fas fa-exclamation-circle"></i> 开启此选项后将会直接以「连接地址」作为数据交互传输域名,否则可能会以桶名称拼接域名(例如http://桶名称.连接地址.com)</small></p>
</div> </div>
</div> </div>
@endif @endif

View File

@@ -5,7 +5,7 @@
<div class="mt-5 md:mt-0 md:col-span-2"> <div class="mt-5 md:mt-0 md:col-span-2">
<form action="{{ route('admin.user.update', ['id' => $user->id ?: '0']) }}" method="POST"> <form action="{{ route('admin.user.update', ['id' => $user->id ?: '0']) }}" method="POST">
<div class="overflow-hidden rounded-md"> <div class="overflow-hidden rounded-md shadow-custom">
<div class="px-4 py-5 bg-white sm:p-6"> <div class="px-4 py-5 bg-white sm:p-6">
<div class="grid grid-cols-6 gap-6"> <div class="grid grid-cols-6 gap-6">
<div class="col-span-6"> <div class="col-span-6">

View File

@@ -48,7 +48,7 @@
@endif @endif
</div> </div>
<x-modal> <x-modal id="user-modal">
<div id="modal-content"></div> <div id="modal-content"></div>
</x-modal> </x-modal>
@@ -135,11 +135,11 @@
$('#modal-content').html(html); $('#modal-content').html(html);
modal.open = true; modal.open('user-modal')
}); });
$('[data-operate="delete"]').click(function () { $('[data-operate="delete"]').click(function () {
Swal.fire({ Swal.fire({
title: `确认删除用户【${$(this).closest('tr').find('td.name').text()}】吗?`, title: `确认删除用户【${$(this).closest('tr').data('json').name}】吗?`,
text: "⚠️注意,删除后不可恢复,且该用户的图片将会变成游客身份!", text: "⚠️注意,删除后不可恢复,且该用户的图片将会变成游客身份!",
icon: 'warning', icon: 'warning',
showCancelButton: true, showCancelButton: true,

View File

@@ -40,16 +40,26 @@
</label> </label>
</div> </div>
<div class="flex items-center justify-end mt-4"> <div class="flex items-center justify-between mt-4">
@if (Route::has('password.request')) <div class="flex items-center text-sm">
<a class="underline text-sm text-gray-600 hover:text-gray-900" href="{{ route('password.request') }}"> @if(\App\Utils::config(\App\Enums\ConfigKey::IsEnableRegistration))
{{ __('Forgot your password?') }} 没有账号?
</a> <a class="underline text-gray-600 hover:text-gray-900" href="{{ route('register') }}">
@endif 注册
</a>
@endif
</div>
<div class="flex items-center">
@if (Route::has('password.request'))
<a class="underline text-sm text-gray-600 hover:text-gray-900" href="{{ route('password.request') }}">
{{ __('Forgot your password?') }}
</a>
@endif
<x-button class="ml-3"> <x-button class="ml-3">
{{ __('Log in') }} {{ __('Log in') }}
</x-button> </x-button>
</div>
</div> </div>
</form> </form>
</x-auth-card> </x-auth-card>

View File

@@ -3,7 +3,7 @@
<x-app-layout> <x-app-layout>
<div class="my-6 md:my-9"> <div class="my-6 md:my-9">
<p class="text-xl mb-2 text-gray-800 font-semibold">接口说明</p> <p class="text-xl mb-2 text-gray-800 font-semibold">接口说明</p>
<div class="space-y-4 bg-white p-3 rounded-md mb-10"> <div class="space-y-4 bg-white p-3 rounded-md mb-10 shadow-custom">
<div> <div>
<p class="text-lg text-gray-700 font-semibold">接口URL</p> <p class="text-lg text-gray-700 font-semibold">接口URL</p>
<x-code>{{ request()->getSchemeAndHttpHost() }}/api/v1</x-code> <x-code>{{ request()->getSchemeAndHttpHost() }}/api/v1</x-code>
@@ -12,7 +12,7 @@
<div> <div>
<p class="text-lg text-gray-700 font-semibold">验证方式</p> <p class="text-lg text-gray-700 font-semibold">验证方式</p>
<div class="my-2 text-sm bg-white rounded-md p-4 overflow-x-auto"> <div class="my-2 text-sm bg-white rounded-md p-4 overflow-x-auto">
当前版本接口采用 「HTTP 基本验证」的方式验证授权,获取到 token 后,通过设置请求 header 标头来验证请求,例如: 当前版本接口采用 「HTTP 基本验证」的方式验证授权,获取到 token 后,通过设置请求 header 标头来验证请求(Bearer Token),例如:
<b class="block my-2 text-gray-600 text-sm">"Authorization": "Bearer 1|1bJbwlqBfnggmOMEZqXT5XusaIwqiZjCDs7r1Ob5"</b> <b class="block my-2 text-gray-600 text-sm">"Authorization": "Bearer 1|1bJbwlqBfnggmOMEZqXT5XusaIwqiZjCDs7r1Ob5"</b>
<p class="text-sm">如果未设置 Authorization 的情况下请求上传接口,将会被视为游客上传。</p> <p class="text-sm">如果未设置 Authorization 的情况下请求上传接口,将会被视为游客上传。</p>
</div> </div>
@@ -118,7 +118,7 @@
</div> </div>
<p class="text-xl mb-2 text-gray-800 font-semibold">授权相关</p> <p class="text-xl mb-2 text-gray-800 font-semibold">授权相关</p>
<div class="space-y-4 bg-white p-3 rounded-md mb-10"> <div class="space-y-4 bg-white p-3 rounded-md mb-10 shadow-custom">
<div> <div>
<p class="text-lg text-gray-700 font-semibold">生成 Token</p> <p class="text-lg text-gray-700 font-semibold">生成 Token</p>
<x-code><span class="text-green-500 select-none">POST </span>/tokens</x-code> <x-code><span class="text-green-500 select-none">POST </span>/tokens</x-code>
@@ -319,7 +319,7 @@
</div> </div>
<p class="text-xl mb-2 text-gray-800 font-semibold">策略相关</p> <p class="text-xl mb-2 text-gray-800 font-semibold">策略相关</p>
<div class="space-y-4 bg-white p-3 rounded-md mb-10"> <div class="space-y-4 bg-white p-3 rounded-md mb-10 shadow-custom">
<div> <div>
<p class="text-lg text-gray-700 font-semibold">策略列表</p> <p class="text-lg text-gray-700 font-semibold">策略列表</p>
<x-code><span class="text-sky-500 select-none">GET </span>/strategies</x-code> <x-code><span class="text-sky-500 select-none">GET </span>/strategies</x-code>
@@ -404,7 +404,7 @@
</div> </div>
<p class="text-xl mb-2 text-gray-800 font-semibold">图片相关</p> <p class="text-xl mb-2 text-gray-800 font-semibold">图片相关</p>
<div class="space-y-4 bg-white p-3 rounded-md mb-10"> <div class="space-y-4 bg-white p-3 rounded-md mb-10 shadow-custom">
<div> <div>
<p class="text-lg text-gray-700 font-semibold">上传图片</p> <p class="text-lg text-gray-700 font-semibold">上传图片</p>
<x-code><span class="text-green-500 select-none">POST </span>/upload</x-code> <x-code><span class="text-green-500 select-none">POST </span>/upload</x-code>
@@ -822,7 +822,7 @@
</div> </div>
<p class="text-xl mb-2 text-gray-800 font-semibold">相册相关</p> <p class="text-xl mb-2 text-gray-800 font-semibold">相册相关</p>
<div class="space-y-4 bg-white p-3 rounded-md mb-10"> <div class="space-y-4 bg-white p-3 rounded-md mb-10 shadow-custom">
<div> <div>
<p class="text-lg text-gray-700 font-semibold">相册列表</p> <p class="text-lg text-gray-700 font-semibold">相册列表</p>
<x-code><span class="text-sky-500 select-none">GET </span>/albums</x-code> <x-code><span class="text-sky-500 select-none">GET </span>/albums</x-code>

View File

@@ -0,0 +1,28 @@
@if($_is_notice)
<x-modal id="notice-modal">
<div class="markdown-body">
{!! (new Parsedown())->parse(\App\Utils::config(\App\Enums\ConfigKey::SiteNotice)) !!}
</div>
<div class="mt-4 w-full text-right">
<x-button type="button" @click="$store.modal.close('notice-modal');">OK</x-button>
</div>
</x-modal>
@push('scripts')
<script>
let noticeHash = "{{ md5(\App\Utils::config(\App\Enums\ConfigKey::SiteNotice)) }}";
let openNotice = function () {
Alpine.store('modal').open('notice-modal');
localStorage.setItem('notice-hash', noticeHash);
}
if (localStorage.getItem('notice-hash') !== noticeHash) {
setTimeout(function () {
openNotice();
}, 1000)
}
</script>
@endpush
@endif

View File

@@ -3,7 +3,7 @@
{{ $logo }} {{ $logo }}
</div> </div>
<div class="w-full sm:max-w-md mt-6 px-6 py-4 bg-white overflow-hidden sm:rounded-lg"> <div class="w-full sm:max-w-md mt-6 px-6 py-4 bg-white overflow-hidden sm:rounded-lg shadow-custom">
{{ $slot }} {{ $slot }}
</div> </div>
</div> </div>

View File

@@ -1,4 +1,4 @@
<div class="rounded w-full overflow-hidden"> <div class="rounded w-full overflow-hidden shadow-custom">
<div class="px-4 py-3 bg-slate-50 text-gray-800 text-md truncate">{{ $title }}</div> <div class="px-4 py-3 bg-slate-50 text-gray-800 text-md truncate">{{ $title }}</div>
<div class="bg-white w-full"> <div class="bg-white w-full">
{{ $content }} {{ $content }}

View File

@@ -1,3 +1,3 @@
<button {{ $attributes->merge(['type' => 'submit', 'class' => 'inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-500 hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500']) }}> <button {{ $attributes->merge(['type' => 'submit', 'class' => 'inline-flex justify-center py-2 px-4 text-sm font-medium rounded-md text-gray-600 bg-black/10 hover:bg-black/20 hover:text-gray-700']) }}>
{{ $slot }} {{ $slot }}
</button> </button>

View File

@@ -1 +1 @@
<a {{$attributes->merge(['class' => 'block px-4 py-2 active:bg-gray-100 text-sm text-gray-700', 'role' => 'menuitem', 'tabindex' => '-1']) }}>{{ $slot }}</a> <a {{$attributes->merge(['class' => 'block px-4 py-2 active:bg-gray-100 text-sm text-gray-700 truncate hover:bg-sky-500 hover:text-white', 'role' => 'menuitem', 'tabindex' => '-1']) }}>{{ $slot }}</a>

View File

@@ -7,12 +7,12 @@
<div x-show="open" <div x-show="open"
x-transition:enter="transition ease-out duration-200" x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="transform opacity-0 scale-95" x-transition:enter-start="transform opacity-0 translate-y-9"
x-transition:enter-end="transform opacity-100 scale-100" x-transition:enter-end="transform opacity-100 translate-y-0"
x-transition:leave="transition ease-in duration-75" x-transition:leave="transition ease-in duration-75"
x-transition:leave-start="transform opacity-100 scale-100" x-transition:leave-start="transform opacity-100 translate-y-0"
x-transition:leave-end="transform opacity-0 scale-95" x-transition:leave-end="transform opacity-0 translate-y-9"
class="absolute z-[9] {{ $classes[$direction] }} mt-2 w-48 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 focus:outline-none" class="absolute z-[9] {{ $classes[$direction] }} mt-2 w-48 rounded-md shadow-[10px_0px_50px_-15px_rgba(0,0,0,0.25)] py-1 bg-white "
role="menu" role="menu"
aria-orientation="vertical" aria-orientation="vertical"
aria-labelledby="user-menu-button" aria-labelledby="user-menu-button"

View File

@@ -1,4 +1,6 @@
<div {{ $attributes->merge(['class' => "fixed z-10 inset-0 overflow-y-auto"]) }} role="dialog" aria-modal="true" x-data x-cloak x-show="$store.modal.open"> @props(['id' => 'modal'])
<div {{ $attributes->merge(['id' => $id, 'class' => "fixed z-10 inset-0 overflow-y-auto"]) }} role="dialog" aria-modal="true" x-data x-cloak x-show="$store.modal.isOpen('{{ $id }}')">
<div class="flex min-h-screen text-center md:block md:px-2 lg:px-4" style="font-size: 0"> <div class="flex min-h-screen text-center md:block md:px-2 lg:px-4" style="font-size: 0">
<div x-transition:enter="transition ease-out duration-300" <div x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="transform opacity-0" x-transition:enter-start="transform opacity-0"
@@ -6,8 +8,8 @@
x-transition:leave="transition ease-in duration-200" x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="transform opacity-100" x-transition:leave-start="transform opacity-100"
x-transition:leave-end="transform opacity-0" x-transition:leave-end="transform opacity-0"
x-show="$store.modal.open" x-show="$store.modal.isOpen('{{ $id }}')"
@click="$store.modal.open = false" @click="$store.modal.close('{{ $id }}')"
class="hidden fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity md:block" class="hidden fixed inset-0 bg-gray-500 bg-opacity-75 transition-opacity md:block"
aria-hidden="true" aria-hidden="true"
> >
@@ -20,22 +22,22 @@
x-transition:leave="transition ease-in duration-200" x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="transform opacity-100 translate-y-0 md:scale-100" x-transition:leave-start="transform opacity-100 translate-y-0 md:scale-100"
x-transition:leave-end="transform opacity-0 translate-y-4 md:translate-y-0 md:scale-95" x-transition:leave-end="transform opacity-0 translate-y-4 md:translate-y-0 md:scale-95"
x-show="$store.modal.open" x-show="$store.modal.isOpen('{{ $id }}')"
class="flex text-base text-left transform transition w-full md:inline-block md:max-w-2xl md:px-4 md:my-8 md:align-middle lg:max-w-4xl" class="flex text-base text-left transform transition w-full md:inline-block md:max-w-2xl md:px-4 md:my-8 md:align-middle lg:max-w-4xl"
> >
<div class="w-full relative flex bg-white px-4 pt-14 pb-8 overflow-hidden shadow-lg sm:px-6 sm:pt-8 md:p-6 lg:p-8 md:rounded-sm"> <div class="w-full relative flex bg-white px-4 pt-14 pb-8 overflow-hidden shadow-lg sm:px-6 sm:pt-8 md:p-6 lg:p-8 md:rounded-sm">
<button type="button" class="absolute top-2 right-2 text-gray-400 hover:text-gray-500 sm:top-4 sm:right-4 md:top-3 md:right-3 lg:top-4 lg:right-4" @click="Alpine.store('modal').toggle()"> <button type="button" class="absolute top-2 right-2 text-gray-400 hover:text-gray-500 sm:top-4 sm:right-4 md:top-3 md:right-3 lg:top-4 lg:right-4" @click="$store.modal.close('{{ $id }}')">
<span class="sr-only">Close</span> <span class="sr-only">Close</span>
<svg class="h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> <svg class="h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg> </svg>
</button> </button>
<div class="flex items-center justify-center h-24 w-full" x-show="$store.modal.loading"> <div class="flex items-center justify-center h-24 w-full" x-show="$store.modal.isLoading('{{ $id }}')">
<x-loading-spin /> <x-loading-spin />
</div> </div>
<div class="w-full" x-show="! $store.modal.loading"> <div class="w-full" x-show="! $store.modal.isLoading('{{ $id }}')">
{{ $slot ?? '' }} {{ $slot ?? '' }}
</div> </div>
</div> </div>

View File

@@ -1,7 +1,7 @@
<div class="flex flex-col"> <div class="flex flex-col">
<div class="-my-2 sm:-mx-6 lg:-mx-8"> <div class="-my-2 sm:-mx-6 lg:-mx-8">
<div class="py-2 align-middle inline-block w-full sm:px-6 lg:px-8"> <div class="py-2 align-middle inline-block w-full sm:px-6 lg:px-8">
<div class="overflow-x-auto sm:rounded-lg bg-white w-full"> <div class="overflow-x-auto sm:rounded-lg bg-white w-full shadow-custom">
<table class="min-w-full w-full divide-y divide-gray-200"> <table class="min-w-full w-full divide-y divide-gray-200">
<thead class="bg-gray-50"> <thead class="bg-gray-50">
<tr> <tr>

File diff suppressed because one or more lines are too long

View File

@@ -45,6 +45,12 @@
@endforeach @endforeach
</div> </div>
<div class="mt-6 text-sm text-gray-500">
<p><i class="fas fa-exclamation-circle"></i> 请确保<b>数据库版本</b>达到要求。</p>
<p><i class="fas fa-exclamation-circle"></i> 请确保程序<b>目录、文件的权限</b>设置正确。</p>
<p><i class="fas fa-exclamation-circle"></i> 出现安装拓展、启用函数后未生效,请尝试<b>重启 PHP</b></p>
</div>
<div class="mt-6 text-right"> <div class="mt-6 text-right">
@if($status) @if($status)
<a href="javascript:void(0)" id="next" class="rounded-md px-4 py-2 bg-blue-500 text-white">下一步</a> <a href="javascript:void(0)" id="next" class="rounded-md px-4 py-2 bg-blue-500 text-white">下一步</a>

View File

@@ -15,11 +15,8 @@
@stack('styles') @stack('styles')
<!-- Styles --> <!-- Styles -->
<link rel="stylesheet" href="{{ asset('css/common.css') }}"> <link rel="stylesheet" href="{{ asset('css/common.css') }}?t=20220817">
<link rel="stylesheet" href="{{ asset('css/app.css') }}"> <link rel="stylesheet" href="{{ asset('css/app.css') }}?t=20220817">
<!-- Scripts -->
<script src="{{ asset('js/app.js') }}"></script>
</head> </head>
<body class="font-sans antialiased overflow-hidden"> <body class="font-sans antialiased overflow-hidden">
<div class="min-h-screen bg-gray-100" x-data x-cloak> <div class="min-h-screen bg-gray-100" x-data x-cloak>
@@ -46,6 +43,9 @@
</x-container> </x-container>
</div> </div>
</body> </body>
<!-- Scripts -->
<script src="{{ asset('js/app.js') }}?t=20220817"></script>
@include('common.notice')
<script> <script>
// 开关组件默认值 // 开关组件默认值
let setSwitch = function (e) { let setSwitch = function (e) {

View File

@@ -15,17 +15,16 @@
@stack('styles') @stack('styles')
<!-- Styles --> <!-- Styles -->
<link rel="stylesheet" href="{{ asset('css/common.css') }}"> <link rel="stylesheet" href="{{ asset('css/common.css') }}?t=20220817">
<link rel="stylesheet" href="{{ asset('css/app.css') }}"> <link rel="stylesheet" href="{{ asset('css/app.css') }}?t=20220817">
<!-- Scripts -->
<script src="{{ asset('js/app.js') }}"></script>
</head> </head>
<body class="font-sans antialiased"> <body class="font-sans antialiased">
<div class="min-h-screen text-gray-900 bg-gray-100"> <div class="min-h-screen text-gray-900 bg-gray-100">
{{ $slot }} {{ $slot }}
</div> </div>
</body> </body>
<!-- Scripts -->
<script src="{{ asset('js/app.js') }}?t=20220817"></script>
@if(file_exists(public_path('js/custom.js'))) @if(file_exists(public_path('js/custom.js')))
<script src="{{ asset('js/custom.js') }}"></script> <script src="{{ asset('js/custom.js') }}"></script>
@endif @endif

View File

@@ -1,4 +1,4 @@
<header class="transition-all duration-300 w-full h-14 bg-gray-700 text-white flex justify-center fixed top-0 z-[9]"> <header class="transition-all duration-300 w-full h-14 bg-gray-700 text-white flex justify-center fixed top-0 z-[9] shadow-[0_15px_10px_-15px_rgba(0,0,0,0.3)]">
<x-container class="w-full px-6 flex justify-between items-center"> <x-container class="w-full px-6 flex justify-between items-center">
<div class="flex justify-start items-center max-w-[70%]"> <div class="flex justify-start items-center max-w-[70%]">
<a href="javascript:void(0)" @click="$store.sidebar.toggle()" class="w-6 h-6 p-4 rounded-full sm:hidden -ml-1 mr-4 flex justify-center items-center"> <a href="javascript:void(0)" @click="$store.sidebar.toggle()" class="w-6 h-6 p-4 rounded-full sm:hidden -ml-1 mr-4 flex justify-center items-center">
@@ -7,9 +7,8 @@
<a href="" class="text-xl truncate" id="header-title">@yield('title', \App\Utils::config(\App\Enums\ConfigKey::AppName))</a> <a href="" class="text-xl truncate" id="header-title">@yield('title', \App\Utils::config(\App\Enums\ConfigKey::AppName))</a>
</div> </div>
<div class="flex justify-end items-center space-x-4"> <div class="flex justify-end items-center space-x-4">
@if($_group->strategies->isNotEmpty()) @includeWhen($_is_notice, 'layouts.notice')
@include('layouts.strategies') @includeWhen($_group->strategies->isNotEmpty(), 'layouts.strategies')
@endif
@include('layouts.user-nav') @include('layouts.user-nav')
</div> </div>
</x-container> </x-container>

View File

@@ -0,0 +1,15 @@
@if($_is_notice)
<button type="button" class="bg-gray-800 flex items-center text-sm rounded-full focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-gray-800 focus:ring-white" id="open-notice" aria-expanded="false" aria-haspopup="true">
<div class="h-8 w-8 rounded-full flex items-center justify-center bg-white">
<i class="fas fa-envelope text-gray-900"></i>
</div>
<span class="px-2 sm:block hidden">公告</span>
</button>
@push('scripts')
<script>
$('#open-notice').click(function () {
openNotice();
});
</script>
@endpush
@endif

View File

@@ -1,7 +1,6 @@
<nav class="transition-all duration-300 -left-64 sm:left-0 h-screen sm:w-64 bg-white fixed z-10 shadow-lg sm:shadow-none" :class="{ <nav class="transition-all duration-300 -left-[600px] sm:left-0 w-3/4 sm:w-64 h-screen bg-white fixed z-10 shadow-custom" :class="{
'-left-64': ! $store.sidebar.open, '-left-[600px]': ! $store.sidebar.open,
'left-0': $store.sidebar.open, 'left-0': $store.sidebar.open
'w-3/4': $store.sidebar.open
}"> }">
<div class="px-6 h-14 flex justify-between sm:justify-center items-center bg-gray-600 text-white text-xl"> <div class="px-6 h-14 flex justify-between sm:justify-center items-center bg-gray-600 text-white text-xl">
<a href="/" class="truncate">{{ \App\Utils::config(\App\Enums\ConfigKey::AppName) }}</a> <a href="/" class="truncate">{{ \App\Utils::config(\App\Enums\ConfigKey::AppName) }}</a>
@@ -41,7 +40,7 @@
</x-nav-link> </x-nav-link>
@endif @endif
@if(\App\Utils::config(\App\Enums\ConfigKey::IsEnableApi)) @if(\App\Utils::config(\App\Enums\ConfigKey::IsEnableApi))
<x-nav-link :href="route('api')" :active="request()->routeIs('apis')"> <x-nav-link :href="route('api')" :active="request()->routeIs('api')">
<x-slot name="icon"><i class="fas fa-link text-blue-500"></i></x-slot> <x-slot name="icon"><i class="fas fa-link text-blue-500"></i></x-slot>
<x-slot name="name">接口</x-slot> <x-slot name="name">接口</x-slot>
</x-nav-link> </x-nav-link>
@@ -81,9 +80,9 @@
<div id="capacity-progress" class="flex flex-col space-y-2 mb-5 px-5 w-full mt-10"> <div id="capacity-progress" class="flex flex-col space-y-2 mb-5 px-5 w-full mt-10">
<p class="text-gray-700 text-sm">容量使用</p> <p class="text-gray-700 text-sm">容量使用</p>
<progress class="w-full h-1.5" value="{{ Auth::user()->images->sum('size') }}" max="{{ Auth::user()->capacity }}"></progress> <progress class="w-full h-1.5" value="{{ Auth::user()->use_capacity }}" max="{{ Auth::user()->capacity }}"></progress>
<p class="text-gray-700 text-sm truncate"> <p class="text-gray-700 text-sm truncate">
<span class="used">{{ \App\Utils::formatSize(Auth::user()->images->sum('size') * 1024) }}</span> <span class="used">{{ \App\Utils::formatSize(Auth::user()->use_capacity * 1024) }}</span>
/ /
<span class="total">{{ \App\Utils::formatSize(Auth::user()->capacity * 1024) }}</span> <span class="total">{{ \App\Utils::formatSize(Auth::user()->capacity * 1024) }}</span>
</p> </p>

View File

@@ -6,7 +6,7 @@
<div class="h-8 w-8 rounded-full flex items-center justify-center bg-white"> <div class="h-8 w-8 rounded-full flex items-center justify-center bg-white">
<i class="fas fa-server text-gray-900"></i> <i class="fas fa-server text-gray-900"></i>
</div> </div>
<span class="px-2 sm:block hidden" id="strategy-selected">获取中...</span> <span class="px-2 sm:block hidden" id="strategy-selected" data-id="0">获取中...</span>
</button> </button>
</x-slot> </x-slot>
@@ -21,7 +21,7 @@
@push('scripts') @push('scripts')
<script> <script>
let defaultStrategy = {{ Auth::check() ? Auth::user()->configs[\App\Enums\UserConfigKey::DefaultStrategy] : 0 }} || (localStorage.getItem('strategy') || 0); let defaultStrategy = {{ Auth::check() ? Auth::user()->configs->get('default_strategy') : 0 }} || (localStorage.getItem('strategy') || 0);
let setStrategy = function (id) { let setStrategy = function (id) {
let isSelected = false; let isSelected = false;
$('#strategies a').each(function () { $('#strategies a').each(function () {
@@ -44,7 +44,7 @@
if (! isSelected) { if (! isSelected) {
let $first = $('#strategies a:first-child'); let $first = $('#strategies a:first-child');
localStorage.setItem('strategy', $first.data('id')) localStorage.setItem('strategy', $first.data('id'))
$('#strategy-selected').text($first.text()); $('#strategy-selected').text($first.text()).data('id', $first.data('id'));
} }
}; };

View File

@@ -3,28 +3,28 @@
<x-app-layout> <x-app-layout>
<div class="my-6 md:my-9"> <div class="my-6 md:my-9">
<div class="space-y-6 md:space-y-0 md:grid md:grid-cols-2 xl:grid-cols-4 md:gap-x-4 xl:gap-x-8 md:gap-y-4 xl:gap-y-8"> <div class="space-y-6 md:space-y-0 md:grid md:grid-cols-2 xl:grid-cols-4 md:gap-x-4 xl:gap-x-8 md:gap-y-4 xl:gap-y-8">
<div class="flex bg-white rounded p-4 space-x-4"> <div class="flex bg-white rounded p-4 space-x-4 shadow-custom">
<i class="fas fa-images text-amber-500 text-5xl"></i> <i class="fas fa-images text-amber-500 text-5xl"></i>
<div class="flex flex-col"> <div class="flex flex-col">
<p class="text-gray-700 text-sm">图片数量</p> <p class="text-gray-700 text-sm">图片数量</p>
<p class="text-gray-800 font-semibold text-xl">{{ $user->image_num }}</p> <p class="text-gray-800 font-semibold text-xl">{{ $user->image_num }}</p>
</div> </div>
</div> </div>
<div class="flex bg-white rounded p-4 space-x-4"> <div class="flex bg-white rounded p-4 space-x-4 shadow-custom">
<i class="fas fa-hdd text-red-500 text-5xl"></i> <i class="fas fa-hdd text-red-500 text-5xl"></i>
<div class="flex flex-col"> <div class="flex flex-col">
<p class="text-gray-700 text-sm">可用储存</p> <p class="text-gray-700 text-sm">可用储存</p>
<p class="text-gray-800 font-semibold text-xl">{{ \App\Utils::formatSize(($user->capacity - $user->images->sum('size')) * 1024) }}</p> <p class="text-gray-800 font-semibold text-xl">{{ \App\Utils::formatSize(($user->capacity - $user->use_capacity) * 1024) }}</p>
</div> </div>
</div> </div>
<div class="flex bg-white rounded p-4 space-x-4"> <div class="flex bg-white rounded p-4 space-x-4 shadow-custom">
<i class="fas fa-hdd text-green-500 text-5xl"></i> <i class="fas fa-hdd text-green-500 text-5xl"></i>
<div class="flex flex-col"> <div class="flex flex-col">
<p class="text-gray-700 text-sm">使用储存</p> <p class="text-gray-700 text-sm">使用储存</p>
<p class="text-gray-800 font-semibold text-xl">{{ \App\Utils::formatSize($user->images->sum('size') * 1024) }}</p> <p class="text-gray-800 font-semibold text-xl">{{ \App\Utils::formatSize($user->use_capacity * 1024) }}</p>
</div> </div>
</div> </div>
<div class="flex bg-white rounded p-4 space-x-4"> <div class="flex bg-white rounded p-4 space-x-4 shadow-custom">
<i class="fas fa-hdd text-emerald-500 text-5xl"></i> <i class="fas fa-hdd text-emerald-500 text-5xl"></i>
<div class="flex flex-col"> <div class="flex flex-col">
<p class="text-gray-700 text-sm">总储存</p> <p class="text-gray-700 text-sm">总储存</p>

View File

@@ -116,8 +116,8 @@
<form class="w-full space-y-2" action="/user/albums"> <form class="w-full space-y-2" action="/user/albums">
<input type="text" class="w-full rounded px-2.5 py-1.5 text-sm border-0 bg-gray-200" name="name" placeholder="请输入名称"> <input type="text" class="w-full rounded px-2.5 py-1.5 text-sm border-0 bg-gray-200" name="name" placeholder="请输入名称">
<textarea class="w-full resize-y rounded-md text-sm border-0 bg-gray-200" name="intro" placeholder="请输入简介"></textarea> <textarea class="w-full resize-y rounded-md text-sm border-0 bg-gray-200" name="intro" placeholder="请输入简介"></textarea>
<button class="w-full py-1 px-2 bg-indigo-500 text-white text-sm text-center tracking-wider font-semibold rounded-md">创建相册</button>
</form> </form>
<a href="javascript:void(0)" class="w-full py-1 px-2 bg-indigo-500 text-white text-sm text-center tracking-wider font-semibold rounded-md">创建相册</a>
</div> </div>
</div> </div>
</script> </script>
@@ -139,8 +139,8 @@
<form class="w-full space-y-2" action="/user/albums/__id__"> <form class="w-full space-y-2" action="/user/albums/__id__">
<input type="text" class="w-full rounded px-2.5 py-1.5 text-sm border-0 bg-gray-200" placeholder="请输入名称" name="name" value="__name__"> <input type="text" class="w-full rounded px-2.5 py-1.5 text-sm border-0 bg-gray-200" placeholder="请输入名称" name="name" value="__name__">
<textarea class="w-full resize-y rounded-md text-sm border-0 bg-gray-200" name="intro" placeholder="请输入简介">__intro__</textarea> <textarea class="w-full resize-y rounded-md text-sm border-0 bg-gray-200" name="intro" placeholder="请输入简介">__intro__</textarea>
<button class="w-full py-1 px-2 bg-indigo-500 text-white text-sm text-center tracking-wider font-semibold rounded-md">确认修改</button>
</form> </form>
<a href="javascript:void(0)" class="w-full py-1 px-2 bg-indigo-500 text-white text-sm text-center tracking-wider font-semibold rounded-md">确认修改</a>
</div> </div>
</script> </script>
@@ -269,14 +269,14 @@
for (const i in images) { for (const i in images) {
html += $('#images-item-tpl').html() html += $('#images-item-tpl').html()
.replace(/__id__/g, images[i].id) .replace(/__id__/g, images[i].id)
.replace(/__name__/g, images[i].filename) .replace(/__name__/g, images[i].filename.replace(/\$/g, '$$$$'))
.replace(/__human_date__/g, images[i].human_date) .replace(/__human_date__/g, images[i].human_date)
.replace(/__date__/g, images[i].date) .replace(/__date__/g, images[i].date)
.replace(/__url__/g, images[i].url) .replace(/__url__/g, images[i].url)
.replace(/__thumb_url__/g, images[i].thumb_url) .replace(/__thumb_url__/g, images[i].thumb_url)
.replace(/__width__/g, images[i].width) .replace(/__width__/g, images[i].width)
.replace(/__height__/g, images[i].height) .replace(/__height__/g, images[i].height)
.replace(/__json__/g, JSON.stringify(images[i])) .replace(/__json__/g, JSON.stringify(images[i]).replace(/\$/g, '$$$$'))
} }
$photos.append(html); $photos.append(html);
@@ -410,8 +410,9 @@
}); });
// confirm create // confirm create
$albums.off('click', CREATE_ID + ' a').on('click', CREATE_ID + ' a', function (e) { $albums.off('submit', CREATE_ID + ' form').on('submit', CREATE_ID + ' form', function (e) {
let $form = $(this).siblings('form'); e.preventDefault();
let $form = $(this);
axios.post($form.attr('action'), $form.serialize()).then(response => { axios.post($form.attr('action'), $form.serialize()).then(response => {
let $errorMessage = $albums.find(CREATE_ID + ' .error-message').html('').hide(); let $errorMessage = $albums.find(CREATE_ID + ' .error-message').html('').hide();
if (response.data.status) { if (response.data.status) {
@@ -424,8 +425,9 @@
}); });
// confirm update // confirm update
$albums.off('click', UPDATE_ID + ' a').on('click', UPDATE_ID + ' a', function (e) { $albums.off('submit', UPDATE_ID + ' form').on('submit', UPDATE_ID + ' form', function (e) {
let $form = $(this).siblings('form'); e.preventDefault();
let $form = $(this);
axios.put($form.attr('action'), $form.serialize()).then(response => { axios.put($form.attr('action'), $form.serialize()).then(response => {
let $errorMessage = $albums.find(UPDATE_ID + ' .error-message').html('').hide(); let $errorMessage = $albums.find(UPDATE_ID + ' .error-message').html('').hide();
if (response.data.status) { if (response.data.status) {
@@ -672,9 +674,6 @@
} else { } else {
toastr.warning(response.data.message); toastr.warning(response.data.message);
} }
}).finally(_ => {
$photos.addClass('reset').html('').justifiedGallery('destroy');
ds.clearSelection();
}); });
} }
}); });
@@ -686,9 +685,9 @@
let image = response.data.data.image; let image = response.data.data.image;
let content = $('#image-detail-tpl').html() let content = $('#image-detail-tpl').html()
.replace(/__album_name__/g, image.album ? image.album.name : '-') .replace(/__album_name__/g, image.album ? image.album.name : '-')
.replace(/__strategy_name__/g, image.strategy.name || '-') .replace(/__strategy_name__/g, image.strategy ? image.strategy.name : '-')
.replace(/__filename__/g, image.filename) .replace(/__filename__/g, image.filename.replace(/\$/g, '$$$$'))
.replace(/__origin_name__/g, image.origin_name) .replace(/__origin_name__/g, image.origin_name.replace(/\$/g, '$$$$'))
.replace(/__size__/g, utils.formatSize(image.size * 1024)) .replace(/__size__/g, utils.formatSize(image.size * 1024))
.replace(/__mimetype__/g, image.mimetype) .replace(/__mimetype__/g, image.mimetype)
.replace(/__width__/g, image.width) .replace(/__width__/g, image.width)

View File

@@ -5,7 +5,7 @@
<p class="mb-3 font-semibold text-lg text-gray-700">基础设置</p> <p class="mb-3 font-semibold text-lg text-gray-700">基础设置</p>
<form action="{{ route('settings.update') }}" method="POST"> <form action="{{ route('settings.update') }}" method="POST">
@csrf @csrf
<div class="overflow-hidden sm:rounded-md"> <div class="overflow-hidden sm:rounded-md shadow-custom">
<div class="px-3 py-4 bg-white sm:p-6"> <div class="px-3 py-4 bg-white sm:p-6">
<div class="grid grid-cols-6 gap-6"> <div class="grid grid-cols-6 gap-6">
<div class="col-span-6 sm:col-span-3"> <div class="col-span-6 sm:col-span-3">
@@ -24,7 +24,7 @@
@if(Auth::user()->group) @if(Auth::user()->group)
<option value="0">未选择</option> <option value="0">未选择</option>
@foreach(Auth::user()->group->strategies as $strategy) @foreach(Auth::user()->group->strategies as $strategy)
<option value="{{ $strategy->id }}" @selected(Auth::user()->configs->get(\App\Enums\UserConfigKey::DefaultStrategy) == $strategy->id)>{{ $strategy->name }}</option> <option value="{{ $strategy->id }}" @selected(Auth::user()->configs->get('default_strategy') == $strategy->id)>{{ $strategy->name }}</option>
@endforeach @endforeach
@else @else
<option value="0">系统默认</option> <option value="0">系统默认</option>
@@ -38,7 +38,7 @@
@if(Auth::user()->albums->isNotEmpty()) @if(Auth::user()->albums->isNotEmpty())
<option value="0">未选择</option> <option value="0">未选择</option>
@foreach(Auth::user()->albums as $album) @foreach(Auth::user()->albums as $album)
<option value="{{ $album->id }}" @selected(Auth::user()->configs->get(\App\Enums\UserConfigKey::DefaultAlbum) == $album->id)>{{ $album->name }}</option> <option value="{{ $album->id }}" @selected(Auth::user()->configs->get('default_album') == $album->id)>{{ $album->name }}</option>
@endforeach @endforeach
@else @else
<option value="0">没有可用相册</option> <option value="0">没有可用相册</option>
@@ -53,20 +53,27 @@
<div class="col-span-6"> <div class="col-span-6">
<label for="password" class="block text-sm font-medium text-gray-700">密码</label> <label for="password" class="block text-sm font-medium text-gray-700">密码</label>
<x-input type="password" name="password" id="password" placeholder="不修改请留空" autocomplete="password" /> <x-input type="password" name="password" id="password" placeholder="不修改请留空" autocomplete="new-password" />
</div> </div>
<div class="col-span-6"> <div class="col-span-6">
<x-fieldset title="是否自动清除预览" faq="设置上传时,文件上传完成以后是否自动清除预览图片"> <x-fieldset title="是否自动清除预览" faq="设置上传时,文件上传完成以后是否自动清除预览图片">
<x-fieldset-radio id="is_auto_clear_preview_yes" name="configs[is_auto_clear_preview]" value="1" :checked="Auth::user()->configs->get(\App\Enums\UserConfigKey::IsAutoClearPreview)"></x-fieldset-radio> <x-fieldset-radio id="is_auto_clear_preview_yes" name="configs[is_auto_clear_preview]" value="1" :checked="Auth::user()->configs->get('is_auto_clear_preview')"></x-fieldset-radio>
<x-fieldset-radio id="is_auto_clear_preview_no" name="configs[is_auto_clear_preview]" value="0" :checked="! Auth::user()->configs->get(\App\Enums\UserConfigKey::IsAutoClearPreview)"></x-fieldset-radio> <x-fieldset-radio id="is_auto_clear_preview_no" name="configs[is_auto_clear_preview]" value="0" :checked="! Auth::user()->configs->get('is_auto_clear_preview')"></x-fieldset-radio>
</x-fieldset>
</div>
<div class="col-span-6">
<x-fieldset title="图片粘贴后动作" faq="设置上传页面粘贴图片后的动作">
<x-fieldset-radio id="pasted_action_upload" name="configs[pasted_action]" value="{{ \App\Enums\PastedAction::Upload }}" :checked="Auth::user()->configs->get('pasted_action') == \App\Enums\PastedAction::Upload">直接上传</x-fieldset-radio>
<x-fieldset-radio id="pasted_action_waiting" name="configs[pasted_action]" value="{{ \App\Enums\PastedAction::Waiting }}" :checked="Auth::user()->configs->get('pasted_action') == \App\Enums\PastedAction::Waiting">等待上传</x-fieldset-radio>
</x-fieldset> </x-fieldset>
</div> </div>
<div class="col-span-6"> <div class="col-span-6">
<x-fieldset title="图片默认权限" faq="设置上传的图片默认的权限(公开还是私有,公开的图片将会出现在画廊中,你也可以通过图片管理单独设置权限)"> <x-fieldset title="图片默认权限" faq="设置上传的图片默认的权限(公开还是私有,公开的图片将会出现在画廊中,你也可以通过图片管理单独设置权限)">
<x-fieldset-radio id="private" name="configs[default_permission]" value="{{ \App\Enums\ImagePermission::Private }}" :checked="Auth::user()->configs->get(\App\Enums\UserConfigKey::DefaultPermission) == \App\Enums\ImagePermission::Private">私有</x-fieldset-radio> <x-fieldset-radio id="default_permission_private" name="configs[default_permission]" value="{{ \App\Enums\ImagePermission::Private }}" :checked="Auth::user()->configs->get('default_permission') == \App\Enums\ImagePermission::Private">私有</x-fieldset-radio>
<x-fieldset-radio id="public" name="configs[default_permission]" value="{{ \App\Enums\ImagePermission::Public }}" :checked="Auth::user()->configs->get(\App\Enums\UserConfigKey::DefaultPermission) == \App\Enums\ImagePermission::Public">公开</x-fieldset-radio> <x-fieldset-radio id="default_permission_public" name="configs[default_permission]" value="{{ \App\Enums\ImagePermission::Public }}" :checked="Auth::user()->configs->get('default_permission') == \App\Enums\ImagePermission::Public">公开</x-fieldset-radio>
</x-fieldset> </x-fieldset>
</div> </div>
</div> </div>

View File

@@ -1,5 +1,5 @@
@push('styles') @push('styles')
<link rel="stylesheet" href="{{ asset('css/markdown-css/github-markdown.css') }}"> <link rel="stylesheet" href="{{ asset('css/markdown-css/github-markdown-light.css') }}">
@endpush @endpush
<x-guest-layout> <x-guest-layout>
@@ -10,9 +10,8 @@
<a href="{{ route('/') }}" class="text-white text-xl truncate">{{ \App\Utils::config(\App\Enums\ConfigKey::AppName) }}</a> <a href="{{ route('/') }}" class="text-white text-xl truncate">{{ \App\Utils::config(\App\Enums\ConfigKey::AppName) }}</a>
</div> </div>
<div class="flex justify-end items-center space-x-4"> <div class="flex justify-end items-center space-x-4">
@if($_group->strategies->isNotEmpty()) @includeWhen($_is_notice, 'layouts.notice')
@include('layouts.strategies') @includeWhen($_group->strategies->isNotEmpty(), 'layouts.strategies')
@endif
@if(Auth::check()) @if(Auth::check())
@include('layouts.user-nav') @include('layouts.user-nav')
@@ -29,27 +28,12 @@
<x-upload/> <x-upload/>
</div> </div>
<footer class="absolute bottom-0 left-0 right-0 w-full bg-gray-200"> <footer class="absolute bottom-0 left-0 right-0 w-full bg-gray-200">
<p class="container mx-auto py-2 px-5 sm:px-10 md:px-10 lg:px-10 xl:px-10 2xl:px-60 flex items-center text-gray-500 text-sm"> <p class="container mx-auto py-2 px-5 sm:px-10 md:px-10 lg:px-10 xl:px-10 2xl:px-60 text-gray-500 text-sm">
Copyright © 2018 - present Lsky Pro. All rights reserved. 请勿上传违反中国大陆和香港法律的图片,违者后果自负。 Copyright © 2018 - present Lsky Pro. All rights reserved. &nbsp;<a href="https://beian.miit.gov.cn/" target="_blank" rel="noreferrer">{{ \App\Utils::config(\App\Enums\ConfigKey::IcpNo) }}</a>&nbsp;请勿上传违反中国大陆和香港法律的图片,违者后果自负。
</p> </p>
</footer> </footer>
</div> </div>
@if(\App\Utils::config(\App\Enums\ConfigKey::SiteNotice)) @include('common.notice')
<x-modal>
<div class="markdown-body">
{!! (new Parsedown())->parse(\App\Utils::config(\App\Enums\ConfigKey::SiteNotice)) !!}
</div>
</x-modal>
@push('scripts')
<script>
if (! sessionStorage.getItem('noticed')) {
Alpine.store('modal').open = true;
sessionStorage.setItem('noticed', '1');
}
</script>
@endpush
@endif
</x-guest-layout> </x-guest-layout>

View File

@@ -8,15 +8,16 @@ use App\Http\Controllers\Auth\NewPasswordController;
use App\Http\Controllers\Auth\PasswordResetLinkController; use App\Http\Controllers\Auth\PasswordResetLinkController;
use App\Http\Controllers\Auth\RegisteredUserController; use App\Http\Controllers\Auth\RegisteredUserController;
use App\Http\Controllers\Auth\VerifyEmailController; use App\Http\Controllers\Auth\VerifyEmailController;
use App\Http\Middleware\CheckIsEnableRegistration;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
Route::get('/register', [ Route::get('/register', [
RegisteredUserController::class, 'create' RegisteredUserController::class, 'create'
])->middleware('guest')->name('register'); ])->middleware('guest')->middleware(CheckIsEnableRegistration::class)->name('register');
Route::post('/register', [ Route::post('/register', [
RegisteredUserController::class, 'store' RegisteredUserController::class, 'store'
])->middleware('guest'); ])->middleware('guest')->middleware(CheckIsEnableRegistration::class);
Route::get('/login', [ Route::get('/login', [
AuthenticatedSessionController::class, 'create', AuthenticatedSessionController::class, 'create',

View File

@@ -5,13 +5,10 @@ use Illuminate\Support\Facades\Route;
use App\Enums\GroupConfigKey; use App\Enums\GroupConfigKey;
use App\Enums\ConfigKey; use App\Enums\ConfigKey;
$extensions = config('convention.app.'.ConfigKey::Group)[GroupConfigKey::AcceptedFileSuffixes]; $extensions = config('convention.group.accepted_file_suffixes');
Route::middleware('cache.headers:public;max_age=2628000;etag')->group(function () use ($extensions) { Route::middleware('cache.headers:public;max_age=2628000;etag')->group(function () use ($extensions) {
$extensions = array_merge(array_map('strtoupper', $extensions), array_map('strtolower', $extensions)); $extensions = array_merge(array_map('strtoupper', $extensions), array_map('strtolower', $extensions));
Route::any('{key}.{extension}', [ Route::any('{key}.{extension}', [
Controller::class, 'output', Controller::class, 'output',
])->where('extension', implode('|', $extensions)); ])->where('extension', implode('|', $extensions));
Route::any('{key}.{extension}!thumbnail', [
Controller::class, 'thumbnail',
])->where('extension', implode('|', $extensions));
}); });

View File

@@ -11,6 +11,8 @@
| |
*/ */
use App\Http\Middleware\CheckIsEnableApi;
use App\Http\Middleware\CheckIsEnableGallery;
use App\Http\Middleware\CheckIsEnableGuestUpload; use App\Http\Middleware\CheckIsEnableGuestUpload;
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use App\Http\Middleware\CheckIsInstalled; use App\Http\Middleware\CheckIsInstalled;
@@ -37,7 +39,7 @@ Route::any('install', [Controller::class, 'install'])->name('install');
Route::post('upload', [Controller::class, 'upload']); Route::post('upload', [Controller::class, 'upload']);
Route::group(['middleware' => ['auth']], function () { Route::group(['middleware' => ['auth']], function () {
Route::get('dashboard', [UserController::class, 'dashboard'])->name('dashboard'); Route::get('dashboard', [UserController::class, 'dashboard'])->name('dashboard');
Route::get('gallery', [GalleryController::class, 'index'])->name('gallery'); Route::get('gallery', [GalleryController::class, 'index'])->middleware(CheckIsEnableGallery::class)->name('gallery');
Route::prefix('settings')->group(function () { Route::prefix('settings')->group(function () {
Route::get('', [UserController::class, 'settings'])->name('settings'); Route::get('', [UserController::class, 'settings'])->name('settings');
@@ -45,7 +47,10 @@ Route::group(['middleware' => ['auth']], function () {
Route::put('set-strategy', [UserController::class, 'setStrategy'])->name('settings.strategy.set'); Route::put('set-strategy', [UserController::class, 'setStrategy'])->name('settings.strategy.set');
}); });
Route::group(['prefix' => 'api'], function () { Route::group([
'prefix' => 'api',
'middleware' => CheckIsEnableApi::class
], function () {
Route::get('', [ApiController::class, 'index'])->name('api'); Route::get('', [ApiController::class, 'index'])->name('api');
}); });

View File

@@ -13,6 +13,9 @@ module.exports = {
fontFamily: { fontFamily: {
sans: ['Nunito', ...defaultTheme.fontFamily.sans], sans: ['Nunito', ...defaultTheme.fontFamily.sans],
}, },
boxShadow: {
custom: '0px 4px 6px -1px rgba(0, 0, 0, 0.04)',
},
}, },
}, },

Some files were not shown because too many files have changed in this diff Show More