This commit is contained in:
oneao committed 2026-04-07 17:19:52 +08:00
1 parent c6671c8a8c
commit f0cb963d23
76 files changed
+6467 -498

No files matched your search

+11 -3
View File
@@ -4144,48 +4144,56 @@ packages:
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
lightningcss-linux-arm64-gnu@1.30.2:
resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [glibc]
lightningcss-linux-arm64-musl@1.27.0:
resolution: {integrity: sha512-rCGBm2ax7kQ9pBSeITfCW9XSVF69VX+fm5DIpvDZQl4NnQoMQyRwhZQm9pd59m8leZ1IesRqWk2v/DntMo26lg==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
lightningcss-linux-arm64-musl@1.30.2:
resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
libc: [musl]
lightningcss-linux-x64-gnu@1.27.0:
resolution: {integrity: sha512-Dk/jovSI7qqhJDiUibvaikNKI2x6kWPN79AQiD/E/KeQWMjdGe9kw51RAgoWFDi0coP4jinaH14Nrt/J8z3U4A==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
lightningcss-linux-x64-gnu@1.30.2:
resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [glibc]
lightningcss-linux-x64-musl@1.27.0:
resolution: {integrity: sha512-QKjTxXm8A9s6v9Tg3Fk0gscCQA1t/HMoF7Woy1u68wCk5kS4fR+q3vXa1p3++REW784cRAtkYKrPy6JKibrEZA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
lightningcss-linux-x64-musl@1.30.2:
resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
libc: [musl]
lightningcss-win32-arm64-msvc@1.27.0:
resolution: {integrity: sha512-/wXegPS1hnhkeG4OXQKEMQeJd48RDC3qdh+OA8pCuOPCyvnm/yEayrJdJVqzBsqpy1aJklRCVxscpFur80o6iQ==}
@@ -6279,7 +6287,7 @@ snapshots:
'@babel/generator@7.28.5':
dependencies:
'@babel/parser': 7.28.5
'@babel/parser': 7.28.6
'@babel/types': 7.28.6
'@jridgewell/gen-mapping': 0.3.13
'@jridgewell/trace-mapping': 0.3.31
@@ -6860,8 +6868,8 @@ snapshots:
'@babel/template@7.27.2':
dependencies:
'@babel/code-frame': 7.27.1
'@babel/parser': 7.28.5
'@babel/code-frame': 7.28.6
'@babel/parser': 7.28.6
'@babel/types': 7.28.6
'@babel/template@7.28.6':
+37
View File
@@ -0,0 +1,37 @@
*.o
*.a
*.so
_obj
_test
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.exe~
*.test
*.prof
*.rar
*.zip
*.gz
*.psd
*.bmd
*.cfg
*.pptx
*.log
*nohup.out
*settings.pyc
*.sublime-project
*.sublime-workspace
!.gitkeep
.DS_Store
/.idea
/.vscode
/output
*.local.yml
dumped_hertz_remote_config.json
+18
View File
@@ -0,0 +1,18 @@
package main
import (
"allapp-go/internal/bootstrap"
)
func main() {
app, err := bootstrap.NewApp()
if err != nil {
panic(err)
}
defer app.Close()
if err := bootstrap.RunServer(app.Config); err != nil {
panic(err)
}
}
+13
View File
@@ -0,0 +1,13 @@
app:
port: 8087
postgres:
host: 117.72.182.135
port: 5432
user: postgres
password: zhang520..
dbname: app
timezone: Asia/Shanghai
max_open_conns: 10
max_idle_conns: 5
conn_max_lifetime: 30m
sll_mode: false
Whitespace-only changes.
+3
View File
@@ -0,0 +1,3 @@
app:
name: allapp
port: 8080
+51
View File
@@ -0,0 +1,51 @@
module allapp-go
go 1.25.0
require (
github.com/go-playground/validator/v10 v10.30.2
github.com/gofiber/fiber/v3 v3.1.0
github.com/golang-jwt/jwt/v5 v5.3.1
github.com/jackc/pgx/v5 v5.9.1
github.com/spf13/viper v1.21.0
go.uber.org/zap v1.27.1
gopkg.in/natefinch/lumberjack.v2 v2.2.1
)
require (
github.com/andybalholm/brotli v1.2.0 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/gofiber/schema v1.7.0 // indirect
github.com/gofiber/utils/v2 v2.0.2 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/klauspost/compress v1.18.4 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/philhofer/fwd v1.2.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tinylib/msgp v1.6.3 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.69.0 // indirect
go.uber.org/multierr v1.10.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.49.0 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.35.0 // indirect
)
+123
View File
@@ -0,0 +1,123 @@
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
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/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.30.2 h1:JiFIMtSSHb2/XBUbWM4i/MpeQm9ZK2xqPNk8vgvu5JQ=
github.com/go-playground/validator/v10 v10.30.2/go.mod h1:mAf2pIOVXjTEBrwUMGKkCWKKPs9NheYGabeB04txQSc=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/gofiber/fiber/v3 v3.1.0 h1:1p4I820pIa+FGxfwWuQZ5rAyX0WlGZbGT6Hnuxt6hKY=
github.com/gofiber/fiber/v3 v3.1.0/go.mod h1:n2nYQovvL9z3Too/FGOfgtERjW3GQcAUqgfoezGBZdU=
github.com/gofiber/schema v1.7.0 h1:yNM+FNRZjyYEli9Ey0AXRBrAY9jTnb+kmGs3lJGPvKg=
github.com/gofiber/schema v1.7.0/go.mod h1:A/X5Ffyru4p9eBdp99qu+nzviHzQiZ7odLT+TwxWhbk=
github.com/gofiber/utils/v2 v2.0.2 h1:ShRRssz0F3AhTlAQcuEj54OEDtWF7+HJDwEi/aa6QLI=
github.com/gofiber/utils/v2 v2.0.2/go.mod h1:+9Ub4NqQ+IaJoTliq5LfdmOJAA/Hzwf4pXOxOa3RrJ0=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
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/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.9.1 h1:uwrxJXBnx76nyISkhr33kQLlUqjv7et7b9FjCen/tdc=
github.com/jackc/pgx/v5 v5.9.1/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
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/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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/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/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
github.com/shamaton/msgpack/v3 v3.1.0 h1:jsk0vEAqVvvS9+fTZ5/EcQ9tz860c9pWxJ4Iwecz8gU=
github.com/shamaton/msgpack/v3 v3.1.0/go.mod h1:DcQG8jrdrQCIxr3HlMYkiXdMhK+KfN2CitkyzsQV4uc=
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/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
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/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
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/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/tinylib/msgp v1.6.3 h1:bCSxiTz386UTgyT1i0MSCvdbWjVW+8sG3PjkGsZQt4s=
github.com/tinylib/msgp v1.6.3/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI=
github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
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/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
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/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+58
View File
@@ -0,0 +1,58 @@
package bootstrap
import (
"allapp-go/internal/bootstrap/boots"
"allapp-go/internal/config"
"allapp-go/pkg/db"
"allapp-go/pkg/logger"
"github.com/jackc/pgx/v5/pgxpool"
)
type App struct {
Config *config.Config
PgSql *pgxpool.Pool
}
// ======================
// 初始化
// ======================
func NewApp() (*App, error) {
cfg, err := config.Load()
if err != nil {
return nil, err
}
// 日志
boots.InitLogger(cfg)
// 数据库
pgsql, err := boots.InitPgsql(cfg)
if err != nil {
return nil, err
}
db.Init(pgsql)
// 其他组件
boots.InitUniqueId(cfg)
boots.InitJwt(cfg)
return &App{
Config: cfg,
PgSql: pgsql,
}, nil
}
// ======================
// 释放资源
// ======================
func (a *App) Close() {
if a.PgSql != nil {
a.PgSql.Close()
}
logger.Sync()
}
@@ -0,0 +1,18 @@
package boots
import (
"allapp-go/internal/config"
"allapp-go/pkg/logger"
)
func InitLogger(cfg *config.Config) {
logger.Init(logger.Config{
Dir: "logs",
Level: "info",
MaxSize: 100, // MB
MaxBackups: 10,
MaxAge: 7, // 天
Compress: true,
})
defer logger.Sync()
}
@@ -0,0 +1,11 @@
package boots
import (
"allapp-go/internal/config"
"allapp-go/pkg/jwtx"
)
// InitJwt 初始化Jwt
func InitJwt(cfg *config.Config) {
jwtx.Init(cfg.JWT.Secret, cfg.JWT.AccessExpiry)
}
@@ -0,0 +1,84 @@
package boots
import (
"allapp-go/internal/config"
"context"
"fmt"
"net/url"
"time"
"github.com/jackc/pgx/v5/pgxpool"
)
func InitPgsql(cfg *config.Config) (*pgxpool.Pool, error) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
pg := cfg.Postgres
// ssl
sslMode := "disable"
if pg.SslMode {
sslMode = "require"
}
// 密码转义
password := url.QueryEscape(pg.Password)
// DSN
dsn := fmt.Sprintf(
"postgres://%s:%s@%s:%d/%s?sslmode=%s",
pg.User,
password,
pg.Host,
pg.Port,
pg.Dbname,
sslMode,
)
// 解析配置
conf, err := pgxpool.ParseConfig(dsn)
if err != nil {
return nil, err
}
// ======================
// 连接池配置
// ======================
// 最大连接数
if pg.MaxOpenConns > 0 {
conf.MaxConns = int32(pg.MaxOpenConns)
}
// 最小连接数(近似 idle)
if pg.MaxIdleConns > 0 {
conf.MinConns = int32(pg.MaxIdleConns)
}
// 连接最大生命周期
if pg.ConnMaxLifetime > 0 {
conf.MaxConnLifetime = pg.ConnMaxLifetime
}
// ======================
// 时区设置(正确方式 ⭐)
// ======================
if pg.TimeZone != "" {
conf.ConnConfig.RuntimeParams["TimeZone"] = pg.TimeZone
}
// 创建连接池
db, err := pgxpool.NewWithConfig(ctx, conf)
if err != nil {
return nil, err
}
// 测试连接
if err := db.Ping(ctx); err != nil {
db.Close()
return nil, err
}
return db, nil
}
@@ -0,0 +1,12 @@
package boots
import (
"allapp-go/internal/config"
"allapp-go/pkg/uniqueid"
)
// InitUniqueId 初始化雪花id
func InitUniqueId(cfg *config.Config) {
options := uniqueid.NewIdGeneratorOptions(cfg.UniqueID.WorkerID)
uniqueid.SetIdGenerator(options)
}
@@ -0,0 +1,34 @@
package bootstrap
import (
"allapp-go/internal/config"
"allapp-go/internal/errors"
"allapp-go/internal/middleware"
"allapp-go/internal/router"
"allapp-go/pkg/logger"
"fmt"
"github.com/gofiber/fiber/v3"
"github.com/gofiber/fiber/v3/middleware/recover"
"github.com/gofiber/fiber/v3/middleware/requestid"
)
func RunServer(cfg *config.Config) error {
app := fiber.New(fiber.Config{
ErrorHandler: errors.ErrorHandler(),
})
// ===== middleware =====
app.Use(recover.New())
app.Use(requestid.New())
app.Use(middleware.Logger())
// ===== router =====
router.SetupRouter(app)
addr := fmt.Sprintf(":%d", cfg.App.Port)
logger.Log.Info("服务启动")
return app.Listen(addr)
}
+38
View File
@@ -0,0 +1,38 @@
package config
import "time"
type Config struct {
App AppConfig `mapstructure:"app"`
JWT JwtConfig `mapstructure:"jwt"`
UniqueID UniqueIDConfig `mapstructure:"unique_id"`
Postgres PostgresConfig `mapstructure:"postgres"`
}
type AppConfig struct {
Name string `mapstructure:"name"`
Port int `mapstructure:"port"`
}
type PostgresConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
User string `mapstructure:"user"`
Password string `mapstructure:"password"`
Dbname string `mapstructure:"dbname"`
SslMode bool `mapstructure:"ssl_mode"`
TimeZone string `mapstructure:"timezone"`
MaxOpenConns int32 `mapstructure:"max_open_conns"`
MaxIdleConns int32 `mapstructure:"max_idle_conns"`
ConnMaxLifetime time.Duration `mapstructure:"conn_max_lifetime"`
}
type JwtConfig struct {
Secret string `mapstructure:"secret"`
AccessExpiry time.Duration `mapstructure:"access_expiry"`
}
type UniqueIDConfig struct {
DataCenterID uint16 `mapstructure:"datacenter_id"`
WorkerID uint16 `mapstructure:"worker_id"`
}
+67
View File
@@ -0,0 +1,67 @@
package config
import (
"fmt"
"os"
"strings"
"github.com/spf13/viper"
)
func Load() (*Config, error) {
env := getEnv("APP_ENV", "dev")
v := viper.New()
v.SetConfigType("yaml")
// ✅ 默认值
setDefaults(v)
// ✅ 支持环境变量(如 APP_PORT → app.port)
v.AutomaticEnv()
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
// 基础配置
v.AddConfigPath("configs")
v.SetConfigName("config")
if err := v.ReadInConfig(); err != nil {
return nil, fmt.Errorf("读取基础配置失败: %w", err)
}
// 环境配置(覆盖)
envConfigName := fmt.Sprintf("config.%s", env)
v.SetConfigName(envConfigName)
if err := v.MergeInConfig(); err != nil {
// dev 可以没有,prod 必须有
if env == "prod" {
return nil, fmt.Errorf("读取环境配置失败(%s): %w", env, err)
}
}
var cfg Config
if err := v.Unmarshal(&cfg); err != nil {
return nil, fmt.Errorf("解析配置失败: %w", err)
}
return &cfg, nil
}
// ✅ 默认值统一在这里管理
func setDefaults(v *viper.Viper) {
v.SetDefault("app.name", "allapp-go")
v.SetDefault("app.port", 8080)
// 可以继续扩展
v.SetDefault("unique_id.datacenter_id", 1)
v.SetDefault("unique_id.worker_id", 1)
}
func getEnv(key, defaultVal string) string {
val := os.Getenv(key)
if val == "" {
return defaultVal
}
return strings.ToLower(val)
}
+10
View File
@@ -0,0 +1,10 @@
package errors
import "github.com/pkg/errors"
func WithStack(err error) error {
if err == nil {
return nil
}
return errors.WithStack(err)
}
+31
View File
@@ -0,0 +1,31 @@
package errors
import (
"allapp-go/internal/httpx"
"allapp-go/pkg/logger"
"errors"
"fmt"
"github.com/gofiber/fiber/v3"
"go.uber.org/zap"
)
func ErrorHandler() fiber.ErrorHandler {
return func(c fiber.Ctx, err error) error {
code := fiber.StatusInternalServerError
msg := "系统异常,请稍后再试!"
var e *fiber.Error
if errors.As(err, &e) {
code = e.Code
msg = e.Message
}
logger.FromCtx(c.Context()).Error("HTTP 请求异常",
zap.Int("status", code),
zap.String("error", fmt.Sprintf("%+v", err)),
)
return httpx.SystemError(c, msg)
}
}
@@ -0,0 +1,50 @@
package handle
import (
"allapp-go/internal/errors"
"allapp-go/internal/httpx"
"allapp-go/internal/types"
"allapp-go/pkg/db"
"github.com/gofiber/fiber/v3"
)
func LoadData(c fiber.Ctx) error {
var req types.LoadDataReq
err := httpx.BindAndValidate(c, &req)
if err != nil {
return errors.WithStack(err)
}
dbClient := db.New()
// 查询
data, err := dbClient.LoadData(c.Context(), req.ViewName, req.SearchCondition, req.OrderBy, req.SearchColumns, req.Args)
if err != nil {
return errors.WithStack(err)
}
return httpx.OK(c, data)
}
func LoadDataBySql(c fiber.Ctx) error {
var req types.LoadDataBySqlReq
err := httpx.BindAndValidate(c, &req)
if err != nil {
return errors.WithStack(err)
}
dbClient := db.New()
data, err := dbClient.LoadDataBySQL(c.Context(), req.Sql, req.Args)
if err != nil {
return errors.WithStack(err)
}
return httpx.OK(c, data)
}
@@ -0,0 +1,56 @@
package handle
import (
"allapp-go/internal/httpx"
"allapp-go/internal/types"
"allapp-go/pkg/db"
"github.com/gofiber/fiber/v3"
)
func SaveData(c fiber.Ctx) error {
var req []types.SaveDataReq
if err := httpx.BindAndValidate(c, &req); err != nil {
return err
}
if len(req) == 0 {
return httpx.OK(c, nil)
}
ctx := c.Context()
dbClient := db.New()
err := dbClient.WithTx(ctx, func(tx *db.Client) error {
for _, dataReq := range req {
tableName := dataReq.TableName
keyField := dataReq.KeyField
for _, insert := range dataReq.Inserts {
if err := tx.Insert(ctx, tableName, keyField, insert); err != nil {
return err
}
}
for _, update := range dataReq.Updates {
if err := tx.Update(ctx, tableName, keyField, update); err != nil {
return err
}
}
for _, del := range dataReq.Deletes {
if err := tx.Delete(ctx, tableName, keyField, del); err != nil {
return err
}
}
}
return nil
})
if err != nil {
return err
}
return httpx.OK(c, nil)
}
+20
View File
@@ -0,0 +1,20 @@
package httpx
import (
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v3"
)
var validate = validator.New()
func BindAndValidate(c fiber.Ctx, v any) error {
if err := c.Bind().Body(v); err != nil {
return err
}
if err := validate.Struct(v); err != nil {
return err
}
return nil
}
@@ -0,0 +1,63 @@
package httpx
import "github.com/gofiber/fiber/v3"
type Result struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data"`
TrackId string `json:"track_id,omitempty"`
}
var BizCode = struct {
Success int // 成功
Unauthorized int // 未授权/Token失效
BusinessError int // 业务异常,例如参数错误、操作不允许
SystemError int // 系统异常,例如数据库崩溃、未知错误
}{
Success: 1000,
Unauthorized: 1001,
BusinessError: 2000,
SystemError: 3000,
}
// ------------------- 统一响应方法 -------------------
// Respond 返回通用响应,code和msg必填,data可选
func Respond(c fiber.Ctx, code int, msg string, data interface{}) error {
trackId := ""
if v := c.Context().Value("request_id"); v != nil {
if s, ok := v.(string); ok {
trackId = s
}
}
return c.JSON(Result{
Code: code,
Message: msg,
Data: data,
TrackId: trackId,
})
}
// ------------------- 便捷封装 -------------------
// OK 成功响应
func OK(c fiber.Ctx, data interface{}) error {
return Respond(c, BizCode.Success, "success", data)
}
// Fail 业务异常
func Fail(c fiber.Ctx, msg string) error {
return Respond(c, BizCode.BusinessError, msg, nil)
}
// SystemError 系统异常
func SystemError(c fiber.Ctx, msg string) error {
return Respond(c, BizCode.SystemError, msg, nil)
}
// Unauthorized 授权失败
func Unauthorized(c fiber.Ctx, msg string) error {
return Respond(c, BizCode.Unauthorized, msg, nil)
}
@@ -0,0 +1,47 @@
package middleware
import (
"allapp-go/internal/httpx"
"allapp-go/pkg/jwtx"
"strings"
"github.com/gofiber/fiber/v3"
)
const CtxUserIDKey = "user_id"
// Auth 登录校验中间件
func Auth() fiber.Handler {
return func(c fiber.Ctx) error {
token := strings.TrimSpace(c.Get("Token"))
if token == "" {
return httpx.Unauthorized(c, "未登录")
}
verifyToken, ok := jwtx.VerifyToken(c.Context(), token)
if !ok {
return httpx.Unauthorized(c, "登录已过期")
}
c.Locals(CtxUserIDKey, verifyToken.Data["user_id"])
return c.Next()
}
}
func GetUserID(c fiber.Ctx) (int64, error) {
v := c.Locals(CtxUserIDKey)
if v == nil {
return 0, httpx.Unauthorized(c, "登录已过期")
}
userID, ok := v.(int64)
if !ok {
return 0, httpx.Unauthorized(c, "登录已过期")
}
return userID, nil
}
@@ -0,0 +1,35 @@
package middleware
import (
"allapp-go/pkg/logger"
"time"
"github.com/gofiber/fiber/v3"
"go.uber.org/zap"
)
func Logger() fiber.Handler {
return func(c fiber.Ctx) error {
traceID := c.RequestID()
logger.SetTraceID(c, traceID)
start := time.Now()
err := c.Next()
// ✅ 计算耗时
cost := time.Since(start)
log := logger.FromCtx(c.Context())
log.Info("HTTP 请求",
zap.String("method", c.Method()),
zap.String("path", c.Path()),
zap.Int("status", c.Response().StatusCode()),
zap.String("ip", c.IP()),
zap.String("duration", cost.String()),
)
return err
}
}
+14
View File
@@ -0,0 +1,14 @@
package router
import (
"allapp-go/internal/handle"
"github.com/gofiber/fiber/v3"
)
func SetupRouter(app *fiber.App) {
// ==================== data ====================
app.Post("/data/loadData", handle.LoadData)
app.Post("/data/loadDataBySql", handle.LoadDataBySql)
app.Post("/data/saveData", handle.SaveData)
}
@@ -0,0 +1,14 @@
package types
type LoadDataReq struct {
ViewName string `json:"view_name" validate:"required"`
SearchCondition string `json:"search_condition"`
OrderBy string `json:"order_by"`
SearchColumns []string `json:"search_columns"`
Args []any `json:"args"`
}
type LoadDataBySqlReq struct {
Sql string `json:"sql" validate:"required"`
Args []any `json:"args"`
}
@@ -0,0 +1,9 @@
package types
type SaveDataReq struct {
TableName string `json:"table_name"`
KeyField string `json:"key_field"`
Inserts []map[string]any `json:"inserts"`
Updates []map[string]any `json:"updates"`
Deletes []map[string]any `json:"deletes"`
}
+52
View File
@@ -0,0 +1,52 @@
package db
import (
"fmt"
"strconv"
"strings"
)
func validateData(table string, keyField string, data map[string]any) (any, error) {
if table == "" {
return nil, fmt.Errorf("表名不能为空")
}
if len(data) == 0 {
return nil, fmt.Errorf("数据不能为空")
}
if keyField == "" {
return nil, fmt.Errorf("主键字段不能为空")
}
val, ok := data[keyField]
if !ok {
return nil, fmt.Errorf("缺少主键字段: %s", keyField)
}
if val == nil {
return nil, fmt.Errorf("主键字段值不能为空")
}
return val, nil
}
// 高效 placeholder 转换
func convertPlaceholder(sql string) string {
var sb strings.Builder
sb.Grow(len(sql))
argIndex := 1
for i := 0; i < len(sql); i++ {
if sql[i] == '?' {
sb.WriteByte('$')
sb.WriteString(strconv.Itoa(argIndex))
argIndex++
} else {
sb.WriteByte(sql[i])
}
}
return sb.String()
}
+42
View File
@@ -0,0 +1,42 @@
package db
import (
"context"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgconn"
"github.com/jackc/pgx/v5/pgxpool"
)
var defaultClient *Client
type Executor interface {
Exec(ctx context.Context, sql string, args ...any) (pgconn.CommandTag, error)
Query(ctx context.Context, sql string, args ...any) (pgx.Rows, error)
QueryRow(ctx context.Context, sql string, args ...any) pgx.Row
}
type Client struct {
pool *pgxpool.Pool
tx pgx.Tx
}
// Init 初始化
func Init(pool *pgxpool.Pool) {
defaultClient = &Client{pool: pool}
}
// New 创建会话
func New() *Client {
return &Client{
pool: defaultClient.pool,
}
}
// 内部获取执行器(关键)
func (c *Client) exec() Executor {
if c.tx != nil {
return c.tx
}
return c.pool
}
+83
View File
@@ -0,0 +1,83 @@
package db
import (
"context"
"fmt"
"strings"
)
func (c *Client) Delete(
ctx context.Context,
table string,
keyField string,
data map[string]any,
) error {
if keyField == "" {
return fmt.Errorf("keyField 不能为空")
}
keyVal, err := validateData(table, keyField, data)
if err != nil {
return err
}
sql := fmt.Sprintf(
"DELETE FROM %s WHERE %s = $1",
table,
keyField,
)
_, err = c.exec().Exec(ctx, sql, keyVal)
return err
}
func (c *Client) BatchDelete(
ctx context.Context,
table string,
keyField string,
dataList []map[string]any,
) error {
if table == "" {
return fmt.Errorf("表名不能为空")
}
if keyField == "" {
return fmt.Errorf("主键字段不能为空")
}
if len(dataList) == 0 {
return fmt.Errorf("数据不能为空")
}
var (
placeholders []string
args []any
argIndex = 1
)
for _, data := range dataList {
keyVal, err := validateData(table, keyField, data)
if err != nil {
return err
}
if keyVal == nil {
return fmt.Errorf("主键字段值不能为空")
}
placeholders = append(placeholders, fmt.Sprintf("$%d", argIndex))
args = append(args, keyVal)
argIndex++
}
sql := fmt.Sprintf(
"DELETE FROM %s WHERE %s IN (%s)",
table,
keyField,
strings.Join(placeholders, ", "),
)
_, err := c.exec().Exec(ctx, sql, args...)
return err
}
+120
View File
@@ -0,0 +1,120 @@
package db
import (
"context"
"fmt"
"sort"
"strings"
)
// 构建 INSERT SQL(单条)
func buildInsertSQL(table string, data map[string]any) (string, []any) {
var (
columns []string
placeholders []string
args []any
)
i := 1
for col, val := range data {
columns = append(columns, col)
placeholders = append(placeholders, fmt.Sprintf("$%d", i))
args = append(args, val)
i++
}
sql := fmt.Sprintf(
"INSERT INTO %s (%s) VALUES (%s)",
table,
strings.Join(columns, ", "),
strings.Join(placeholders, ", "),
)
return sql, args
}
func (c *Client) Insert(
ctx context.Context,
table string,
keyField string,
data map[string]any,
) error {
if _, err := validateData(table, keyField, data); err != nil {
return err
}
sql, args := buildInsertSQL(table, data)
_, err := c.exec().Exec(ctx, sql, args...)
return err
}
func (c *Client) BatchInsert(
ctx context.Context,
table string,
keyField string,
dataList []map[string]any,
) error {
if len(dataList) == 0 {
return fmt.Errorf("数据不能为空")
}
// 用第一条确定列顺序
first := dataList[0]
if _, err := validateData(table, keyField, first); err != nil {
return err
}
var columns []string
for col := range first {
columns = append(columns, col)
}
sort.Strings(columns)
var (
valueStrings []string
args []any
argIndex = 1
)
for _, data := range dataList {
// 统一校验
if _, err := validateData(table, keyField, data); err != nil {
return err
}
// 字段数量检查
if len(data) != len(columns) {
return fmt.Errorf("批量插入失败:数据字段不一致")
}
var placeholders []string
for _, col := range columns {
val, ok := data[col]
if !ok {
return fmt.Errorf("缺少字段: %s", col)
}
placeholders = append(placeholders, fmt.Sprintf("$%d", argIndex))
args = append(args, val)
argIndex++
}
valueStrings = append(
valueStrings,
fmt.Sprintf("(%s)", strings.Join(placeholders, ", ")),
)
}
sql := fmt.Sprintf(
"INSERT INTO %s (%s) VALUES %s",
table,
strings.Join(columns, ", "),
strings.Join(valueStrings, ", "),
)
_, err := c.exec().Exec(ctx, sql, args...)
return err
}
+82
View File
@@ -0,0 +1,82 @@
package db
import (
"context"
"fmt"
"strings"
)
func (c *Client) LoadData(
ctx context.Context,
viewName string,
searchCondition string,
orderBy string,
searchColumns []string,
args []any,
) ([][]any, error) {
var where string
// WHERE 构造
if searchCondition != "" && len(searchColumns) > 0 {
var conditions []string
for _, col := range searchColumns {
conditions = append(conditions, fmt.Sprintf("%s LIKE ?", col))
}
where = " WHERE (" + strings.Join(conditions, " OR ") + ")"
// 自动加 %
for i := range args {
if s, ok := args[i].(string); ok {
args[i] = "%" + s + "%"
}
}
}
// ORDER BY
var order string
if orderBy != "" {
order = " ORDER BY " + orderBy
}
sql := fmt.Sprintf("SELECT * FROM %s%s%s", viewName, where, order)
return c.query(ctx, sql, args...)
}
func (c *Client) LoadDataBySQL(
ctx context.Context,
sql string,
args []any,
) ([][]any, error) {
return c.query(ctx, sql, args...)
}
// 统一查询方法(核心优化)
func (c *Client) query(ctx context.Context, sql string, args ...any) ([][]any, error) {
sql = convertPlaceholder(sql)
rows, err := c.exec().Query(ctx, sql, args...)
if err != nil {
return nil, err
}
defer rows.Close()
result := make([][]any, 0, 16)
for rows.Next() {
values, err := rows.Values()
if err != nil {
return nil, err
}
row := make([]any, len(values))
copy(row, values)
result = append(result, row)
}
return result, rows.Err()
}
+34
View File
@@ -0,0 +1,34 @@
package db
import (
"context"
"github.com/jackc/pgx/v5"
)
// WithTx 开启事务(在当前 client 上)
func (c *Client) WithTx(ctx context.Context, fn func(tx *Client) error) error {
tx, err := c.pool.Begin(ctx)
if err != nil {
return err
}
// 创建事务 client
txClient := &Client{
pool: c.pool,
tx: tx,
}
defer func(tx pgx.Tx, ctx context.Context) {
err := tx.Rollback(ctx)
if err != nil {
}
}(tx, ctx)
if err := fn(txClient); err != nil {
return err
}
return tx.Commit(ctx)
}
+140
View File
@@ -0,0 +1,140 @@
package db
import (
"context"
"fmt"
"sort"
"strings"
)
func (c *Client) Update(
ctx context.Context,
table string,
keyField string,
data map[string]any,
) error {
keyVal, err := validateData(table, keyField, data)
if err != nil {
return err
}
var (
setClauses []string
args []any
i = 1
)
for col, val := range data {
if col == keyField {
continue
}
setClauses = append(setClauses, fmt.Sprintf("%s=$%d", col, i))
args = append(args, val)
i++
}
// WHERE 条件
where := fmt.Sprintf("%s=$%d", keyField, i)
args = append(args, keyVal)
sql := fmt.Sprintf(
"UPDATE %s SET %s WHERE %s",
table,
strings.Join(setClauses, ", "),
where,
)
_, err = c.exec().Exec(ctx, sql, args...)
return err
}
func (c *Client) BatchUpdate(
ctx context.Context,
table string,
keyField string,
dataList []map[string]any,
) error {
if len(dataList) == 0 {
return fmt.Errorf("数据不能为空")
}
// 用第一条数据确定字段
first := dataList[0]
if _, err := validateData(table, keyField, first); err != nil {
return err
}
// 提取字段(排除主键)+ 排序(关键)
var columns []string
for col := range first {
if col != keyField {
columns = append(columns, col)
}
}
sort.Strings(columns)
var (
args []any
argIndex = 1
)
// CASE 语句
var setClauses []string
for _, col := range columns {
var caseBuilder strings.Builder
caseBuilder.WriteString(fmt.Sprintf("%s = CASE %s ", col, keyField))
for _, data := range dataList {
// 校验 key
keyVal, err := validateData(table, keyField, data)
if err != nil {
return err
}
val, ok := data[col]
if !ok {
return fmt.Errorf("缺少字段: %s", col)
}
caseBuilder.WriteString(fmt.Sprintf(
"WHEN $%d THEN $%d ",
argIndex,
argIndex+1,
))
args = append(args, keyVal, val)
argIndex += 2
}
caseBuilder.WriteString("END")
setClauses = append(setClauses, caseBuilder.String())
}
// WHERE IN
var wherePlaceholders []string
for _, data := range dataList {
keyVal, ok := data[keyField]
if !ok || keyVal == nil {
return fmt.Errorf("缺少主键字段值: %s", keyField)
}
wherePlaceholders = append(wherePlaceholders, fmt.Sprintf("$%d", argIndex))
args = append(args, keyVal)
argIndex++
}
sql := fmt.Sprintf(
"UPDATE %s SET %s WHERE %s IN (%s)",
table,
strings.Join(setClauses, ", "),
keyField,
strings.Join(wherePlaceholders, ", "),
)
_, err := c.exec().Exec(ctx, sql, args...)
return err
}
+112
View File
@@ -0,0 +1,112 @@
package jwtx
import (
"context"
"sync"
"time"
"github.com/golang-jwt/jwt/v5"
)
// ==================
// 内部管理器
// ==================
type jwtManager struct {
secret string
tokenExpiry time.Duration
}
var (
manager *jwtManager
once sync.Once
)
// ==================
// 初始化
// ==================
// Init 初始化(只执行一次)
// expiry = -1 表示不过期
func Init(secret string, expiry time.Duration) {
once.Do(func() {
manager = &jwtManager{
secret: secret,
tokenExpiry: expiry,
}
})
}
func getManager() *jwtManager {
if manager == nil {
panic("jwt未初始化,请先调用Init")
}
return manager
}
// ==================
// Claims(通用结构)
// ==================
type CustomClaims struct {
Data map[string]any `json:"data"`
jwt.RegisteredClaims
}
// ==================
// 对外方法
// ==================
// CreateToken 生成 JWT
func CreateToken(ctx context.Context, data map[string]any) (string, error) {
m := getManager()
return m.createToken(data)
}
// VerifyToken 校验 token 并返回 claims
func VerifyToken(ctx context.Context, tokenString string) (*CustomClaims, bool) {
if tokenString == "" {
return nil, false
}
m := getManager()
token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
return []byte(m.secret), nil
})
if err != nil || !token.Valid {
return nil, false
}
claims, ok := token.Claims.(*CustomClaims)
if !ok {
return nil, false
}
return claims, true
}
// ==================
// 内部实现
// ==================
func (j *jwtManager) createToken(data map[string]any) (string, error) {
now := time.Now()
claims := &CustomClaims{
Data: data,
RegisteredClaims: jwt.RegisteredClaims{
IssuedAt: jwt.NewNumericDate(now),
},
}
// ✅ 只有 >0 才设置过期时间
// -1 / 0 都表示不过期
if j.tokenExpiry > 0 {
claims.ExpiresAt = jwt.NewNumericDate(now.Add(j.tokenExpiry))
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(j.secret))
}
+233
View File
@@ -0,0 +1,233 @@
package logger
import (
"context"
"os"
"path/filepath"
"strings"
"time"
"github.com/gofiber/fiber/v3"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"gopkg.in/natefinch/lumberjack.v2"
)
var Log *zap.Logger
const TraceIDKey = "request_id"
// ======================
// 配置结构
// ======================
type Config struct {
Dir string // 日志目录
Level string // debug info warn error
MaxSize int // MB
MaxBackups int
MaxAge int // 天
Compress bool // 是否压缩
}
// ======================
// 初始化
// ======================
func Init(cfg Config) {
if cfg.Dir == "" {
cfg.Dir = "logs"
}
if cfg.Level == "" {
cfg.Level = "info"
}
level := parseLevel(cfg.Level)
// ======================
// encoder 配置
// ======================
encoderConfig := zapcore.EncoderConfig{
TimeKey: "time",
LevelKey: "level",
MessageKey: "msg",
CallerKey: "caller",
EncodeCaller: zapcore.ShortCallerEncoder,
}
// 👉 文件 JSON encoder
jsonEncoder := zapcore.NewJSONEncoder(func() zapcore.EncoderConfig {
cfg := encoderConfig
cfg.EncodeLevel = zapcore.CapitalLevelEncoder
cfg.EncodeTime = zapcore.TimeEncoderOfLayout("2006-01-02 15:04:05")
return cfg
}())
// 👉 控制台 encoder(可读)
consoleEncoder := zapcore.NewConsoleEncoder(func() zapcore.EncoderConfig {
cfg := encoderConfig
cfg.EncodeLevel = zapcore.CapitalColorLevelEncoder
cfg.EncodeTime = zapcore.TimeEncoderOfLayout("2006-01-02 15:04:05")
return cfg
}())
// ======================
// writer
// ======================
consoleWriter := zapcore.AddSync(os.Stdout)
infoWriter := getWriter(cfg, "info")
errorWriter := getWriter(cfg, "error")
// ======================
// core
// ======================
// 控制台(所有级别)
consoleCore := zapcore.NewCore(
consoleEncoder,
consoleWriter,
level,
)
// info 文件
infoCore := zapcore.NewCore(
jsonEncoder,
infoWriter,
zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
return lvl < zapcore.ErrorLevel && lvl >= level
}),
)
// error 文件
errorCore := zapcore.NewCore(
jsonEncoder,
errorWriter,
zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
return lvl >= zapcore.ErrorLevel
}),
)
core := zapcore.NewTee(
consoleCore,
infoCore,
errorCore,
)
Log = zap.New(
core,
zap.AddCaller(), // 打印调用位置
zap.AddCallerSkip(1), // 跳过封装层
)
}
// ======================
// Daily Writer(按天切割)
// ======================
type DailyWriter struct {
cfg Config
level string
log *lumberjack.Logger
date string
}
func newDailyWriter(cfg Config, level string) *DailyWriter {
return &DailyWriter{
cfg: cfg,
level: level,
}
}
func (w *DailyWriter) Write(p []byte) (n int, err error) {
today := time.Now().Format("2006-01-02")
if w.log == nil || w.date != today {
dir := filepath.Join(w.cfg.Dir, w.level)
_ = os.MkdirAll(dir, os.ModePerm)
filename := filepath.Join(dir, today+".log")
w.log = &lumberjack.Logger{
Filename: filename,
MaxSize: w.cfg.MaxSize,
MaxBackups: w.cfg.MaxBackups,
MaxAge: w.cfg.MaxAge,
Compress: w.cfg.Compress,
}
w.date = today
}
return w.log.Write(p)
}
func (w *DailyWriter) Sync() error {
if w.log != nil {
return w.log.Close()
}
return nil
}
// ======================
// writer 封装
// ======================
func getWriter(cfg Config, level string) zapcore.WriteSyncer {
return zapcore.AddSync(newDailyWriter(cfg, level))
}
// ======================
// level 解析
// ======================
func parseLevel(lvl string) zapcore.Level {
switch strings.ToLower(lvl) {
case "debug":
return zap.DebugLevel
case "warn":
return zap.WarnLevel
case "error":
return zap.ErrorLevel
default:
return zap.InfoLevel
}
}
// ======================
// trace 支持
// ======================
func SetTraceID(c fiber.Ctx, traceID string) {
if c == nil {
return
}
ctx := context.WithValue(c.Context(), TraceIDKey, traceID)
c.SetContext(ctx)
}
func FromCtx(ctx context.Context) *zap.Logger {
if ctx == nil {
return Log
}
traceID, _ := ctx.Value(TraceIDKey).(string)
if traceID == "" {
return Log
}
return Log.With(zap.String("trace_id", traceID))
}
// ======================
// 优雅退出
// ======================
func Sync() {
if Log != nil {
_ = Log.Sync()
}
}
@@ -0,0 +1,83 @@
package uniqueid
import (
"strconv"
"time"
)
type DefaultIdGenerator struct {
Options *IdGeneratorOptions
SnowWorker ISnowWorker
IdGeneratorException IdGeneratorException
}
func NewDefaultIdGenerator(options *IdGeneratorOptions) *DefaultIdGenerator {
if options == nil {
panic("dig.Options error.")
}
// 1.BaseTime
minTime := int64(631123200000) // time.Now().AddDate(-30, 0, 0).UnixNano() / 1e6
if options.BaseTime < minTime || options.BaseTime > time.Now().UnixNano()/1e6 {
panic("BaseTime error.")
}
// 2.WorkerIdBitLength
if options.WorkerIdBitLength <= 0 {
panic("WorkerIdBitLength error.(range:[1, 21])")
}
if options.WorkerIdBitLength+options.SeqBitLength > 22 {
panic("error:WorkerIdBitLength + SeqBitLength <= 22")
}
// 3.WorkerId
maxWorkerIdNumber := uint16(1<<options.WorkerIdBitLength) - 1
if maxWorkerIdNumber == 0 {
maxWorkerIdNumber = 63
}
if options.WorkerId < 0 || options.WorkerId > maxWorkerIdNumber {
panic("WorkerId error. (range:[0, " + strconv.FormatUint(uint64(maxWorkerIdNumber), 10) + "]")
}
// 4.SeqBitLength
if options.SeqBitLength < 2 || options.SeqBitLength > 21 {
panic("SeqBitLength error. (range:[2, 21])")
}
// 5.MaxSeqNumber
maxSeqNumber := uint32(1<<options.SeqBitLength) - 1
if maxSeqNumber == 0 {
maxSeqNumber = 63
}
if options.MaxSeqNumber < 0 || options.MaxSeqNumber > maxSeqNumber {
panic("MaxSeqNumber error. (range:[1, " + strconv.FormatUint(uint64(maxSeqNumber), 10) + "]")
}
// 6.MinSeqNumber
if options.MinSeqNumber < 5 || options.MinSeqNumber > maxSeqNumber {
panic("MinSeqNumber error. (range:[5, " + strconv.FormatUint(uint64(maxSeqNumber), 10) + "]")
}
var snowWorker ISnowWorker
switch options.Method {
case 1:
snowWorker = NewSnowWorkerM1(options)
case 2:
snowWorker = NewSnowWorkerM2(options)
default:
snowWorker = NewSnowWorkerM1(options)
}
if options.Method == 1 {
time.Sleep(time.Duration(500) * time.Microsecond)
}
return &DefaultIdGenerator{
Options: options,
SnowWorker: snowWorker,
}
}
func (dig DefaultIdGenerator) NewLong() int64 {
return dig.SnowWorker.NextId()
}
@@ -0,0 +1,5 @@
package uniqueid
type IIdGenerator interface {
NewLong() uint64
}
@@ -0,0 +1,5 @@
package uniqueid
type ISnowWorker interface {
NextId() int64
}
@@ -0,0 +1,12 @@
package uniqueid
import "fmt"
type IdGeneratorException struct {
message string
error error
}
func (e IdGeneratorException) IdGeneratorException(message ...interface{}) {
fmt.Println(message)
}
@@ -0,0 +1,25 @@
package uniqueid
type IdGeneratorOptions struct {
Method uint16 // 雪花计算方法,(1-漂移算法|2-传统算法),默认1
BaseTime int64 // 基础时间(ms单位),不能超过当前系统时间
WorkerId uint16 // 机器码,必须由外部设定,最大值 2^WorkerIdBitLength-1
WorkerIdBitLength byte // 机器码位长,默认值6,取值范围 [1, 15](要求:序列数位长+机器码位长不超过22)
SeqBitLength byte // 序列数位长,默认值6,取值范围 [3, 21](要求:序列数位长+机器码位长不超过22)
MaxSeqNumber uint32 // 最大序列数(含),设置范围 [MinSeqNumber, 2^SeqBitLength-1],默认值0,表示最大序列数取最大值(2^SeqBitLength-1])
MinSeqNumber uint32 // 最小序列数(含),默认值5,取值范围 [5, MaxSeqNumber],每毫秒的前5个序列数对应编号0-4是保留位,其中1-4是时间回拨相应预留位,0是手工新值预留位
TopOverCostCount uint32 // 最大漂移次数(含),默认2000,推荐范围500-10000(与计算能力有关)
}
func NewIdGeneratorOptions(workerId uint16) *IdGeneratorOptions {
return &IdGeneratorOptions{
Method: 1,
WorkerId: workerId,
BaseTime: 1582136402000,
WorkerIdBitLength: 6,
SeqBitLength: 6,
MaxSeqNumber: 0,
MinSeqNumber: 5,
TopOverCostCount: 2000,
}
}
+29
View File
@@ -0,0 +1,29 @@
package uniqueid
import (
"sync"
)
var singletonMutex sync.Mutex
var idGenerator *DefaultIdGenerator
// SetIdGenerator .
func SetIdGenerator(options *IdGeneratorOptions) {
singletonMutex.Lock()
idGenerator = NewDefaultIdGenerator(options)
singletonMutex.Unlock()
}
// NextId .
func NextId() int64 {
if idGenerator == nil {
singletonMutex.Lock()
defer singletonMutex.Unlock()
if idGenerator == nil {
options := NewIdGeneratorOptions(1)
idGenerator = NewDefaultIdGenerator(options)
}
}
return idGenerator.NewLong()
}
@@ -0,0 +1,19 @@
package uniqueid
type OverCostActionArg struct {
ActionType int32
TimeTick int64
WorkerId uint16
OverCostCountInOneTerm int32
GenCountInOneTerm int32
TermIndex int32
}
func (ocaa OverCostActionArg) OverCostActionArg(workerId uint16, timeTick int64, actionType int32, overCostCountInOneTerm int32, genCountWhenOverCost int32, index int32) {
ocaa.ActionType = actionType
ocaa.TimeTick = timeTick
ocaa.WorkerId = workerId
ocaa.OverCostCountInOneTerm = overCostCountInOneTerm
ocaa.GenCountInOneTerm = genCountWhenOverCost
ocaa.TermIndex = index
}
@@ -0,0 +1,243 @@
package uniqueid
import (
"sync"
"time"
)
// SnowWorkerM1 .
type SnowWorkerM1 struct {
BaseTime int64 //基础时间
WorkerId uint16 //机器码
WorkerIdBitLength byte //机器码位长
SeqBitLength byte //自增序列数位长
MaxSeqNumber uint32 //最大序列数(含)
MinSeqNumber uint32 //最小序列数(含)
TopOverCostCount uint32 //最大漂移次数
_TimestampShift byte
_CurrentSeqNumber uint32
_LastTimeTick int64
_TurnBackTimeTick int64
_TurnBackIndex byte
_IsOverCost bool
_OverCostCountInOneTerm uint32
_GenCountInOneTerm uint32
_TermIndex uint32
sync.Mutex
}
// NewSnowWorkerM1 .
func NewSnowWorkerM1(options *IdGeneratorOptions) ISnowWorker {
var workerIdBitLength byte
var seqBitLength byte
var maxSeqNumber uint32
// 1.BaseTime
var baseTime int64
if options.BaseTime != 0 {
baseTime = options.BaseTime
} else {
baseTime = 1582136402000
}
// 2.WorkerIdBitLength
if options.WorkerIdBitLength == 0 {
workerIdBitLength = 6
} else {
workerIdBitLength = options.WorkerIdBitLength
}
// 3.WorkerId
var workerId = options.WorkerId
// 4.SeqBitLength
if options.SeqBitLength == 0 {
seqBitLength = 6
} else {
seqBitLength = options.SeqBitLength
}
// 5.MaxSeqNumber
if options.MaxSeqNumber <= 0 {
maxSeqNumber = (1 << seqBitLength) - 1
} else {
maxSeqNumber = options.MaxSeqNumber
}
// 6.MinSeqNumber
var minSeqNumber = options.MinSeqNumber
// 7.Others
var topOverCostCount = options.TopOverCostCount
if topOverCostCount == 0 {
topOverCostCount = 2000
}
timestampShift := (byte)(workerIdBitLength + seqBitLength)
currentSeqNumber := minSeqNumber
return &SnowWorkerM1{
BaseTime: baseTime,
WorkerIdBitLength: workerIdBitLength,
WorkerId: workerId,
SeqBitLength: seqBitLength,
MaxSeqNumber: maxSeqNumber,
MinSeqNumber: minSeqNumber,
TopOverCostCount: topOverCostCount,
_TimestampShift: timestampShift,
_CurrentSeqNumber: currentSeqNumber,
_LastTimeTick: 0,
_TurnBackTimeTick: 0,
_TurnBackIndex: 0,
_IsOverCost: false,
_OverCostCountInOneTerm: 0,
_GenCountInOneTerm: 0,
_TermIndex: 0,
}
}
// DoGenIDAction .
func (m1 *SnowWorkerM1) DoGenIdAction(arg *OverCostActionArg) {
}
func (m1 *SnowWorkerM1) BeginOverCostAction(useTimeTick int64) {
}
func (m1 *SnowWorkerM1) EndOverCostAction(useTimeTick int64) {
if m1._TermIndex > 10000 {
m1._TermIndex = 0
}
}
func (m1 *SnowWorkerM1) BeginTurnBackAction(useTimeTick int64) {
}
func (m1 *SnowWorkerM1) EndTurnBackAction(useTimeTick int64) {
}
func (m1 *SnowWorkerM1) NextOverCostId() int64 {
currentTimeTick := m1.GetCurrentTimeTick()
if currentTimeTick > m1._LastTimeTick {
m1.EndOverCostAction(currentTimeTick)
m1._LastTimeTick = currentTimeTick
m1._CurrentSeqNumber = m1.MinSeqNumber
m1._IsOverCost = false
m1._OverCostCountInOneTerm = 0
m1._GenCountInOneTerm = 0
return m1.CalcId(m1._LastTimeTick)
}
if m1._OverCostCountInOneTerm >= m1.TopOverCostCount {
m1.EndOverCostAction(currentTimeTick)
m1._LastTimeTick = m1.GetNextTimeTick()
m1._CurrentSeqNumber = m1.MinSeqNumber
m1._IsOverCost = false
m1._OverCostCountInOneTerm = 0
m1._GenCountInOneTerm = 0
return m1.CalcId(m1._LastTimeTick)
}
if m1._CurrentSeqNumber > m1.MaxSeqNumber {
m1._LastTimeTick++
m1._CurrentSeqNumber = m1.MinSeqNumber
m1._IsOverCost = true
m1._OverCostCountInOneTerm++
m1._GenCountInOneTerm++
return m1.CalcId(m1._LastTimeTick)
}
m1._GenCountInOneTerm++
return m1.CalcId(m1._LastTimeTick)
}
// NextNormalID .
func (m1 *SnowWorkerM1) NextNormalId() int64 {
currentTimeTick := m1.GetCurrentTimeTick()
if currentTimeTick < m1._LastTimeTick {
if m1._TurnBackTimeTick < 1 {
m1._TurnBackTimeTick = m1._LastTimeTick - 1
m1._TurnBackIndex++
// 每毫秒序列数的前5位是预留位,0用于手工新值,1-4是时间回拨次序
// 最多4次回拨(防止回拨重叠)
if m1._TurnBackIndex > 4 {
m1._TurnBackIndex = 1
}
m1.BeginTurnBackAction(m1._TurnBackTimeTick)
}
// time.Sleep(time.Duration(1) * time.Millisecond)
return m1.CalcTurnBackId(m1._TurnBackTimeTick)
}
// 时间追平时,_TurnBackTimeTick清零
if m1._TurnBackTimeTick > 0 {
m1.EndTurnBackAction(m1._TurnBackTimeTick)
m1._TurnBackTimeTick = 0
}
if currentTimeTick > m1._LastTimeTick {
m1._LastTimeTick = currentTimeTick
m1._CurrentSeqNumber = m1.MinSeqNumber
return m1.CalcId(m1._LastTimeTick)
}
if m1._CurrentSeqNumber > m1.MaxSeqNumber {
m1.BeginOverCostAction(currentTimeTick)
m1._TermIndex++
m1._LastTimeTick++
m1._CurrentSeqNumber = m1.MinSeqNumber
m1._IsOverCost = true
m1._OverCostCountInOneTerm = 1
m1._GenCountInOneTerm = 1
return m1.CalcId(m1._LastTimeTick)
}
return m1.CalcId(m1._LastTimeTick)
}
// CalcID .
func (m1 *SnowWorkerM1) CalcId(useTimeTick int64) int64 {
result := int64(useTimeTick<<m1._TimestampShift) + int64(m1.WorkerId<<m1.SeqBitLength) + int64(m1._CurrentSeqNumber)
m1._CurrentSeqNumber++
return result
}
// CalcTurnBackID .
func (m1 *SnowWorkerM1) CalcTurnBackId(useTimeTick int64) int64 {
result := int64(useTimeTick<<m1._TimestampShift) + int64(m1.WorkerId<<m1.SeqBitLength) + int64(m1._TurnBackIndex)
m1._TurnBackTimeTick--
return result
}
// GetCurrentTimeTick .
func (m1 *SnowWorkerM1) GetCurrentTimeTick() int64 {
var millis = time.Now().UnixNano() / 1e6
return millis - m1.BaseTime
}
// GetNextTimeTick .
func (m1 *SnowWorkerM1) GetNextTimeTick() int64 {
tempTimeTicker := m1.GetCurrentTimeTick()
for tempTimeTicker <= m1._LastTimeTick {
tempTimeTicker = m1.GetCurrentTimeTick()
}
return tempTimeTicker
}
// NextId .
func (m1 *SnowWorkerM1) NextId() int64 {
m1.Lock()
defer m1.Unlock()
if m1._IsOverCost {
return m1.NextOverCostId()
} else {
return m1.NextNormalId()
}
}
@@ -0,0 +1,37 @@
package uniqueid
import (
"fmt"
"strconv"
)
type SnowWorkerM2 struct {
*SnowWorkerM1
}
func NewSnowWorkerM2(options *IdGeneratorOptions) ISnowWorker {
return &SnowWorkerM2{
NewSnowWorkerM1(options).(*SnowWorkerM1),
}
}
func (m2 SnowWorkerM2) NextId() int64 {
m2.Lock()
defer m2.Unlock()
currentTimeTick := m2.GetCurrentTimeTick()
if m2._LastTimeTick == currentTimeTick {
m2._CurrentSeqNumber++
if m2._CurrentSeqNumber > m2.MaxSeqNumber {
m2._CurrentSeqNumber = m2.MinSeqNumber
currentTimeTick = m2.GetNextTimeTick()
}
} else {
m2._CurrentSeqNumber = m2.MinSeqNumber
}
if currentTimeTick < m2._LastTimeTick {
fmt.Println("Time error for {0} milliseconds", strconv.FormatInt(m2._LastTimeTick-currentTimeTick, 10))
}
m2._LastTimeTick = currentTimeTick
result := int64(currentTimeTick<<m2._TimestampShift) + int64(m2.WorkerId<<m2.SeqBitLength) + int64(m2._CurrentSeqNumber)
return result
}
+1 -1
View File
@@ -1 +1 @@
{ "recommendations": ["expo.vscode-expo-tools"] }
{ "recommendations": ["expo.vscode-expo-tools", "gluestack.gluestack-vscode"] }
-50
View File
@@ -1,50 +0,0 @@
# Welcome to your Expo app 👋
This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app).
## Get started
1. Install dependencies
```bash
npm install
```
2. Start the app
```bash
npx expo start
```
In the output, you'll find options to open the app in a
- [development build](https://docs.expo.dev/develop/development-builds/introduction/)
- [Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/)
- [iOS simulator](https://docs.expo.dev/workflow/ios-simulator/)
- [Expo Go](https://expo.dev/go), a limited sandbox for trying out app development with Expo
You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction).
## Get a fresh project
When you're ready, run:
```bash
npm run reset-project
```
This command will move the starter code to the **app-example** directory and create a blank **app** directory where you can start developing.
## Learn more
To learn more about developing your project with Expo, look at the following resources:
- [Expo documentation](https://docs.expo.dev/): Learn fundamentals, or go into advanced topics with our [guides](https://docs.expo.dev/guides).
- [Learn Expo tutorial](https://docs.expo.dev/tutorial/introduction/): Follow a step-by-step tutorial where you'll create a project that runs on Android, iOS, and the web.
## Join the community
Join our community of developers creating universal apps.
- [Expo on GitHub](https://github.com/expo/expo): View our open source platform and contribute.
- [Discord community](https://chat.expo.dev): Chat with Expo users and ask questions.
+2 -1
View File
@@ -23,7 +23,8 @@
},
"web": {
"output": "static",
"favicon": "./assets/images/favicon.png"
"favicon": "./assets/images/favicon.png",
"bundler": "metro"
},
"plugins": [
"expo-router",
+10 -9
View File
@@ -1,13 +1,13 @@
import { Tabs } from 'expo-router';
import React from 'react';
import { Tabs } from 'expo-router'
import * as React from 'react'
import { HapticTab } from '@/components/haptic-tab';
import { IconSymbol } from '@/components/ui/icon-symbol';
import { Colors } from '@/constants/theme';
import { useColorScheme } from '@/hooks/use-color-scheme';
import { HapticTab } from '@/components/haptic-tab'
import { IconSymbol } from '@/components/ui/icon-symbol'
import { Colors } from '@/constants/theme'
import { useColorScheme } from '@/hooks/use-color-scheme'
export default function TabLayout() {
const colorScheme = useColorScheme();
const colorScheme = useColorScheme()
return (
<Tabs
@@ -15,7 +15,8 @@ export default function TabLayout() {
tabBarActiveTintColor: Colors[colorScheme ?? 'light'].tint,
headerShown: false,
tabBarButton: HapticTab,
}}>
}}
>
<Tabs.Screen
name="index"
options={{
@@ -31,5 +32,5 @@ export default function TabLayout() {
}}
/>
</Tabs>
);
)
}
+58 -28
View File
@@ -1,44 +1,53 @@
import { Image } from 'expo-image';
import { Platform, StyleSheet } from 'react-native';
import { Image } from 'expo-image'
import { Platform, StyleSheet } from 'react-native'
import { Collapsible } from '@/components/ui/collapsible';
import { ExternalLink } from '@/components/external-link';
import ParallaxScrollView from '@/components/parallax-scroll-view';
import { ThemedText } from '@/components/themed-text';
import { ThemedView } from '@/components/themed-view';
import { IconSymbol } from '@/components/ui/icon-symbol';
import { Fonts } from '@/constants/theme';
import { ExternalLink } from '@/components/external-link'
import ParallaxScrollView from '@/components/parallax-scroll-view'
import { ThemedText } from '@/components/themed-text'
import { ThemedView } from '@/components/themed-view'
import { Collapsible } from '@/components/ui/collapsible'
import { IconSymbol } from '@/components/ui/icon-symbol'
import { Fonts } from '@/constants/theme'
export default function TabTwoScreen() {
return (
<ParallaxScrollView
headerBackgroundColor={{ light: '#D0D0D0', dark: '#353636' }}
headerImage={
headerImage={(
<IconSymbol
size={310}
color="#808080"
name="chevron.left.forwardslash.chevron.right"
style={styles.headerImage}
/>
}>
)}
>
<ThemedView style={styles.titleContainer}>
<ThemedText
type="title"
style={{
fontFamily: Fonts.rounded,
}}>
}}
>
Explore
</ThemedText>
</ThemedView>
<ThemedText>This app includes example code to help you get started.</ThemedText>
<Collapsible title="File-based routing">
<ThemedText>
This app has two screens:{' '}
<ThemedText type="defaultSemiBold">app/(tabs)/index.tsx</ThemedText> and{' '}
This app has two screens:
{' '}
<ThemedText type="defaultSemiBold">app/(tabs)/index.tsx</ThemedText>
{' '}
and
{' '}
<ThemedText type="defaultSemiBold">app/(tabs)/explore.tsx</ThemedText>
</ThemedText>
<ThemedText>
The layout file in <ThemedText type="defaultSemiBold">app/(tabs)/_layout.tsx</ThemedText>{' '}
The layout file in
{' '}
<ThemedText type="defaultSemiBold">app/(tabs)/_layout.tsx</ThemedText>
{' '}
sets up the tab navigator.
</ThemedText>
<ExternalLink href="https://docs.expo.dev/router/introduction">
@@ -47,14 +56,24 @@ export default function TabTwoScreen() {
</Collapsible>
<Collapsible title="Android, iOS, and web support">
<ThemedText>
You can open this project on Android, iOS, and the web. To open the web version, press{' '}
<ThemedText type="defaultSemiBold">w</ThemedText> in the terminal running this project.
You can open this project on Android, iOS, and the web. To open the web version, press
{' '}
<ThemedText type="defaultSemiBold">w</ThemedText>
{' '}
in the terminal running this project.
</ThemedText>
</Collapsible>
<Collapsible title="Images">
<ThemedText>
For static images, you can use the <ThemedText type="defaultSemiBold">@2x</ThemedText> and{' '}
<ThemedText type="defaultSemiBold">@3x</ThemedText> suffixes to provide files for
For static images, you can use the
{' '}
<ThemedText type="defaultSemiBold">@2x</ThemedText>
{' '}
and
{' '}
<ThemedText type="defaultSemiBold">@3x</ThemedText>
{' '}
suffixes to provide files for
different screen densities
</ThemedText>
<Image
@@ -67,8 +86,11 @@ export default function TabTwoScreen() {
</Collapsible>
<Collapsible title="Light and dark mode components">
<ThemedText>
This template has light and dark mode support. The{' '}
<ThemedText type="defaultSemiBold">useColorScheme()</ThemedText> hook lets you inspect
This template has light and dark mode support. The
{' '}
<ThemedText type="defaultSemiBold">useColorScheme()</ThemedText>
{' '}
hook lets you inspect
what the user&apos;s current color scheme is, and so you can adjust UI colors accordingly.
</ThemedText>
<ExternalLink href="https://docs.expo.dev/develop/user-interface/color-themes/">
@@ -77,25 +99,33 @@ export default function TabTwoScreen() {
</Collapsible>
<Collapsible title="Animations">
<ThemedText>
This template includes an example of an animated component. The{' '}
<ThemedText type="defaultSemiBold">components/HelloWave.tsx</ThemedText> component uses
the powerful{' '}
This template includes an example of an animated component. The
{' '}
<ThemedText type="defaultSemiBold">components/HelloWave.tsx</ThemedText>
{' '}
component uses
the powerful
{' '}
<ThemedText type="defaultSemiBold" style={{ fontFamily: Fonts.mono }}>
react-native-reanimated
</ThemedText>{' '}
</ThemedText>
{' '}
library to create a waving hand animation.
</ThemedText>
{Platform.select({
ios: (
<ThemedText>
The <ThemedText type="defaultSemiBold">components/ParallaxScrollView.tsx</ThemedText>{' '}
The
{' '}
<ThemedText type="defaultSemiBold">components/ParallaxScrollView.tsx</ThemedText>
{' '}
component provides a parallax effect for the header image.
</ThemedText>
),
})}
</Collapsible>
</ParallaxScrollView>
);
)
}
const styles = StyleSheet.create({
@@ -109,4 +139,4 @@ const styles = StyleSheet.create({
flexDirection: 'row',
gap: 8,
},
});
})
+8 -94
View File
@@ -1,98 +1,12 @@
import { Image } from 'expo-image';
import { Platform, StyleSheet } from 'react-native';
import { HelloWave } from '@/components/hello-wave';
import ParallaxScrollView from '@/components/parallax-scroll-view';
import { ThemedText } from '@/components/themed-text';
import { ThemedView } from '@/components/themed-view';
import { Link } from 'expo-router';
import { Platform, StyleSheet, Text, View } from 'react-native'
import { Button, ButtonIcon, ButtonSpinner, ButtonText } from '@/components/ui/button'
export default function HomeScreen() {
return (
<ParallaxScrollView
headerBackgroundColor={{ light: '#A1CEDC', dark: '#1D3D47' }}
headerImage={
<Image
source={require('@/assets/images/partial-react-logo.png')}
style={styles.reactLogo}
/>
}>
<ThemedView style={styles.titleContainer}>
<ThemedText type="title">Welcome!</ThemedText>
<HelloWave />
</ThemedView>
<ThemedView style={styles.stepContainer}>
<ThemedText type="subtitle">Step 1: Try it</ThemedText>
<ThemedText>
Edit <ThemedText type="defaultSemiBold">app/(tabs)/index.tsx</ThemedText> to see changes.
Press{' '}
<ThemedText type="defaultSemiBold">
{Platform.select({
ios: 'cmd + d',
android: 'cmd + m',
web: 'F12',
})}
</ThemedText>{' '}
to open developer tools.
</ThemedText>
</ThemedView>
<ThemedView style={styles.stepContainer}>
<Link href="/modal">
<Link.Trigger>
<ThemedText type="subtitle">Step 2: Explore</ThemedText>
</Link.Trigger>
<Link.Preview />
<Link.Menu>
<Link.MenuAction title="Action" icon="cube" onPress={() => alert('Action pressed')} />
<Link.MenuAction
title="Share"
icon="square.and.arrow.up"
onPress={() => alert('Share pressed')}
/>
<Link.Menu title="More" icon="ellipsis">
<Link.MenuAction
title="Delete"
icon="trash"
destructive
onPress={() => alert('Delete pressed')}
/>
</Link.Menu>
</Link.Menu>
</Link>
<ThemedText>
{`Tap the Explore tab to learn more about what's included in this starter app.`}
</ThemedText>
</ThemedView>
<ThemedView style={styles.stepContainer}>
<ThemedText type="subtitle">Step 3: Get a fresh start</ThemedText>
<ThemedText>
{`When you're ready, run `}
<ThemedText type="defaultSemiBold">npm run reset-project</ThemedText> to get a fresh{' '}
<ThemedText type="defaultSemiBold">app</ThemedText> directory. This will move the current{' '}
<ThemedText type="defaultSemiBold">app</ThemedText> to{' '}
<ThemedText type="defaultSemiBold">app-example</ThemedText>.
</ThemedText>
</ThemedView>
</ParallaxScrollView>
);
<View className="flex-1 items-center justify-center dark:bg-black">
<Button action="primary" size="sm" isDisabled={false}>
<ButtonText>Hello World</ButtonText>
</Button>
</View>
)
}
const styles = StyleSheet.create({
titleContainer: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
},
stepContainer: {
gap: 8,
marginBottom: 8,
},
reactLogo: {
height: 178,
width: 290,
bottom: 0,
left: 0,
position: 'absolute',
},
});
+29 -15
View File
@@ -1,24 +1,38 @@
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native';
import { Stack } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
import 'react-native-reanimated';
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native'
import { Stack } from 'expo-router'
import { StatusBar } from 'expo-status-bar'
import { useEffect } from 'react'
import { Appearance } from 'react-native'
import { GluestackUIProvider } from '@/components/ui/gluestack-ui-provider'
import { useAppStore } from '@/store/app'
import 'react-native-reanimated'
import { useColorScheme } from '@/hooks/use-color-scheme';
import '@/styles/global.css'
export const unstable_settings = {
anchor: '(tabs)',
};
}
export default function RootLayout() {
const colorScheme = useColorScheme();
const appStore = useAppStore()
useEffect(() => {
const sub = Appearance.addChangeListener(() => {
useAppStore.getState().syncWithSystem()
})
return () => sub.remove()
}, [])
return (
<ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="modal" options={{ presentation: 'modal', title: 'Modal' }} />
</Stack>
<StatusBar style="auto" />
</ThemeProvider>
);
<GluestackUIProvider mode={appStore.isDark ? 'dark' : 'light'}>
<ThemeProvider value={appStore.isDark ? DarkTheme : DefaultTheme}>
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="modal" options={{ presentation: 'modal', title: 'Modal' }} />
</Stack>
<StatusBar style="auto" />
</ThemeProvider>
</GluestackUIProvider>
)
}
+7 -4
View File
@@ -1,6 +1,9 @@
module.exports = function (api) {
api.cache(true);
api.cache(true)
return {
presets: ['babel-preset-expo'],
};
};
presets: [
['babel-preset-expo', { jsxImportSource: 'nativewind' }],
'nativewind/babel',
],
}
}
@@ -0,0 +1,436 @@
'use client'
import type { VariantProps } from '@gluestack-ui/utils/nativewind-utils'
import { createButton } from '@gluestack-ui/core/button/creator'
import { PrimitiveIcon, UIIcon } from '@gluestack-ui/core/icon/creator'
import {
tva,
useStyleContext,
withStyleContext,
} from '@gluestack-ui/utils/nativewind-utils'
import { cssInterop } from 'nativewind'
import * as React from 'react'
import { ActivityIndicator, Pressable, Text, View } from 'react-native'
const SCOPE = 'BUTTON'
const Root = withStyleContext(Pressable, SCOPE)
const UIButton = createButton({
Root,
Text,
Group: View,
Spinner: ActivityIndicator,
Icon: UIIcon,
})
cssInterop(PrimitiveIcon, {
className: {
target: 'style',
nativeStyleToProp: {
height: true,
width: true,
fill: true,
color: 'classNameColor',
stroke: true,
},
},
})
const buttonStyle = tva({
base: 'group/button rounded bg-primary-500 flex-row items-center justify-center data-[focus-visible=true]:web:outline-none data-[focus-visible=true]:web:ring-2 data-[disabled=true]:opacity-40 gap-2',
variants: {
action: {
primary:
'bg-primary-500 data-[hover=true]:bg-primary-600 data-[active=true]:bg-primary-700 border-primary-300 data-[hover=true]:border-primary-400 data-[active=true]:border-primary-500 data-[focus-visible=true]:web:ring-indicator-info',
secondary:
'bg-secondary-500 border-secondary-300 data-[hover=true]:bg-secondary-600 data-[hover=true]:border-secondary-400 data-[active=true]:bg-secondary-700 data-[active=true]:border-secondary-700 data-[focus-visible=true]:web:ring-indicator-info',
positive:
'bg-success-500 border-success-300 data-[hover=true]:bg-success-600 data-[hover=true]:border-success-400 data-[active=true]:bg-success-700 data-[active=true]:border-success-500 data-[focus-visible=true]:web:ring-indicator-info',
negative:
'bg-error-500 border-error-300 data-[hover=true]:bg-error-600 data-[hover=true]:border-error-400 data-[active=true]:bg-error-700 data-[active=true]:border-error-500 data-[focus-visible=true]:web:ring-indicator-info',
default:
'bg-transparent data-[hover=true]:bg-background-50 data-[active=true]:bg-transparent',
},
variant: {
link: 'px-0',
outline:
'bg-transparent border data-[hover=true]:bg-background-50 data-[active=true]:bg-transparent',
solid: '',
},
size: {
xs: 'px-3.5 h-8',
sm: 'px-4 h-9',
md: 'px-5 h-10',
lg: 'px-6 h-11',
xl: 'px-7 h-12',
},
},
compoundVariants: [
{
action: 'primary',
variant: 'link',
class:
'px-0 bg-transparent data-[hover=true]:bg-transparent data-[active=true]:bg-transparent',
},
{
action: 'secondary',
variant: 'link',
class:
'px-0 bg-transparent data-[hover=true]:bg-transparent data-[active=true]:bg-transparent',
},
{
action: 'positive',
variant: 'link',
class:
'px-0 bg-transparent data-[hover=true]:bg-transparent data-[active=true]:bg-transparent',
},
{
action: 'negative',
variant: 'link',
class:
'px-0 bg-transparent data-[hover=true]:bg-transparent data-[active=true]:bg-transparent',
},
{
action: 'primary',
variant: 'outline',
class:
'bg-transparent data-[hover=true]:bg-background-50 data-[active=true]:bg-transparent',
},
{
action: 'secondary',
variant: 'outline',
class:
'bg-transparent data-[hover=true]:bg-background-50 data-[active=true]:bg-transparent',
},
{
action: 'positive',
variant: 'outline',
class:
'bg-transparent data-[hover=true]:bg-background-50 data-[active=true]:bg-transparent',
},
{
action: 'negative',
variant: 'outline',
class:
'bg-transparent data-[hover=true]:bg-background-50 data-[active=true]:bg-transparent',
},
],
})
const buttonTextStyle = tva({
base: 'text-typography-0 font-semibold web:select-none',
parentVariants: {
action: {
primary:
'text-primary-600 data-[hover=true]:text-primary-600 data-[active=true]:text-primary-700',
secondary:
'text-typography-500 data-[hover=true]:text-typography-600 data-[active=true]:text-typography-700',
positive:
'text-success-600 data-[hover=true]:text-success-600 data-[active=true]:text-success-700',
negative:
'text-error-600 data-[hover=true]:text-error-600 data-[active=true]:text-error-700',
},
variant: {
link: 'data-[hover=true]:underline data-[active=true]:underline',
outline: '',
solid:
'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0',
},
size: {
xs: 'text-xs',
sm: 'text-sm',
md: 'text-base',
lg: 'text-lg',
xl: 'text-xl',
},
},
parentCompoundVariants: [
{
variant: 'solid',
action: 'primary',
class:
'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0',
},
{
variant: 'solid',
action: 'secondary',
class:
'text-typography-800 data-[hover=true]:text-typography-800 data-[active=true]:text-typography-800',
},
{
variant: 'solid',
action: 'positive',
class:
'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0',
},
{
variant: 'solid',
action: 'negative',
class:
'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0',
},
{
variant: 'outline',
action: 'primary',
class:
'text-primary-500 data-[hover=true]:text-primary-500 data-[active=true]:text-primary-500',
},
{
variant: 'outline',
action: 'secondary',
class:
'text-typography-500 data-[hover=true]:text-primary-600 data-[active=true]:text-typography-700',
},
{
variant: 'outline',
action: 'positive',
class:
'text-primary-500 data-[hover=true]:text-primary-500 data-[active=true]:text-primary-500',
},
{
variant: 'outline',
action: 'negative',
class:
'text-primary-500 data-[hover=true]:text-primary-500 data-[active=true]:text-primary-500',
},
],
})
const buttonIconStyle = tva({
base: 'fill-none',
parentVariants: {
variant: {
link: 'data-[hover=true]:underline data-[active=true]:underline',
outline: '',
solid:
'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0',
},
size: {
xs: 'h-3.5 w-3.5',
sm: 'h-4 w-4',
md: 'h-[18px] w-[18px]',
lg: 'h-[18px] w-[18px]',
xl: 'h-5 w-5',
},
action: {
primary:
'text-primary-600 data-[hover=true]:text-primary-600 data-[active=true]:text-primary-700',
secondary:
'text-typography-500 data-[hover=true]:text-typography-600 data-[active=true]:text-typography-700',
positive:
'text-success-600 data-[hover=true]:text-success-600 data-[active=true]:text-success-700',
negative:
'text-error-600 data-[hover=true]:text-error-600 data-[active=true]:text-error-700',
},
},
parentCompoundVariants: [
{
variant: 'solid',
action: 'primary',
class:
'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0',
},
{
variant: 'solid',
action: 'secondary',
class:
'text-typography-800 data-[hover=true]:text-typography-800 data-[active=true]:text-typography-800',
},
{
variant: 'solid',
action: 'positive',
class:
'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0',
},
{
variant: 'solid',
action: 'negative',
class:
'text-typography-0 data-[hover=true]:text-typography-0 data-[active=true]:text-typography-0',
},
],
})
const buttonGroupStyle = tva({
base: '',
variants: {
space: {
'xs': 'gap-1',
'sm': 'gap-2',
'md': 'gap-3',
'lg': 'gap-4',
'xl': 'gap-5',
'2xl': 'gap-6',
'3xl': 'gap-7',
'4xl': 'gap-8',
},
isAttached: {
true: 'gap-0',
},
flexDirection: {
'row': 'flex-row',
'column': 'flex-col',
'row-reverse': 'flex-row-reverse',
'column-reverse': 'flex-col-reverse',
},
},
})
type IButtonProps = Omit<
React.ComponentPropsWithoutRef<typeof UIButton>,
'context'
>
& VariantProps<typeof buttonStyle> & { className?: string }
const Button = React.forwardRef<
React.ElementRef<typeof UIButton>,
IButtonProps
>(
(
{ className, variant = 'solid', size = 'md', action = 'primary', ...props },
ref,
) => {
return (
<UIButton
ref={ref}
{...props}
className={buttonStyle({ variant, size, action, class: className })}
context={{ variant, size, action }}
/>
)
},
)
type IButtonTextProps = React.ComponentPropsWithoutRef<typeof UIButton.Text>
& VariantProps<typeof buttonTextStyle> & { className?: string }
const ButtonText = React.forwardRef<
React.ElementRef<typeof UIButton.Text>,
IButtonTextProps
>(({ className, variant, size, action, ...props }, ref) => {
const {
variant: parentVariant,
size: parentSize,
action: parentAction,
} = useStyleContext(SCOPE)
return (
<UIButton.Text
ref={ref}
{...props}
className={buttonTextStyle({
parentVariants: {
variant: parentVariant,
size: parentSize,
action: parentAction,
},
variant,
size,
action,
class: className,
})}
/>
)
})
const ButtonSpinner = UIButton.Spinner
type IButtonIcon = React.ComponentPropsWithoutRef<typeof UIButton.Icon>
& VariantProps<typeof buttonIconStyle> & {
className?: string | undefined
as?: React.ElementType
height?: number
width?: number
}
const ButtonIcon = React.forwardRef<
React.ElementRef<typeof UIButton.Icon>,
IButtonIcon
>(({ className, size, ...props }, ref) => {
const {
variant: parentVariant,
size: parentSize,
action: parentAction,
} = useStyleContext(SCOPE)
if (typeof size === 'number') {
return (
<UIButton.Icon
ref={ref}
{...props}
className={buttonIconStyle({ class: className })}
size={size}
/>
)
}
else if (
(props.height !== undefined || props.width !== undefined)
&& size === undefined
) {
return (
<UIButton.Icon
ref={ref}
{...props}
className={buttonIconStyle({ class: className })}
/>
)
}
return (
<UIButton.Icon
{...props}
className={buttonIconStyle({
parentVariants: {
size: parentSize,
variant: parentVariant,
action: parentAction,
},
size,
class: className,
})}
ref={ref}
/>
)
})
type IButtonGroupProps = React.ComponentPropsWithoutRef<typeof UIButton.Group>
& VariantProps<typeof buttonGroupStyle>
const ButtonGroup = React.forwardRef<
React.ElementRef<typeof UIButton.Group>,
IButtonGroupProps
>(
(
{
className,
space = 'md',
isAttached = false,
flexDirection = 'column',
...props
},
ref,
) => {
return (
<UIButton.Group
className={buttonGroupStyle({
class: className,
space,
isAttached,
flexDirection,
})}
{...props}
ref={ref}
/>
)
},
)
Button.displayName = 'Button'
ButtonText.displayName = 'ButtonText'
ButtonSpinner.displayName = 'ButtonSpinner'
ButtonIcon.displayName = 'ButtonIcon'
ButtonGroup.displayName = 'ButtonGroup'
export { Button, ButtonGroup, ButtonIcon, ButtonSpinner, ButtonText }
@@ -0,0 +1,309 @@
'use client'
import { vars } from 'nativewind'
export const config = {
light: vars({
'--color-primary-0': '240 249 255',
'--color-primary-50': '224 242 254',
'--color-primary-100': '186 230 253',
'--color-primary-200': '125 211 252',
'--color-primary-300': '56 189 248',
'--color-primary-400': '14 165 233',
'--color-primary-500': '2 132 199',
'--color-primary-600': '3 105 161',
'--color-primary-700': '7 89 133',
'--color-primary-800': '12 74 110',
'--color-primary-900': '8 47 73',
'--color-primary-950': '2 22 36',
/* Secondary */
'--color-secondary-0': '253 253 253',
'--color-secondary-50': '251 251 251',
'--color-secondary-100': '246 246 246',
'--color-secondary-200': '242 242 242',
'--color-secondary-300': '237 237 237',
'--color-secondary-400': '230 230 231',
'--color-secondary-500': '217 217 219',
'--color-secondary-600': '198 199 199',
'--color-secondary-700': '189 189 189',
'--color-secondary-800': '177 177 177',
'--color-secondary-900': '165 164 164',
'--color-secondary-950': '157 157 157',
/* Tertiary */
'--color-tertiary-0': '255 250 245',
'--color-tertiary-50': '255 242 229',
'--color-tertiary-100': '255 233 213',
'--color-tertiary-200': '254 209 170',
'--color-tertiary-300': '253 180 116',
'--color-tertiary-400': '251 157 75',
'--color-tertiary-500': '231 129 40',
'--color-tertiary-600': '215 117 31',
'--color-tertiary-700': '180 98 26',
'--color-tertiary-800': '130 73 23',
'--color-tertiary-900': '108 61 19',
'--color-tertiary-950': '84 49 18',
/* Error */
'--color-error-0': '254 233 233',
'--color-error-50': '254 226 226',
'--color-error-100': '254 202 202',
'--color-error-200': '252 165 165',
'--color-error-300': '248 113 113',
'--color-error-400': '239 68 68',
'--color-error-500': '230 53 53',
'--color-error-600': '220 38 38',
'--color-error-700': '185 28 28',
'--color-error-800': '153 27 27',
'--color-error-900': '127 29 29',
'--color-error-950': '83 19 19',
/* Success */
'--color-success-0': '228 255 244',
'--color-success-50': '202 255 232',
'--color-success-100': '162 241 192',
'--color-success-200': '132 211 162',
'--color-success-300': '102 181 132',
'--color-success-400': '72 151 102',
'--color-success-500': '52 131 82',
'--color-success-600': '42 121 72',
'--color-success-700': '32 111 62',
'--color-success-800': '22 101 52',
'--color-success-900': '20 83 45',
'--color-success-950': '27 50 36',
/* Warning */
'--color-warning-0': '255 249 245',
'--color-warning-50': '255 244 236',
'--color-warning-100': '255 231 213',
'--color-warning-200': '254 205 170',
'--color-warning-300': '253 173 116',
'--color-warning-400': '251 149 75',
'--color-warning-500': '231 120 40',
'--color-warning-600': '215 108 31',
'--color-warning-700': '180 90 26',
'--color-warning-800': '130 68 23',
'--color-warning-900': '108 56 19',
'--color-warning-950': '84 45 18',
/* Info */
'--color-info-0': '236 248 254',
'--color-info-50': '199 235 252',
'--color-info-100': '162 221 250',
'--color-info-200': '124 207 248',
'--color-info-300': '87 194 246',
'--color-info-400': '50 180 244',
'--color-info-500': '13 166 242',
'--color-info-600': '11 141 205',
'--color-info-700': '9 115 168',
'--color-info-800': '7 90 131',
'--color-info-900': '5 64 93',
'--color-info-950': '3 38 56',
/* Typography */
'--color-typography-0': '254 254 255',
'--color-typography-50': '245 245 245',
'--color-typography-100': '229 229 229',
'--color-typography-200': '219 219 220',
'--color-typography-300': '212 212 212',
'--color-typography-400': '163 163 163',
'--color-typography-500': '140 140 140',
'--color-typography-600': '115 115 115',
'--color-typography-700': '82 82 82',
'--color-typography-800': '64 64 64',
'--color-typography-900': '38 38 39',
'--color-typography-950': '23 23 23',
/* Outline */
'--color-outline-0': '253 254 254',
'--color-outline-50': '243 243 243',
'--color-outline-100': '230 230 230',
'--color-outline-200': '221 220 219',
'--color-outline-300': '211 211 211',
'--color-outline-400': '165 163 163',
'--color-outline-500': '140 141 141',
'--color-outline-600': '115 116 116',
'--color-outline-700': '83 82 82',
'--color-outline-800': '65 65 65',
'--color-outline-900': '39 38 36',
'--color-outline-950': '26 23 23',
/* Background */
'--color-background-0': '255 255 255',
'--color-background-50': '246 246 246',
'--color-background-100': '242 241 241',
'--color-background-200': '220 219 219',
'--color-background-300': '213 212 212',
'--color-background-400': '162 163 163',
'--color-background-500': '142 142 142',
'--color-background-600': '116 116 116',
'--color-background-700': '83 82 82',
'--color-background-800': '65 64 64',
'--color-background-900': '39 38 37',
'--color-background-950': '18 18 18',
/* Background Special */
'--color-background-error': '254 241 241',
'--color-background-warning': '255 243 234',
'--color-background-success': '237 252 242',
'--color-background-muted': '247 248 247',
'--color-background-info': '235 248 254',
/* Focus Ring Indicator */
'--color-indicator-primary': '55 55 55',
'--color-indicator-info': '83 153 236',
'--color-indicator-error': '185 28 28',
}),
dark: vars({
'--color-primary-0': '2 22 36',
'--color-primary-50': '8 47 73',
'--color-primary-100': '12 74 110',
'--color-primary-200': '7 89 133',
'--color-primary-300': '3 105 161',
'--color-primary-400': '2 132 199',
'--color-primary-500': '56 189 248',
'--color-primary-600': '125 211 252',
'--color-primary-700': '186 230 253',
'--color-primary-800': '224 242 254',
'--color-primary-900': '240 249 255',
'--color-primary-950': '248 252 255',
/* Secondary */
'--color-secondary-0': '20 20 20',
'--color-secondary-50': '23 23 23',
'--color-secondary-100': '31 31 31',
'--color-secondary-200': '39 39 39',
'--color-secondary-300': '44 44 44',
'--color-secondary-400': '56 57 57',
'--color-secondary-500': '63 64 64',
'--color-secondary-600': '86 86 86',
'--color-secondary-700': '110 110 110',
'--color-secondary-800': '135 135 135',
'--color-secondary-900': '150 150 150',
'--color-secondary-950': '164 164 164',
/* Tertiary */
'--color-tertiary-0': '84 49 18',
'--color-tertiary-50': '108 61 19',
'--color-tertiary-100': '130 73 23',
'--color-tertiary-200': '180 98 26',
'--color-tertiary-300': '215 117 31',
'--color-tertiary-400': '231 129 40',
'--color-tertiary-500': '251 157 75',
'--color-tertiary-600': '253 180 116',
'--color-tertiary-700': '254 209 170',
'--color-tertiary-800': '255 233 213',
'--color-tertiary-900': '255 242 229',
'--color-tertiary-950': '255 250 245',
/* Error */
'--color-error-0': '83 19 19',
'--color-error-50': '127 29 29',
'--color-error-100': '153 27 27',
'--color-error-200': '185 28 28',
'--color-error-300': '220 38 38',
'--color-error-400': '230 53 53',
'--color-error-500': '239 68 68',
'--color-error-600': '249 97 96',
'--color-error-700': '229 91 90',
'--color-error-800': '254 202 202',
'--color-error-900': '254 226 226',
'--color-error-950': '254 233 233',
/* Success */
'--color-success-0': '27 50 36',
'--color-success-50': '20 83 45',
'--color-success-100': '22 101 52',
'--color-success-200': '32 111 62',
'--color-success-300': '42 121 72',
'--color-success-400': '52 131 82',
'--color-success-500': '72 151 102',
'--color-success-600': '102 181 132',
'--color-success-700': '132 211 162',
'--color-success-800': '162 241 192',
'--color-success-900': '202 255 232',
'--color-success-950': '228 255 244',
/* Warning */
'--color-warning-0': '84 45 18',
'--color-warning-50': '108 56 19',
'--color-warning-100': '130 68 23',
'--color-warning-200': '180 90 26',
'--color-warning-300': '215 108 31',
'--color-warning-400': '231 120 40',
'--color-warning-500': '251 149 75',
'--color-warning-600': '253 173 116',
'--color-warning-700': '254 205 170',
'--color-warning-800': '255 231 213',
'--color-warning-900': '255 244 237',
'--color-warning-950': '255 249 245',
/* Info */
'--color-info-0': '3 38 56',
'--color-info-50': '5 64 93',
'--color-info-100': '7 90 131',
'--color-info-200': '9 115 168',
'--color-info-300': '11 141 205',
'--color-info-400': '13 166 242',
'--color-info-500': '50 180 244',
'--color-info-600': '87 194 246',
'--color-info-700': '124 207 248',
'--color-info-800': '162 221 250',
'--color-info-900': '199 235 252',
'--color-info-950': '236 248 254',
/* Typography */
'--color-typography-0': '23 23 23',
'--color-typography-50': '38 38 39',
'--color-typography-100': '64 64 64',
'--color-typography-200': '82 82 82',
'--color-typography-300': '115 115 115',
'--color-typography-400': '140 140 140',
'--color-typography-500': '163 163 163',
'--color-typography-600': '212 212 212',
'--color-typography-700': '219 219 220',
'--color-typography-800': '229 229 229',
'--color-typography-900': '245 245 245',
'--color-typography-950': '254 254 255',
/* Outline */
'--color-outline-0': '26 23 23',
'--color-outline-50': '39 38 36',
'--color-outline-100': '65 65 65',
'--color-outline-200': '83 82 82',
'--color-outline-300': '115 116 116',
'--color-outline-400': '140 141 141',
'--color-outline-500': '165 163 163',
'--color-outline-600': '211 211 211',
'--color-outline-700': '221 220 219',
'--color-outline-800': '230 230 230',
'--color-outline-900': '243 243 243',
'--color-outline-950': '253 254 254',
/* Background */
'--color-background-0': '18 18 18',
'--color-background-50': '39 38 37',
'--color-background-100': '65 64 64',
'--color-background-200': '83 82 82',
'--color-background-300': '116 116 116',
'--color-background-400': '142 142 142',
'--color-background-500': '162 163 163',
'--color-background-600': '213 212 212',
'--color-background-700': '229 228 228',
'--color-background-800': '242 241 241',
'--color-background-900': '246 246 246',
'--color-background-950': '255 255 255',
/* Background Special */
'--color-background-error': '66 43 43',
'--color-background-warning': '65 47 35',
'--color-background-success': '28 43 33',
'--color-background-muted': '51 51 51',
'--color-background-info': '26 40 46',
/* Focus Ring Indicator */
'--color-indicator-primary': '247 247 247',
'--color-indicator-info': '161 199 245',
'--color-indicator-error': '232 70 69',
}),
}
@@ -0,0 +1,5 @@
{
"dependencies": {},
"devDependencies": {},
"components": []
}
@@ -0,0 +1,87 @@
// This is a Next.js 15 compatible version of the GluestackUIProvider
'use client';
import React, { useEffect, useLayoutEffect } from 'react';
import { config } from './config';
import { OverlayProvider } from '@gluestack-ui/core/overlay/creator';
import { ToastProvider } from '@gluestack-ui/core/toast/creator';
import { setFlushStyles } from '@gluestack-ui/utils/nativewind-utils';
import { script } from './script';
const variableStyleTagId = 'nativewind-style';
const createStyle = (styleTagId: string) => {
const style = document.createElement('style');
style.id = styleTagId;
style.appendChild(document.createTextNode(''));
return style;
};
export const useSafeLayoutEffect =
typeof window !== 'undefined' ? useLayoutEffect : useEffect;
export function GluestackUIProvider({
mode = 'light',
...props
}: {
mode?: 'light' | 'dark' | 'system';
children?: React.ReactNode;
}) {
let cssVariablesWithMode = ``;
Object.keys(config).forEach((configKey) => {
cssVariablesWithMode +=
configKey === 'dark' ? `\n .dark {\n ` : `\n:root {\n`;
const cssVariables = Object.keys(
config[configKey as keyof typeof config]
).reduce((acc: string, curr: string) => {
acc += `${curr}:${config[configKey as keyof typeof config][curr]}; `;
return acc;
}, '');
cssVariablesWithMode += `${cssVariables} \n}`;
});
setFlushStyles(cssVariablesWithMode);
const handleMediaQuery = React.useCallback((e: MediaQueryListEvent) => {
script(e.matches ? 'dark' : 'light');
}, []);
useSafeLayoutEffect(() => {
if (mode !== 'system') {
const documentElement = document.documentElement;
if (documentElement) {
documentElement.classList.add(mode);
documentElement.classList.remove(mode === 'light' ? 'dark' : 'light');
documentElement.style.colorScheme = mode;
}
}
}, [mode]);
useSafeLayoutEffect(() => {
if (mode !== 'system') return;
const media = window.matchMedia('(prefers-color-scheme: dark)');
media.addListener(handleMediaQuery);
return () => media.removeListener(handleMediaQuery);
}, [handleMediaQuery]);
useSafeLayoutEffect(() => {
if (typeof window !== 'undefined') {
const documentElement = document.documentElement;
if (documentElement) {
const head = documentElement.querySelector('head');
let style = head?.querySelector(`[id='${variableStyleTagId}']`);
if (!style) {
style = createStyle(variableStyleTagId);
style.innerHTML = cssVariablesWithMode;
if (head) head.appendChild(style);
}
}
}
}, []);
return (
<OverlayProvider>
<ToastProvider>{props.children}</ToastProvider>
</OverlayProvider>
);
}
@@ -0,0 +1,40 @@
import type { ViewProps } from 'react-native'
import { OverlayProvider } from '@gluestack-ui/core/overlay/creator'
import { ToastProvider } from '@gluestack-ui/core/toast/creator'
import { useColorScheme } from 'nativewind'
import * as React from 'react'
import { useEffect } from 'react'
import { View } from 'react-native'
import { config } from './config'
export type ModeType = 'light' | 'dark' | 'system'
export function GluestackUIProvider({
mode = 'light',
...props
}: {
mode?: ModeType
children?: React.ReactNode
style?: ViewProps['style']
}) {
const { colorScheme, setColorScheme } = useColorScheme()
useEffect(() => {
setColorScheme(mode)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [mode])
return (
<View
style={[
config[colorScheme!],
{ flex: 1, height: '100%', width: '100%' },
props.style,
]}
>
<OverlayProvider>
<ToastProvider>{props.children}</ToastProvider>
</OverlayProvider>
</View>
)
}
@@ -0,0 +1,96 @@
'use client';
import React, { useEffect, useLayoutEffect } from 'react';
import { config } from './config';
import { OverlayProvider } from '@gluestack-ui/core/overlay/creator';
import { ToastProvider } from '@gluestack-ui/core/toast/creator';
import { setFlushStyles } from '@gluestack-ui/utils/nativewind-utils';
import { script } from './script';
export type ModeType = 'light' | 'dark' | 'system';
const variableStyleTagId = 'nativewind-style';
const createStyle = (styleTagId: string) => {
const style = document.createElement('style');
style.id = styleTagId;
style.appendChild(document.createTextNode(''));
return style;
};
export const useSafeLayoutEffect =
typeof window !== 'undefined' ? useLayoutEffect : useEffect;
export function GluestackUIProvider({
mode = 'light',
...props
}: {
mode?: ModeType;
children?: React.ReactNode;
}) {
let cssVariablesWithMode = ``;
Object.keys(config).forEach((configKey) => {
cssVariablesWithMode +=
configKey === 'dark' ? `\n .dark {\n ` : `\n:root {\n`;
const cssVariables = Object.keys(
config[configKey as keyof typeof config]
).reduce((acc: string, curr: string) => {
acc += `${curr}:${config[configKey as keyof typeof config][curr]}; `;
return acc;
}, '');
cssVariablesWithMode += `${cssVariables} \n}`;
});
setFlushStyles(cssVariablesWithMode);
const handleMediaQuery = React.useCallback((e: MediaQueryListEvent) => {
script(e.matches ? 'dark' : 'light');
}, []);
useSafeLayoutEffect(() => {
if (mode !== 'system') {
const documentElement = document.documentElement;
if (documentElement) {
documentElement.classList.add(mode);
documentElement.classList.remove(mode === 'light' ? 'dark' : 'light');
documentElement.style.colorScheme = mode;
}
}
}, [mode]);
useSafeLayoutEffect(() => {
if (mode !== 'system') return;
const media = window.matchMedia('(prefers-color-scheme: dark)');
media.addListener(handleMediaQuery);
return () => media.removeListener(handleMediaQuery);
}, [handleMediaQuery]);
useSafeLayoutEffect(() => {
if (typeof window !== 'undefined') {
const documentElement = document.documentElement;
if (documentElement) {
const head = documentElement.querySelector('head');
let style = head?.querySelector(`[id='${variableStyleTagId}']`);
if (!style) {
style = createStyle(variableStyleTagId);
style.innerHTML = cssVariablesWithMode;
if (head) head.appendChild(style);
}
}
}
}, []);
return (
<>
<script
suppressHydrationWarning
dangerouslySetInnerHTML={{
__html: `(${script.toString()})('${mode}')`,
}}
/>
<OverlayProvider>
<ToastProvider>{props.children}</ToastProvider>
</OverlayProvider>
</>
);
}
@@ -0,0 +1,19 @@
export const script = (mode: string) => {
const documentElement = document.documentElement;
function getSystemColorMode() {
return window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light';
}
try {
const isSystem = mode === 'system';
const theme = isSystem ? getSystemColorMode() : mode;
documentElement.classList.remove(theme === 'light' ? 'dark' : 'light');
documentElement.classList.add(theme);
documentElement.style.colorScheme = theme;
} catch (e) {
console.error(e);
}
};
+18 -22
View File
@@ -1,24 +1,20 @@
import antfu from '@antfu/eslint-config'
// eslint.config.mjs
import expoConfig from 'eslint-config-expo/flat.js'
export default [
...expoConfig,
...antfu({
formatters: true,
react: true,
stylistic: {
indent: 2, // 缩进为 2 个空格(可选 4 或 'tab')
quotes: 'single', // 使用单引号(可选 'double')
},
rules: {
'unused-imports/no-unused-imports': 'warn',
'unused-imports/no-unused-vars': 'warn',
'@typescript-eslint/no-use-before-define': 'off', // 关闭 TS 的“定义前使用”警告
'no-console': 'off', // 关闭 console 报错
'node/prefer-global/process': 'off',
'@typescript-eslint/no-require-imports': 'off',
'no-useless-catch': 'off',
},
}),
]
export default antfu({
formatters: true,
react: true,
stylistic: {
indent: 2, // 缩进为 2 个空格(可选 4 或 'tab')
quotes: 'single', // 使用单引号(可选 'double')
},
rules: {
'unused-imports/no-unused-imports': 'off',
'unused-imports/no-unused-vars': 'off',
'@typescript-eslint/no-use-before-define': 'off', // 关闭 TS 的“定义前使用”警告
'no-console': 'off', // 关闭 console 报错
'node/prefer-global/process': 'off',
'@typescript-eslint/no-require-imports': 'off',
'no-useless-catch': 'off',
'react-refresh/only-export-components': 'off',
},
})
@@ -1,21 +0,0 @@
import { useEffect, useState } from 'react';
import { useColorScheme as useRNColorScheme } from 'react-native';
/**
* To support static rendering, this value needs to be re-calculated on the client side for web
*/
export function useColorScheme() {
const [hasHydrated, setHasHydrated] = useState(false);
useEffect(() => {
setHasHydrated(true);
}, []);
const colorScheme = useRNColorScheme();
if (hasHydrated) {
return colorScheme;
}
return 'light';
}
+4 -5
View File
@@ -1,7 +1,6 @@
// Learn more https://docs.expo.io/guides/customizing-metro
const { getDefaultConfig } = require('expo/metro-config');
const { getDefaultConfig } = require('expo/metro-config')
const { withNativeWind } = require('nativewind/metro')
/** @type {import('expo/metro-config').MetroConfig} */
const config = getDefaultConfig(__dirname);
const config = getDefaultConfig(__dirname)
module.exports = config;
module.exports = withNativeWind(config, { input: './styles/global.css' })
+3
View File
@@ -0,0 +1,3 @@
/// <reference types="nativewind/types" />
// NOTE: This file should not be edited and should be committed with your source code. It is generated by NativeWind.
+15 -3
View File
@@ -13,6 +13,10 @@
},
"dependencies": {
"@expo/vector-icons": "^15.0.3",
"@gluestack-ui/core": "^3.0.16",
"@gluestack-ui/utils": "^3.0.19",
"@gluestack/ui-next-adapter": "^3.0.3",
"@react-native-async-storage/async-storage": "^3.0.2",
"@react-navigation/bottom-tabs": "^7.4.0",
"@react-navigation/elements": "^2.6.3",
"@react-navigation/native": "^7.1.8",
@@ -28,24 +32,32 @@
"expo-symbols": "~1.0.8",
"expo-system-ui": "~6.0.9",
"expo-web-browser": "~15.0.10",
"nativewind": "^4.2.3",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-native": "0.81.5",
"react-native-css-interop": "^0.2.3",
"react-native-gesture-handler": "~2.28.0",
"react-native-reanimated": "~4.1.1",
"react-native-safe-area-context": "~5.6.0",
"react-native-reanimated": "~4.1.7",
"react-native-safe-area-context": "~5.6.2",
"react-native-screens": "~4.16.0",
"react-native-svg": "^15.12.1",
"react-native-web": "~0.21.0",
"react-native-worklets": "0.5.1"
"react-native-worklets": "0.5.1",
"sonner-native": "^0.24.0",
"zustand": "^5.0.12"
},
"devDependencies": {
"@antfu/eslint-config": "^8.0.0",
"@eslint-react/eslint-plugin": "^3.0.0",
"@types/react": "~19.1.0",
"babel-preset-expo": "~54.0.10",
"eslint": "^9.25.0",
"eslint-config-expo": "~10.0.0",
"eslint-plugin-format": "^2.0.1",
"eslint-plugin-react-refresh": "^0.5.2",
"prettier-plugin-tailwindcss": "0.5.11",
"tailwindcss": "3.4.17",
"typescript": "~5.9.2"
}
}
+2737 -232
View File
File diff suppressed because it is too large. Load diff
+64
View File
@@ -0,0 +1,64 @@
import type { ColorSchemeName } from 'react-native'
import AsyncStorage from '@react-native-async-storage/async-storage'
import { Appearance } from 'react-native'
import { create } from 'zustand'
import { createJSONStorage, persist } from 'zustand/middleware'
export type ThemeScheme = 'light' | 'dark' | 'auto'
interface AppState {
themeScheme: ThemeScheme
isDark: boolean
setThemeScheme: (theme: ThemeScheme) => void
syncWithSystem: () => void
}
function getIsDark(theme: ThemeScheme, system: ColorSchemeName) {
if (theme === 'auto') {
return system === 'dark'
}
return theme === 'dark'
}
export const useAppStore = create<AppState>()(
persist(
(set, get) => {
const system = Appearance.getColorScheme()
return {
themeScheme: 'auto',
isDark: getIsDark('auto', system),
setThemeScheme: (theme) => {
const system = Appearance.getColorScheme()
set({
themeScheme: theme,
isDark: getIsDark(theme, system),
})
},
syncWithSystem: () => {
const { themeScheme } = get()
const system = Appearance.getColorScheme()
if (themeScheme === 'auto') {
set({
isDark: getIsDark(themeScheme, system),
})
}
},
}
},
{
name: 'app-storage',
partialize: state => ({
themeScheme: state.themeScheme,
}),
storage: createJSONStorage(() => AsyncStorage),
},
),
)
+3
View File
@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
+207
View File
@@ -0,0 +1,207 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: process.env.DARK_MODE ? process.env.DARK_MODE : 'class',
content: [
'./app/**/*.{html,js,jsx,ts,tsx,mdx}',
'./components/**/*.{html,js,jsx,ts,tsx,mdx}',
'./utils/**/*.{html,js,jsx,ts,tsx,mdx}',
'./*.{html,js,jsx,ts,tsx,mdx}',
'./src/**/*.{html,js,jsx,ts,tsx,mdx}',
],
presets: [require('nativewind/preset')],
important: 'html',
safelist: [
{
pattern:
// eslint-disable-next-line regexp/no-dupe-disjunctions
/(bg|border|text|stroke|fill)-(primary|secondary|tertiary|error|success|warning|info|typography|outline|background|indicator)-(0|50|100|200|300|400|500|600|700|800|900|950|white|gray|black|error|warning|muted|success|info|light|dark|primary)/,
},
],
theme: {
extend: {
colors: {
primary: {
0: 'rgb(var(--color-primary-0)/<alpha-value>)',
50: 'rgb(var(--color-primary-50)/<alpha-value>)',
100: 'rgb(var(--color-primary-100)/<alpha-value>)',
200: 'rgb(var(--color-primary-200)/<alpha-value>)',
300: 'rgb(var(--color-primary-300)/<alpha-value>)',
400: 'rgb(var(--color-primary-400)/<alpha-value>)',
500: 'rgb(var(--color-primary-500)/<alpha-value>)',
600: 'rgb(var(--color-primary-600)/<alpha-value>)',
700: 'rgb(var(--color-primary-700)/<alpha-value>)',
800: 'rgb(var(--color-primary-800)/<alpha-value>)',
900: 'rgb(var(--color-primary-900)/<alpha-value>)',
950: 'rgb(var(--color-primary-950)/<alpha-value>)',
},
secondary: {
0: 'rgb(var(--color-secondary-0)/<alpha-value>)',
50: 'rgb(var(--color-secondary-50)/<alpha-value>)',
100: 'rgb(var(--color-secondary-100)/<alpha-value>)',
200: 'rgb(var(--color-secondary-200)/<alpha-value>)',
300: 'rgb(var(--color-secondary-300)/<alpha-value>)',
400: 'rgb(var(--color-secondary-400)/<alpha-value>)',
500: 'rgb(var(--color-secondary-500)/<alpha-value>)',
600: 'rgb(var(--color-secondary-600)/<alpha-value>)',
700: 'rgb(var(--color-secondary-700)/<alpha-value>)',
800: 'rgb(var(--color-secondary-800)/<alpha-value>)',
900: 'rgb(var(--color-secondary-900)/<alpha-value>)',
950: 'rgb(var(--color-secondary-950)/<alpha-value>)',
},
tertiary: {
50: 'rgb(var(--color-tertiary-50)/<alpha-value>)',
100: 'rgb(var(--color-tertiary-100)/<alpha-value>)',
200: 'rgb(var(--color-tertiary-200)/<alpha-value>)',
300: 'rgb(var(--color-tertiary-300)/<alpha-value>)',
400: 'rgb(var(--color-tertiary-400)/<alpha-value>)',
500: 'rgb(var(--color-tertiary-500)/<alpha-value>)',
600: 'rgb(var(--color-tertiary-600)/<alpha-value>)',
700: 'rgb(var(--color-tertiary-700)/<alpha-value>)',
800: 'rgb(var(--color-tertiary-800)/<alpha-value>)',
900: 'rgb(var(--color-tertiary-900)/<alpha-value>)',
950: 'rgb(var(--color-tertiary-950)/<alpha-value>)',
},
error: {
0: 'rgb(var(--color-error-0)/<alpha-value>)',
50: 'rgb(var(--color-error-50)/<alpha-value>)',
100: 'rgb(var(--color-error-100)/<alpha-value>)',
200: 'rgb(var(--color-error-200)/<alpha-value>)',
300: 'rgb(var(--color-error-300)/<alpha-value>)',
400: 'rgb(var(--color-error-400)/<alpha-value>)',
500: 'rgb(var(--color-error-500)/<alpha-value>)',
600: 'rgb(var(--color-error-600)/<alpha-value>)',
700: 'rgb(var(--color-error-700)/<alpha-value>)',
800: 'rgb(var(--color-error-800)/<alpha-value>)',
900: 'rgb(var(--color-error-900)/<alpha-value>)',
950: 'rgb(var(--color-error-950)/<alpha-value>)',
},
success: {
0: 'rgb(var(--color-success-0)/<alpha-value>)',
50: 'rgb(var(--color-success-50)/<alpha-value>)',
100: 'rgb(var(--color-success-100)/<alpha-value>)',
200: 'rgb(var(--color-success-200)/<alpha-value>)',
300: 'rgb(var(--color-success-300)/<alpha-value>)',
400: 'rgb(var(--color-success-400)/<alpha-value>)',
500: 'rgb(var(--color-success-500)/<alpha-value>)',
600: 'rgb(var(--color-success-600)/<alpha-value>)',
700: 'rgb(var(--color-success-700)/<alpha-value>)',
800: 'rgb(var(--color-success-800)/<alpha-value>)',
900: 'rgb(var(--color-success-900)/<alpha-value>)',
950: 'rgb(var(--color-success-950)/<alpha-value>)',
},
warning: {
0: 'rgb(var(--color-warning-0)/<alpha-value>)',
50: 'rgb(var(--color-warning-50)/<alpha-value>)',
100: 'rgb(var(--color-warning-100)/<alpha-value>)',
200: 'rgb(var(--color-warning-200)/<alpha-value>)',
300: 'rgb(var(--color-warning-300)/<alpha-value>)',
400: 'rgb(var(--color-warning-400)/<alpha-value>)',
500: 'rgb(var(--color-warning-500)/<alpha-value>)',
600: 'rgb(var(--color-warning-600)/<alpha-value>)',
700: 'rgb(var(--color-warning-700)/<alpha-value>)',
800: 'rgb(var(--color-warning-800)/<alpha-value>)',
900: 'rgb(var(--color-warning-900)/<alpha-value>)',
950: 'rgb(var(--color-warning-950)/<alpha-value>)',
},
info: {
0: 'rgb(var(--color-info-0)/<alpha-value>)',
50: 'rgb(var(--color-info-50)/<alpha-value>)',
100: 'rgb(var(--color-info-100)/<alpha-value>)',
200: 'rgb(var(--color-info-200)/<alpha-value>)',
300: 'rgb(var(--color-info-300)/<alpha-value>)',
400: 'rgb(var(--color-info-400)/<alpha-value>)',
500: 'rgb(var(--color-info-500)/<alpha-value>)',
600: 'rgb(var(--color-info-600)/<alpha-value>)',
700: 'rgb(var(--color-info-700)/<alpha-value>)',
800: 'rgb(var(--color-info-800)/<alpha-value>)',
900: 'rgb(var(--color-info-900)/<alpha-value>)',
950: 'rgb(var(--color-info-950)/<alpha-value>)',
},
typography: {
0: 'rgb(var(--color-typography-0)/<alpha-value>)',
50: 'rgb(var(--color-typography-50)/<alpha-value>)',
100: 'rgb(var(--color-typography-100)/<alpha-value>)',
200: 'rgb(var(--color-typography-200)/<alpha-value>)',
300: 'rgb(var(--color-typography-300)/<alpha-value>)',
400: 'rgb(var(--color-typography-400)/<alpha-value>)',
500: 'rgb(var(--color-typography-500)/<alpha-value>)',
600: 'rgb(var(--color-typography-600)/<alpha-value>)',
700: 'rgb(var(--color-typography-700)/<alpha-value>)',
800: 'rgb(var(--color-typography-800)/<alpha-value>)',
900: 'rgb(var(--color-typography-900)/<alpha-value>)',
950: 'rgb(var(--color-typography-950)/<alpha-value>)',
white: '#FFFFFF',
gray: '#D4D4D4',
black: '#181718',
},
outline: {
0: 'rgb(var(--color-outline-0)/<alpha-value>)',
50: 'rgb(var(--color-outline-50)/<alpha-value>)',
100: 'rgb(var(--color-outline-100)/<alpha-value>)',
200: 'rgb(var(--color-outline-200)/<alpha-value>)',
300: 'rgb(var(--color-outline-300)/<alpha-value>)',
400: 'rgb(var(--color-outline-400)/<alpha-value>)',
500: 'rgb(var(--color-outline-500)/<alpha-value>)',
600: 'rgb(var(--color-outline-600)/<alpha-value>)',
700: 'rgb(var(--color-outline-700)/<alpha-value>)',
800: 'rgb(var(--color-outline-800)/<alpha-value>)',
900: 'rgb(var(--color-outline-900)/<alpha-value>)',
950: 'rgb(var(--color-outline-950)/<alpha-value>)',
},
background: {
0: 'rgb(var(--color-background-0)/<alpha-value>)',
50: 'rgb(var(--color-background-50)/<alpha-value>)',
100: 'rgb(var(--color-background-100)/<alpha-value>)',
200: 'rgb(var(--color-background-200)/<alpha-value>)',
300: 'rgb(var(--color-background-300)/<alpha-value>)',
400: 'rgb(var(--color-background-400)/<alpha-value>)',
500: 'rgb(var(--color-background-500)/<alpha-value>)',
600: 'rgb(var(--color-background-600)/<alpha-value>)',
700: 'rgb(var(--color-background-700)/<alpha-value>)',
800: 'rgb(var(--color-background-800)/<alpha-value>)',
900: 'rgb(var(--color-background-900)/<alpha-value>)',
950: 'rgb(var(--color-background-950)/<alpha-value>)',
error: 'rgb(var(--color-background-error)/<alpha-value>)',
warning: 'rgb(var(--color-background-warning)/<alpha-value>)',
muted: 'rgb(var(--color-background-muted)/<alpha-value>)',
success: 'rgb(var(--color-background-success)/<alpha-value>)',
info: 'rgb(var(--color-background-info)/<alpha-value>)',
light: '#FBFBFB',
dark: '#181719',
},
indicator: {
primary: 'rgb(var(--color-indicator-primary)/<alpha-value>)',
info: 'rgb(var(--color-indicator-info)/<alpha-value>)',
error: 'rgb(var(--color-indicator-error)/<alpha-value>)',
},
},
fontFamily: {
'heading': undefined,
'body': undefined,
'mono': undefined,
'jakarta': ['var(--font-plus-jakarta-sans)'],
'roboto': ['var(--font-roboto)'],
'code': ['var(--font-source-code-pro)'],
'inter': ['var(--font-inter)'],
'space-mono': ['var(--font-space-mono)'],
},
fontWeight: {
extrablack: '950',
},
fontSize: {
'2xs': '10px',
},
boxShadow: {
'hard-1': '-2px 2px 8px 0px rgba(38, 38, 38, 0.20)',
'hard-2': '0px 3px 10px 0px rgba(38, 38, 38, 0.20)',
'hard-3': '2px 2px 8px 0px rgba(38, 38, 38, 0.20)',
'hard-4': '0px -3px 10px 0px rgba(38, 38, 38, 0.20)',
'hard-5': '0px 2px 10px 0px rgba(38, 38, 38, 0.10)',
'soft-1': '0px 0px 10px rgba(38, 38, 38, 0.1)',
'soft-2': '0px 0px 20px rgba(38, 38, 38, 0.2)',
'soft-3': '0px 0px 30px rgba(38, 38, 38, 0.1)',
'soft-4': '0px 0px 40px rgba(38, 38, 38, 0.1)',
},
},
},
}
+3 -2
View File
@@ -12,6 +12,7 @@
"**/*.ts",
"**/*.tsx",
".expo/types/**/*.ts",
"expo-env.d.ts"
"expo-env.d.ts",
"nativewind-env.d.ts"
]
}
}
+3
View File
@@ -0,0 +1,3 @@
declare namespace App {
type ThemeScheme = 'light' | 'dark' | 'auto'
}
+5
View File
@@ -0,0 +1,5 @@
export {}
declare global {
type ThemeScheme = 'light' | 'dark' | 'auto'
}
@@ -64,6 +64,8 @@ async function loadModLevelList() {
soption: '',
}).then((res) => {
modLevelList.value = res.data
console.log(modLevelList.value)
})
}
@@ -134,6 +136,7 @@ async function queryList(pageNo: number, pageSize: number) {
if (mlvSearch) {
sql = `${sql} and (${mlvSearch})`
}
if (searchValue.value) {
sql = `${sql} and b_id like '%${searchValue.value}%'`
}
@@ -148,6 +151,7 @@ async function queryList(pageNo: number, pageSize: number) {
})
.then((res) => {
const { rows, total } = res.data
console.log(rows)
let idList = []
rows.forEach((row) => {
@@ -186,6 +190,7 @@ async function queryList(pageNo: number, pageSize: number) {
row.b_module_id = bModuleId
}
for (const modLevel of modLevelList.value) {
if (modLevel.b_module_id === bModuleId) {
slevel = modLevel.b_level
@@ -202,6 +207,7 @@ async function queryList(pageNo: number, pageSize: number) {
}
}
}
if (canbh || cansh) {
if (cansh) {
row.tongGuoButton = true
@@ -219,7 +225,7 @@ async function queryList(pageNo: number, pageSize: number) {
}
})
if (rows) {
if (rows2) {
pagingRef.value.complete(rows2)
}
})
@@ -197,17 +197,18 @@ function formatDateTime(dateTime: string): string {
<LoadingPage v-if="loading" />
<view v-else-if="stepsData.length > 0" class="">
<view style="display: flex;align-items: center;">
<text style="font-size: 28rpx; margin-right: 8rpx;">
当前进度 -
<view style="display: flex;flex-direction:column;align-items: center;justify-content: center;width: 100%;">
<text style="font-size: 28rpx;margin-bottom: 16rpx;font-weight: 500;">
当前进度
</text>
<view style="flex:1">
<view style="flex:1;width: 100%;">
<sar-progress-bar
:percent="computedProgress"
inside
striped
animated
thickness="38rpx"
/>
</view>
</view>
+1 -1
View File
@@ -1,3 +1,3 @@
{
"g3hd": "http://192.168.1.137:8084/g3hy2024-app/h5"
"g3hd": "http://192.168.1.137:8085/g3hy2024-app/h5"
}
+2 -2
View File
@@ -1,6 +1,6 @@
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
import { ref } from 'vue'
import { getModuleQxApi } from '@/request/api'
import { useUserStore } from './user'
@@ -12,7 +12,7 @@ const PERMISSION_PATH_MAP = [
{
moduleId: 'car_daily',
path: '/pages-sub/baoxiao',
}
},
]
export const usePermissionStore = defineStore(