mirror of
https://github.com/keven1024/015.git
synced 2026-06-07 21:04:33 +00:00
Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
759318813c | ||
|
|
a91345f39c | ||
|
|
123b1ec4fb | ||
|
|
79887d6c6c | ||
|
|
f1f10de051 | ||
|
|
80d60cc0a0 | ||
|
|
30d0abc2b5 | ||
|
|
8d3675cfa1 | ||
|
|
cb6b0fae6a | ||
|
|
6631e1e1a2 | ||
|
|
625399bdd9 | ||
|
|
10b79615b7 | ||
|
|
7c38773451 | ||
|
|
976011697c | ||
|
|
c25b41e20e | ||
|
|
3c031dcee9 | ||
|
|
d6c54de659 | ||
|
|
72ca69330f | ||
|
|
7f74441f5d | ||
|
|
28abd8d1a2 | ||
|
|
c50bb5d0bf | ||
|
|
95ab8b97da | ||
|
|
d6880dbf00 | ||
|
|
c871c55f79 | ||
|
|
82e9292b66 | ||
|
|
1d86f2bdf6 | ||
|
|
52cc89a73d | ||
|
|
af9a5b45d2 | ||
|
|
b26821a504 | ||
|
|
560387d8f1 | ||
|
|
b69af056aa | ||
|
|
707ade5dd2 | ||
|
|
b4570c5961 | ||
|
|
60a588c92a |
@@ -21,8 +21,9 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
images: fudaoyuanicu/015-app
|
images: fudaoyuanicu/015-app
|
||||||
tags: |
|
tags: |
|
||||||
type=pep440,pattern={{version}}
|
type=semver,pattern={{version}}
|
||||||
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }}
|
type=raw,value=edge,enable=${{ contains(github.ref_name, '-') }}
|
||||||
|
type=raw,value=latest,enable=${{ !contains(github.ref_name, '-') }}
|
||||||
- name: Set build time
|
- name: Set build time
|
||||||
id: build-time
|
id: build-time
|
||||||
run: |
|
run: |
|
||||||
@@ -56,8 +57,9 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
images: fudaoyuanicu/015-worker
|
images: fudaoyuanicu/015-worker
|
||||||
tags: |
|
tags: |
|
||||||
type=pep440,pattern={{version}}
|
type=semver,pattern={{version}}
|
||||||
type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') }}
|
type=raw,value=edge,enable=${{ contains(github.ref_name, '-') }}
|
||||||
|
type=raw,value=latest,enable=${{ !contains(github.ref_name, '-') }}
|
||||||
- name: Set build time
|
- name: Set build time
|
||||||
id: build-time
|
id: build-time
|
||||||
run: |
|
run: |
|
||||||
@@ -76,6 +78,7 @@ jobs:
|
|||||||
deploy:
|
deploy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [build-app, build-worker]
|
needs: [build-app, build-worker]
|
||||||
|
if: ${{ !contains(github.ref_name, '-') }}
|
||||||
steps:
|
steps:
|
||||||
- name: Send deployment webhook
|
- name: Send deployment webhook
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
BIN
.github/image/0.png
vendored
BIN
.github/image/0.png
vendored
Binary file not shown.
|
Before Width: | Height: | Size: 716 KiB After Width: | Height: | Size: 804 KiB |
16
.vscode/launch.json
vendored
Normal file
16
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
// 使用 IntelliSense 了解相关属性。
|
||||||
|
// 悬停以查看现有属性的描述。
|
||||||
|
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Launch Backend",
|
||||||
|
"type": "go",
|
||||||
|
"request": "launch",
|
||||||
|
"mode": "auto",
|
||||||
|
"program": "${workspaceFolder}/backend",
|
||||||
|
"cwd": "${workspaceFolder}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
55
backend/.air.toml
Normal file
55
backend/.air.toml
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
#:schema https://json.schemastore.org/any.json
|
||||||
|
|
||||||
|
root = "."
|
||||||
|
testdata_dir = "testdata"
|
||||||
|
tmp_dir = "tmp"
|
||||||
|
|
||||||
|
[build]
|
||||||
|
args_bin = []
|
||||||
|
bin = "./tmp/main"
|
||||||
|
cmd = "go build -o ./tmp/main ."
|
||||||
|
delay = 1000
|
||||||
|
entrypoint = ["./tmp/main"]
|
||||||
|
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
|
||||||
|
exclude_file = []
|
||||||
|
exclude_regex = ["_test.go"]
|
||||||
|
exclude_unchanged = false
|
||||||
|
follow_symlink = false
|
||||||
|
full_bin = ""
|
||||||
|
include_dir = ["../pkg"]
|
||||||
|
include_ext = ["go", "tpl", "tmpl", "html"]
|
||||||
|
include_file = []
|
||||||
|
kill_delay = "0s"
|
||||||
|
log = "build-errors.log"
|
||||||
|
poll = false
|
||||||
|
poll_interval = 0
|
||||||
|
post_cmd = []
|
||||||
|
pre_cmd = []
|
||||||
|
rerun = false
|
||||||
|
rerun_delay = 500
|
||||||
|
send_interrupt = false
|
||||||
|
stop_on_error = false
|
||||||
|
|
||||||
|
[color]
|
||||||
|
app = ""
|
||||||
|
build = "yellow"
|
||||||
|
main = "magenta"
|
||||||
|
runner = "green"
|
||||||
|
watcher = "cyan"
|
||||||
|
|
||||||
|
[log]
|
||||||
|
main_only = false
|
||||||
|
silent = false
|
||||||
|
time = false
|
||||||
|
|
||||||
|
[misc]
|
||||||
|
clean_on_exit = false
|
||||||
|
|
||||||
|
[proxy]
|
||||||
|
app_port = 0
|
||||||
|
enabled = false
|
||||||
|
proxy_port = 0
|
||||||
|
|
||||||
|
[screen]
|
||||||
|
clear_on_rebuild = false
|
||||||
|
keep_scroll = true
|
||||||
@@ -5,15 +5,14 @@ go 1.25.5
|
|||||||
require (
|
require (
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.1
|
github.com/golang-jwt/jwt/v5 v5.3.1
|
||||||
github.com/gorilla/sessions v1.4.0
|
github.com/gorilla/sessions v1.4.0
|
||||||
github.com/hibiken/asynq v0.25.1
|
github.com/hibiken/asynq v0.26.0
|
||||||
github.com/labstack/echo-contrib v0.50.0
|
|
||||||
github.com/labstack/echo/v5 v5.0.1
|
github.com/labstack/echo/v5 v5.0.1
|
||||||
github.com/matoous/go-nanoid/v2 v2.1.0
|
github.com/matoous/go-nanoid/v2 v2.1.0
|
||||||
github.com/samber/lo v1.52.0
|
github.com/samber/lo v1.53.0
|
||||||
github.com/spf13/cast v1.10.0
|
github.com/spf13/cast v1.10.0
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
go.uber.org/zap v1.27.1
|
go.uber.org/zap v1.27.1
|
||||||
golang.org/x/crypto v0.47.0
|
golang.org/x/crypto v0.49.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -21,15 +20,16 @@ require (
|
|||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/gorilla/context v1.1.2 // indirect
|
|
||||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/redis/go-redis/v9 v9.17.3 // indirect
|
github.com/redis/go-redis/v9 v9.18.0 // indirect
|
||||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||||
|
go.uber.org/atomic v1.11.0 // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
golang.org/x/sys v0.40.0 // indirect
|
golang.org/x/net v0.52.0 // indirect
|
||||||
golang.org/x/text v0.33.0 // indirect
|
golang.org/x/sys v0.42.0 // indirect
|
||||||
golang.org/x/time v0.14.0 // indirect
|
golang.org/x/text v0.35.0 // indirect
|
||||||
|
golang.org/x/time v0.15.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.11 // indirect
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
|||||||
@@ -18,54 +18,45 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
|||||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o=
|
|
||||||
github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM=
|
|
||||||
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
|
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
|
||||||
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
|
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
|
||||||
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
|
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
|
||||||
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
|
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
|
||||||
github.com/hibiken/asynq v0.25.1 h1:phj028N0nm15n8O2ims+IvJ2gz4k2auvermngh9JhTw=
|
github.com/hibiken/asynq v0.26.0 h1:1Zxr92MlDnb1Zt/QR5g2vSCqUS03i95lUfqx5X7/wrw=
|
||||||
github.com/hibiken/asynq v0.25.1/go.mod h1:pazWNOLBu0FEynQRBvHA26qdIKRSmfdIfUm4HdsLmXg=
|
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/labstack/echo-contrib v0.50.0 h1:MLTQdqME3BEBczV2thYz9yPT5sBhzkoUEpwAOY9llds=
|
|
||||||
github.com/labstack/echo-contrib v0.50.0/go.mod h1:oftqJL4enNg9ao1VLpVZmisVE5/8uwHtIYE4zTpqyWU=
|
|
||||||
github.com/labstack/echo/v5 v5.0.1 h1:60L7x1KMWRIJuaFqvnEHH322g+YnsMWq5Rzaeo6lcP4=
|
github.com/labstack/echo/v5 v5.0.1 h1:60L7x1KMWRIJuaFqvnEHH322g+YnsMWq5Rzaeo6lcP4=
|
||||||
github.com/labstack/echo/v5 v5.0.1/go.mod h1:SyvlSdObGjRXeQfCCXW/sybkZdOOQZBmpKF0bvALaeo=
|
github.com/labstack/echo/v5 v5.0.1/go.mod h1:SyvlSdObGjRXeQfCCXW/sybkZdOOQZBmpKF0bvALaeo=
|
||||||
github.com/matoous/go-nanoid/v2 v2.1.0 h1:P64+dmq21hhWdtvZfEAofnvJULaRR1Yib0+PnU669bE=
|
github.com/matoous/go-nanoid/v2 v2.1.0 h1:P64+dmq21hhWdtvZfEAofnvJULaRR1Yib0+PnU669bE=
|
||||||
github.com/matoous/go-nanoid/v2 v2.1.0/go.mod h1:KlbGNQ+FhrUNIHUxZdL63t7tl4LaPkZNpUULS8H4uVM=
|
github.com/matoous/go-nanoid/v2 v2.1.0/go.mod h1:KlbGNQ+FhrUNIHUxZdL63t7tl4LaPkZNpUULS8H4uVM=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/redis/go-redis/v9 v9.17.3 h1:fN29NdNrE17KttK5Ndf20buqfDZwGNgoUr9qjl1DQx4=
|
github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs=
|
||||||
github.com/redis/go-redis/v9 v9.17.3/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
|
|
||||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
|
github.com/samber/lo v1.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM=
|
||||||
github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
|
|
||||||
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||||
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
|
||||||
|
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
|
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
|
||||||
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||||
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||||
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||||
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
|
||||||
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
|
||||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
|
||||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
|
||||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
|
||||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ func GetAbout(c *echo.Context) error {
|
|||||||
|
|
||||||
return utils.HTTPSuccessHandler(c, map[string]any{
|
return utils.HTTPSuccessHandler(c, map[string]any{
|
||||||
"bg_url": u.GetEnv("about.bg_url"),
|
"bg_url": u.GetEnv("about.bg_url"),
|
||||||
"content": u.GetEnvMapString("about.content"),
|
"content": u.GetEnvMap("about.content"),
|
||||||
"email": u.GetEnv("about.email"),
|
"email": u.GetEnv("about.email"),
|
||||||
"name": u.GetEnv("about.name"),
|
"name": u.GetEnv("about.name"),
|
||||||
"url": u.GetEnv("about.url"),
|
"url": u.GetEnv("about.url"),
|
||||||
|
|||||||
@@ -6,17 +6,25 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/labstack/echo/v5"
|
"github.com/labstack/echo/v5"
|
||||||
|
"github.com/samber/lo"
|
||||||
"github.com/spf13/cast"
|
"github.com/spf13/cast"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetConfig(c *echo.Context) error {
|
func GetConfig(c *echo.Context) error {
|
||||||
|
featureConfig := u.GetEnvMap("features")
|
||||||
|
features := lo.FilterMap(lo.Entries(featureConfig), func(e lo.Entry[string, any], _ int) (string, bool) {
|
||||||
|
node, ok := e.Value.(map[string]any)
|
||||||
|
return e.Key, ok && cast.ToBool(node["enabled"])
|
||||||
|
})
|
||||||
|
|
||||||
return utils.HTTPSuccessHandler(c, map[string]any{
|
return utils.HTTPSuccessHandler(c, map[string]any{
|
||||||
"site_title": u.GetEnvMapString("site.title"),
|
"site_title": u.GetEnvMap("site.title"),
|
||||||
"site_desc": u.GetEnvMapString("site.desc"),
|
"site_desc": u.GetEnvMap("site.desc"),
|
||||||
"site_url": u.GetEnv("site.url"),
|
"site_url": u.GetEnv("site.url"),
|
||||||
"site_icon": u.GetEnvWithDefault("site.icon", "/logo.png"),
|
"site_icon": u.GetEnvWithDefault("site.icon", "/logo.png"),
|
||||||
"site_bg_url": u.GetEnvWithDefault("site.bg_url", "https://img.fudaoyuan.icu/api/1/random/?scale_min=1.5&webp=true&md=false&format=302"),
|
"site_bg_url": u.GetEnvWithDefault("site.bg_url", "https://img.fudaoyuan.icu/api/1/random/?scale_min=1.5&webp=true&md=false&format=302"),
|
||||||
"version": u.GetEnvWithDefault("VERSION", "dev"),
|
"version": u.GetEnvWithDefault("VERSION", "dev"),
|
||||||
"build_time": cast.ToInt(u.GetEnvWithDefault("BUILD_TIME", cast.ToString(time.Now().Unix()))),
|
"build_time": cast.ToInt(u.GetEnvWithDefault("BUILD_TIME", cast.ToString(time.Now().Unix()))),
|
||||||
|
"features": features,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -125,17 +125,10 @@ func VaildateShare(c *echo.Context) error {
|
|||||||
|
|
||||||
// 统计分享数
|
// 统计分享数
|
||||||
currentDate := time.Now().Format("2006-01-02")
|
currentDate := time.Now().Format("2006-01-02")
|
||||||
statData, _ := models.GetRedisStat(currentDate)
|
err = models.SetRedisStat(currentDate, func(stat *models.StatData) *models.StatData {
|
||||||
if statData == nil {
|
stat.DownloadNum += 1
|
||||||
statData = &models.StatData{
|
return stat
|
||||||
FileSize: 0,
|
})
|
||||||
FileNum: 0,
|
|
||||||
ShareNum: 0,
|
|
||||||
DownloadNum: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
statData.DownloadNum += 1
|
|
||||||
err = models.SetRedisStat(currentDate, *statData)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return utils.HTTPErrorHandler(c, err)
|
return utils.HTTPErrorHandler(c, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -241,18 +241,11 @@ func FinishUploadTask(c *echo.Context) error {
|
|||||||
}
|
}
|
||||||
// 统计
|
// 统计
|
||||||
currentDate := time.Now().Format("2006-01-02")
|
currentDate := time.Now().Format("2006-01-02")
|
||||||
statData, _ := models.GetRedisStat(currentDate)
|
err = models.SetRedisStat(currentDate, func(stat *models.StatData) *models.StatData {
|
||||||
if statData == nil {
|
stat.FileSize += fileInfo.FileSize
|
||||||
statData = &models.StatData{
|
stat.FileNum += 1
|
||||||
FileSize: 0,
|
return stat
|
||||||
FileNum: 0,
|
})
|
||||||
ShareNum: 0,
|
|
||||||
DownloadNum: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
statData.FileSize += fileInfo.FileSize
|
|
||||||
statData.FileNum += 1
|
|
||||||
err = models.SetRedisStat(currentDate, *statData)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return utils.HTTPErrorHandler(c, err)
|
return utils.HTTPErrorHandler(c, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -128,17 +128,10 @@ func CreateShareInfo(c *echo.Context) error {
|
|||||||
|
|
||||||
// 统计分享数
|
// 统计分享数
|
||||||
currentDate := time.Now().Format("2006-01-02")
|
currentDate := time.Now().Format("2006-01-02")
|
||||||
statData, _ := models.GetRedisStat(currentDate)
|
err = models.SetRedisStat(currentDate, func(stat *models.StatData) *models.StatData {
|
||||||
if statData == nil {
|
stat.ShareNum += 1
|
||||||
statData = &models.StatData{
|
return stat
|
||||||
FileSize: 0,
|
})
|
||||||
FileNum: 0,
|
|
||||||
ShareNum: 0,
|
|
||||||
DownloadNum: 0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
statData.ShareNum += 1
|
|
||||||
err = models.SetRedisStat(currentDate, *statData)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return utils.HTTPErrorHandler(c, err)
|
return utils.HTTPErrorHandler(c, err)
|
||||||
}
|
}
|
||||||
|
|||||||
16
backend/internal/utils/session.go
Normal file
16
backend/internal/utils/session.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/gorilla/sessions"
|
||||||
|
"github.com/labstack/echo/v5"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetSession(c *echo.Context, name string) (*sessions.Session, error) {
|
||||||
|
store, err := echo.ContextGet[sessions.Store](c, "_session_store")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get session store: %w", err)
|
||||||
|
}
|
||||||
|
return store.Get(c.Request(), name)
|
||||||
|
}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"backend/internal/utils"
|
||||||
|
|
||||||
"github.com/gorilla/sessions"
|
"github.com/gorilla/sessions"
|
||||||
"github.com/labstack/echo-contrib/session"
|
|
||||||
"github.com/labstack/echo/v5"
|
"github.com/labstack/echo/v5"
|
||||||
gonanoid "github.com/matoous/go-nanoid/v2"
|
gonanoid "github.com/matoous/go-nanoid/v2"
|
||||||
)
|
)
|
||||||
@@ -11,7 +12,7 @@ import (
|
|||||||
func AuthMiddleware() echo.MiddlewareFunc {
|
func AuthMiddleware() echo.MiddlewareFunc {
|
||||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
return func(c *echo.Context) error {
|
return func(c *echo.Context) error {
|
||||||
sess, err := session.Get("session", c)
|
sess, err := utils.GetSession(c, "session")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,15 @@ package middleware
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/gorilla/sessions"
|
"github.com/gorilla/sessions"
|
||||||
"github.com/labstack/echo-contrib/session"
|
|
||||||
"github.com/labstack/echo/v5"
|
"github.com/labstack/echo/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
func SessionMiddleware() echo.MiddlewareFunc {
|
func SessionMiddleware() echo.MiddlewareFunc {
|
||||||
return session.Middleware(sessions.NewCookieStore([]byte("secret")))
|
store := sessions.NewCookieStore([]byte("secret")) // TODO: 从配置中获取密钥
|
||||||
|
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
|
return func(c *echo.Context) error {
|
||||||
|
c.Set("_session_store", store)
|
||||||
|
return next(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ func TestGeneratePasswordHash(t *testing.T) {
|
|||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
// 设置环境变量
|
// 设置环境变量
|
||||||
u.InitEnv(u.EnvOption{
|
u.InitTestViper(u.EnvOption{
|
||||||
ConfigData: bytes.NewBuffer([]byte(fmt.Sprintf(`
|
ConfigData: bytes.NewBuffer([]byte(fmt.Sprintf(`
|
||||||
share:
|
share:
|
||||||
password_salt: %s
|
password_salt: %s
|
||||||
|
|||||||
@@ -16,6 +16,17 @@ redis:
|
|||||||
# (必填)redis 地址
|
# (必填)redis 地址
|
||||||
url: redis://redis:6379/0
|
url: redis://redis:6379/0
|
||||||
|
|
||||||
|
# 实例功能配置
|
||||||
|
features:
|
||||||
|
file-share:
|
||||||
|
enabled: true
|
||||||
|
text-share:
|
||||||
|
enabled: true
|
||||||
|
file-image-compress:
|
||||||
|
enabled: true
|
||||||
|
file-image-convert:
|
||||||
|
enabled: true
|
||||||
|
|
||||||
# 站点基本信息
|
# 站点基本信息
|
||||||
site:
|
site:
|
||||||
# 必填,对应你的公网域名
|
# 必填,对应你的公网域名
|
||||||
|
|||||||
@@ -4,12 +4,14 @@ import { Skeleton } from '@/components/ui/skeleton'
|
|||||||
import getFileSize from '~/lib/getFileSize'
|
import getFileSize from '~/lib/getFileSize'
|
||||||
import SparkMD5 from 'spark-md5'
|
import SparkMD5 from 'spark-md5'
|
||||||
import useMyAppConfig from '@/composables/useMyAppConfig'
|
import useMyAppConfig from '@/composables/useMyAppConfig'
|
||||||
|
import { useFeatureMeta } from '@/composables/useFeatureMeta'
|
||||||
import Progress from '~/components/ui/progress/Progress.vue'
|
import Progress from '~/components/ui/progress/Progress.vue'
|
||||||
import renderI18n from '~/lib/renderI18n'
|
import renderI18n from '~/lib/renderI18n'
|
||||||
import { I18nT } from 'vue-i18n'
|
import { I18nT } from 'vue-i18n'
|
||||||
|
|
||||||
const { locale } = useI18n()
|
const { locale } = useI18n()
|
||||||
const appConfig = useMyAppConfig()
|
const appConfig = useMyAppConfig()
|
||||||
|
const featureMeta = useFeatureMeta()
|
||||||
const { data, isLoading } = useQuery({
|
const { data, isLoading } = useQuery({
|
||||||
queryKey: ['about'],
|
queryKey: ['about'],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
@@ -107,6 +109,29 @@ const genUserAvatar = (email: string) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
<template v-if="isLoading">
|
||||||
|
<Skeleton class="w-full h-24 rounded-xl" />
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="rounded-xl bg-white/50 flex flex-col p-3 gap-3">
|
||||||
|
<div class="font-semibold">{{ t('page.about.enabledFeatures') }}</div>
|
||||||
|
<div v-if="featureMeta.length" class="flex flex-row flex-wrap gap-2">
|
||||||
|
<div
|
||||||
|
v-for="feature in featureMeta"
|
||||||
|
:key="feature.key"
|
||||||
|
class="flex flex-row items-center gap-2 rounded-full bg-black/5 px-2 py-1 text-sm font-medium"
|
||||||
|
>
|
||||||
|
<div class="flex size-6 items-center justify-center rounded-full text-black/80" :style="feature.style">
|
||||||
|
<component :is="feature.icon" class="size-3.5" />
|
||||||
|
</div>
|
||||||
|
<span>{{ feature.label }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else class="text-sm opacity-75">
|
||||||
|
{{ t('page.about.enabledFeaturesEmpty') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<template v-if="isLoading">
|
<template v-if="isLoading">
|
||||||
<Skeleton class="w-full h-16 rounded-xl" />
|
<Skeleton class="w-full h-16 rounded-xl" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,34 +1,33 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { CurveType } from '@unovis/ts'
|
|
||||||
import { AreaChart } from '@/components/ui/chart-area'
|
|
||||||
import { cx } from 'class-variance-authority'
|
import { cx } from 'class-variance-authority'
|
||||||
import { useQuery } from '@tanstack/vue-query'
|
import { useQuery } from '@tanstack/vue-query'
|
||||||
import { Skeleton } from '@/components/ui/skeleton'
|
import { Skeleton } from '@/components/ui/skeleton'
|
||||||
import AboutChartTooltip from '@/components/AboutChartTooltip.vue'
|
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { times } from 'lodash-es'
|
import { times } from 'lodash-es'
|
||||||
|
import type { ChartConfig } from '@/components/ui/chart'
|
||||||
|
import { VisArea, VisAxis, VisLine, VisXYContainer } from '@unovis/vue'
|
||||||
|
import { ChartContainer, ChartTooltip, ChartCrosshair, ChartLegendContent, componentToString, ChartTooltipContent } from '@/components/ui/chart'
|
||||||
|
|
||||||
interface StatChartData {
|
interface StatChartData {
|
||||||
file_size: number
|
file_size: number
|
||||||
file_num: number
|
file_num: number
|
||||||
share_num: number
|
share_num: number
|
||||||
download_num: number
|
download_num: number
|
||||||
date: string
|
date: Date
|
||||||
}
|
}
|
||||||
|
|
||||||
interface QueueChartData {
|
interface QueueChartData {
|
||||||
processed: number
|
processed: number
|
||||||
failed: number
|
failed: number
|
||||||
date: string
|
date: Date
|
||||||
}
|
}
|
||||||
|
|
||||||
type ChartDataItem = StatChartData | QueueChartData
|
type ChartDataItem = StatChartData | QueueChartData
|
||||||
|
|
||||||
type ChartConfig = {
|
type AreaChartConfig = {
|
||||||
data: ChartDataItem[]
|
data: ChartDataItem[]
|
||||||
index: string
|
index: string
|
||||||
categories: string[]
|
config: ChartConfig
|
||||||
colors: string[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data, isLoading } = useQuery({
|
const { data, isLoading } = useQuery({
|
||||||
@@ -82,12 +81,12 @@ const chartTabs = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const currentChartTab = ref<'storage' | 'queue' | 'share' | 'download'>('storage')
|
const currentChartTab = ref<'storage' | 'queue' | 'share' | 'download'>('storage')
|
||||||
const currentChartData = computed((): ChartConfig => {
|
const currentChartData = computed((): AreaChartConfig => {
|
||||||
const { storage, queue } = data.value?.chart || {}
|
const { storage, queue } = data.value?.chart || {}
|
||||||
if (currentChartTab.value === 'queue') {
|
if (currentChartTab.value === 'queue') {
|
||||||
const queueData = times(30, (i) => {
|
const queueData = times(30, (i) => {
|
||||||
return {
|
return {
|
||||||
date: dayjs().subtract(i, 'day').format('YYYY-MM-DD'),
|
date: dayjs().subtract(i, 'day').toDate(),
|
||||||
processed: queue?.[dayjs().subtract(i, 'day').format('YYYY-MM-DD')]?.processed || 0,
|
processed: queue?.[dayjs().subtract(i, 'day').format('YYYY-MM-DD')]?.processed || 0,
|
||||||
failed: queue?.[dayjs().subtract(i, 'day').format('YYYY-MM-DD')]?.failed || 0,
|
failed: queue?.[dayjs().subtract(i, 'day').format('YYYY-MM-DD')]?.failed || 0,
|
||||||
}
|
}
|
||||||
@@ -95,12 +94,14 @@ const currentChartData = computed((): ChartConfig => {
|
|||||||
return {
|
return {
|
||||||
data: queueData,
|
data: queueData,
|
||||||
index: 'date' as const,
|
index: 'date' as const,
|
||||||
categories: ['processed', 'failed'] as const,
|
config: {
|
||||||
colors: ['#4ade80', '#f87171'],
|
processed: { color: '#4ade80', label: t('page.about.processed') },
|
||||||
|
failed: { color: '#f87171', label: t('page.about.failed') },
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const storageData = times(30, (i) => {
|
const storageData = times(30, (i) => {
|
||||||
const base = { date: dayjs().subtract(i, 'day').format('YYYY-MM-DD') }
|
const base = { date: dayjs().subtract(i, 'day').toDate() }
|
||||||
if (currentChartTab.value === 'share') {
|
if (currentChartTab.value === 'share') {
|
||||||
return {
|
return {
|
||||||
...base,
|
...base,
|
||||||
@@ -120,25 +121,31 @@ const currentChartData = computed((): ChartConfig => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
let categories = ['file_size', 'file_num']
|
|
||||||
if (currentChartTab.value === 'share') {
|
if (currentChartTab.value === 'share') {
|
||||||
categories = ['share_num']
|
return {
|
||||||
|
data: storageData as ChartDataItem[],
|
||||||
|
index: 'date' as const,
|
||||||
|
config: {
|
||||||
|
share_num: { color: '#ea580c', label: t('page.about.share') },
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (currentChartTab.value === 'download') {
|
if (currentChartTab.value === 'download') {
|
||||||
categories = ['download_num']
|
return {
|
||||||
}
|
data: storageData as ChartDataItem[],
|
||||||
let colors = ['#38bdf8', '#a78bfa']
|
index: 'date' as const,
|
||||||
if (currentChartTab.value === 'share') {
|
config: {
|
||||||
colors = ['#ea580c']
|
download_num: { color: '#a3e635', label: t('page.about.download') },
|
||||||
}
|
},
|
||||||
if (currentChartTab.value === 'download') {
|
}
|
||||||
colors = ['#a3e635']
|
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
data: storageData as ChartDataItem[],
|
data: storageData as ChartDataItem[],
|
||||||
index: 'date' as const,
|
index: 'date' as const,
|
||||||
categories,
|
config: {
|
||||||
colors,
|
file_size: { color: '#38bdf8', label: t('page.about.fileSize') },
|
||||||
|
file_num: { color: '#a78bfa', label: t('page.about.fileNum') },
|
||||||
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
@@ -167,21 +174,51 @@ const currentChartData = computed((): ChartConfig => {
|
|||||||
<div class="text-lg font-semibold">{{ tab.total }}</div>
|
<div class="text-lg font-semibold">{{ tab.total }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<AreaChart
|
<ChartContainer :config="currentChartData.config" class="h-64 w-full p-5" :cursor="false">
|
||||||
v-if="currentChartData"
|
<VisXYContainer :data="currentChartData.data" :x-domain="[dayjs().toDate(), dayjs().subtract(29, 'day').toDate()]">
|
||||||
class="h-64 w-full"
|
<VisArea
|
||||||
:key="currentChartTab"
|
:key="currentChartTab"
|
||||||
:index="currentChartData.index"
|
:x="(d: ChartDataItem) => d.date"
|
||||||
:data="currentChartData.data"
|
:y="Object.keys(currentChartData.config).map((key) => (d: ChartDataItem) => d?.[key as keyof ChartDataItem])"
|
||||||
:categories="currentChartData.categories"
|
:color="Object.values(currentChartData.config).map((c) => c.color)"
|
||||||
:show-grid-line="false"
|
:opacity="0.6"
|
||||||
:show-legend="false"
|
/>
|
||||||
:show-y-axis="true"
|
<VisLine
|
||||||
:show-x-axis="true"
|
:key="currentChartTab"
|
||||||
:colors="currentChartData.colors"
|
:x="(d: ChartDataItem) => d.date"
|
||||||
:custom-tooltip="AboutChartTooltip"
|
:y="Object.keys(currentChartData.config).map((key) => (d: ChartDataItem) => d?.[key as keyof ChartDataItem])"
|
||||||
:curve-type="CurveType.CatmullRom"
|
:color="Object.values(currentChartData.config).map((c) => c.color)"
|
||||||
/>
|
:line-width="1"
|
||||||
|
/>
|
||||||
|
<VisAxis
|
||||||
|
:key="currentChartTab"
|
||||||
|
type="x"
|
||||||
|
:tick-line="false"
|
||||||
|
:domain-line="false"
|
||||||
|
:grid-line="false"
|
||||||
|
:num-ticks="6"
|
||||||
|
:tick-format="
|
||||||
|
(d: Date) => {
|
||||||
|
return dayjs(d).format('MMM')
|
||||||
|
}
|
||||||
|
"
|
||||||
|
:tick-values="currentChartData.data.map((d) => d.date)"
|
||||||
|
/>
|
||||||
|
<ChartTooltip />
|
||||||
|
<ChartCrosshair
|
||||||
|
:key="currentChartTab"
|
||||||
|
:template="
|
||||||
|
componentToString(currentChartData.config, ChartTooltipContent, {
|
||||||
|
labelFormatter: (d) => {
|
||||||
|
return dayjs(d).format('MMM D')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
"
|
||||||
|
:color="(d: any, i: number) => Object.values(currentChartData.config).map((c) => c.color as string)[i]"
|
||||||
|
/>
|
||||||
|
</VisXYContainer>
|
||||||
|
<ChartLegendContent />
|
||||||
|
</ChartContainer>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import getFileSize from '~/lib/getFileSize'
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
data: { name: string; value: string; color: string }[]
|
|
||||||
title: string
|
|
||||||
}>()
|
|
||||||
const dataKeyMap = {
|
|
||||||
file_size: {
|
|
||||||
'zh-CN': '文件大小',
|
|
||||||
en: 'File Size',
|
|
||||||
},
|
|
||||||
file_num: {
|
|
||||||
'zh-CN': '文件数量',
|
|
||||||
en: 'File Num',
|
|
||||||
},
|
|
||||||
processed: {
|
|
||||||
'zh-CN': '处理数量',
|
|
||||||
en: 'Processed',
|
|
||||||
},
|
|
||||||
failed: {
|
|
||||||
'zh-CN': '失败数量',
|
|
||||||
en: 'Failed',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="rounded-md bg-white p-2 flex flex-col gap-2">
|
|
||||||
<div class="text-sm font-medium">{{ title }}</div>
|
|
||||||
<div v-for="(item, index) in data" :key="index">
|
|
||||||
<div class="flex flex-row items-center gap-2">
|
|
||||||
<div class="h-5 w-2 rounded-full" :style="{ backgroundColor: item.color ?? '#222' }"></div>
|
|
||||||
<div class="text-xs font-medium">
|
|
||||||
{{ dataKeyMap?.[item.name as keyof typeof dataKeyMap]?.['en'] ?? item.name }}
|
|
||||||
</div>
|
|
||||||
<div class="text-sm">
|
|
||||||
{{ ['file_size']?.includes(item?.name) ? getFileSize(item.value) : item.value }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,23 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import {
|
|
||||||
LucideShare,
|
|
||||||
LucideImage,
|
|
||||||
LucideBot,
|
|
||||||
LucideLanguages,
|
|
||||||
LucideFileText,
|
|
||||||
LucideImageMinus,
|
|
||||||
LucideArrowRightLeft,
|
|
||||||
LucideImagePlus,
|
|
||||||
LucideAudioLines,
|
|
||||||
LucideListMusic,
|
|
||||||
} from 'lucide-vue-next'
|
|
||||||
import { cx } from 'class-variance-authority'
|
|
||||||
import { isObject } from 'lodash-es'
|
|
||||||
import showDrawer from '@/lib/showDrawer'
|
import showDrawer from '@/lib/showDrawer'
|
||||||
import FileShareHandle from '@/components/Preprocessing/FileShareHandle.vue'
|
import FileShareHandle from '@/components/Preprocessing/FileShareHandle.vue'
|
||||||
import ImageConvertHandle from '@/components/Preprocessing/ImageConvertHandle.vue'
|
import ImageConvertHandle from '@/components/Preprocessing/ImageConvertHandle.vue'
|
||||||
|
import { useFeatureMeta, type FeatureKey } from '@/composables/useFeatureMeta'
|
||||||
import type { FileShareHandleProps } from '../Preprocessing/types'
|
import type { FileShareHandleProps } from '../Preprocessing/types'
|
||||||
const { t } = useI18n()
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
hide: () => void
|
hide: () => void
|
||||||
file: File[]
|
file: File[]
|
||||||
@@ -25,43 +12,25 @@ const props = defineProps<{
|
|||||||
}>()
|
}>()
|
||||||
|
|
||||||
const isImage = computed(() => props.file.every((r) => r?.type?.startsWith('image/')))
|
const isImage = computed(() => props.file.every((r) => r?.type?.startsWith('image/')))
|
||||||
const isVideo = computed(() => props.file.every((r) => r?.type?.startsWith('video/')))
|
|
||||||
const isAudio = computed(() => props.file.every((r) => r?.type?.startsWith('audio/')))
|
|
||||||
const isMedia = computed(() => isImage.value || isVideo.value || isAudio.value)
|
|
||||||
|
|
||||||
const isPDF = computed(() => props.file.every((r) => r?.type?.startsWith('application/pdf')))
|
const featureMeta = useFeatureMeta()
|
||||||
const isDOC = computed(() => props.file.every((r) => r?.type?.startsWith('application/msword')))
|
|
||||||
const isXLS = computed(() => props.file.every((r) => r?.type?.startsWith('application/vnd.ms-excel')))
|
type ActionHandler = {
|
||||||
const isPPT = computed(() => props.file.every((r) => r?.type?.startsWith('application/vnd.ms-powerpoint')))
|
condition?: () => boolean
|
||||||
const isDocument = computed(() => isPDF.value || isDOC.value || isXLS.value || isPPT.value)
|
onClick: () => void
|
||||||
const actions = [
|
}
|
||||||
{
|
|
||||||
label: t('page.upload.file.handleType.file-share'),
|
const actionHandlers: Partial<Record<FeatureKey, ActionHandler>> = {
|
||||||
icon: LucideShare,
|
'file-share': {
|
||||||
className: 'bg-green-300',
|
onClick: () => showDrawer({ render: ({ hide }) => h(FileShareHandle, { ...props, hide }) }),
|
||||||
onClick: () => {
|
|
||||||
showDrawer({
|
|
||||||
render: ({ hide }) => h(FileShareHandle, { ...props, hide }),
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
isImage.value && {
|
'file-image-compress': {
|
||||||
label: t('page.upload.file.handleType.file-image-compress'),
|
condition: () => isImage.value,
|
||||||
icon: LucideImageMinus,
|
onClick: () => props.onFileHandle({ type: 'file-image-compress', config: {} }),
|
||||||
className: 'bg-red-300',
|
|
||||||
onClick: () => {
|
|
||||||
props.onFileHandle({ type: 'file-image-compress', config: {} })
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
isImage.value && {
|
'file-image-convert': {
|
||||||
label: t('page.upload.file.handleType.file-image-convert'),
|
condition: () => isImage.value,
|
||||||
icon: LucideArrowRightLeft,
|
onClick: () => showDrawer({ render: ({ hide }) => h(ImageConvertHandle, { ...props, hide }) }),
|
||||||
className: 'bg-purple-300',
|
|
||||||
onClick: () => {
|
|
||||||
showDrawer({
|
|
||||||
render: ({ hide }) => h(ImageConvertHandle, { ...props, hide }),
|
|
||||||
})
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
// isImage.value && {
|
// isImage.value && {
|
||||||
// label: '图片翻译', icon: LucideLanguages, className: 'bg-orange-300', onClick: () => {
|
// label: '图片翻译', icon: LucideLanguages, className: 'bg-orange-300', onClick: () => {
|
||||||
@@ -88,19 +57,24 @@ const actions = [
|
|||||||
// console.log('复制链接')
|
// console.log('复制链接')
|
||||||
// }
|
// }
|
||||||
// },
|
// },
|
||||||
]?.filter(isObject) as {
|
}
|
||||||
label: string
|
|
||||||
icon: any
|
const actions = computed(() =>
|
||||||
className: string
|
featureMeta.value
|
||||||
onClick: () => void
|
.filter((meta) => {
|
||||||
}[]
|
const { key } = meta || {}
|
||||||
|
const handler = actionHandlers?.[key]
|
||||||
|
return handler && (!handler.condition || handler.condition())
|
||||||
|
})
|
||||||
|
.map((meta) => ({ ...meta, onClick: actionHandlers[meta.key]!.onClick }))
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col gap-5 p-5">
|
<div class="flex flex-col gap-5 p-5 overflow-x-auto">
|
||||||
<div class="flex flex-row gap-2">
|
<div class="flex flex-row gap-2">
|
||||||
<div
|
<div
|
||||||
v-for="item in actions"
|
v-for="item in actions"
|
||||||
:key="item.label"
|
:key="item.key"
|
||||||
class="flex flex-col items-center gap-2 max-w-20"
|
class="flex flex-col items-center gap-2 max-w-20"
|
||||||
@click="
|
@click="
|
||||||
() => {
|
() => {
|
||||||
@@ -109,7 +83,7 @@ const actions = [
|
|||||||
}
|
}
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<div :class="cx('size-14 flex justify-center items-center rounded-full mx-3', item?.className)">
|
<div class="size-14 flex justify-center items-center rounded-full mx-3" :style="item?.style">
|
||||||
<component :is="item?.icon" />
|
<component :is="item?.icon" />
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs truncate w-full text-center">{{ item?.label }}</div>
|
<div class="text-xs truncate w-full text-center">{{ item?.label }}</div>
|
||||||
|
|||||||
@@ -1,41 +1,37 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { cx } from "class-variance-authority";
|
import { cx } from 'class-variance-authority'
|
||||||
|
|
||||||
const { availableLocales, setLocale, locale: currentLocale, t } = useI18n();
|
const props = defineProps<{
|
||||||
const route = useRoute();
|
hide: () => void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { availableLocales, setLocale, locale: currentLocale, t } = useI18n()
|
||||||
|
|
||||||
const localeMap = {
|
const localeMap = {
|
||||||
"zh-CN": "简体中文",
|
'zh-CN': '简体中文',
|
||||||
en: "English",
|
en: 'English',
|
||||||
// 'ja': '日本語',
|
// 'ja': '日本語',
|
||||||
// 'ko': '한국어',
|
// 'ko': '한국어',
|
||||||
// 'fr': 'Français',
|
// 'fr': 'Français',
|
||||||
// 'de': 'Deutsch',
|
// 'de': 'Deutsch',
|
||||||
};
|
}
|
||||||
|
|
||||||
const switchLocale = async (locale: string) => {
|
const switchLocale = async (locale: string) => {
|
||||||
await setLocale(locale as keyof typeof localeMap);
|
await setLocale(locale as keyof typeof localeMap)
|
||||||
navigateTo(route.path, {
|
props.hide()
|
||||||
external: true,
|
}
|
||||||
});
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col gap-1 py-2">
|
<div class="flex flex-col gap-1 py-2">
|
||||||
<div class="text-xl font-bold mb-3">{{ t("i18n.switchLocale") }}</div>
|
<div class="text-xl font-bold mb-3">{{ t('i18n.switchLocale') }}</div>
|
||||||
<div
|
<div
|
||||||
v-for="locale in availableLocales"
|
v-for="locale in availableLocales"
|
||||||
:key="locale"
|
:key="locale"
|
||||||
:class="
|
:class="cx('rounded-md hover:bg-black/10 p-2 cursor-pointer', currentLocale === locale && 'bg-black/10 font-bold')"
|
||||||
cx(
|
@click="() => switchLocale(locale)"
|
||||||
'rounded-md hover:bg-black/10 p-2 cursor-pointer',
|
>
|
||||||
currentLocale === locale && 'bg-black/10 font-bold',
|
{{ localeMap?.[locale as keyof typeof localeMap] }}
|
||||||
)
|
</div>
|
||||||
"
|
|
||||||
@click="() => switchLocale(locale)"
|
|
||||||
>
|
|
||||||
{{ localeMap?.[locale as keyof typeof localeMap] }}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import FormButton from '@/components/Field/FormButton.vue'
|
|||||||
import InputField from '@/components/Field/InputField.vue'
|
import InputField from '@/components/Field/InputField.vue'
|
||||||
import type { FormContext, GenericObject } from 'vee-validate'
|
import type { FormContext, GenericObject } from 'vee-validate'
|
||||||
import { toast } from 'vue-sonner'
|
import { toast } from 'vue-sonner'
|
||||||
|
const { t } = useI18n()
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
share_id: string
|
share_id: string
|
||||||
hide: any
|
hide: any
|
||||||
@@ -15,14 +16,14 @@ const handleSubmit = async (form: FormContext<GenericObject, GenericObject>) =>
|
|||||||
const password = form.values.password
|
const password = form.values.password
|
||||||
const token = await getShareToken(props.share_id, { password })
|
const token = await getShareToken(props.share_id, { password })
|
||||||
if (!token) {
|
if (!token) {
|
||||||
toast.error('密码错误')
|
toast.error(t('page.shareView.passwall.passwordError'))
|
||||||
form.resetForm()
|
form.resetForm()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
props?.hide(token)
|
props?.hide(token)
|
||||||
return
|
return
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error('密码错误')
|
toast.error(t('page.shareView.passwall.passwordError'))
|
||||||
form.resetForm()
|
form.resetForm()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -31,9 +32,9 @@ const handleSubmit = async (form: FormContext<GenericObject, GenericObject>) =>
|
|||||||
<template>
|
<template>
|
||||||
<VeeForm>
|
<VeeForm>
|
||||||
<div class="flex flex-col gap-5">
|
<div class="flex flex-col gap-5">
|
||||||
<div class="text-xl font-bold">输入密码</div>
|
<div class="text-xl font-bold">{{ t('page.shareView.passwall.title') }}</div>
|
||||||
<InputField name="password" type="password" rules="required" placeholder="请输入密码" />
|
<InputField name="password" type="password" rules="required" :placeholder="t('page.shareView.passwall.passwordPlaceholder')" />
|
||||||
<FormButton @click="handleSubmit">提交</FormButton>
|
<FormButton @click="handleSubmit">{{ t('btn.submit') }}</FormButton>
|
||||||
</div>
|
</div>
|
||||||
</VeeForm>
|
</VeeForm>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,20 +1,21 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import QRCode from "qrcode";
|
import QRCode from 'qrcode'
|
||||||
|
const { t } = useI18n()
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
hide: () => void;
|
hide: () => void
|
||||||
data: any;
|
data: any
|
||||||
}>();
|
}>()
|
||||||
const { state } = useAsyncState(async () => {
|
const { state } = useAsyncState(async () => {
|
||||||
return await QRCode.toDataURL(props.data);
|
return await QRCode.toDataURL(props.data)
|
||||||
}, null);
|
}, null)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col gap-5">
|
<div class="flex flex-col gap-5">
|
||||||
<div class="text-xl font-bold">分享二维码</div>
|
<div class="text-xl font-bold">{{ t('page.result.qrCode.title') }}</div>
|
||||||
<div class="flex flex-row justify-center">
|
<div class="flex flex-row justify-center">
|
||||||
<img :src="state" v-if="!!state" />
|
<img :src="state" v-if="!!state" />
|
||||||
<Skeleton class="size-20" v-else />
|
<Skeleton class="size-20" v-else />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,49 +1,55 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { LucideShare, LucideImage, LucideBot, LucideLanguages } from 'lucide-vue-next'
|
|
||||||
import { cx } from 'class-variance-authority'
|
|
||||||
import showDrawer from '@/lib/showDrawer'
|
import showDrawer from '@/lib/showDrawer'
|
||||||
import TextShareHandle from '@/components/Preprocessing/TextShareHandle.vue'
|
import TextShareHandle from '@/components/Preprocessing/TextShareHandle.vue'
|
||||||
|
import { useFeatureMeta, type FeatureKey } from '@/composables/useFeatureMeta'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
hide: () => void
|
hide: () => void
|
||||||
text: string
|
text: string
|
||||||
onTextHandle: ({ type, config }: { type: string; config: any }) => void
|
onTextHandle: ({ type, config }: { type: string; config: any }) => void
|
||||||
}>()
|
}>()
|
||||||
const { t } = useI18n()
|
|
||||||
const actions = [
|
const featureMeta = useFeatureMeta()
|
||||||
{
|
|
||||||
label: t('page.upload.text.handleType.text-share'),
|
type ActionHandler = {
|
||||||
icon: LucideShare,
|
condition?: () => boolean
|
||||||
className: 'bg-green-300',
|
onClick: () => void
|
||||||
onClick: () => {
|
}
|
||||||
showDrawer({
|
|
||||||
render: ({ hide }) => h(TextShareHandle, { ...props, hide }),
|
const actionHandlers: Partial<Record<FeatureKey, ActionHandler>> = {
|
||||||
})
|
'text-share': {
|
||||||
},
|
onClick: () => showDrawer({ render: ({ hide }) => h(TextShareHandle, { ...props, hide }) }),
|
||||||
},
|
},
|
||||||
// {
|
// 'text-image-generate': {
|
||||||
// label: '生成配图', icon: LucideImage, className: 'bg-red-300', onClick: () => {
|
// label: '生成配图', icon: LucideImage, className: 'bg-red-300',
|
||||||
// console.log('复制链接')
|
// onClick: () => { console.log('复制链接') }
|
||||||
// }
|
|
||||||
// },
|
// },
|
||||||
// {
|
// 'text-ai-ask': {
|
||||||
// label: '问大模型', icon: LucideBot, className: 'bg-blue-300', onClick: () => {
|
// label: '问大模型', icon: LucideBot, className: 'bg-blue-300',
|
||||||
// console.log('复制链接')
|
// onClick: () => { console.log('复制链接') }
|
||||||
// }
|
|
||||||
// },
|
// },
|
||||||
// {
|
// 'text-translate': {
|
||||||
// label: '文本翻译', icon: LucideLanguages, className: 'bg-orange-300', onClick: () => {
|
// label: '文本翻译', icon: LucideLanguages, className: 'bg-orange-300',
|
||||||
// console.log('复制链接')
|
// onClick: () => { console.log('复制链接') }
|
||||||
// }
|
|
||||||
// },
|
// },
|
||||||
]
|
}
|
||||||
|
|
||||||
|
const actions = computed(() =>
|
||||||
|
featureMeta.value
|
||||||
|
.filter((meta) => {
|
||||||
|
const { key } = meta || {}
|
||||||
|
const handler = actionHandlers?.[key]
|
||||||
|
return handler && (!handler.condition || handler.condition())
|
||||||
|
})
|
||||||
|
.map((meta) => ({ ...meta, onClick: actionHandlers[meta.key]!.onClick }))
|
||||||
|
)
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col gap-5 p-5">
|
<div class="flex flex-col gap-5 p-5 overflow-x-auto">
|
||||||
<div class="flex flex-row gap-2">
|
<div class="flex flex-row gap-2">
|
||||||
<div
|
<div
|
||||||
v-for="item in actions"
|
v-for="item in actions"
|
||||||
:key="item.label"
|
:key="item.key"
|
||||||
class="flex flex-col items-center gap-2 max-w-20"
|
class="flex flex-col items-center gap-2 max-w-20"
|
||||||
@click="
|
@click="
|
||||||
() => {
|
() => {
|
||||||
@@ -52,7 +58,7 @@ const actions = [
|
|||||||
}
|
}
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<div :class="cx('size-14 flex justify-center items-center rounded-full mx-3', item?.className)">
|
<div class="size-14 flex justify-center items-center rounded-full mx-3" :style="item?.style">
|
||||||
<component :is="item?.icon" />
|
<component :is="item?.icon" />
|
||||||
</div>
|
</div>
|
||||||
<div class="text-xs truncate w-full text-center">{{ item?.label }}</div>
|
<div class="text-xs truncate w-full text-center">{{ item?.label }}</div>
|
||||||
|
|||||||
@@ -12,14 +12,25 @@ const props = defineProps<{
|
|||||||
}>()
|
}>()
|
||||||
const { value, setValue } = useField<File[]>(props?.name, props?.rules)
|
const { value, setValue } = useField<File[]>(props?.name, props?.rules)
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const filterOutSameFile = (files: File[] | undefined, targetFile: File[] | undefined) => {
|
||||||
|
return files?.filter((file) => !targetFile?.some((r) => r?.name === file?.name && r?.type === file?.type && r?.size === file?.size)) || []
|
||||||
|
}
|
||||||
|
|
||||||
|
useEventListener(document, 'paste', (evt: ClipboardEvent) => {
|
||||||
|
const { files } = evt.clipboardData || {}
|
||||||
|
if (files?.length) {
|
||||||
|
setValue([...filterOutSameFile(value?.value, Array.from(files)), ...Array.from(files)])
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<FileUpload
|
<FileUpload
|
||||||
@onChange="
|
@onChange="
|
||||||
(file) => {
|
(files) => {
|
||||||
// 这里没hash,我们姑且认为name和size,type都一样的为同一个文件
|
// 这里没hash,我们姑且认为name和size,type都一样的为同一个文件
|
||||||
setValue([...(value?.filter((r) => r?.name !== file?.name || r?.type !== file?.type || r?.size !== file?.size) || []), file])
|
setValue([...filterOutSameFile(value, files), ...files])
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
v-slot="{ isOverDropZone }"
|
v-slot="{ isOverDropZone }"
|
||||||
@@ -41,9 +52,7 @@ const { t } = useI18n()
|
|||||||
@click="
|
@click="
|
||||||
(e: any) => {
|
(e: any) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
setValue(
|
setValue(filterOutSameFile(value, [item]))
|
||||||
value?.filter((r) => r?.name !== item?.name || r?.type !== item?.type || r?.size !== item?.size) || []
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -7,10 +7,16 @@ export type filePreview = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
import { LucideFileAudio, LucideFileVideo, LucideFile, LucideFileCode, LucideFileArchive, LucideFileText } from 'lucide-vue-next'
|
import { LucideFileAudio, LucideFileVideo, LucideFile, LucideFileCode, LucideFileArchive, LucideFileText } from 'lucide-vue-next'
|
||||||
const props = defineProps<{
|
const props = withDefaults(
|
||||||
file: File | filePreview
|
defineProps<{
|
||||||
class?: string
|
file: File | filePreview
|
||||||
}>()
|
class?: string
|
||||||
|
size?: 'sm' | 'md' | 'lg'
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
size: 'md',
|
||||||
|
}
|
||||||
|
)
|
||||||
const imageUrl = computed(() => {
|
const imageUrl = computed(() => {
|
||||||
if (props?.file?.type?.startsWith('image/') && props?.file instanceof File) {
|
if (props?.file?.type?.startsWith('image/') && props?.file instanceof File) {
|
||||||
return URL.createObjectURL(props?.file)
|
return URL.createObjectURL(props?.file)
|
||||||
@@ -56,12 +62,20 @@ const fileIcon = computed(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="!!imageUrl" class="flex max-w-30 max-h-20">
|
<div v-if="!!imageUrl" :class="cx('flex overflow-hidden', size === 'sm' && 'max-w-20 max-h-16', size === 'md' && 'max-w-30 max-h-20')">
|
||||||
<div class="object-contain m-auto h-full">
|
<img :src="imageUrl" class="block max-w-full max-h-full object-contain border border-black/20 rounded" />
|
||||||
<img :src="imageUrl" class="w-full h-full border border-black/20 rounded" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-if="!imageUrl" :class="cx('flex justify-center items-center rounded-xl bg-white/80 size-16', props?.class)">
|
<div
|
||||||
|
v-if="!imageUrl"
|
||||||
|
:class="
|
||||||
|
cx(
|
||||||
|
'flex justify-center items-center bg-white/80',
|
||||||
|
size === 'sm' && 'size-7 rounded-md',
|
||||||
|
size === 'md' && 'size-16 rounded-xl',
|
||||||
|
props?.class
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
<component :is="fileIcon" class="size-[62.5%]" />
|
<component :is="fileIcon" class="size-[62.5%]" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -3,63 +3,63 @@ import { useDropZone } from '@vueuse/core'
|
|||||||
const dropZoneRef = ref()
|
const dropZoneRef = ref()
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
accept?: string[]
|
accept?: string[]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const accept = computed(() => (props?.accept || ['*'])?.join(','))
|
const accept = computed(() => (props?.accept || ['*'])?.join(','))
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'onChange', file: File): void
|
(e: 'onChange', file: File[]): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const { isOverDropZone } = useDropZone(dropZoneRef, {
|
const { isOverDropZone } = useDropZone(dropZoneRef, {
|
||||||
onDrop: (file) => {
|
onDrop: (file) => {
|
||||||
if (file?.[0]) {
|
if (!!file && file?.length > 0) {
|
||||||
emit('onChange', file?.[0])
|
emit('onChange', file)
|
||||||
}
|
|
||||||
},
|
|
||||||
// 指定要接收的数据类型
|
|
||||||
dataTypes: (types) => {
|
|
||||||
for (const type of types) {
|
|
||||||
for (const acceptType of accept.value.split(',')) {
|
|
||||||
if (acceptType === '*') {
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
if (acceptType?.endsWith('*')) {
|
},
|
||||||
const [acceptTypePrefix,] = acceptType?.split('/')
|
// 指定要接收的数据类型
|
||||||
if (!acceptTypePrefix) {
|
dataTypes: (types) => {
|
||||||
return true
|
for (const type of types) {
|
||||||
}
|
for (const acceptType of accept.value.split(',')) {
|
||||||
if (type?.startsWith(acceptTypePrefix)) {
|
if (acceptType === '*') {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
if (acceptType?.endsWith('*')) {
|
||||||
|
const [acceptTypePrefix] = acceptType?.split('/')
|
||||||
|
if (!acceptTypePrefix) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (type?.startsWith(acceptTypePrefix)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (acceptType === type) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (acceptType === type) {
|
return false
|
||||||
return true
|
},
|
||||||
}
|
// 控制多文件拖放
|
||||||
}
|
multiple: true,
|
||||||
}
|
// 是否阻止未处理事件的默认行为
|
||||||
return false
|
preventDefaultForUnhandled: false,
|
||||||
},
|
|
||||||
// 控制多文件拖放
|
|
||||||
multiple: false,
|
|
||||||
// 是否阻止未处理事件的默认行为
|
|
||||||
preventDefaultForUnhandled: false,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const { open, onChange } = useFileDialog({
|
const { open, onChange } = useFileDialog({
|
||||||
accept: accept.value, // Set to accept only image files
|
accept: accept.value, // Set to accept only image files
|
||||||
directory: false,
|
directory: false,
|
||||||
})
|
})
|
||||||
onChange((files) => {
|
onChange((files) => {
|
||||||
if (files?.[0]) {
|
if (!!files && files?.length > 0) {
|
||||||
emit('onChange', files?.[0])
|
emit('onChange', Array.from(files))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div ref="dropZoneRef" @click="open">
|
<div ref="dropZoneRef" @click="() => open()">
|
||||||
<slot :isOverDropZone="isOverDropZone" />
|
<slot :isOverDropZone="isOverDropZone" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -28,7 +28,7 @@ const Children = () =>
|
|||||||
"
|
"
|
||||||
>
|
>
|
||||||
<DrawerContent>
|
<DrawerContent>
|
||||||
<div class="mx-auto w-full max-w-lg pb-10 px-3">
|
<div class="mx-auto min-w-lg max-w-[80vw] pb-10 px-3">
|
||||||
<Children />
|
<Children />
|
||||||
</div>
|
</div>
|
||||||
</DrawerContent>
|
</DrawerContent>
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ const handleShowSpeedInfo = () => {
|
|||||||
:style="{
|
:style="{
|
||||||
height: `${clamp((i.value / Math.max(...(speedChartList?.map((r) => r.value) || [1]))) * 100, 1, 100)}%`,
|
height: `${clamp((i.value / Math.max(...(speedChartList?.map((r) => r.value) || [1]))) * 100, 1, 100)}%`,
|
||||||
}"
|
}"
|
||||||
:layoutId="i.timestamp"
|
:layoutId="String(i.timestamp)"
|
||||||
v-for="i in speedChartList"
|
v-for="i in speedChartList"
|
||||||
:key="i.timestamp"
|
:key="i.timestamp"
|
||||||
:initial="{ x: 10, opacity: 0 }"
|
:initial="{ x: 10, opacity: 0 }"
|
||||||
|
|||||||
@@ -1,90 +1,81 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div class="flex flex-row bg-white/50 backdrop-blur-xl p-2 rounded-full gap-1 sticky top-0 z-10">
|
||||||
class="flex flex-row bg-white/50 backdrop-blur-xl p-2 rounded-full gap-1 sticky top-0 z-10"
|
<div
|
||||||
>
|
v-for="item in routes"
|
||||||
<div
|
:key="item.key"
|
||||||
v-for="item in routes"
|
:class="
|
||||||
:key="item.key"
|
cx(
|
||||||
:class="
|
'flex flex-row items-center text-sm px-4 py-2 font-bold rounded-full relative select-none cursor-pointer',
|
||||||
cx(
|
!isActive(item) && 'hover:bg-black/5',
|
||||||
'flex flex-row items-center text-sm px-4 py-2 font-bold rounded-full relative select-none cursor-pointer',
|
item?.name && 'gap-2',
|
||||||
!isActive(item) && 'hover:bg-black/5',
|
item?.className
|
||||||
item?.name && 'gap-2',
|
)
|
||||||
item?.className,
|
"
|
||||||
)
|
@click="handleClick(item)"
|
||||||
"
|
>
|
||||||
@click="handleClick(item)"
|
<motion.div v-if="isActive(item)" layoutId="navbar-active" class="absolute inset-0 rounded-full w-full h-full bg-black/10" />
|
||||||
>
|
<component :is="item.icon" />
|
||||||
<motion.div
|
<div class="hidden sm:block">{{ item.name }}</div>
|
||||||
v-if="isActive(item)"
|
</div>
|
||||||
layoutId="navbar-active"
|
|
||||||
class="absolute inset-0 rounded-full w-full h-full bg-black/10"
|
|
||||||
/>
|
|
||||||
<component :is="item.icon" />
|
|
||||||
<div class="hidden sm:block">{{ item.name }}</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { cx } from "class-variance-authority";
|
import { cx } from 'class-variance-authority'
|
||||||
import { LucideClipboardType, LucidePaperclip } from "#components";
|
import { LucideClipboardType, LucidePaperclip } from '#components'
|
||||||
import { motion } from "motion-v";
|
import { motion } from 'motion-v'
|
||||||
import { LucideGlobe } from "lucide-vue-next";
|
import { LucideGlobe } from 'lucide-vue-next'
|
||||||
import showDrawer from "@/lib/showDrawer";
|
import showDrawer from '@/lib/showDrawer'
|
||||||
import I18nSwitchDrawer from "./Drawer/I18nSwitchDrawer.vue";
|
import I18nSwitchDrawer from './Drawer/I18nSwitchDrawer.vue'
|
||||||
const { t } = useI18n();
|
const { t } = useI18n()
|
||||||
const routes = [
|
const routes = computed(() => [
|
||||||
{
|
{
|
||||||
key: "about",
|
key: 'about',
|
||||||
icon: () =>
|
icon: () =>
|
||||||
h("img", {
|
h('img', {
|
||||||
class: "size-10 rounded-full border-2 border-white/50",
|
class: 'size-10 rounded-full border-2 border-white/50',
|
||||||
src: "/logo.png",
|
src: '/logo.png',
|
||||||
}),
|
}),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
router.push("/about");
|
router.push('/about')
|
||||||
|
},
|
||||||
|
isActive: (item: { key: string }) => route.path?.endsWith(item.key),
|
||||||
|
className: '!p-1.5',
|
||||||
},
|
},
|
||||||
isActive: (item: { key: string }) => route.path?.endsWith(item.key),
|
{ name: t('navbar.file'), key: 'file', icon: LucidePaperclip },
|
||||||
className: "!p-1.5",
|
{ name: t('navbar.text'), key: 'text', icon: LucideClipboardType },
|
||||||
},
|
{
|
||||||
{ name: t("navbar.file"), key: "file", icon: LucidePaperclip },
|
key: 'i18n',
|
||||||
{ name: t("navbar.text"), key: "text", icon: LucideClipboardType },
|
icon: LucideGlobe,
|
||||||
{
|
onClick: () => {
|
||||||
key: "i18n",
|
showDrawer({
|
||||||
icon: LucideGlobe,
|
render: (props) => h(I18nSwitchDrawer, { ...props }),
|
||||||
onClick: () => {
|
})
|
||||||
showDrawer({
|
},
|
||||||
render: () => h(I18nSwitchDrawer),
|
className: 'size-12 !p-1.5 justify-center items-center',
|
||||||
});
|
|
||||||
},
|
},
|
||||||
className: "size-12 !p-1.5 justify-center items-center",
|
])
|
||||||
},
|
const route = useRoute()
|
||||||
];
|
const router = useRouter()
|
||||||
const route = useRoute();
|
const type = computed(() => route?.query?.type)
|
||||||
const router = useRouter();
|
|
||||||
const type = computed(() => route?.query?.type);
|
|
||||||
|
|
||||||
const isActive = (item: {
|
const isActive = (item: { key: string; isActive?: (item: { key: string }) => boolean }) => {
|
||||||
key: string;
|
const { key, isActive } = item || {}
|
||||||
isActive?: (item: { key: string }) => boolean;
|
return isActive ? isActive(item) : type.value === key
|
||||||
}) => {
|
}
|
||||||
const { key, isActive } = item || {};
|
|
||||||
return isActive ? isActive(item) : type.value === key;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClick = (item: { key: string; onClick?: () => void }) => {
|
const handleClick = (item: { key: string; onClick?: () => void }) => {
|
||||||
const { key, onClick } = item || {};
|
const { key, onClick } = item || {}
|
||||||
if (onClick) {
|
if (onClick) {
|
||||||
onClick();
|
onClick()
|
||||||
return;
|
return
|
||||||
}
|
}
|
||||||
router.push({
|
router.push({
|
||||||
path: "/",
|
path: '/',
|
||||||
query: {
|
query: {
|
||||||
...route.query,
|
...route.query,
|
||||||
type: key,
|
type: key,
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import FilePreviewView from '@/components/FilePreviewView.vue'
|
import FilePreviewView from '@/components/FilePreviewView.vue'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { useClipboard } from '@vueuse/core'
|
import { useClipboard, useShare } from '@vueuse/core'
|
||||||
import { toast } from 'vue-sonner'
|
import { toast } from 'vue-sonner'
|
||||||
import { useQuery } from '@tanstack/vue-query'
|
import { useQuery } from '@tanstack/vue-query'
|
||||||
import useMyAppShare from '@/composables/useMyAppShare'
|
import useMyAppShare from '@/composables/useMyAppShare'
|
||||||
@@ -44,16 +44,30 @@ watchEffect(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
watchEffect(() => {
|
|
||||||
console.log('data', data?.value)
|
|
||||||
})
|
|
||||||
|
|
||||||
const appConfig = useMyAppConfig()
|
const appConfig = useMyAppConfig()
|
||||||
const getShareUrl = (id: string) => {
|
const getShareUrl = (id: string) => {
|
||||||
return `${appConfig?.value?.site_url}/s/${id}`
|
return `${appConfig?.value?.site_url}/s/${id}`
|
||||||
}
|
}
|
||||||
|
|
||||||
const { copy } = useClipboard()
|
const { copy } = useClipboard()
|
||||||
|
const { share, isSupported: isShareSupported } = useShare()
|
||||||
|
|
||||||
|
const handleShare = async (id: string, fileName?: string) => {
|
||||||
|
await share({
|
||||||
|
title: fileName || 'File Share',
|
||||||
|
url: getShareUrl(id),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleShowQrCode = (id: string) => {
|
||||||
|
showDrawer({
|
||||||
|
render: ({ ...rest }) =>
|
||||||
|
h(QrCoreDrawer, {
|
||||||
|
...rest,
|
||||||
|
data: getShareUrl(id),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -77,16 +91,26 @@ const { copy } = useClipboard()
|
|||||||
<div class="flex flex-row items-center gap-2 flex-1 min-w-0">
|
<div class="flex flex-row items-center gap-2 flex-1 min-w-0">
|
||||||
<FileIcon
|
<FileIcon
|
||||||
:file="props?.data?.files?.[data?.findIndex((i) => i?.id === file?.id) as number]?.file as File"
|
:file="props?.data?.files?.[data?.findIndex((i) => i?.id === file?.id) as number]?.file as File"
|
||||||
:class="cx('!size-7 !rounded-md shrink-0', selectedFile === file?.id && '!bg-white/50')"
|
size="sm"
|
||||||
|
:class="cx('shrink-0', selectedFile === file?.id && 'bg-white/50!')"
|
||||||
/>
|
/>
|
||||||
<div class="text-sm flex-1 truncate">{{ file?.file_name }}</div>
|
<div class="text-sm flex-1 truncate">{{ file?.file_name }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-row items-center gap-2 shrink-0">
|
<div class="flex flex-row items-center gap-2 shrink-0">
|
||||||
<Button
|
<Button
|
||||||
|
v-if="isShareSupported"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
:class="cx('bg-white/70', selectedFile === file?.id && '!bg-white/30 border-none hover:text-white/80')"
|
:class="cx('bg-white/70', selectedFile === file?.id && '!bg-white/30 border-none hover:text-white/80')"
|
||||||
size="icon"
|
size="icon"
|
||||||
@click="
|
@click.stop="handleShare(file?.id as string, file?.file_name)"
|
||||||
|
>
|
||||||
|
<LucideShare />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
:class="cx('bg-white/70', selectedFile === file?.id && '!bg-white/30 border-none hover:text-white/80')"
|
||||||
|
size="icon"
|
||||||
|
@click.stop="
|
||||||
() => {
|
() => {
|
||||||
copy(getShareUrl(file?.id as string))
|
copy(getShareUrl(file?.id as string))
|
||||||
toast.success(t('page.result.file.copySuccess'))
|
toast.success(t('page.result.file.copySuccess'))
|
||||||
@@ -99,17 +123,7 @@ const { copy } = useClipboard()
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
:class="cx('bg-white/70', selectedFile === file?.id && '!bg-white/30 border-none hover:text-white/80')"
|
:class="cx('bg-white/70', selectedFile === file?.id && '!bg-white/30 border-none hover:text-white/80')"
|
||||||
size="icon"
|
size="icon"
|
||||||
@click="
|
@click.stop="handleShowQrCode(file?.id as string)"
|
||||||
() => {
|
|
||||||
showDrawer({
|
|
||||||
render: ({ ...rest }) =>
|
|
||||||
h(QrCoreDrawer, {
|
|
||||||
...rest,
|
|
||||||
data: getShareUrl(file?.id as string),
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
<LucideQrCode />
|
<LucideQrCode />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -159,6 +173,20 @@ const { copy } = useClipboard()
|
|||||||
<div class="text-sm font-semibold">{{ t('page.result.file.link') }}</div>
|
<div class="text-sm font-semibold">{{ t('page.result.file.link') }}</div>
|
||||||
<div class="flex flex-row gap-2">
|
<div class="flex flex-row gap-2">
|
||||||
<Input :model-value="getShareUrl(selectedFileShare?.id as string)" class="bg-white/70" readonly />
|
<Input :model-value="getShareUrl(selectedFileShare?.id as string)" class="bg-white/70" readonly />
|
||||||
|
<Button
|
||||||
|
v-if="isShareSupported"
|
||||||
|
variant="outline"
|
||||||
|
class="bg-white/70"
|
||||||
|
size="icon"
|
||||||
|
@click="
|
||||||
|
handleShare(
|
||||||
|
selectedFileShare?.id as string,
|
||||||
|
props?.data?.files?.[data?.findIndex((item) => item?.id === selectedFileShare?.id) as number]?.file?.name
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<LucideShare />
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
class="bg-white/70"
|
class="bg-white/70"
|
||||||
@@ -173,22 +201,7 @@ const { copy } = useClipboard()
|
|||||||
<LucideCopy />
|
<LucideCopy />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button variant="outline" class="bg-white/70" size="icon" @click="handleShowQrCode(selectedFileShare?.id as string)">
|
||||||
variant="outline"
|
|
||||||
class="bg-white/70"
|
|
||||||
size="icon"
|
|
||||||
@click="
|
|
||||||
() => {
|
|
||||||
showDrawer({
|
|
||||||
render: ({ ...rest }) =>
|
|
||||||
h(QrCoreDrawer, {
|
|
||||||
...rest,
|
|
||||||
data: getShareUrl(selectedFileShare?.id as string),
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<LucideQrCode />
|
<LucideQrCode />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import PasswallShareDrawer from '~/components/Drawer/PasswallShareDrawer.vue'
|
|||||||
dayjs.extend(duration)
|
dayjs.extend(duration)
|
||||||
dayjs.extend(relativeTime)
|
dayjs.extend(relativeTime)
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
data: any
|
data: any
|
||||||
}>()
|
}>()
|
||||||
@@ -31,7 +32,7 @@ const handleDownload = async () => {
|
|||||||
token = await getShareToken(id)
|
token = await getShareToken(id)
|
||||||
}
|
}
|
||||||
if (!token) {
|
if (!token) {
|
||||||
throw new Error('获取token失败')
|
throw new Error(t('page.shareView.fileShare.getTokenFailed'))
|
||||||
}
|
}
|
||||||
downloadFile(token)
|
downloadFile(token)
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@@ -53,19 +54,19 @@ onMounted(() => {
|
|||||||
|
|
||||||
const fileShareInfo = computed(() => {
|
const fileShareInfo = computed(() => {
|
||||||
return [
|
return [
|
||||||
{ label: '需要密码', value: props?.data?.has_password ?? false },
|
{ label: t('page.shareView.fileShare.needPassword'), value: props?.data?.has_password ?? false },
|
||||||
{
|
{
|
||||||
label: '过期时间',
|
label: t('page.shareView.fileShare.expireTime'),
|
||||||
value: dayjs.duration(remaining.value, 'seconds').format(`D天 HH:mm:ss`),
|
value: dayjs.duration(remaining.value, 'seconds').format(t('page.shareView.fileShare.durationFormat')),
|
||||||
},
|
},
|
||||||
{ label: '剩余下载次数', value: props?.data?.download_nums ?? 0 },
|
{ label: t('page.shareView.fileShare.remainingDownloads'), value: props?.data?.download_nums ?? 0 },
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col gap-5 items-center">
|
<div class="flex flex-col gap-5 items-center">
|
||||||
<h1 class="text-xl font-bold">下载文件</h1>
|
<h1 class="text-xl font-bold">{{ t('page.shareView.fileShare.title') }}</h1>
|
||||||
<FilePreviewView :value="props?.data" />
|
<FilePreviewView :value="props?.data" />
|
||||||
<div class="flex flex-col gap-2 md:flex-row w-full">
|
<div class="flex flex-col gap-2 md:flex-row w-full">
|
||||||
<div class="flex flex-row md:flex-col md:gap-1 justify-between items-center md:flex-1" v-for="item in fileShareInfo">
|
<div class="flex flex-row md:flex-col md:gap-1 justify-between items-center md:flex-1" v-for="item in fileShareInfo">
|
||||||
@@ -75,7 +76,7 @@ const fileShareInfo = computed(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<AsyncButton @click="handleDownload" class="w-full">下载</AsyncButton>
|
<AsyncButton @click="handleDownload" class="w-full">{{ t('page.shareView.fileShare.downloadBtn') }}</AsyncButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import PasswallShareDrawer from '~/components/Drawer/PasswallShareDrawer.vue'
|
|||||||
dayjs.extend(duration)
|
dayjs.extend(duration)
|
||||||
dayjs.extend(relativeTime)
|
dayjs.extend(relativeTime)
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
data: any
|
data: any
|
||||||
}>()
|
}>()
|
||||||
@@ -37,12 +38,12 @@ onMounted(() => {
|
|||||||
|
|
||||||
const fileShareInfo = computed(() => {
|
const fileShareInfo = computed(() => {
|
||||||
return [
|
return [
|
||||||
{ label: '需要密码', value: props?.data?.has_password ?? false },
|
{ label: t('page.shareView.textShare.needPassword'), value: props?.data?.has_password ?? false },
|
||||||
{
|
{
|
||||||
label: '过期时间',
|
label: t('page.shareView.textShare.expireTime'),
|
||||||
value: dayjs.duration(remaining.value, 'seconds').format(`D天 HH:mm:ss`),
|
value: dayjs.duration(remaining.value, 'seconds').format(t('page.shareView.textShare.durationFormat')),
|
||||||
},
|
},
|
||||||
{ label: '剩余浏览次数', value: props?.data?.download_nums ?? 0 },
|
{ label: t('page.shareView.textShare.remainingViews'), value: props?.data?.download_nums ?? 0 },
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
const previewText = ref<string | null>(null)
|
const previewText = ref<string | null>(null)
|
||||||
@@ -72,7 +73,7 @@ const handlePreview = async () => {
|
|||||||
<template>
|
<template>
|
||||||
<div :class="cx('flex flex-col max-h-full', !!previewText ? 'gap-3' : 'gap-16 items-center')">
|
<div :class="cx('flex flex-col max-h-full', !!previewText ? 'gap-3' : 'gap-16 items-center')">
|
||||||
<div :class="cx('flex flex-row w-full', !!previewText ? 'justify-between' : 'justify-center')">
|
<div :class="cx('flex flex-row w-full', !!previewText ? 'justify-between' : 'justify-center')">
|
||||||
<h1 class="text-xl">查看文本</h1>
|
<h1 class="text-xl">{{ t('page.shareView.textShare.title') }}</h1>
|
||||||
<Button
|
<Button
|
||||||
v-if="!!previewText"
|
v-if="!!previewText"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
@@ -80,7 +81,7 @@ const handlePreview = async () => {
|
|||||||
@click="
|
@click="
|
||||||
() => {
|
() => {
|
||||||
copy(previewText as string)
|
copy(previewText as string)
|
||||||
toast.success('复制成功')
|
toast.success(t('page.result.text.copySuccess'))
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
@@ -96,7 +97,7 @@ const handlePreview = async () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<AsyncButton @click="handlePreview" class="w-full">浏览</AsyncButton>
|
<AsyncButton @click="handlePreview" class="w-full">{{ t('page.shareView.textShare.viewBtn') }}</AsyncButton>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
|
|||||||
@@ -1,179 +0,0 @@
|
|||||||
<script setup lang="ts" generic="T extends Record<string, any>">
|
|
||||||
import type { BaseChartProps } from ".";
|
|
||||||
import { type BulletLegendItemInterface, CurveType } from "@unovis/ts";
|
|
||||||
import { Area, Axis, Line } from "@unovis/ts";
|
|
||||||
import { VisArea, VisAxis, VisLine, VisXYContainer } from "@unovis/vue";
|
|
||||||
import { useMounted } from "@vueuse/core";
|
|
||||||
import { useId } from "reka-ui";
|
|
||||||
import { type Component, computed, ref } from "vue";
|
|
||||||
import { cn } from "@/lib/utils";
|
|
||||||
import { ChartCrosshair, ChartLegend, defaultColors } from "../chart";
|
|
||||||
|
|
||||||
const props = withDefaults(
|
|
||||||
defineProps<
|
|
||||||
BaseChartProps<T> & {
|
|
||||||
/**
|
|
||||||
* Render custom tooltip component.
|
|
||||||
*/
|
|
||||||
customTooltip?: Component;
|
|
||||||
/**
|
|
||||||
* Type of curve
|
|
||||||
*/
|
|
||||||
curveType?: CurveType;
|
|
||||||
/**
|
|
||||||
* Controls the visibility of gradient.
|
|
||||||
* @default true
|
|
||||||
*/
|
|
||||||
showGradiant?: boolean;
|
|
||||||
}
|
|
||||||
>(),
|
|
||||||
{
|
|
||||||
curveType: CurveType.MonotoneX,
|
|
||||||
filterOpacity: 0.2,
|
|
||||||
margin: () => ({ top: 0, bottom: 0, left: 0, right: 0 }),
|
|
||||||
showXAxis: true,
|
|
||||||
showYAxis: true,
|
|
||||||
showTooltip: true,
|
|
||||||
showLegend: true,
|
|
||||||
showGridLine: true,
|
|
||||||
showGradiant: true,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const emits = defineEmits<{
|
|
||||||
legendItemClick: [d: BulletLegendItemInterface, i: number];
|
|
||||||
}>();
|
|
||||||
|
|
||||||
type KeyOfT = Extract<keyof T, string>;
|
|
||||||
type Data = (typeof props.data)[number];
|
|
||||||
|
|
||||||
const chartRef = useId();
|
|
||||||
|
|
||||||
const index = computed(() => props.index as KeyOfT);
|
|
||||||
const colors = computed(() =>
|
|
||||||
props.colors?.length ? props.colors : defaultColors(props.categories.length),
|
|
||||||
);
|
|
||||||
|
|
||||||
const legendItems = ref<BulletLegendItemInterface[]>(
|
|
||||||
props.categories.map((category, i) => ({
|
|
||||||
name: category,
|
|
||||||
color: colors.value[i],
|
|
||||||
inactive: false,
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
|
|
||||||
const isMounted = useMounted();
|
|
||||||
|
|
||||||
function handleLegendItemClick(d: BulletLegendItemInterface, i: number) {
|
|
||||||
emits("legendItemClick", d, i);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
:class="cn('w-full h-[400px] flex flex-col items-end', $attrs.class ?? '')"
|
|
||||||
>
|
|
||||||
<ChartLegend
|
|
||||||
v-if="showLegend"
|
|
||||||
v-model:items="legendItems"
|
|
||||||
@legend-item-click="handleLegendItemClick"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<VisXYContainer
|
|
||||||
:style="{ height: isMounted ? '100%' : 'auto' }"
|
|
||||||
:margin="{ left: 20, right: 20 }"
|
|
||||||
:data="data"
|
|
||||||
>
|
|
||||||
<svg width="0" height="0">
|
|
||||||
<defs>
|
|
||||||
<linearGradient
|
|
||||||
v-for="(color, i) in colors"
|
|
||||||
:id="`${chartRef}-color-${i}`"
|
|
||||||
:key="i"
|
|
||||||
x1="0"
|
|
||||||
y1="0"
|
|
||||||
x2="0"
|
|
||||||
y2="1"
|
|
||||||
>
|
|
||||||
<template v-if="showGradiant">
|
|
||||||
<stop offset="5%" :stop-color="color" stop-opacity="0.4" />
|
|
||||||
<stop offset="95%" :stop-color="color" stop-opacity="0" />
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<stop offset="0%" :stop-color="color" />
|
|
||||||
</template>
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
<ChartCrosshair
|
|
||||||
v-if="showTooltip"
|
|
||||||
:colors="colors"
|
|
||||||
:items="legendItems"
|
|
||||||
:index="index"
|
|
||||||
:custom-tooltip="customTooltip"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<template v-for="(category, i) in categories" :key="category">
|
|
||||||
<VisArea
|
|
||||||
:x="(d: Data, i: number) => i"
|
|
||||||
:y="(d: Data) => d[category]"
|
|
||||||
color="auto"
|
|
||||||
:curve-type="curveType"
|
|
||||||
:attributes="{
|
|
||||||
[Area.selectors.area]: {
|
|
||||||
fill: `url(#${chartRef}-color-${i})`,
|
|
||||||
},
|
|
||||||
}"
|
|
||||||
:opacity="
|
|
||||||
legendItems.find((item) => item.name === category)?.inactive
|
|
||||||
? filterOpacity
|
|
||||||
: 1
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template v-for="(category, i) in categories" :key="category">
|
|
||||||
<VisLine
|
|
||||||
:x="(d: Data, i: number) => i"
|
|
||||||
:y="(d: Data) => d[category]"
|
|
||||||
:color="colors[i]"
|
|
||||||
:curve-type="curveType"
|
|
||||||
:attributes="{
|
|
||||||
[Line.selectors.line]: {
|
|
||||||
opacity: legendItems.find((item) => item.name === category)
|
|
||||||
?.inactive
|
|
||||||
? filterOpacity
|
|
||||||
: 1,
|
|
||||||
},
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<VisAxis
|
|
||||||
v-if="showXAxis"
|
|
||||||
type="x"
|
|
||||||
:tick-format="xFormatter ?? ((v: number) => data[v]?.[index])"
|
|
||||||
:grid-line="false"
|
|
||||||
:tick-line="false"
|
|
||||||
tick-text-color="hsl(var(--vis-text-color))"
|
|
||||||
/>
|
|
||||||
<VisAxis
|
|
||||||
v-if="showYAxis"
|
|
||||||
type="y"
|
|
||||||
:tick-line="false"
|
|
||||||
:tick-format="yFormatter"
|
|
||||||
:domain-line="false"
|
|
||||||
:grid-line="showGridLine"
|
|
||||||
:attributes="{
|
|
||||||
[Axis.selectors.grid]: {
|
|
||||||
class: 'text-muted',
|
|
||||||
},
|
|
||||||
}"
|
|
||||||
tick-text-color="hsl(var(--vis-text-color))"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<slot />
|
|
||||||
</VisXYContainer>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
export { default as AreaChart } from "./AreaChart.vue";
|
|
||||||
|
|
||||||
import type { Spacing } from "@unovis/ts";
|
|
||||||
|
|
||||||
type KeyOf<T extends Record<string, any>> = Extract<keyof T, string>;
|
|
||||||
|
|
||||||
export interface BaseChartProps<T extends Record<string, any>> {
|
|
||||||
/**
|
|
||||||
* The source data, in which each entry is a dictionary.
|
|
||||||
*/
|
|
||||||
data: T[];
|
|
||||||
/**
|
|
||||||
* Select the categories from your data. Used to populate the legend and toolip.
|
|
||||||
*/
|
|
||||||
categories: KeyOf<T>[];
|
|
||||||
/**
|
|
||||||
* Sets the key to map the data to the axis.
|
|
||||||
*/
|
|
||||||
index: KeyOf<T>;
|
|
||||||
/**
|
|
||||||
* Change the default colors.
|
|
||||||
*/
|
|
||||||
colors?: string[];
|
|
||||||
/**
|
|
||||||
* Margin of each the container
|
|
||||||
*/
|
|
||||||
margin?: Spacing;
|
|
||||||
/**
|
|
||||||
* Change the opacity of the non-selected field
|
|
||||||
* @default 0.2
|
|
||||||
*/
|
|
||||||
filterOpacity?: number;
|
|
||||||
/**
|
|
||||||
* Function to format X label
|
|
||||||
*/
|
|
||||||
xFormatter?: (
|
|
||||||
tick: number | Date,
|
|
||||||
i: number,
|
|
||||||
ticks: number[] | Date[],
|
|
||||||
) => string;
|
|
||||||
/**
|
|
||||||
* Function to format Y label
|
|
||||||
*/
|
|
||||||
yFormatter?: (
|
|
||||||
tick: number | Date,
|
|
||||||
i: number,
|
|
||||||
ticks: number[] | Date[],
|
|
||||||
) => string;
|
|
||||||
/**
|
|
||||||
* Controls the visibility of the X axis.
|
|
||||||
* @default true
|
|
||||||
*/
|
|
||||||
showXAxis?: boolean;
|
|
||||||
/**
|
|
||||||
* Controls the visibility of the Y axis.
|
|
||||||
* @default true
|
|
||||||
*/
|
|
||||||
showYAxis?: boolean;
|
|
||||||
/**
|
|
||||||
* Controls the visibility of tooltip.
|
|
||||||
* @default true
|
|
||||||
*/
|
|
||||||
showTooltip?: boolean;
|
|
||||||
/**
|
|
||||||
* Controls the visibility of legend.
|
|
||||||
* @default true
|
|
||||||
*/
|
|
||||||
showLegend?: boolean;
|
|
||||||
/**
|
|
||||||
* Controls the visibility of gridline.
|
|
||||||
* @default true
|
|
||||||
*/
|
|
||||||
showGridLine?: boolean;
|
|
||||||
}
|
|
||||||
61
front/components/ui/chart/ChartContainer.vue
Normal file
61
front/components/ui/chart/ChartContainer.vue
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import type { ChartConfig } from '.'
|
||||||
|
import { useId } from 'reka-ui'
|
||||||
|
import { computed, toRefs } from 'vue'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { provideChartContext } from '.'
|
||||||
|
import ChartStyle from './ChartStyle.vue'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const props = defineProps<{
|
||||||
|
id?: HTMLAttributes['id']
|
||||||
|
class?: HTMLAttributes['class']
|
||||||
|
config: ChartConfig
|
||||||
|
cursor?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
defineSlots<{
|
||||||
|
default: {
|
||||||
|
id: string
|
||||||
|
config: ChartConfig
|
||||||
|
}
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { config } = toRefs(props)
|
||||||
|
const uniqueId = useId()
|
||||||
|
const chartId = computed(() => `chart-${props.id || uniqueId.replace(/:/g, '')}`)
|
||||||
|
|
||||||
|
provideChartContext({
|
||||||
|
id: uniqueId,
|
||||||
|
config,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
data-slot="chart"
|
||||||
|
:data-chart="chartId"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
`[&_.tick_text]:!fill-muted-foreground [&_.tick_line]:!stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border flex flex-col aspect-video justify-center text-xs [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-hidden [&_.recharts-sector]:outline-hidden [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-surface]:outline-hidden [&_[data-vis-xy-container]]:h-full [&_[data-vis-single-container]]:h-full h-full [&_[data-vis-xy-container]]:w-full [&_[data-vis-single-container]]:w-full w-full `,
|
||||||
|
props.class
|
||||||
|
)
|
||||||
|
"
|
||||||
|
:style="{
|
||||||
|
'--vis-tooltip-padding': '0px',
|
||||||
|
'--vis-tooltip-background-color': 'transparent',
|
||||||
|
'--vis-tooltip-border-color': 'transparent',
|
||||||
|
'--vis-tooltip-text-color': 'none',
|
||||||
|
'--vis-tooltip-shadow-color': 'none',
|
||||||
|
'--vis-tooltip-backdrop-filter': 'none',
|
||||||
|
'--vis-crosshair-circle-stroke-color': '#0000',
|
||||||
|
'--vis-crosshair-line-stroke-width': cursor ? '1px' : '0px',
|
||||||
|
'--vis-font-family': 'var(--font-sans)',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<slot :id="uniqueId" :config="config" />
|
||||||
|
<ChartStyle :id="chartId" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import type { BulletLegendItemInterface } from "@unovis/ts";
|
|
||||||
import { omit } from "@unovis/ts";
|
|
||||||
import { VisCrosshair, VisTooltip } from "@unovis/vue";
|
|
||||||
import { type Component, createApp } from "vue";
|
|
||||||
import { ChartTooltip } from ".";
|
|
||||||
|
|
||||||
const props = withDefaults(
|
|
||||||
defineProps<{
|
|
||||||
colors: string[];
|
|
||||||
index: string;
|
|
||||||
items: BulletLegendItemInterface[];
|
|
||||||
customTooltip?: Component;
|
|
||||||
}>(),
|
|
||||||
{
|
|
||||||
colors: () => [],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// Use weakmap to store reference to each datapoint for Tooltip
|
|
||||||
const wm = new WeakMap();
|
|
||||||
function template(d: any) {
|
|
||||||
if (wm.has(d)) {
|
|
||||||
return wm.get(d);
|
|
||||||
} else {
|
|
||||||
const componentDiv = document.createElement("div");
|
|
||||||
const omittedData = Object.entries(omit(d, [props.index])).map(
|
|
||||||
([key, value]) => {
|
|
||||||
const legendReference = props.items.find((i) => i.name === key);
|
|
||||||
return { ...legendReference, value };
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const TooltipComponent = props.customTooltip ?? ChartTooltip;
|
|
||||||
createApp(TooltipComponent, {
|
|
||||||
title: d[props.index].toString(),
|
|
||||||
data: omittedData,
|
|
||||||
}).mount(componentDiv);
|
|
||||||
wm.set(d, componentDiv.innerHTML);
|
|
||||||
return componentDiv.innerHTML;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function color(d: unknown, i: number) {
|
|
||||||
return props.colors[i] ?? "transparent";
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<VisTooltip :horizontal-shift="20" :vertical-shift="20" />
|
|
||||||
<VisCrosshair :template="template" :color="color" />
|
|
||||||
</template>
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import type { BulletLegendItemInterface } from "@unovis/ts";
|
|
||||||
import { BulletLegend } from "@unovis/ts";
|
|
||||||
import { VisBulletLegend } from "@unovis/vue";
|
|
||||||
import { nextTick, onMounted, ref } from "vue";
|
|
||||||
import { buttonVariants } from "../button";
|
|
||||||
|
|
||||||
const props = withDefaults(
|
|
||||||
defineProps<{ items: BulletLegendItemInterface[] }>(),
|
|
||||||
{
|
|
||||||
items: () => [],
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const emits = defineEmits<{
|
|
||||||
legendItemClick: [d: BulletLegendItemInterface, i: number];
|
|
||||||
"update:items": [payload: BulletLegendItemInterface[]];
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const elRef = ref<HTMLElement>();
|
|
||||||
|
|
||||||
function keepStyling() {
|
|
||||||
const selector = `.${BulletLegend.selectors.item}`;
|
|
||||||
nextTick(() => {
|
|
||||||
const elements = elRef.value?.querySelectorAll(selector);
|
|
||||||
const classes = buttonVariants({ variant: "ghost", size: "sm" }).split(" ");
|
|
||||||
elements?.forEach((el) =>
|
|
||||||
el.classList.add(...classes, "!inline-flex", "!mr-2"),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
keepStyling();
|
|
||||||
});
|
|
||||||
|
|
||||||
function onLegendItemClick(d: BulletLegendItemInterface, i: number) {
|
|
||||||
emits("legendItemClick", d, i);
|
|
||||||
const isBulletActive = !props.items[i].inactive;
|
|
||||||
const isFilterApplied = props.items.some((i) => i.inactive);
|
|
||||||
if (isFilterApplied && isBulletActive) {
|
|
||||||
// reset filter
|
|
||||||
emits(
|
|
||||||
"update:items",
|
|
||||||
props.items.map((item) => ({ ...item, inactive: false })),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// apply selection, set other item as inactive
|
|
||||||
emits(
|
|
||||||
"update:items",
|
|
||||||
props.items.map((item) =>
|
|
||||||
item.name === d.name
|
|
||||||
? { ...d, inactive: false }
|
|
||||||
: { ...item, inactive: true },
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
keepStyling();
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div
|
|
||||||
ref="elRef"
|
|
||||||
class="w-max"
|
|
||||||
:style="{
|
|
||||||
'--vis-legend-bullet-size': '16px',
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<VisBulletLegend :items="items" :on-legend-item-click="onLegendItemClick" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
56
front/components/ui/chart/ChartLegendContent.vue
Normal file
56
front/components/ui/chart/ChartLegendContent.vue
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { computed, onMounted, ref } from 'vue'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { useChart } from '.'
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
hideIcon?: boolean
|
||||||
|
nameKey?: string
|
||||||
|
verticalAlign?: 'bottom' | 'top'
|
||||||
|
// payload?: any[]
|
||||||
|
class?: HTMLAttributes['class']
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
verticalAlign: 'bottom',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const { id, config } = useChart()
|
||||||
|
|
||||||
|
const payload = computed(() =>
|
||||||
|
Object.entries(config.value).map(([key, value]) => {
|
||||||
|
return {
|
||||||
|
key: props.nameKey || key,
|
||||||
|
itemConfig: config.value[key],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const containerSelector = ref('')
|
||||||
|
onMounted(() => {
|
||||||
|
containerSelector.value = `[data-chart="chart-${id}"]>[data-vis-xy-container]`
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="containerSelector" :class="cn('flex items-center justify-center gap-4', verticalAlign === 'top' ? 'pb-3' : 'pt-3', props.class)">
|
||||||
|
<div
|
||||||
|
v-for="{ key, itemConfig } in payload"
|
||||||
|
:key="key"
|
||||||
|
:class="cn('[&>svg]:text-muted-foreground flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3')"
|
||||||
|
>
|
||||||
|
<component :is="itemConfig.icon" v-if="itemConfig?.icon" />
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="h-2 w-2 shrink-0 rounded-[2px]"
|
||||||
|
:style="{
|
||||||
|
backgroundColor: itemConfig?.color,
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{{ itemConfig?.label }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import type { BulletLegendItemInterface } from "@unovis/ts";
|
|
||||||
import { omit } from "@unovis/ts";
|
|
||||||
import { VisTooltip } from "@unovis/vue";
|
|
||||||
import { type Component, createApp } from "vue";
|
|
||||||
import { ChartTooltip } from ".";
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
selector: string;
|
|
||||||
index: string;
|
|
||||||
items?: BulletLegendItemInterface[];
|
|
||||||
valueFormatter?: (tick: number, i?: number, ticks?: number[]) => string;
|
|
||||||
customTooltip?: Component;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
// Use weakmap to store reference to each datapoint for Tooltip
|
|
||||||
const wm = new WeakMap();
|
|
||||||
function template(d: any, i: number, elements: (HTMLElement | SVGElement)[]) {
|
|
||||||
const valueFormatter = props.valueFormatter ?? ((tick: number) => `${tick}`);
|
|
||||||
if (props.index in d) {
|
|
||||||
if (wm.has(d)) {
|
|
||||||
return wm.get(d);
|
|
||||||
} else {
|
|
||||||
const componentDiv = document.createElement("div");
|
|
||||||
const omittedData = Object.entries(omit(d, [props.index])).map(
|
|
||||||
([key, value]) => {
|
|
||||||
const legendReference = props.items?.find((i) => i.name === key);
|
|
||||||
return { ...legendReference, value: valueFormatter(value) };
|
|
||||||
},
|
|
||||||
);
|
|
||||||
const TooltipComponent = props.customTooltip ?? ChartTooltip;
|
|
||||||
createApp(TooltipComponent, {
|
|
||||||
title: d[props.index],
|
|
||||||
data: omittedData,
|
|
||||||
}).mount(componentDiv);
|
|
||||||
wm.set(d, componentDiv.innerHTML);
|
|
||||||
return componentDiv.innerHTML;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const data = d.data;
|
|
||||||
|
|
||||||
if (wm.has(data)) {
|
|
||||||
return wm.get(data);
|
|
||||||
} else {
|
|
||||||
const style = getComputedStyle(elements[i]);
|
|
||||||
const omittedData = [
|
|
||||||
{
|
|
||||||
name: data.name,
|
|
||||||
value: valueFormatter(data[props.index]),
|
|
||||||
color: style.fill,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const componentDiv = document.createElement("div");
|
|
||||||
const TooltipComponent = props.customTooltip ?? ChartTooltip;
|
|
||||||
createApp(TooltipComponent, {
|
|
||||||
title: d[props.index],
|
|
||||||
data: omittedData,
|
|
||||||
}).mount(componentDiv);
|
|
||||||
wm.set(d, componentDiv.innerHTML);
|
|
||||||
return componentDiv.innerHTML;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<VisTooltip
|
|
||||||
:horizontal-shift="20"
|
|
||||||
:vertical-shift="20"
|
|
||||||
:triggers="{
|
|
||||||
[selector]: template,
|
|
||||||
}"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
37
front/components/ui/chart/ChartStyle.vue
Normal file
37
front/components/ui/chart/ChartStyle.vue
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import { Primitive } from 'reka-ui'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { THEMES, useChart } from '.'
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
id?: HTMLAttributes['id']
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const { config } = useChart()
|
||||||
|
|
||||||
|
const colorConfig = computed(() => {
|
||||||
|
return Object.entries(config.value).filter(([, config]) => config.theme || config.color)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Primitive v-if="colorConfig.length" as="style">
|
||||||
|
{{
|
||||||
|
Object.entries(THEMES)
|
||||||
|
.map(
|
||||||
|
([theme, prefix]) => `
|
||||||
|
${prefix} [data-chart=${id}] {
|
||||||
|
${colorConfig
|
||||||
|
.map(([key, itemConfig]) => {
|
||||||
|
const color = itemConfig.theme?.[theme as keyof typeof itemConfig.theme] || itemConfig.color
|
||||||
|
return color ? ` --color-${key}: ${color};` : null
|
||||||
|
})
|
||||||
|
.join('\n')}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.join('\n')
|
||||||
|
}}
|
||||||
|
</Primitive>
|
||||||
|
</template>
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "../card";
|
|
||||||
|
|
||||||
defineProps<{
|
|
||||||
title?: string;
|
|
||||||
data: {
|
|
||||||
name: string;
|
|
||||||
color: string;
|
|
||||||
value: any;
|
|
||||||
}[];
|
|
||||||
}>();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Card class="text-sm">
|
|
||||||
<CardHeader v-if="title" class="p-3 border-b">
|
|
||||||
<CardTitle>
|
|
||||||
{{ title }}
|
|
||||||
</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent class="p-3 min-w-[180px] flex flex-col gap-1">
|
|
||||||
<div v-for="(item, key) in data" :key="key" class="flex justify-between">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<span class="w-2.5 h-2.5 mr-2">
|
|
||||||
<svg width="100%" height="100%" viewBox="0 0 30 30">
|
|
||||||
<path
|
|
||||||
d=" M 15 15 m -14, 0 a 14,14 0 1,1 28,0 a 14,14 0 1,1 -28,0"
|
|
||||||
:stroke="item.color"
|
|
||||||
:fill="item.color"
|
|
||||||
stroke-width="1"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
<span>{{ item.name }}</span>
|
|
||||||
</div>
|
|
||||||
<span class="font-semibold ml-4">{{ item.value }}</span>
|
|
||||||
</div>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</template>
|
|
||||||
109
front/components/ui/chart/ChartTooltipContent.vue
Normal file
109
front/components/ui/chart/ChartTooltipContent.vue
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { HTMLAttributes } from 'vue'
|
||||||
|
import type { ChartConfig } from '.'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
|
hideLabel?: boolean
|
||||||
|
hideIndicator?: boolean
|
||||||
|
indicator?: 'line' | 'dot' | 'dashed'
|
||||||
|
nameKey?: string
|
||||||
|
labelKey?: string
|
||||||
|
labelFormatter?: (d: number | Date) => string
|
||||||
|
payload?: Record<string, any>
|
||||||
|
config?: ChartConfig
|
||||||
|
class?: HTMLAttributes['class']
|
||||||
|
color?: string
|
||||||
|
x?: number | Date
|
||||||
|
}>(),
|
||||||
|
{
|
||||||
|
payload: () => ({}),
|
||||||
|
config: () => ({}),
|
||||||
|
indicator: 'dot',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO: currently we use `createElement` and `render` to render the
|
||||||
|
// const chartContext = useChart(null)
|
||||||
|
|
||||||
|
const payload = computed(() => {
|
||||||
|
return Object.entries(props.payload)
|
||||||
|
.map(([key, value]) => {
|
||||||
|
// const key = `${props.nameKey || item.name || item.dataKey || "value"}`
|
||||||
|
const itemConfig = props.config[key]
|
||||||
|
const indicatorColor = props.config[key]?.color ?? props.payload.fill
|
||||||
|
|
||||||
|
return { key, value, itemConfig, indicatorColor }
|
||||||
|
})
|
||||||
|
.filter((i) => i.itemConfig)
|
||||||
|
})
|
||||||
|
|
||||||
|
const nestLabel = computed(() => Object.keys(props.payload).length === 1 && props.indicator !== 'dot')
|
||||||
|
const tooltipLabel = computed(() => {
|
||||||
|
if (props.hideLabel) return null
|
||||||
|
if (props.labelFormatter && props.x !== undefined) {
|
||||||
|
return props.labelFormatter(props.x)
|
||||||
|
}
|
||||||
|
return props.labelKey ? props.config[props.labelKey]?.label || props.payload[props.labelKey] : props.x
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
:class="
|
||||||
|
cn('border-border/50 bg-background grid min-w-[8rem] items-start gap-1.5 rounded-lg border px-2.5 py-1.5 text-xs shadow-xl', props.class)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<slot>
|
||||||
|
<div v-if="!nestLabel && tooltipLabel" class="font-medium">
|
||||||
|
{{ tooltipLabel }}
|
||||||
|
</div>
|
||||||
|
<div class="grid gap-1.5">
|
||||||
|
<div
|
||||||
|
v-for="{ value, itemConfig, indicatorColor, key } in payload"
|
||||||
|
:key="key"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'[&>svg]:text-muted-foreground flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5',
|
||||||
|
indicator === 'dot' && 'items-center'
|
||||||
|
)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<component :is="itemConfig.icon" v-if="itemConfig?.icon" />
|
||||||
|
<template v-else-if="!hideIndicator">
|
||||||
|
<div
|
||||||
|
:class="
|
||||||
|
cn('shrink-0 rounded-[2px] border-(--color-border) bg-(--color-bg)', {
|
||||||
|
'h-2.5 w-2.5': indicator === 'dot',
|
||||||
|
'w-1': indicator === 'line',
|
||||||
|
'w-0 border-[1.5px] border-dashed bg-transparent': indicator === 'dashed',
|
||||||
|
'my-0.5': nestLabel && indicator === 'dashed',
|
||||||
|
})
|
||||||
|
"
|
||||||
|
:style="{
|
||||||
|
'--color-bg': indicatorColor,
|
||||||
|
'--color-border': indicatorColor,
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div :class="cn('flex flex-1 justify-between leading-none', nestLabel ? 'items-end' : 'items-center')">
|
||||||
|
<div class="grid gap-1.5">
|
||||||
|
<div v-if="nestLabel" class="font-medium">
|
||||||
|
{{ tooltipLabel }}
|
||||||
|
</div>
|
||||||
|
<span class="text-muted-foreground">
|
||||||
|
{{ itemConfig?.label || value }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span v-if="value" class="text-foreground font-mono font-medium tabular-nums">
|
||||||
|
{{ value.toLocaleString() }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -1,21 +1,26 @@
|
|||||||
export { default as ChartCrosshair } from "./ChartCrosshair.vue";
|
import type { Component, Ref } from 'vue'
|
||||||
export { default as ChartLegend } from "./ChartLegend.vue";
|
import { createContext } from 'reka-ui'
|
||||||
export { default as ChartSingleTooltip } from "./ChartSingleTooltip.vue";
|
|
||||||
export { default as ChartTooltip } from "./ChartTooltip.vue";
|
|
||||||
|
|
||||||
export function defaultColors(count: number = 3) {
|
export { default as ChartContainer } from './ChartContainer.vue'
|
||||||
const quotient = Math.floor(count / 2);
|
export { default as ChartLegendContent } from './ChartLegendContent.vue'
|
||||||
const remainder = count % 2;
|
export { default as ChartTooltipContent } from './ChartTooltipContent.vue'
|
||||||
|
export { componentToString } from './utils'
|
||||||
|
|
||||||
const primaryCount = quotient + remainder;
|
// Format: { THEME_NAME: CSS_SELECTOR }
|
||||||
const secondaryCount = quotient;
|
export const THEMES = { light: '', dark: '.dark' } as const
|
||||||
return [
|
|
||||||
...Array.from(new Array(primaryCount).keys()).map(
|
export type ChartConfig = {
|
||||||
(i) => `hsl(var(--vis-primary-color) / ${1 - (1 / primaryCount) * i})`,
|
[k in string]: {
|
||||||
),
|
label?: string | Component
|
||||||
...Array.from(new Array(secondaryCount).keys()).map(
|
icon?: string | Component
|
||||||
(i) =>
|
} & ({ color?: string; theme?: never } | { color?: never; theme: Record<keyof typeof THEMES, string> })
|
||||||
`hsl(var(--vis-secondary-color) / ${1 - (1 / secondaryCount) * i})`,
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ChartContextProps {
|
||||||
|
id: string
|
||||||
|
config: Ref<ChartConfig>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const [useChart, provideChartContext] = createContext<ChartContextProps>('Chart')
|
||||||
|
|
||||||
|
export { VisCrosshair as ChartCrosshair, VisTooltip as ChartTooltip } from '@unovis/vue'
|
||||||
|
|||||||
42
front/components/ui/chart/utils.ts
Normal file
42
front/components/ui/chart/utils.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import type { ChartConfig } from '.'
|
||||||
|
import { isClient } from '@vueuse/core'
|
||||||
|
import { useId } from 'reka-ui'
|
||||||
|
import { h, render } from 'vue'
|
||||||
|
|
||||||
|
// Simple cache using a Map to store serialized object keys
|
||||||
|
const cache = new Map<string, string>()
|
||||||
|
|
||||||
|
// Convert object to a consistent string key
|
||||||
|
function serializeKey(key: Record<string, any>): string {
|
||||||
|
return JSON.stringify(key, Object.keys(key).sort())
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Constructor<P = any> {
|
||||||
|
__isFragment?: never
|
||||||
|
__isTeleport?: never
|
||||||
|
__isSuspense?: never
|
||||||
|
new (...args: any[]): {
|
||||||
|
$props: P
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function componentToString<P>(config: ChartConfig, component: Constructor<P>, props?: P) {
|
||||||
|
if (!isClient) return
|
||||||
|
|
||||||
|
// This function will be called once during mount lifecycle
|
||||||
|
const id = useId()
|
||||||
|
|
||||||
|
// https://unovis.dev/docs/auxiliary/Crosshair#component-props
|
||||||
|
return (_data: any, x: number | Date) => {
|
||||||
|
const data = 'data' in _data ? _data.data : _data
|
||||||
|
const serializedKey = `${id}-${serializeKey(data)}`
|
||||||
|
const cachedContent = cache.get(serializedKey)
|
||||||
|
if (cachedContent) return cachedContent
|
||||||
|
|
||||||
|
const vnode = h<unknown>(component, { ...props, payload: data, config, x })
|
||||||
|
const div = document.createElement('div')
|
||||||
|
render(vnode, div)
|
||||||
|
cache.set(serializedKey, div.innerHTML)
|
||||||
|
return div.innerHTML
|
||||||
|
}
|
||||||
|
}
|
||||||
47
front/composables/useFeatureMeta.ts
Normal file
47
front/composables/useFeatureMeta.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { LucideShare, LucideImageMinus, LucideArrowRightLeft } from 'lucide-vue-next'
|
||||||
|
import useMyAppConfig from '@/composables/useMyAppConfig'
|
||||||
|
import type { FileHandleKey, TextHandleKey } from '../components/Preprocessing/types'
|
||||||
|
import generateRandomColors from '@/lib/generateRandomColors'
|
||||||
|
|
||||||
|
export type FeatureKey = FileHandleKey | TextHandleKey
|
||||||
|
|
||||||
|
export type FeatureMeta = {
|
||||||
|
key: FeatureKey
|
||||||
|
label: string
|
||||||
|
icon: any
|
||||||
|
}
|
||||||
|
|
||||||
|
const allFeatureMeta = (t: (key: string) => string): FeatureMeta[] => [
|
||||||
|
{
|
||||||
|
key: 'file-share',
|
||||||
|
label: t('page.upload.file.handleType.file-share'),
|
||||||
|
icon: LucideShare,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'file-image-compress',
|
||||||
|
label: t('page.upload.file.handleType.file-image-compress'),
|
||||||
|
icon: LucideImageMinus,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'file-image-convert',
|
||||||
|
label: t('page.upload.file.handleType.file-image-convert'),
|
||||||
|
icon: LucideArrowRightLeft,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'text-share',
|
||||||
|
label: t('page.upload.text.handleType.text-share'),
|
||||||
|
icon: LucideShare,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export function useFeatureMeta() {
|
||||||
|
const { t } = useI18n()
|
||||||
|
const appConfig = useMyAppConfig()
|
||||||
|
|
||||||
|
return computed(() => {
|
||||||
|
const enabledKeys = appConfig.value?.features ?? []
|
||||||
|
const result = allFeatureMeta(t).filter((meta) => enabledKeys.includes(meta.key))
|
||||||
|
const colors = generateRandomColors(result.length)
|
||||||
|
return result.map((meta, index) => ({ ...meta, style: { backgroundColor: colors[index] } }))
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ const useMyAppConfig = () => {
|
|||||||
site_bg_url: string
|
site_bg_url: string
|
||||||
version: string
|
version: string
|
||||||
build_time: number
|
build_time: number
|
||||||
|
features: string[]
|
||||||
}
|
}
|
||||||
}>('/api/config')
|
}>('/api/config')
|
||||||
return computed(() => data?.value?.data)
|
return computed(() => data?.value?.data)
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ const useSeo = async (props: UseSeoProps = {}) => {
|
|||||||
site_bg_url: string
|
site_bg_url: string
|
||||||
}>()
|
}>()
|
||||||
if (import.meta.server) {
|
if (import.meta.server) {
|
||||||
|
const nuxtApp = useNuxtApp()
|
||||||
await fetch(`${getApiBaseUrl()}/config`)
|
await fetch(`${getApiBaseUrl()}/config`)
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
.then(({ data }) => {
|
.then(({ data }) => {
|
||||||
@@ -24,33 +25,35 @@ const useSeo = async (props: UseSeoProps = {}) => {
|
|||||||
const { title } = head || {}
|
const { title } = head || {}
|
||||||
const siteTitle = computed(() => renderI18n(seoMeta?.value?.site_title || {}, 'en', locale))
|
const siteTitle = computed(() => renderI18n(seoMeta?.value?.site_title || {}, 'en', locale))
|
||||||
const siteDesc = computed(() => renderI18n(seoMeta?.value?.site_desc || {}, 'en', locale))
|
const siteDesc = computed(() => renderI18n(seoMeta?.value?.site_desc || {}, 'en', locale))
|
||||||
useHead({
|
await nuxtApp.runWithContext(() => {
|
||||||
link: [
|
useHead({
|
||||||
{ rel: 'icon', href: seoMeta.value?.site_icon || '/logo.png', sizes: 'any' },
|
link: [
|
||||||
// { rel: 'icon', href: '/favicon.svg', sizes: 'any', type: 'image/svg+xml' },
|
{ rel: 'icon', href: seoMeta.value?.site_icon || '/logo.png', sizes: 'any' },
|
||||||
{ rel: 'apple-touch-icon', sizes: '180x180', href: seoMeta.value?.site_icon || '/logo.png' },
|
// { rel: 'icon', href: '/favicon.svg', sizes: 'any', type: 'image/svg+xml' },
|
||||||
],
|
{ rel: 'apple-touch-icon', sizes: '180x180', href: seoMeta.value?.site_icon || '/logo.png' },
|
||||||
meta: [
|
],
|
||||||
// used on some mobile browsers
|
meta: [
|
||||||
{ name: 'theme-color', content: '#395276' },
|
// used on some mobile browsers
|
||||||
],
|
{ name: 'theme-color', content: '#395276' },
|
||||||
...head,
|
],
|
||||||
title: title ? `${title} - ${siteTitle.value}` : siteTitle.value,
|
...head,
|
||||||
})
|
title: title ? `${title} - ${siteTitle.value}` : siteTitle.value,
|
||||||
useSeoMeta({
|
})
|
||||||
...seo,
|
useSeoMeta({
|
||||||
title: siteTitle.value,
|
...seo,
|
||||||
description: siteDesc.value,
|
title: siteTitle.value,
|
||||||
ogTitle: siteTitle.value,
|
description: siteDesc.value,
|
||||||
ogDescription: siteDesc.value,
|
ogTitle: siteTitle.value,
|
||||||
ogImage: {
|
ogDescription: siteDesc.value,
|
||||||
url: `${seoMeta?.value?.site_url}${seoMeta?.value?.site_icon || '/logo.png'}`,
|
ogImage: {
|
||||||
width: 1024,
|
url: `${seoMeta?.value?.site_url}${seoMeta?.value?.site_icon || '/logo.png'}`,
|
||||||
height: 1024,
|
width: 1024,
|
||||||
alt: 'logo',
|
height: 1024,
|
||||||
type: 'image/png',
|
alt: 'logo',
|
||||||
},
|
type: 'image/png',
|
||||||
twitterCard: 'summary',
|
},
|
||||||
|
twitterCard: 'summary',
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -160,6 +160,34 @@
|
|||||||
"link": "Link",
|
"link": "Link",
|
||||||
"content": "Content",
|
"content": "Content",
|
||||||
"copySuccess": "Copy Success"
|
"copySuccess": "Copy Success"
|
||||||
|
},
|
||||||
|
"qrCode": {
|
||||||
|
"title": "Share QR code"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"shareView": {
|
||||||
|
"linkExpired": "This link has expired.",
|
||||||
|
"passwall": {
|
||||||
|
"title": "Enter password",
|
||||||
|
"passwordError": "Wrong password",
|
||||||
|
"passwordPlaceholder": "Enter password"
|
||||||
|
},
|
||||||
|
"fileShare": {
|
||||||
|
"title": "Download file",
|
||||||
|
"downloadBtn": "Download",
|
||||||
|
"needPassword": "Password required",
|
||||||
|
"expireTime": "Expire time",
|
||||||
|
"remainingDownloads": "Remaining downloads",
|
||||||
|
"getTokenFailed": "Failed to get token",
|
||||||
|
"durationFormat": "D [days] HH:mm:ss"
|
||||||
|
},
|
||||||
|
"textShare": {
|
||||||
|
"title": "View text",
|
||||||
|
"viewBtn": "View",
|
||||||
|
"needPassword": "Password required",
|
||||||
|
"expireTime": "Expire time",
|
||||||
|
"remainingViews": "Remaining views",
|
||||||
|
"durationFormat": "D [days] HH:mm:ss"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"about": {
|
"about": {
|
||||||
@@ -173,6 +201,8 @@
|
|||||||
"title": "About",
|
"title": "About",
|
||||||
"about": "About",
|
"about": "About",
|
||||||
"systemInfo": "System Info",
|
"systemInfo": "System Info",
|
||||||
|
"enabledFeatures": "Instance feature",
|
||||||
|
"enabledFeaturesEmpty": "No extra features are enabled for this instance",
|
||||||
"systemVersion": "System Version",
|
"systemVersion": "System Version",
|
||||||
"storage": "Storage",
|
"storage": "Storage",
|
||||||
"analysis": "Analysis",
|
"analysis": "Analysis",
|
||||||
|
|||||||
@@ -160,6 +160,34 @@
|
|||||||
"link": "链接",
|
"link": "链接",
|
||||||
"content": "内容",
|
"content": "内容",
|
||||||
"copySuccess": "复制成功"
|
"copySuccess": "复制成功"
|
||||||
|
},
|
||||||
|
"qrCode": {
|
||||||
|
"title": "分享二维码"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"shareView": {
|
||||||
|
"linkExpired": "此链接已过期。",
|
||||||
|
"passwall": {
|
||||||
|
"title": "输入密码",
|
||||||
|
"passwordError": "密码错误",
|
||||||
|
"passwordPlaceholder": "请输入密码"
|
||||||
|
},
|
||||||
|
"fileShare": {
|
||||||
|
"title": "下载文件",
|
||||||
|
"downloadBtn": "下载",
|
||||||
|
"needPassword": "需要密码",
|
||||||
|
"expireTime": "过期时间",
|
||||||
|
"remainingDownloads": "剩余下载次数",
|
||||||
|
"getTokenFailed": "获取token失败",
|
||||||
|
"durationFormat": "D天 HH:mm:ss"
|
||||||
|
},
|
||||||
|
"textShare": {
|
||||||
|
"title": "查看文本",
|
||||||
|
"viewBtn": "浏览",
|
||||||
|
"needPassword": "需要密码",
|
||||||
|
"expireTime": "过期时间",
|
||||||
|
"remainingViews": "剩余浏览次数",
|
||||||
|
"durationFormat": "D天 HH:mm:ss"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"about": {
|
"about": {
|
||||||
@@ -173,6 +201,8 @@
|
|||||||
"title": "关于",
|
"title": "关于",
|
||||||
"about": "关于",
|
"about": "关于",
|
||||||
"systemInfo": "系统信息",
|
"systemInfo": "系统信息",
|
||||||
|
"enabledFeatures": "实例功能",
|
||||||
|
"enabledFeaturesEmpty": "当前实例暂未启用额外功能",
|
||||||
"systemVersion": "系统版本",
|
"systemVersion": "系统版本",
|
||||||
"storage": "已托管的文件",
|
"storage": "已托管的文件",
|
||||||
"analysis": "分析",
|
"analysis": "分析",
|
||||||
|
|||||||
17
front/lib/generateRandomColors.ts
Normal file
17
front/lib/generateRandomColors.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
function generateRandomColors(count: number, minHueDiff = 30) {
|
||||||
|
const colors: { h: number; s: number; l: number }[] = []
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
let hue: number,
|
||||||
|
attempts = 0
|
||||||
|
const { h: previousHue } = colors?.[colors.length - 1] ?? {}
|
||||||
|
do {
|
||||||
|
hue = Math.random() * 360
|
||||||
|
attempts++
|
||||||
|
} while (attempts < 100 && previousHue !== undefined && Math.abs(previousHue - hue) < minHueDiff)
|
||||||
|
|
||||||
|
colors.push({ h: hue, s: 70, l: 75 })
|
||||||
|
}
|
||||||
|
return colors.map((c) => `hsl(${c.h}, ${c.s}%, ${c.l}%)`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default generateRandomColors
|
||||||
@@ -10,53 +10,53 @@
|
|||||||
"postinstall": "nuxt prepare"
|
"postinstall": "nuxt prepare"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nuxt/image": "1.10.0",
|
"@nuxt/image": "^2.0.0",
|
||||||
"@nuxtjs/i18n": "9.5.5",
|
"@nuxtjs/i18n": "^10.2.4",
|
||||||
"@pinia/nuxt": "^0.11.3",
|
"@pinia/nuxt": "^0.11.3",
|
||||||
"@tailwindcss/postcss": "^4.1.18",
|
"@tailwindcss/postcss": "^4.2.2",
|
||||||
"@tailwindcss/vite": "^4.1.18",
|
"@tailwindcss/vite": "^4.2.2",
|
||||||
"@tanstack/vue-query": "^5.92.9",
|
"@tanstack/vue-query": "^5.96.2",
|
||||||
"@tiptap/extension-blockquote": "^3.18.0",
|
"@tiptap/extension-blockquote": "^3.22.2",
|
||||||
"@tiptap/extension-bold": "^3.18.0",
|
"@tiptap/extension-bold": "^3.22.2",
|
||||||
"@tiptap/extension-bubble-menu": "^3.18.0",
|
"@tiptap/extension-bubble-menu": "^3.22.2",
|
||||||
"@tiptap/extension-heading": "^3.18.0",
|
"@tiptap/extension-heading": "^3.22.2",
|
||||||
"@tiptap/extension-italic": "^3.18.0",
|
"@tiptap/extension-italic": "^3.22.2",
|
||||||
"@tiptap/extension-paragraph": "^3.18.0",
|
"@tiptap/extension-paragraph": "^3.22.2",
|
||||||
"@tiptap/extension-placeholder": "^3.18.0",
|
"@tiptap/extension-placeholder": "^3.22.2",
|
||||||
"@tiptap/extension-strike": "^3.18.0",
|
"@tiptap/extension-strike": "^3.22.2",
|
||||||
"@tiptap/extension-text": "^3.18.0",
|
"@tiptap/extension-text": "^3.22.2",
|
||||||
"@tiptap/pm": "^3.18.0",
|
"@tiptap/pm": "^3.22.2",
|
||||||
"@tiptap/starter-kit": "^3.18.0",
|
"@tiptap/starter-kit": "^3.22.2",
|
||||||
"@tiptap/vue-3": "^3.18.0",
|
"@tiptap/vue-3": "^3.22.2",
|
||||||
"@unovis/ts": "^1.6.2",
|
"@unovis/ts": "^1.6.4",
|
||||||
"@unovis/vue": "^1.6.2",
|
"@unovis/vue": "^1.6.4",
|
||||||
"@vee-validate/nuxt": "^4.15.1",
|
"@vee-validate/nuxt": "^4.15.1",
|
||||||
"@vee-validate/rules": "^4.15.1",
|
"@vee-validate/rules": "^4.15.1",
|
||||||
"axios": "^1.13.4",
|
"axios": "^1.14.0",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"dayjs": "^1.11.19",
|
"dayjs": "^1.11.20",
|
||||||
"filesize": "^10.1.6",
|
"filesize": "^10.1.6",
|
||||||
"js-md5": "^0.8.3",
|
"js-md5": "^0.8.3",
|
||||||
"lodash-es": "^4.17.23",
|
"lodash-es": "^4.18.1",
|
||||||
"lucide-vue-next": "^0.542.0",
|
"lucide-vue-next": "^0.542.0",
|
||||||
"markdown-it": "^14.1.0",
|
"markdown-it": "^14.1.1",
|
||||||
"motion-v": "^1.10.2",
|
"motion-v": "^1.10.3",
|
||||||
"nanoid": "^5.1.6",
|
"nanoid": "^5.1.7",
|
||||||
"nuxt": "4.2.2",
|
"nuxt": "4.4.2",
|
||||||
"nuxt-lucide-icons": "1.0.5",
|
"nuxt-lucide-icons": "1.0.5",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"pixi.js": "^8.15.0",
|
"pixi.js": "^8.17.1",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"reka-ui": "^2.8.0",
|
"reka-ui": "^2.9.3",
|
||||||
"shadcn-nuxt": "2.0.1",
|
"shadcn-nuxt": "2.0.1",
|
||||||
"spark-md5": "^3.0.2",
|
"spark-md5": "^3.0.2",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.5.0",
|
||||||
"tailwindcss": "^4.1.18",
|
"tailwindcss": "^4.2.2",
|
||||||
"tiptap-markdown": "^0.9.0",
|
"tiptap-markdown": "^0.9.0",
|
||||||
"tw-animate-css": "^1.4.0",
|
"tw-animate-css": "^1.4.0",
|
||||||
"vaul-vue": "^0.4.1",
|
"vaul-vue": "^0.4.1",
|
||||||
"vue": "^3.5.27",
|
"vue": "^3.5.32",
|
||||||
"vue-router": "^4.6.4",
|
"vue-router": "^4.6.4",
|
||||||
"vue-sonner": "^1.3.2",
|
"vue-sonner": "^1.3.2",
|
||||||
"vue3-pixi": "1.0.0-beta.2"
|
"vue3-pixi": "1.0.0-beta.2"
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import TextShareView from '@/components/Share/TextShareView.vue'
|
|||||||
import { useQuery } from '@tanstack/vue-query'
|
import { useQuery } from '@tanstack/vue-query'
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const { t } = useI18n()
|
||||||
const id = computed(() => route.params.id)
|
const id = computed(() => route.params.id)
|
||||||
|
|
||||||
const { data, isLoading, error } = useQuery({
|
const { data, isLoading, error } = useQuery({
|
||||||
@@ -51,14 +52,14 @@ const componentMap = {
|
|||||||
<template v-else>
|
<template v-else>
|
||||||
<div v-if="isExpired || !data" class="flex flex-col gap-5 items-center">
|
<div v-if="isExpired || !data" class="flex flex-col gap-5 items-center">
|
||||||
<LucideAlertCircle :size="48" class="text-orange-500 rounded-full bg-orange-500/30 p-2" />
|
<LucideAlertCircle :size="48" class="text-orange-500 rounded-full bg-orange-500/30 p-2" />
|
||||||
<div class="text-xl">此链接已过期。</div>
|
<div class="text-xl">{{ t('page.shareView.linkExpired') }}</div>
|
||||||
<Button
|
<Button
|
||||||
@click="
|
@click="
|
||||||
() => {
|
() => {
|
||||||
router.push('/')
|
router.push('/')
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
>返回首页</Button
|
>{{ t('btn.backToHome') }}</Button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
import { defaultCache } from "@serwist/vite/worker";
|
|
||||||
import type { PrecacheEntry, SerwistGlobalConfig } from "serwist";
|
|
||||||
import { Serwist } from "serwist";
|
|
||||||
|
|
||||||
// This declares the value of `injectionPoint` to TypeScript.
|
|
||||||
// `injectionPoint` is the string that will be replaced by the
|
|
||||||
// actual precache manifest. By default, this string is set to
|
|
||||||
// `"self.__SW_MANIFEST"`.
|
|
||||||
declare global {
|
|
||||||
interface WorkerGlobalScope extends SerwistGlobalConfig {
|
|
||||||
__SW_MANIFEST: (PrecacheEntry | string)[] | undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare const self: ServiceWorkerGlobalScope;
|
|
||||||
|
|
||||||
const serwist = new Serwist({
|
|
||||||
precacheEntries: self.__SW_MANIFEST,
|
|
||||||
skipWaiting: true,
|
|
||||||
clientsClaim: true,
|
|
||||||
navigationPreload: true,
|
|
||||||
runtimeCaching: defaultCache,
|
|
||||||
});
|
|
||||||
|
|
||||||
serwist.addEventListeners();
|
|
||||||
@@ -1,12 +1,4 @@
|
|||||||
{
|
{
|
||||||
// https://nuxt.com/docs/guide/concepts/typescript
|
// https://nuxt.com/docs/guide/concepts/typescript
|
||||||
"extends": "./.nuxt/tsconfig.json",
|
"extends": "./.nuxt/tsconfig.json"
|
||||||
"compilerOptions": {
|
|
||||||
// Other options...
|
|
||||||
"lib": [
|
|
||||||
// Other libs...
|
|
||||||
// Add this! Doing so adds WebWorker and ServiceWorker types to the global.
|
|
||||||
"webworker"
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
27
go.work.sum
27
go.work.sum
@@ -1,11 +1,14 @@
|
|||||||
github.com/HdrHistogram/hdrhistogram-go v1.1.2 h1:5IcZpTvzydCQeHzK4Ef/D5rrSqwxob0t8PQPMybUNFM=
|
github.com/HdrHistogram/hdrhistogram-go v1.1.2 h1:5IcZpTvzydCQeHzK4Ef/D5rrSqwxob0t8PQPMybUNFM=
|
||||||
github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo=
|
github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo=
|
||||||
|
github.com/HdrHistogram/hdrhistogram-go v1.2.0/go.mod h1:CiIeGiHSd06zjX+FypuEJ5EQ07KKtxZ+8J6hszwVQig=
|
||||||
|
github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM=
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38=
|
github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38=
|
||||||
github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
||||||
github.com/bmatcuk/doublestar/v4 v4.9.2 h1:b0mc6WyRSYLjzofB2v/0cuDUZ+MqoGyH3r0dVij35GI=
|
github.com/bmatcuk/doublestar/v4 v4.9.2 h1:b0mc6WyRSYLjzofB2v/0cuDUZ+MqoGyH3r0dVij35GI=
|
||||||
github.com/bmatcuk/doublestar/v4 v4.9.2/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
github.com/bmatcuk/doublestar/v4 v4.9.2/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
||||||
|
github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
||||||
github.com/casbin/casbin/v2 v2.105.0 h1:dLj5P6pLApBRat9SADGiLxLZjiDPvA1bsPkyV4PGx6I=
|
github.com/casbin/casbin/v2 v2.105.0 h1:dLj5P6pLApBRat9SADGiLxLZjiDPvA1bsPkyV4PGx6I=
|
||||||
github.com/casbin/casbin/v2 v2.105.0/go.mod h1:Ee33aqGrmES+GNL17L0h9X28wXuo829wnNUnS0edAco=
|
github.com/casbin/casbin/v2 v2.105.0/go.mod h1:Ee33aqGrmES+GNL17L0h9X28wXuo829wnNUnS0edAco=
|
||||||
github.com/casbin/casbin/v2 v2.135.0 h1:6BLkMQiGotYyS5yYeWgW19vxqugUlvHFkFiLnLR/bxk=
|
github.com/casbin/casbin/v2 v2.135.0 h1:6BLkMQiGotYyS5yYeWgW19vxqugUlvHFkFiLnLR/bxk=
|
||||||
@@ -16,12 +19,14 @@ github.com/casbin/govaluate v1.10.0 h1:ffGw51/hYH3w3rZcxO/KcaUIDOLP84w7nsidMVgaD
|
|||||||
github.com/casbin/govaluate v1.10.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
|
github.com/casbin/govaluate v1.10.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A=
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
|
||||||
github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4=
|
github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4=
|
||||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM=
|
||||||
|
github.com/labstack/echo-contrib v0.50.0/go.mod h1:oftqJL4enNg9ao1VLpVZmisVE5/8uwHtIYE4zTpqyWU=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
|
github.com/onsi/gomega v1.38.3/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4=
|
||||||
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
|
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
|
||||||
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
|
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
|
||||||
github.com/openzipkin/zipkin-go v0.4.3 h1:9EGwpqkgnwdEIJ+Od7QVSEIH+ocmm5nPat0G7sjsSdg=
|
github.com/openzipkin/zipkin-go v0.4.3 h1:9EGwpqkgnwdEIJ+Od7QVSEIH+ocmm5nPat0G7sjsSdg=
|
||||||
@@ -42,6 +47,7 @@ github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzM
|
|||||||
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
|
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
|
||||||
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
|
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws=
|
||||||
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw=
|
||||||
|
github.com/redis/go-redis/v9 v9.14.1/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw=
|
||||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
|
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
|
||||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
|
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
@@ -59,27 +65,37 @@ github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVK
|
|||||||
github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
|
github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
|
||||||
github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE=
|
github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE=
|
||||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
||||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
|
||||||
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
||||||
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
||||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||||
|
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||||
|
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||||
|
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||||
|
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||||
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
|
golang.org/x/mod v0.26.0/go.mod h1:/j6NAhSk8iQ723BGAUyoAcn7SlD7s15Dp9Nd/SfeaFQ=
|
||||||
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
||||||
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
||||||
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
|
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
|
||||||
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
|
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
|
||||||
|
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
||||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||||
|
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||||
|
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
||||||
|
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
||||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
|
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
|
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/telemetry v0.0.0-20250807160809-1a19826ec488 h1:3doPGa+Gg4snce233aCWnbZVFsyFMo/dR40KK/6skyE=
|
golang.org/x/telemetry v0.0.0-20250807160809-1a19826ec488 h1:3doPGa+Gg4snce233aCWnbZVFsyFMo/dR40KK/6skyE=
|
||||||
golang.org/x/telemetry v0.0.0-20250807160809-1a19826ec488/go.mod h1:fGb/2+tgXXjhjHsTNdVEEMZNWA0quBnfrO+AfoDSAKw=
|
golang.org/x/telemetry v0.0.0-20250807160809-1a19826ec488/go.mod h1:fGb/2+tgXXjhjHsTNdVEEMZNWA0quBnfrO+AfoDSAKw=
|
||||||
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
|
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
|
||||||
@@ -87,13 +103,17 @@ golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
|
|||||||
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
|
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
|
||||||
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
|
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
|
||||||
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
|
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
|
||||||
|
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
||||||
|
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
|
||||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||||
|
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||||
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
|
golang.org/x/tools v0.35.0/go.mod h1:NKdj5HkL/73byiZSJjqJgKn3ep7KjFkBOkR/Hps3VPw=
|
||||||
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
||||||
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
||||||
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||||
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||||
|
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM=
|
google.golang.org/grpc v1.72.0 h1:S7UkcVa60b5AAQTaO6ZKamFp1zMZSU0fGDK2WZLbBnM=
|
||||||
@@ -101,4 +121,5 @@ google.golang.org/grpc v1.72.0/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3i
|
|||||||
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc=
|
||||||
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U=
|
||||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
|
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"pkg/utils"
|
"pkg/utils"
|
||||||
|
|
||||||
"dario.cat/mergo"
|
"dario.cat/mergo"
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/rueidis"
|
||||||
)
|
)
|
||||||
|
|
||||||
type FileInfo struct {
|
type FileInfo struct {
|
||||||
@@ -31,8 +31,8 @@ type RedisFileInfo struct {
|
|||||||
|
|
||||||
func GetRedisFileInfo(fileId string) (*RedisFileInfo, error) {
|
func GetRedisFileInfo(fileId string) (*RedisFileInfo, error) {
|
||||||
rdb, ctx := utils.GetRedisClient()
|
rdb, ctx := utils.GetRedisClient()
|
||||||
fileInfoUnmarshalData, err := rdb.HGet(ctx, "015:fileInfoMap", fileId).Result()
|
fileInfoUnmarshalData, err := rdb.Do(ctx, rdb.B().Hget().Key("015:fileInfoMap").Field(fileId).Build()).ToString()
|
||||||
if err == redis.Nil {
|
if rueidis.IsRedisNil(err) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -55,11 +55,10 @@ func SetRedisFileInfo(fileId string, fileInfo RedisFileInfo) error {
|
|||||||
mergo.Merge(&fileInfo, old_fileInfo)
|
mergo.Merge(&fileInfo, old_fileInfo)
|
||||||
}
|
}
|
||||||
jsonData, _ := json.Marshal(fileInfo)
|
jsonData, _ := json.Marshal(fileInfo)
|
||||||
_, err = rdb.HSet(ctx, "015:fileInfoMap", fileId, string(jsonData)).Result()
|
return rdb.Do(ctx, rdb.B().Hset().Key("015:fileInfoMap").FieldValue().FieldValue(fileId, string(jsonData)).Build()).Error()
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetRedisFileInfoAll() (map[string]string, error) {
|
func GetRedisFileInfoAll() (map[string]string, error) {
|
||||||
rdb, ctx := utils.GetRedisClient()
|
rdb, ctx := utils.GetRedisClient()
|
||||||
return rdb.HGetAll(ctx, "015:fileInfoMap").Result()
|
return rdb.Do(ctx, rdb.B().Hgetall().Key("015:fileInfoMap").Build()).AsStrMap()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"pkg/utils"
|
"pkg/utils"
|
||||||
|
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/rueidis"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetRedisFileShareRelational(fileId string) ([]string, error) {
|
func GetRedisFileShareRelational(fileId string) ([]string, error) {
|
||||||
rdb, ctx := utils.GetRedisClient()
|
rdb, ctx := utils.GetRedisClient()
|
||||||
fileShareRelationalUnmarshalData, err := rdb.HGet(ctx, "015:fileShareRelational", fileId).Result()
|
fileShareRelationalUnmarshalData, err := rdb.Do(ctx, rdb.B().Hget().Key("015:fileShareRelational").Field(fileId).Build()).ToString()
|
||||||
if err == redis.Nil {
|
if rueidis.IsRedisNil(err) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -26,6 +26,5 @@ func GetRedisFileShareRelational(fileId string) ([]string, error) {
|
|||||||
func SetRedisFileShareRelational(fileId string, shareIDs []string) error {
|
func SetRedisFileShareRelational(fileId string, shareIDs []string) error {
|
||||||
rdb, ctx := utils.GetRedisClient()
|
rdb, ctx := utils.GetRedisClient()
|
||||||
jsonData, _ := json.Marshal(shareIDs)
|
jsonData, _ := json.Marshal(shareIDs)
|
||||||
_, err := rdb.HSet(ctx, "015:fileShareRelational", fileId, string(jsonData)).Result()
|
return rdb.Do(ctx, rdb.B().Hset().Key("015:fileShareRelational").FieldValue().FieldValue(fileId, string(jsonData)).Build()).Error()
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ go 1.25.5
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
dario.cat/mergo v1.0.2
|
dario.cat/mergo v1.0.2
|
||||||
github.com/redis/go-redis/v9 v9.17.3
|
github.com/redis/rueidis v1.0.73
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
golang.org/x/net v0.52.0 // indirect
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
golang.org/x/sys v0.42.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
|
||||||
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
|
||||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
github.com/onsi/gomega v1.38.3 h1:eTX+W6dobAYfFeGC2PV6RwXRu/MyT+cQguijutvkpSM=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
github.com/redis/rueidis v1.0.73 h1:0Enrg0VuMdaYyNDDj0lLIheWY0uybCeQOh+jTp2GG3M=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/redis/rueidis v1.0.73/go.mod h1:lfdcZzJ1oKGKL37vh9fO3ymwt+0TdjkkUCJxbgpmcgQ=
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||||
github.com/redis/go-redis/v9 v9.17.3 h1:fN29NdNrE17KttK5Ndf20buqfDZwGNgoUr9qjl1DQx4=
|
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||||
github.com/redis/go-redis/v9 v9.17.3/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
|
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
|
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ import (
|
|||||||
|
|
||||||
"pkg/utils"
|
"pkg/utils"
|
||||||
|
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/rueidis"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetRedisPickupData(pickupCode string) (string, error) {
|
func GetRedisPickupData(pickupCode string) (string, error) {
|
||||||
rdb, ctx := utils.GetRedisClient()
|
rdb, ctx := utils.GetRedisClient()
|
||||||
ShareId, err := rdb.Get(ctx, fmt.Sprintf("015:pickupCode:%s", pickupCode)).Result()
|
ShareId, err := rdb.Do(ctx, rdb.B().Get().Key(fmt.Sprintf("015:pickupCode:%s", pickupCode)).Build()).ToString()
|
||||||
if err == redis.Nil {
|
if rueidis.IsRedisNil(err) {
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -23,6 +23,8 @@ func GetRedisPickupData(pickupCode string) (string, error) {
|
|||||||
|
|
||||||
func SetRedisPickupData(pickupCode string, shareId string) (bool, error) {
|
func SetRedisPickupData(pickupCode string, shareId string) (bool, error) {
|
||||||
rdb, ctx := utils.GetRedisClient()
|
rdb, ctx := utils.GetRedisClient()
|
||||||
ok, err := rdb.SetNX(ctx, fmt.Sprintf("015:pickupCode:%s", pickupCode), shareId, time.Until(time.Now().Add(24*time.Hour))).Result()
|
return rdb.Do(
|
||||||
return ok, err
|
ctx,
|
||||||
|
rdb.B().Set().Key(fmt.Sprintf("015:pickupCode:%s", pickupCode)).Value(shareId).Nx().Ex(24*time.Hour).Build(),
|
||||||
|
).AsBool()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"pkg/utils"
|
"pkg/utils"
|
||||||
|
|
||||||
"dario.cat/mergo"
|
"dario.cat/mergo"
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/rueidis"
|
||||||
)
|
)
|
||||||
|
|
||||||
type RedisShareInfo struct {
|
type RedisShareInfo struct {
|
||||||
@@ -34,21 +34,21 @@ const (
|
|||||||
|
|
||||||
func GetRedisShareInfo(shareId string) (*RedisShareInfo, error) {
|
func GetRedisShareInfo(shareId string) (*RedisShareInfo, error) {
|
||||||
rdb, ctx := utils.GetRedisClient()
|
rdb, ctx := utils.GetRedisClient()
|
||||||
shareInfo := rdb.Get(ctx, fmt.Sprintf("015:shareInfoMap:%s", shareId))
|
key := fmt.Sprintf("015:shareInfoMap:%s", shareId)
|
||||||
shareInfoUnmarshalData, err := shareInfo.Result()
|
shareInfoUnmarshalData, err := rdb.Do(ctx, rdb.B().Get().Key(key).Build()).ToString()
|
||||||
ttl, _ := rdb.TTL(ctx, fmt.Sprintf("015:shareInfoMap:%s", shareId)).Result()
|
if rueidis.IsRedisNil(err) {
|
||||||
if err == redis.Nil {
|
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
ttl, _ := rdb.Do(ctx, rdb.B().Ttl().Key(key).Build()).AsInt64()
|
||||||
var shareInfoData RedisShareInfo
|
var shareInfoData RedisShareInfo
|
||||||
|
|
||||||
if err := json.Unmarshal([]byte(shareInfoUnmarshalData), &shareInfoData); err != nil {
|
if err := json.Unmarshal([]byte(shareInfoUnmarshalData), &shareInfoData); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
shareInfoData.ExpireAt = time.Now().Add(ttl).Unix()
|
shareInfoData.ExpireAt = time.Now().Add(time.Duration(ttl) * time.Second).Unix()
|
||||||
return &shareInfoData, nil
|
return &shareInfoData, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,6 +62,12 @@ func SetRedisShareInfo(shareId string, shareInfo RedisShareInfo) error {
|
|||||||
mergo.Merge(&shareInfo, old_shareInfo)
|
mergo.Merge(&shareInfo, old_shareInfo)
|
||||||
}
|
}
|
||||||
jsonData, _ := json.Marshal(shareInfo)
|
jsonData, _ := json.Marshal(shareInfo)
|
||||||
_, err = rdb.Set(ctx, fmt.Sprintf("015:shareInfoMap:%s", shareId), string(jsonData), time.Until(time.Unix(shareInfo.ExpireAt, 0))).Result()
|
return rdb.Do(
|
||||||
return err
|
ctx,
|
||||||
|
rdb.B().Set().
|
||||||
|
Key(fmt.Sprintf("015:shareInfoMap:%s", shareId)).
|
||||||
|
Value(string(jsonData)).
|
||||||
|
Ex(time.Until(time.Unix(shareInfo.ExpireAt, 0))).
|
||||||
|
Build(),
|
||||||
|
).Error()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
package models
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|
||||||
"pkg/utils"
|
"pkg/utils"
|
||||||
|
|
||||||
"dario.cat/mergo"
|
"github.com/redis/rueidis"
|
||||||
"github.com/redis/go-redis/v9"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// 统计数据结构
|
// 统计数据结构
|
||||||
@@ -19,8 +19,8 @@ type StatData struct {
|
|||||||
|
|
||||||
func GetRedisStat(key string) (*StatData, error) {
|
func GetRedisStat(key string) (*StatData, error) {
|
||||||
rdb, ctx := utils.GetRedisClient()
|
rdb, ctx := utils.GetRedisClient()
|
||||||
statUnmarshalData, err := rdb.HGet(ctx, "015:stat", key).Result()
|
statUnmarshalData, err := rdb.Do(ctx, rdb.B().Hget().Key("015:stat").Field(key).Build()).ToString()
|
||||||
if err == redis.Nil {
|
if rueidis.IsRedisNil(err) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -33,21 +33,28 @@ func GetRedisStat(key string) (*StatData, error) {
|
|||||||
return &stat, nil
|
return &stat, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetRedisStat(key string, stat StatData) error {
|
func SetRedisStat(key string, handler func(stat *StatData) *StatData) error {
|
||||||
rdb, ctx := utils.GetRedisClient()
|
return utils.WithLocker(context.Background(), "015:stat:"+key, 0, func(ctx context.Context) error {
|
||||||
old_stat, err := GetRedisStat(key)
|
rdb, _ := utils.GetRedisClient()
|
||||||
if err != nil {
|
old_stat, err := GetRedisStat(key)
|
||||||
return err
|
if err != nil {
|
||||||
}
|
return err
|
||||||
if old_stat != nil {
|
}
|
||||||
mergo.Merge(&stat, old_stat)
|
if old_stat == nil {
|
||||||
}
|
old_stat = &StatData{
|
||||||
jsonData, _ := json.Marshal(stat)
|
FileSize: 0,
|
||||||
_, err = rdb.HSet(ctx, "015:stat", key, string(jsonData)).Result()
|
FileNum: 0,
|
||||||
return err
|
ShareNum: 0,
|
||||||
|
DownloadNum: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stat := handler(old_stat)
|
||||||
|
jsonData, _ := json.Marshal(stat)
|
||||||
|
return rdb.Do(ctx, rdb.B().Hset().Key("015:stat").FieldValue().FieldValue(key, string(jsonData)).Build()).Error()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetRedisStatAll() (map[string]string, error) {
|
func GetRedisStatAll() (map[string]string, error) {
|
||||||
rdb, ctx := utils.GetRedisClient()
|
rdb, ctx := utils.GetRedisClient()
|
||||||
return rdb.HGetAll(ctx, "015:stat").Result()
|
return rdb.Do(ctx, rdb.B().Hgetall().Key("015:stat").Build()).AsStrMap()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,14 +7,13 @@ import (
|
|||||||
|
|
||||||
"pkg/utils"
|
"pkg/utils"
|
||||||
|
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/rueidis"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetRedisTaskInfo(taskId string) (*map[string]any, error) {
|
func GetRedisTaskInfo(taskId string) (*map[string]any, error) {
|
||||||
rdb, ctx := utils.GetRedisClient()
|
rdb, ctx := utils.GetRedisClient()
|
||||||
taskInfo := rdb.Get(ctx, fmt.Sprintf("015:taskInfoMap:%s", taskId))
|
taskInfoUnmarshalData, err := rdb.Do(ctx, rdb.B().Get().Key(fmt.Sprintf("015:taskInfoMap:%s", taskId)).Build()).ToString()
|
||||||
taskInfoUnmarshalData, err := taskInfo.Result()
|
if rueidis.IsRedisNil(err) {
|
||||||
if err == redis.Nil {
|
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -31,6 +30,8 @@ func GetRedisTaskInfo(taskId string) (*map[string]any, error) {
|
|||||||
func SetRedisTaskInfo(taskId string, taskInfo map[string]any) error {
|
func SetRedisTaskInfo(taskId string, taskInfo map[string]any) error {
|
||||||
rdb, ctx := utils.GetRedisClient()
|
rdb, ctx := utils.GetRedisClient()
|
||||||
jsonData, _ := json.Marshal(taskInfo)
|
jsonData, _ := json.Marshal(taskInfo)
|
||||||
_, err := rdb.Set(ctx, fmt.Sprintf("015:taskInfoMap:%s", taskId), jsonData, time.Hour).Result()
|
return rdb.Do(
|
||||||
return err
|
ctx,
|
||||||
|
rdb.B().Set().Key(fmt.Sprintf("015:taskInfoMap:%s", taskId)).Value(string(jsonData)).Ex(time.Hour).Build(),
|
||||||
|
).Error()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,16 +2,18 @@ module pkg/services
|
|||||||
|
|
||||||
go 1.25.5
|
go 1.25.5
|
||||||
|
|
||||||
require github.com/hibiken/asynq v0.25.1
|
require github.com/hibiken/asynq v0.26.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/redis/go-redis/v9 v9.17.3 // indirect
|
github.com/redis/go-redis/v9 v9.18.0 // indirect
|
||||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||||
github.com/spf13/cast v1.10.0 // indirect
|
github.com/spf13/cast v1.10.0 // indirect
|
||||||
golang.org/x/sys v0.40.0 // indirect
|
github.com/stretchr/testify v1.11.1 // indirect
|
||||||
golang.org/x/time v0.14.0 // indirect
|
go.uber.org/atomic v1.11.0 // indirect
|
||||||
|
golang.org/x/sys v0.42.0 // indirect
|
||||||
|
golang.org/x/time v0.15.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.11 // indirect
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,18 +1,25 @@
|
|||||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/hibiken/asynq v0.25.1 h1:phj028N0nm15n8O2ims+IvJ2gz4k2auvermngh9JhTw=
|
github.com/hibiken/asynq v0.26.0 h1:1Zxr92MlDnb1Zt/QR5g2vSCqUS03i95lUfqx5X7/wrw=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/redis/go-redis/v9 v9.17.3 h1:fN29NdNrE17KttK5Ndf20buqfDZwGNgoUr9qjl1DQx4=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs=
|
||||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||||
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||||
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
|
||||||
|
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
|||||||
@@ -13,33 +13,42 @@ var (
|
|||||||
envOnce sync.Once
|
envOnce sync.Once
|
||||||
)
|
)
|
||||||
|
|
||||||
func InitEnv(props EnvOption) {
|
func createViperInstance(props EnvOption) *viper.Viper {
|
||||||
if v != nil {
|
instance := viper.New()
|
||||||
return
|
for _, viperConfigType := range props.ConfigType {
|
||||||
|
instance.SetConfigType(viperConfigType)
|
||||||
}
|
}
|
||||||
|
if props.ConfigData != nil {
|
||||||
|
instance.ReadConfig(props.ConfigData)
|
||||||
|
return instance
|
||||||
|
}
|
||||||
|
for _, name := range props.ConfigName {
|
||||||
|
instance.SetConfigName(name)
|
||||||
|
}
|
||||||
|
instance.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||||||
|
for _, path := range props.ConfigPath {
|
||||||
|
instance.AddConfigPath(path)
|
||||||
|
}
|
||||||
|
instance.AutomaticEnv()
|
||||||
|
instance.WatchConfig()
|
||||||
|
if err := instance.ReadInConfig(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return instance
|
||||||
|
}
|
||||||
|
|
||||||
|
func InitTestViper(props EnvOption) *viper.Viper {
|
||||||
|
instance := createViperInstance(props)
|
||||||
|
v = instance
|
||||||
|
envOnce.Do(func() {}) // 消费 once,防止 GetViperClient 覆盖已注入的实例
|
||||||
|
return instance
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetViperClient() *viper.Viper {
|
||||||
envOnce.Do(func() {
|
envOnce.Do(func() {
|
||||||
v = viper.New()
|
v = createViperInstance(getEnvOptions())
|
||||||
for _, viperConfigType := range props.ConfigType {
|
|
||||||
v.SetConfigType(viperConfigType)
|
|
||||||
}
|
|
||||||
if props.ConfigData != nil {
|
|
||||||
v.ReadConfig(props.ConfigData)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for _, name := range props.ConfigName {
|
|
||||||
v.SetConfigName(name)
|
|
||||||
}
|
|
||||||
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
|
||||||
for _, path := range props.ConfigPath {
|
|
||||||
v.AddConfigPath(path)
|
|
||||||
}
|
|
||||||
v.AutomaticEnv()
|
|
||||||
v.WatchConfig()
|
|
||||||
err := v.ReadInConfig()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
type Option interface {
|
type Option interface {
|
||||||
@@ -75,8 +84,7 @@ func getEnvOptions(options ...Option) EnvOption {
|
|||||||
|
|
||||||
func GetEnv(key string, options ...Option) string {
|
func GetEnv(key string, options ...Option) string {
|
||||||
props := getEnvOptions(options...)
|
props := getEnvOptions(options...)
|
||||||
InitEnv(props)
|
value := GetViperClient().GetString(key)
|
||||||
value := v.GetString(key)
|
|
||||||
|
|
||||||
if value == "" && props.DefaultValue != "" {
|
if value == "" && props.DefaultValue != "" {
|
||||||
return props.DefaultValue
|
return props.DefaultValue
|
||||||
@@ -88,12 +96,10 @@ func GetEnvWithDefault(key string, defaultValue string) string {
|
|||||||
return GetEnv(key, WithDefaultValue(defaultValue))
|
return GetEnv(key, WithDefaultValue(defaultValue))
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetEnvMapString(key string) map[string]string {
|
func GetEnvMap(key string) map[string]any {
|
||||||
props := getEnvOptions()
|
return GetViperClient().GetStringMap(key)
|
||||||
InitEnv(props)
|
|
||||||
return v.GetStringMapString(key)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetEnv(key string, value string) {
|
func SetEnv(key string, value string) {
|
||||||
v.Set(key, value)
|
GetViperClient().Set(key, value)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ go 1.25.5
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/dustin/go-humanize v1.0.1
|
github.com/dustin/go-humanize v1.0.1
|
||||||
github.com/hibiken/asynq v0.25.1
|
github.com/hibiken/asynq v0.26.0
|
||||||
github.com/redis/go-redis/v9 v9.17.3
|
github.com/redis/rueidis v1.0.73
|
||||||
github.com/spf13/viper v1.21.0
|
github.com/spf13/viper v1.21.0
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
)
|
)
|
||||||
@@ -17,18 +17,21 @@ require (
|
|||||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
github.com/pelletier/go-toml/v2 v2.3.0 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/redis/go-redis/v9 v9.18.0 // indirect
|
||||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||||
github.com/sagikazarmark/locafero v0.12.0 // indirect
|
github.com/sagikazarmark/locafero v0.12.0 // indirect
|
||||||
github.com/spf13/afero v1.15.0 // indirect
|
github.com/spf13/afero v1.15.0 // indirect
|
||||||
github.com/spf13/cast v1.10.0 // indirect
|
github.com/spf13/cast v1.10.0 // indirect
|
||||||
github.com/spf13/pflag v1.0.10 // indirect
|
github.com/spf13/pflag v1.0.10 // indirect
|
||||||
github.com/subosito/gotenv v1.6.0 // indirect
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
|
go.uber.org/atomic v1.11.0 // indirect
|
||||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
golang.org/x/sys v0.40.0 // indirect
|
golang.org/x/net v0.52.0 // indirect
|
||||||
golang.org/x/text v0.33.0 // indirect
|
golang.org/x/sys v0.42.0 // indirect
|
||||||
golang.org/x/time v0.14.0 // indirect
|
golang.org/x/text v0.35.0 // indirect
|
||||||
|
golang.org/x/time v0.15.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.11 // indirect
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
|
|||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||||
@@ -16,16 +17,20 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
|||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/hibiken/asynq v0.25.1 h1:phj028N0nm15n8O2ims+IvJ2gz4k2auvermngh9JhTw=
|
github.com/hibiken/asynq v0.26.0 h1:1Zxr92MlDnb1Zt/QR5g2vSCqUS03i95lUfqx5X7/wrw=
|
||||||
github.com/hibiken/asynq v0.25.1/go.mod h1:pazWNOLBu0FEynQRBvHA26qdIKRSmfdIfUm4HdsLmXg=
|
github.com/hibiken/asynq v0.26.0/go.mod h1:Qk4e57bTnWDoyJ67VkchuV6VzSM9IQW2nPvAGuDyw58=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
github.com/onsi/gomega v1.38.3 h1:eTX+W6dobAYfFeGC2PV6RwXRu/MyT+cQguijutvkpSM=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/redis/go-redis/v9 v9.17.3 h1:fN29NdNrE17KttK5Ndf20buqfDZwGNgoUr9qjl1DQx4=
|
github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs=
|
||||||
github.com/redis/go-redis/v9 v9.17.3/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
|
github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0=
|
||||||
|
github.com/redis/rueidis v1.0.73 h1:0Enrg0VuMdaYyNDDj0lLIheWY0uybCeQOh+jTp2GG3M=
|
||||||
|
github.com/redis/rueidis v1.0.73/go.mod h1:lfdcZzJ1oKGKL37vh9fO3ymwt+0TdjkkUCJxbgpmcgQ=
|
||||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||||
@@ -43,18 +48,23 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
|
|||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||||
|
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
|
||||||
|
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||||
|
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||||
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||||
|
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
|||||||
@@ -4,28 +4,30 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/rueidis"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
rdb *redis.Client
|
rdb rueidis.Client
|
||||||
ctx = context.Background()
|
ctx = context.Background()
|
||||||
onceRedis sync.Once
|
onceRedis sync.Once
|
||||||
)
|
)
|
||||||
|
|
||||||
func InitRedis() *redis.Client {
|
func InitRedis() rueidis.Client {
|
||||||
opt, err := redis.ParseURL(GetEnv("redis.url"))
|
opt, err := rueidis.ParseURL(GetEnv("redis.url"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
return redis.NewClient(opt)
|
client, err := rueidis.NewClient(opt)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return client
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetRedisClient() (*redis.Client, context.Context) {
|
func GetRedisClient() (rueidis.Client, context.Context) {
|
||||||
onceRedis.Do(func() {
|
onceRedis.Do(func() {
|
||||||
if rdb == nil {
|
rdb = InitRedis()
|
||||||
rdb = InitRedis()
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
return rdb, ctx
|
return rdb, ctx
|
||||||
}
|
}
|
||||||
|
|||||||
82
pkg/utils/redis_lock.go
Normal file
82
pkg/utils/redis_lock.go
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/redis/rueidis"
|
||||||
|
"github.com/redis/rueidis/rueidislock"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultRedisLockPrefix = "015:lock"
|
||||||
|
defaultRedisLockValidity = 15 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrRedisLockNotAcquired indicates that TryRedisLock did not obtain the lock.
|
||||||
|
ErrRedisLockNotAcquired = errors.New("redis lock not acquired")
|
||||||
|
|
||||||
|
onceLocker sync.Once
|
||||||
|
redisLock rueidislock.Locker
|
||||||
|
newRueidisLocker = rueidislock.NewLocker
|
||||||
|
)
|
||||||
|
|
||||||
|
// RedisLockOption defines the caller-controlled lock settings.
|
||||||
|
type RedisLockOption struct {
|
||||||
|
KeyValidity time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetRedisLocker() rueidislock.Locker {
|
||||||
|
onceLocker.Do(func() {
|
||||||
|
opt, err := rueidis.ParseURL(GetEnv("redis.url"))
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
locker, err := newRueidisLocker(rueidislock.LockerOption{
|
||||||
|
ClientOption: opt,
|
||||||
|
KeyPrefix: defaultRedisLockPrefix,
|
||||||
|
KeyValidity: defaultRedisLockValidity,
|
||||||
|
KeyMajority: 1,
|
||||||
|
NoLoopTracking: false,
|
||||||
|
FallbackSETPX: false,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
redisLock = locker
|
||||||
|
})
|
||||||
|
return redisLock
|
||||||
|
}
|
||||||
|
|
||||||
|
func baseLocker(ctx context.Context, key string, expired time.Duration) (context.Context, context.CancelFunc, error) {
|
||||||
|
if expired <= 0 {
|
||||||
|
expired = defaultRedisLockValidity
|
||||||
|
}
|
||||||
|
locker := GetRedisLocker()
|
||||||
|
timeoutCtx, timeoutCancel := context.WithTimeout(ctx, expired)
|
||||||
|
lockCtx, lockCancel, err := locker.WithContext(timeoutCtx, key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
return lockCtx, func() {
|
||||||
|
lockCancel()
|
||||||
|
timeoutCancel()
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Locker(key string, expired time.Duration) (context.CancelFunc, error) {
|
||||||
|
_, cancel, err := baseLocker(context.Background(), key, expired)
|
||||||
|
return cancel, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithLocker(ctx context.Context, key string, expired time.Duration, fn func(context.Context) error) error {
|
||||||
|
lockCtx, cancel, err := baseLocker(ctx, key, expired)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer cancel()
|
||||||
|
return fn(lockCtx)
|
||||||
|
}
|
||||||
@@ -22,7 +22,7 @@ func TestInitEnvAndGetEnv(t *testing.T) {
|
|||||||
ConfigData: bytes.NewBufferString(jsonData),
|
ConfigData: bytes.NewBufferString(jsonData),
|
||||||
ConfigType: []string{"json"},
|
ConfigType: []string{"json"},
|
||||||
}
|
}
|
||||||
utils.InitEnv(props)
|
utils.InitTestViper(props)
|
||||||
|
|
||||||
// GetEnv应能拿到值
|
// GetEnv应能拿到值
|
||||||
val := utils.GetEnv("test.value")
|
val := utils.GetEnv("test.value")
|
||||||
|
|||||||
6729
pnpm-lock.yaml
generated
6729
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
55
worker/.air.toml
Normal file
55
worker/.air.toml
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
#:schema https://json.schemastore.org/any.json
|
||||||
|
|
||||||
|
root = "."
|
||||||
|
testdata_dir = "testdata"
|
||||||
|
tmp_dir = "tmp"
|
||||||
|
|
||||||
|
[build]
|
||||||
|
args_bin = []
|
||||||
|
bin = "./tmp/main"
|
||||||
|
cmd = "go build -o ./tmp/main ."
|
||||||
|
delay = 1000
|
||||||
|
entrypoint = ["./tmp/main"]
|
||||||
|
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
|
||||||
|
exclude_file = []
|
||||||
|
exclude_regex = ["_test.go"]
|
||||||
|
exclude_unchanged = false
|
||||||
|
follow_symlink = false
|
||||||
|
full_bin = ""
|
||||||
|
include_dir = ["../pkg"]
|
||||||
|
include_ext = ["go", "tpl", "tmpl", "html"]
|
||||||
|
include_file = []
|
||||||
|
kill_delay = "0s"
|
||||||
|
log = "build-errors.log"
|
||||||
|
poll = false
|
||||||
|
poll_interval = 0
|
||||||
|
post_cmd = []
|
||||||
|
pre_cmd = []
|
||||||
|
rerun = false
|
||||||
|
rerun_delay = 500
|
||||||
|
send_interrupt = false
|
||||||
|
stop_on_error = false
|
||||||
|
|
||||||
|
[color]
|
||||||
|
app = ""
|
||||||
|
build = "yellow"
|
||||||
|
main = "magenta"
|
||||||
|
runner = "green"
|
||||||
|
watcher = "cyan"
|
||||||
|
|
||||||
|
[log]
|
||||||
|
main_only = false
|
||||||
|
silent = false
|
||||||
|
time = false
|
||||||
|
|
||||||
|
[misc]
|
||||||
|
clean_on_exit = false
|
||||||
|
|
||||||
|
[proxy]
|
||||||
|
app_port = 0
|
||||||
|
enabled = false
|
||||||
|
proxy_port = 0
|
||||||
|
|
||||||
|
[screen]
|
||||||
|
clear_on_rebuild = false
|
||||||
|
keep_scroll = true
|
||||||
@@ -4,8 +4,8 @@ go 1.25.5
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/hibiken/asynq v0.25.1
|
github.com/hibiken/asynq v0.26.0
|
||||||
github.com/samber/lo v1.52.0
|
github.com/samber/lo v1.53.0
|
||||||
github.com/spf13/cast v1.10.0
|
github.com/spf13/cast v1.10.0
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
go.uber.org/zap v1.27.1
|
go.uber.org/zap v1.27.1
|
||||||
@@ -16,12 +16,13 @@ require (
|
|||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/redis/go-redis/v9 v9.17.3 // indirect
|
github.com/redis/go-redis/v9 v9.18.0 // indirect
|
||||||
github.com/robfig/cron/v3 v3.0.1 // indirect
|
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||||
|
go.uber.org/atomic v1.11.0 // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
golang.org/x/sys v0.40.0 // indirect
|
golang.org/x/sys v0.42.0 // indirect
|
||||||
golang.org/x/text v0.33.0 // indirect
|
golang.org/x/text v0.35.0 // indirect
|
||||||
golang.org/x/time v0.14.0 // indirect
|
golang.org/x/time v0.15.0 // indirect
|
||||||
google.golang.org/protobuf v1.36.11 // indirect
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
|||||||
@@ -14,38 +14,44 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
|||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/hibiken/asynq v0.25.1 h1:phj028N0nm15n8O2ims+IvJ2gz4k2auvermngh9JhTw=
|
github.com/hibiken/asynq v0.26.0 h1:1Zxr92MlDnb1Zt/QR5g2vSCqUS03i95lUfqx5X7/wrw=
|
||||||
github.com/hibiken/asynq v0.25.1/go.mod h1:pazWNOLBu0FEynQRBvHA26qdIKRSmfdIfUm4HdsLmXg=
|
github.com/hibiken/asynq v0.26.0/go.mod h1:Qk4e57bTnWDoyJ67VkchuV6VzSM9IQW2nPvAGuDyw58=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/redis/go-redis/v9 v9.17.3 h1:fN29NdNrE17KttK5Ndf20buqfDZwGNgoUr9qjl1DQx4=
|
github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs=
|
||||||
github.com/redis/go-redis/v9 v9.17.3/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
|
github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0=
|
||||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
|
github.com/samber/lo v1.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM=
|
||||||
github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
|
github.com/samber/lo v1.53.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
|
||||||
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||||
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
|
||||||
|
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
|
||||||
|
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||||
|
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
|
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
|
||||||
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||||
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ func GenStandardFile(filePath string, mimeType string) (GenStandardFileReturn, e
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return GenStandardFileReturn{}, err
|
return GenStandardFileReturn{}, err
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close() //nolint:errcheck
|
||||||
|
|
||||||
fileInfo, err := file.Stat()
|
fileInfo, err := file.Stat()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -53,7 +53,7 @@ func GenStandardFile(filePath string, mimeType string) (GenStandardFileReturn, e
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return GenStandardFileReturn{}, err
|
return GenStandardFileReturn{}, err
|
||||||
}
|
}
|
||||||
models.SetRedisFileInfo(fileId, models.RedisFileInfo{
|
if err := models.SetRedisFileInfo(fileId, models.RedisFileInfo{
|
||||||
FileInfo: models.FileInfo{
|
FileInfo: models.FileInfo{
|
||||||
FileSize: fileSize,
|
FileSize: fileSize,
|
||||||
FileHash: fileHash,
|
FileHash: fileHash,
|
||||||
@@ -62,7 +62,9 @@ func GenStandardFile(filePath string, mimeType string) (GenStandardFileReturn, e
|
|||||||
FileType: models.FileTypeUpload,
|
FileType: models.FileTypeUpload,
|
||||||
CreatedAt: time.Now().Unix(),
|
CreatedAt: time.Now().Unix(),
|
||||||
Expire: expire,
|
Expire: expire,
|
||||||
})
|
}); err != nil {
|
||||||
|
return GenStandardFileReturn{}, err
|
||||||
|
}
|
||||||
return GenStandardFileReturn{
|
return GenStandardFileReturn{
|
||||||
FileId: fileId,
|
FileId: fileId,
|
||||||
FileInfo: models.FileInfo{
|
FileInfo: models.FileInfo{
|
||||||
|
|||||||
@@ -48,7 +48,14 @@ func ConvertImageWithMagick(filePath, mimeType, targetExt string) (string, error
|
|||||||
|
|
||||||
outputPath := filePath + "_converted." + targetExt
|
outputPath := filePath + "_converted." + targetExt
|
||||||
|
|
||||||
_, err := utils.RunCommand("magick", filePath, outputPath)
|
// JPG 不支持透明通道,透明 PNG 转 JPG 前需压平到背景色(白底),否则透明处会变黑
|
||||||
|
args := []string{filePath}
|
||||||
|
if lo.Contains([]string{"jpg", "jpeg"}, targetExt) {
|
||||||
|
args = append(args, "-background", "white", "-flatten")
|
||||||
|
}
|
||||||
|
args = append(args, outputPath)
|
||||||
|
|
||||||
|
_, err := utils.RunCommand("convert", args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,11 @@ func RemoveFile(ctx context.Context, task *asynq.Task) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
filePath := filepath.Join(uploadPath, payload.FileId)
|
filePath := filepath.Join(uploadPath, payload.FileId)
|
||||||
rdb.HDel(rctx, "015:fileInfoMap", payload.FileId)
|
if err := rdb.Do(rctx, rdb.B().Hdel().Key("015:fileInfoMap").Field(payload.FileId).Build()).Error(); err != nil {
|
||||||
os.RemoveAll(filePath)
|
return err
|
||||||
|
}
|
||||||
|
if err := os.RemoveAll(filePath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,11 +38,11 @@ func CompressImage(ctx context.Context, task *asynq.Task) error {
|
|||||||
}
|
}
|
||||||
compressedFileInfo, err := services.GenStandardFile(compressedPath, originalFileInfo.MimeType)
|
compressedFileInfo, err := services.GenStandardFile(compressedPath, originalFileInfo.MimeType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
defer os.Remove(compressedPath)
|
defer os.Remove(compressedPath) //nolint:errcheck
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
models.SetRedisTaskInfo(task.ResultWriter().TaskID(), map[string]any{
|
if err := models.SetRedisTaskInfo(task.ResultWriter().TaskID(), map[string]any{
|
||||||
"status": "success",
|
"status": "success",
|
||||||
"result": []any{
|
"result": []any{
|
||||||
map[string]any{
|
map[string]any{
|
||||||
@@ -56,7 +56,9 @@ func CompressImage(ctx context.Context, task *asynq.Task) error {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -84,16 +86,16 @@ func ConvertImage(ctx context.Context, task *asynq.Task) error {
|
|||||||
}
|
}
|
||||||
mimeType := mime.TypeByExtension(fmt.Sprintf(".%s", payload.TargetExt))
|
mimeType := mime.TypeByExtension(fmt.Sprintf(".%s", payload.TargetExt))
|
||||||
if mimeType == "" {
|
if mimeType == "" {
|
||||||
defer os.Remove(convertedPath)
|
defer os.Remove(convertedPath) //nolint:errcheck
|
||||||
return ErrUnknown
|
return ErrUnknown
|
||||||
}
|
}
|
||||||
convertedFileInfo, err := services.GenStandardFile(convertedPath, mimeType)
|
convertedFileInfo, err := services.GenStandardFile(convertedPath, mimeType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
defer os.Remove(convertedPath)
|
defer os.Remove(convertedPath) //nolint:errcheck
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
models.SetRedisTaskInfo(task.ResultWriter().TaskID(), map[string]any{
|
if err := models.SetRedisTaskInfo(task.ResultWriter().TaskID(), map[string]any{
|
||||||
"status": "success",
|
"status": "success",
|
||||||
"result": []any{
|
"result": []any{
|
||||||
map[string]any{
|
map[string]any{
|
||||||
@@ -107,7 +109,9 @@ func ConvertImage(ctx context.Context, task *asynq.Task) error {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,12 +36,20 @@ func RemoveShare(ctx context.Context, task *asynq.Task) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
filePath := filepath.Join(uploadPath, payload.FileId)
|
filePath := filepath.Join(uploadPath, payload.FileId)
|
||||||
rdb.HDel(ctx, "015:fileShareRelational", payload.FileId)
|
if err := rdb.Do(ctx, rdb.B().Hdel().Key("015:fileShareRelational").Field(payload.FileId).Build()).Error(); err != nil {
|
||||||
rdb.HDel(ctx, "015:fileInfoMap", payload.FileId)
|
return err
|
||||||
os.RemoveAll(filePath)
|
}
|
||||||
|
if err := rdb.Do(ctx, rdb.B().Hdel().Key("015:fileInfoMap").Field(payload.FileId).Build()).Error(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := os.RemoveAll(filePath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
models.SetRedisFileShareRelational(payload.FileId, shareIDs)
|
if err := models.SetRedisFileShareRelational(payload.FileId, shareIDs); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ func main() {
|
|||||||
} else {
|
} else {
|
||||||
logger, _ = zap.NewDevelopment()
|
logger, _ = zap.NewDevelopment()
|
||||||
}
|
}
|
||||||
defer logger.Sync()
|
defer logger.Sync() //nolint:errcheck
|
||||||
zap.ReplaceGlobals(logger)
|
zap.ReplaceGlobals(logger)
|
||||||
|
|
||||||
srv := asynq.NewServer(
|
srv := asynq.NewServer(
|
||||||
|
|||||||
BIN
worker/test/resource/test.webp
Normal file
BIN
worker/test/resource/test.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 55 KiB |
@@ -73,6 +73,8 @@ func TestConvertImageWithMagickA2B(t *testing.T) {
|
|||||||
{"png", "jpg"},
|
{"png", "jpg"},
|
||||||
{"jpg", "webp"},
|
{"jpg", "webp"},
|
||||||
{"png", "webp"},
|
{"png", "webp"},
|
||||||
|
{"webp", "jpg"},
|
||||||
|
{"webp", "png"},
|
||||||
}
|
}
|
||||||
_, self, _, _ := runtime.Caller(0)
|
_, self, _, _ := runtime.Caller(0)
|
||||||
for _, test := range testList {
|
for _, test := range testList {
|
||||||
|
|||||||
Reference in New Issue
Block a user