1.0.1
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"pomdtr.excalidraw-editor",
|
||||||
|
"editorconfig.editorconfig",
|
||||||
|
"lokalise.i18n-ally"
|
||||||
|
]
|
||||||
|
}
|
|
@ -1,3 +1,3 @@
|
||||||
# good-news
|
# good-news
|
||||||
|
|
||||||
云桌面喜报管理系统
|
云桌面喜报管理系统,实现用户上传喜报照片,自动解析照片中的关键内容,比如项目名称、签单办事处、签单点数等。
|
|
@ -0,0 +1 @@
|
||||||
|
./hello.sh
|
|
@ -0,0 +1,41 @@
|
||||||
|
module goodnews
|
||||||
|
|
||||||
|
go 1.20
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/gin-gonic/gin v1.9.1
|
||||||
|
github.com/google/uuid v1.3.0
|
||||||
|
github.com/otiai10/gosseract/v2 v2.4.1
|
||||||
|
gorm.io/driver/sqlite v1.5.2
|
||||||
|
gorm.io/gorm v1.25.2
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/bytedance/sonic v1.9.1 // indirect
|
||||||
|
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||||
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
|
github.com/go-playground/validator/v10 v10.14.0 // indirect
|
||||||
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
|
||||||
|
github.com/leodido/go-urn v1.2.4 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.17 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.0.8 // indirect
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
|
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||||
|
golang.org/x/arch v0.3.0 // indirect
|
||||||
|
golang.org/x/crypto v0.9.0 // indirect
|
||||||
|
golang.org/x/net v0.10.0 // indirect
|
||||||
|
golang.org/x/sys v0.8.0 // indirect
|
||||||
|
golang.org/x/text v0.9.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.30.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
|
@ -0,0 +1,100 @@
|
||||||
|
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||||
|
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
|
||||||
|
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
|
||||||
|
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||||
|
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
|
||||||
|
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||||
|
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/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
||||||
|
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||||
|
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||||
|
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
||||||
|
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
||||||
|
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||||
|
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.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
|
||||||
|
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||||
|
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||||
|
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
|
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||||
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||||
|
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
|
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||||
|
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
|
||||||
|
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||||
|
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
||||||
|
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||||
|
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
|
github.com/otiai10/gosseract/v2 v2.4.1 h1:G8AyBpXEeSlcq8TI85LH/pM5SXk8Djy2GEXisgyblRw=
|
||||||
|
github.com/otiai10/gosseract/v2 v2.4.1/go.mod h1:1gNWP4Hgr2o7yqWfs6r5bZxAatjOIdqWxJLWsTsembk=
|
||||||
|
github.com/otiai10/mint v1.6.3 h1:87qsV/aw1F5as1eH1zS/yqHY85ANKVMgkDrf9rcxbQs=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
|
||||||
|
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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
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.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
|
||||||
|
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
|
github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
|
||||||
|
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||||
|
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||||
|
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
||||||
|
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||||
|
golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g=
|
||||||
|
golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0=
|
||||||
|
golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
|
||||||
|
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||||
|
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
|
||||||
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
|
||||||
|
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
|
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
|
||||||
|
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
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=
|
||||||
|
gorm.io/driver/sqlite v1.5.2 h1:TpQ+/dqCY4uCigCFyrfnrJnrW9zjpelWVoEVNy5qJkc=
|
||||||
|
gorm.io/driver/sqlite v1.5.2/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4=
|
||||||
|
gorm.io/gorm v1.25.2 h1:gs1o6Vsa+oVKG/a9ElL3XgyGfghFfkKA2SInQaCyMho=
|
||||||
|
gorm.io/gorm v1.25.2/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
|
||||||
|
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
|
@ -0,0 +1,5 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
while :; do
|
||||||
|
{ echo -ne "HTTP/1.1 200 OK\r\nContent-Length: $(echo -n "Hello, World!")\r\n\r\nHello, World!"; } | nc -l -p 8080 -q 1
|
||||||
|
done
|
|
@ -0,0 +1,259 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"gorm.io/driver/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"goodnews/models"
|
||||||
|
"goodnews/services"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 全局数据库连接
|
||||||
|
var db *gorm.DB
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// 初始化数据库
|
||||||
|
var err error
|
||||||
|
db, err = gorm.Open(sqlite.Open("goodnews.db"), &gorm.Config{})
|
||||||
|
if err != nil {
|
||||||
|
panic("failed to connect database")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自动迁移数据库
|
||||||
|
db.AutoMigrate(&models.GoodNews{})
|
||||||
|
|
||||||
|
// 初始化Gin路由
|
||||||
|
r := gin.Default()
|
||||||
|
|
||||||
|
// 设置静态文件服务
|
||||||
|
r.Static("/uploads", "./uploads")
|
||||||
|
// 设置页面路由
|
||||||
|
r.StaticFile("/", "./templates/index.html")
|
||||||
|
r.StaticFile("/manage", "./templates/manage.html")
|
||||||
|
|
||||||
|
// API路由组
|
||||||
|
api := r.Group("/api")
|
||||||
|
{
|
||||||
|
api.GET("/news", getGoodNewsList)
|
||||||
|
api.PUT("/news/:id", updateGoodNews)
|
||||||
|
api.DELETE("/news/:id", deleteGoodNews)
|
||||||
|
api.POST("/upload", handleUploadAndCreate)
|
||||||
|
api.GET("/offices", getOfficesList)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 启动服务
|
||||||
|
r.Run(":8080")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取喜报列表
|
||||||
|
func getGoodNewsList(c *gin.Context) {
|
||||||
|
var goodNewsList []models.GoodNews
|
||||||
|
result := db.Find(&goodNewsList)
|
||||||
|
if result.Error != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": result.Error.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理每条记录,添加图片URL并设置Office字段
|
||||||
|
for i := range goodNewsList {
|
||||||
|
// 将ImagePath转换为可访问的URL
|
||||||
|
goodNewsList[i].ImageURL = "/" + goodNewsList[i].ImagePath
|
||||||
|
// 确保Office字段与Representative字段一致
|
||||||
|
goodNewsList[i].Office = goodNewsList[i].Representative
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, goodNewsList)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取代表处列表
|
||||||
|
func getOfficesList(c *gin.Context) {
|
||||||
|
var offices []string
|
||||||
|
result := db.Model(&models.GoodNews{}).Distinct().Pluck("representative", &offices)
|
||||||
|
if result.Error != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": result.Error.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, offices)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理文件上传并创建喜报
|
||||||
|
func handleUploadAndCreate(c *gin.Context) {
|
||||||
|
// 获取上传的图片
|
||||||
|
file, err := c.FormFile("file")
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "请选择要上传的图片文件"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证文件类型
|
||||||
|
ext := filepath.Ext(file.Filename)
|
||||||
|
allowedExts := map[string]bool{".jpg": true, ".jpeg": true, ".png": true}
|
||||||
|
if !allowedExts[strings.ToLower(ext)] {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "只支持jpg、jpeg和png格式的图片"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证文件大小(限制为10MB)
|
||||||
|
if file.Size > 10<<20 { // 10 MB
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "图片大小不能超过10MB"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成唯一的文件名
|
||||||
|
newFileName := uuid.New().String() + ext
|
||||||
|
uploadDir := filepath.Join(".", "uploads")
|
||||||
|
filePath := filepath.Join(uploadDir, newFileName)
|
||||||
|
|
||||||
|
// 确保上传目录存在
|
||||||
|
if err := os.MkdirAll(uploadDir, 0755); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("创建上传目录失败: %v", err)})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存图片
|
||||||
|
if err := c.SaveUploadedFile(file, filePath); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("图片保存失败: %v", err)})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建OCR服务
|
||||||
|
ocrService, err := services.NewOCRService()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "OCR服务初始化失败"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer ocrService.Close()
|
||||||
|
|
||||||
|
// 提取图片信息
|
||||||
|
projectName, points, representative, err := ocrService.ExtractInfo(filePath)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("图片识别失败: %v", err)})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("OCR识别信息:", projectName, points, representative)
|
||||||
|
// 创建喜报记录
|
||||||
|
goodNews := models.GoodNews{
|
||||||
|
ProjectName: projectName,
|
||||||
|
Points: points,
|
||||||
|
Representative: representative,
|
||||||
|
ImagePath: filePath,
|
||||||
|
}
|
||||||
|
|
||||||
|
result := db.Create(&goodNews)
|
||||||
|
if result.Error != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": result.Error.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新获取记录以确保包含ID
|
||||||
|
db.First(&goodNews, goodNews.ID)
|
||||||
|
goodNews.ImageURL = "/" + goodNews.ImagePath
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, goodNews)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新喜报
|
||||||
|
func updateGoodNews(c *gin.Context) {
|
||||||
|
var goodNews models.GoodNews
|
||||||
|
if err := db.First(&goodNews, c.Param("id")).Error; err != nil {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "记录不存在"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.ShouldBindJSON(&goodNews); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
db.Save(&goodNews)
|
||||||
|
c.JSON(http.StatusOK, goodNews)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除喜报
|
||||||
|
func deleteGoodNews(c *gin.Context) {
|
||||||
|
var goodNews models.GoodNews
|
||||||
|
if err := db.First(&goodNews, c.Param("id")).Error; err != nil {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "记录不存在"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
db.Delete(&goodNews)
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "删除成功"})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理文件上传
|
||||||
|
func handleFileUpload(c *gin.Context) {
|
||||||
|
// 获取上传的文件
|
||||||
|
file, err := c.FormFile("file")
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "请选择要上传的文件"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证文件类型
|
||||||
|
ext := filepath.Ext(file.Filename)
|
||||||
|
allowedExts := map[string]bool{".jpg": true, ".jpeg": true, ".png": true}
|
||||||
|
if !allowedExts[strings.ToLower(ext)] {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "只支持jpg、jpeg和png格式的图片"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成唯一的文件名
|
||||||
|
newFileName := uuid.New().String() + ext
|
||||||
|
uploadDir := filepath.Join(".", "uploads")
|
||||||
|
filePath := filepath.Join(uploadDir, newFileName)
|
||||||
|
|
||||||
|
// 确保上传目录存在
|
||||||
|
if err := os.MkdirAll(uploadDir, 0755); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("创建上传目录失败: %v", err)})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存图片
|
||||||
|
if err := c.SaveUploadedFile(file, filePath); err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("图片保存失败: %v", err)})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建OCR服务
|
||||||
|
ocrService, err := services.NewOCRService()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "OCR服务初始化失败"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer ocrService.Close()
|
||||||
|
|
||||||
|
// 提取图片信息
|
||||||
|
projectName, points, representative, err := ocrService.ExtractInfo(filePath)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("图片识别失败: %v", err)})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建喜报记录
|
||||||
|
goodNews := models.GoodNews{
|
||||||
|
ProjectName: projectName,
|
||||||
|
Points: points,
|
||||||
|
Representative: representative,
|
||||||
|
ImagePath: filePath,
|
||||||
|
}
|
||||||
|
|
||||||
|
result := db.Create(&goodNews)
|
||||||
|
if result.Error != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": result.Error.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新获取记录以确保包含ID
|
||||||
|
db.First(&goodNews, goodNews.ID)
|
||||||
|
goodNews.ImageURL = "/" + goodNews.ImagePath
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, goodNews)
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GoodNews 喜报数据模型
|
||||||
|
type GoodNews struct {
|
||||||
|
gorm.Model
|
||||||
|
ID uint `json:"id" gorm:"primarykey"`
|
||||||
|
ProjectName string `json:"project_name"`
|
||||||
|
Points int `json:"points"`
|
||||||
|
Representative string `json:"representative"`
|
||||||
|
ImagePath string `json:"image_path"`
|
||||||
|
Office string `json:"office"`
|
||||||
|
UploadTime time.Time `json:"upload_time" gorm:"autoCreateTime"`
|
||||||
|
ImageURL string `json:"image_url" gorm:"-"`
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
# 功能说明
|
||||||
|
## 页面规划
|
||||||
|
本项目有两个页面
|
||||||
|
+ 首页展示数据库中已有的喜报图片,采用流式布局,每行3个,自动适配手机。
|
||||||
|
+ 瀑布流展示数据库中的喜报图片,默认按时间倒序展示,可以按月份、所属代表处进行过滤。
|
||||||
|
+ 瀑布流展示数据库中的喜报图片,点击图片后可查看原图。
|
||||||
|
+ 管理页面以列表形式展现数据库记录,数据库字段包括:id,项目名称,下单点数,所属代表处,上传时间,图片位置等。 提供修改和删除功能。
|
||||||
|
+ 管理页面上提供上传按钮,上传喜报图片,服务端解析图片后自动生成新一条的数据库记录。
|
||||||
|
|
||||||
|
# 技术栈
|
||||||
|
+ 本项目后端采用golang+gin
|
||||||
|
+ 前端采用element-ui
|
||||||
|
+ 数据库采用sqlite
|
||||||
|
+ 图片存储采用七牛云
|
||||||
|
+ 部署采用docker
|
|
@ -0,0 +1,88 @@
|
||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/otiai10/gosseract/v2"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// OCRService 提供OCR文字识别服务
|
||||||
|
type OCRService struct {
|
||||||
|
client *gosseract.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewOCRService 创建新的OCR服务实例
|
||||||
|
func NewOCRService() (*OCRService, error) {
|
||||||
|
client := gosseract.NewClient()
|
||||||
|
return &OCRService{client: client}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close 关闭OCR服务
|
||||||
|
func (s *OCRService) Close() {
|
||||||
|
s.client.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExtractInfo 从图片中提取喜报信息
|
||||||
|
func (s *OCRService) ExtractInfo(imagePath string) (string, int, string, error) {
|
||||||
|
// 设置中文语言包
|
||||||
|
err := s.client.SetLanguage("chi_sim")
|
||||||
|
if err != nil {
|
||||||
|
return "", 0, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置图片
|
||||||
|
err = s.client.SetImage(imagePath)
|
||||||
|
if err != nil {
|
||||||
|
return "", 0, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取文本
|
||||||
|
text, err := s.client.Text()
|
||||||
|
if err != nil {
|
||||||
|
return "", 0, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取项目名称
|
||||||
|
projectName := extractProjectName(text)
|
||||||
|
|
||||||
|
// 提取点数
|
||||||
|
points := extractPoints(text)
|
||||||
|
|
||||||
|
// 提取代表处
|
||||||
|
representative := extractRepresentative(text)
|
||||||
|
|
||||||
|
return projectName, points, representative, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取项目名称
|
||||||
|
func extractProjectName(text string) string {
|
||||||
|
lines := strings.Split(text, "\n")
|
||||||
|
for _, line := range lines {
|
||||||
|
if strings.Contains(line, "项目") {
|
||||||
|
return strings.TrimSpace(line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取点数
|
||||||
|
func extractPoints(text string) int {
|
||||||
|
re := regexp.MustCompile(`(\d+)\s*点|points?`)
|
||||||
|
matches := re.FindStringSubmatch(text)
|
||||||
|
if len(matches) > 1 {
|
||||||
|
points, _ := strconv.Atoi(matches[1])
|
||||||
|
return points
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取代表处
|
||||||
|
func extractRepresentative(text string) string {
|
||||||
|
re := regexp.MustCompile(`([^\s]+代表处)`)
|
||||||
|
matches := re.FindStringSubmatch(text)
|
||||||
|
if len(matches) > 1 {
|
||||||
|
return matches[1]
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
|
@ -0,0 +1,54 @@
|
||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestOCRService_ExtractInfo(t *testing.T) {
|
||||||
|
// 创建OCR服务实例
|
||||||
|
ocr, err := NewOCRService()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create OCR service: %v", err)
|
||||||
|
}
|
||||||
|
defer ocr.Close()
|
||||||
|
|
||||||
|
// 测试用例
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
imagePath string
|
||||||
|
wantProject string
|
||||||
|
wantPoints int
|
||||||
|
wantRep string
|
||||||
|
wantErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "test_image",
|
||||||
|
imagePath: "../uploads/test_image.jpg",
|
||||||
|
// 预期结果将根据实际测试图片调整
|
||||||
|
wantProject: "乌省旗信创云桌面项目",
|
||||||
|
wantPoints: 360,
|
||||||
|
wantRep: "内蒙代表处",
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// 运行测试用例
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
project, points, rep, err := ocr.ExtractInfo(tt.imagePath)
|
||||||
|
if (err != nil) != tt.wantErr {
|
||||||
|
t.Errorf("ExtractInfo() error = %v, wantErr %v", err, tt.wantErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if project != tt.wantProject {
|
||||||
|
t.Errorf("ExtractInfo() project = %v, want %v", project, tt.wantProject)
|
||||||
|
}
|
||||||
|
if points != tt.wantPoints {
|
||||||
|
t.Errorf("ExtractInfo() points = %v, want %v", points, tt.wantPoints)
|
||||||
|
}
|
||||||
|
if rep != tt.wantRep {
|
||||||
|
t.Errorf("ExtractInfo() representative = %v, want %v", rep, tt.wantRep)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,284 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>喜报管理系统</title>
|
||||||
|
<link rel="stylesheet" href="//unpkg.com/element-plus/dist/index.css">
|
||||||
|
<script src="//unpkg.com/vue@3"></script>
|
||||||
|
<script src="//unpkg.com/element-plus"></script>
|
||||||
|
<style>
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.waterfall {
|
||||||
|
column-count: 3;
|
||||||
|
column-gap: 20px;
|
||||||
|
}
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.waterfall {
|
||||||
|
column-count: 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.waterfall {
|
||||||
|
column-count: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.waterfall-item {
|
||||||
|
break-inside: avoid;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.waterfall-item img {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 0.3s;
|
||||||
|
}
|
||||||
|
.waterfall-item img:hover {
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.filter-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app">
|
||||||
|
<el-container>
|
||||||
|
<el-main>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<el-row :gutter="20" justify="space-between" align="middle">
|
||||||
|
<el-col :span="16">
|
||||||
|
<h1>喜报管理系统</h1>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="8" style="text-align: right;">
|
||||||
|
<el-button type="primary" @click="handleManageClick">管理喜报</el-button>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="filter-section">
|
||||||
|
<el-row :gutter="20">
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="filterMonth"
|
||||||
|
type="month"
|
||||||
|
placeholder="选择月份"
|
||||||
|
format="YYYY年MM月"
|
||||||
|
value-format="YYYY-MM"
|
||||||
|
@change="handleFilterChange">
|
||||||
|
</el-date-picker>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="8">
|
||||||
|
<el-select v-model="filterOffice" placeholder="选择代表处" @change="handleFilterChange" clearable>
|
||||||
|
<el-option
|
||||||
|
v-for="office in offices"
|
||||||
|
:key="office"
|
||||||
|
:label="office"
|
||||||
|
:value="office">
|
||||||
|
</el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="8" style="text-align: right;">
|
||||||
|
<el-button @click="resetFilters">重置筛选</el-button>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="waterfall">
|
||||||
|
<div v-for="item in filteredNews" :key="item.id" class="waterfall-item">
|
||||||
|
<el-card>
|
||||||
|
<img :src="item.image_url" @click="showImageDetail(item)">
|
||||||
|
<div style="padding: 10px;">
|
||||||
|
<h3>{{ item.project_name }}</h3>
|
||||||
|
<p>签单点数: {{ item.points }}</p>
|
||||||
|
<p>代表处: {{ item.office }}</p>
|
||||||
|
<p>上传时间: {{ formatDate(item.upload_time) }}</p>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- 图片详情对话框 -->
|
||||||
|
<el-dialog v-model="imageDialogVisible" width="80%">
|
||||||
|
<img :src="currentImage.image_url" style="width: 100%;">
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="imageDialogVisible = false">关闭</el-button>
|
||||||
|
<el-button type="primary" @click="editNews(currentImage)">编辑</el-button>
|
||||||
|
<el-button type="danger" @click="deleteNews(currentImage)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 编辑对话框 -->
|
||||||
|
<el-dialog v-model="editDialogVisible" title="编辑喜报信息" width="500px">
|
||||||
|
<el-form :model="editForm" label-width="100px">
|
||||||
|
<el-form-item label="项目名称">
|
||||||
|
<el-input v-model="editForm.project_name"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="签单点数">
|
||||||
|
<el-input-number v-model="editForm.points"></el-input-number>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="代表处">
|
||||||
|
<el-select v-model="editForm.office">
|
||||||
|
<el-option
|
||||||
|
v-for="office in offices"
|
||||||
|
:key="office"
|
||||||
|
:label="office"
|
||||||
|
:value="office">
|
||||||
|
</el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="editDialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="submitEdit">确定</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</el-main>
|
||||||
|
</el-container>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const { createApp, ref, computed } = Vue
|
||||||
|
|
||||||
|
const app = createApp({
|
||||||
|
setup() {
|
||||||
|
const news = ref([])
|
||||||
|
const filterMonth = ref('')
|
||||||
|
const filterOffice = ref('')
|
||||||
|
const offices = ref([])
|
||||||
|
const imageDialogVisible = ref(false)
|
||||||
|
const editDialogVisible = ref(false)
|
||||||
|
const currentImage = ref({})
|
||||||
|
const editForm = ref({})
|
||||||
|
|
||||||
|
const filteredNews = computed(() => {
|
||||||
|
return news.value.filter(item => {
|
||||||
|
const monthMatch = !filterMonth.value || item.upload_time.startsWith(filterMonth.value)
|
||||||
|
const officeMatch = !filterOffice.value || item.office === filterOffice.value
|
||||||
|
return monthMatch && officeMatch
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleManageClick = () => {
|
||||||
|
window.location.href = '/manage'
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchNews = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/news')
|
||||||
|
const data = await response.json()
|
||||||
|
news.value = data
|
||||||
|
// 提取所有代表处
|
||||||
|
offices.value = [...new Set(data.map(item => item.office))]
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('获取数据失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFilterChange = () => {
|
||||||
|
// 筛选会自动通过computed属性完成
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetFilters = () => {
|
||||||
|
filterMonth.value = ''
|
||||||
|
filterOffice.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const showImageDetail = (item) => {
|
||||||
|
currentImage.value = item
|
||||||
|
imageDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const editNews = (item) => {
|
||||||
|
editForm.value = { ...item }
|
||||||
|
imageDialogVisible.value = false
|
||||||
|
editDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitEdit = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/news/${editForm.value.id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(editForm.value)
|
||||||
|
})
|
||||||
|
if (response.ok) {
|
||||||
|
ElMessage.success('更新成功')
|
||||||
|
editDialogVisible.value = false
|
||||||
|
fetchNews()
|
||||||
|
} else {
|
||||||
|
throw new Error('更新失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('更新失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteNews = async (item) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/news/${item.id}`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
})
|
||||||
|
if (response.ok) {
|
||||||
|
ElMessage.success('删除成功')
|
||||||
|
imageDialogVisible.value = false
|
||||||
|
fetchNews()
|
||||||
|
} else {
|
||||||
|
throw new Error('删除失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('删除失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatDate = (dateString) => {
|
||||||
|
if (!dateString) return ''
|
||||||
|
const date = new Date(dateString)
|
||||||
|
return `${date.getFullYear()}年${date.getMonth() + 1}月${date.getDate()}日`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化时获取数据
|
||||||
|
fetchNews()
|
||||||
|
|
||||||
|
return {
|
||||||
|
news,
|
||||||
|
filterMonth,
|
||||||
|
filterOffice,
|
||||||
|
offices,
|
||||||
|
imageDialogVisible,
|
||||||
|
editDialogVisible,
|
||||||
|
currentImage,
|
||||||
|
editForm,
|
||||||
|
filteredNews,
|
||||||
|
handleManageClick,
|
||||||
|
handleFilterChange,
|
||||||
|
resetFilters,
|
||||||
|
showImageDetail,
|
||||||
|
editNews,
|
||||||
|
submitEdit,
|
||||||
|
deleteNews,
|
||||||
|
formatDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
app.use(ElementPlus)
|
||||||
|
app.mount('#app')
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,343 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>喜报管理系统 - 管理页面</title>
|
||||||
|
<link rel="stylesheet" href="//unpkg.com/element-plus/dist/index.css">
|
||||||
|
<script src="//unpkg.com/vue@3"></script>
|
||||||
|
<script src="//unpkg.com/element-plus"></script>
|
||||||
|
<script src="//unpkg.com/@element-plus/icons-vue"></script>
|
||||||
|
<style>
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
margin-bottom: 30px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
padding-bottom: 20px;
|
||||||
|
}
|
||||||
|
.table-operations {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
.el-table {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.el-pagination {
|
||||||
|
margin-top: 20px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.el-dialog__body {
|
||||||
|
padding: 30px 20px;
|
||||||
|
}
|
||||||
|
.upload-demo {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app">
|
||||||
|
<el-container>
|
||||||
|
<el-main>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<el-row :gutter="20" justify="space-between" align="middle">
|
||||||
|
<el-col :span="16">
|
||||||
|
<h1>喜报管理系统 - 管理页面</h1>
|
||||||
|
</el-col>
|
||||||
|
<el-col :span="8" style="text-align: right;">
|
||||||
|
<el-button @click="goToIndex" style="margin-right: 10px">返回首页</el-button>
|
||||||
|
<el-button type="primary" @click="showUploadDialog">上传喜报</el-button>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-table :data="paginatedNews" style="width: 100%" v-loading="loading">
|
||||||
|
<el-table-column prop="id" label="ID" width="80"></el-table-column>
|
||||||
|
<el-table-column prop="project_name" label="项目名称" width="250"></el-table-column>
|
||||||
|
<el-table-column prop="points" label="签单点数" width="120"></el-table-column>
|
||||||
|
<el-table-column prop="office" label="代表处" width="150"></el-table-column>
|
||||||
|
<el-table-column prop="upload_time" label="上传时间" width="180">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ formatDate(scope.row.upload_time) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="操作" width="200">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button size="small" @click="showEditDialog(scope.row)">编辑</el-button>
|
||||||
|
<el-button size="small" type="danger" @click="handleDelete(scope.row)">删除</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<el-pagination
|
||||||
|
v-model:current-page="currentPage"
|
||||||
|
v-model:page-size="pageSize"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
layout="total, sizes, prev, pager, next, jumper"
|
||||||
|
:total="newsList.length"
|
||||||
|
@size-change="handleSizeChange"
|
||||||
|
@current-change="handleCurrentChange"
|
||||||
|
background
|
||||||
|
class="pagination"> </el-pagination>
|
||||||
|
|
||||||
|
<el-dialog v-model="uploadDialogVisible" title="上传喜报" width="500px">
|
||||||
|
<el-upload
|
||||||
|
class="upload-demo"
|
||||||
|
drag
|
||||||
|
action="/api/upload"
|
||||||
|
:on-success="handleUploadSuccess"
|
||||||
|
:on-error="handleUploadError">
|
||||||
|
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
|
||||||
|
<div class="el-upload__text">拖拽文件到此处或 <em>点击上传</em></div>
|
||||||
|
</el-upload>
|
||||||
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button @click="uploadDialogVisible = false">取消</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<!-- 编辑对话框 -->
|
||||||
|
<el-dialog v-model="editDialogVisible" title="编辑喜报信息" width="500px">
|
||||||
|
<el-form :model="editForm" label-width="100px">
|
||||||
|
<el-form-item label="项目名称">
|
||||||
|
<el-input v-model="editForm.project_name"></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="签单点数">
|
||||||
|
<el-input-number v-model="editForm.points" :min="0"></el-input-number>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="代表处">
|
||||||
|
<el-select v-model="editForm.office" placeholder="选择代表处">
|
||||||
|
<el-option
|
||||||
|
v-for="office in offices"
|
||||||
|
:key="office"
|
||||||
|
:label="office"
|
||||||
|
:value="office">
|
||||||
|
</el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<span class="dialog-footer">
|
||||||
|
<el-button @click="editDialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" @click="handleEdit">确定</el-button>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</div>
|
||||||
|
</el-main>
|
||||||
|
|
||||||
|
</el-container>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const { createApp, ref, computed } = Vue
|
||||||
|
const app = createApp({
|
||||||
|
setup() {
|
||||||
|
// 导入所需的图标组件
|
||||||
|
const { UploadFilled } = ElementPlusIconsVue
|
||||||
|
// 确保返回UploadFilled组件以供模板使用
|
||||||
|
const newsList = ref([])
|
||||||
|
const offices = ref([])
|
||||||
|
const uploadDialogVisible = ref(false)
|
||||||
|
const editDialogVisible = ref(false)
|
||||||
|
const loading = ref(false)
|
||||||
|
const currentPage = ref(1)
|
||||||
|
const pageSize = ref(10)
|
||||||
|
const editForm = ref({
|
||||||
|
id: null,
|
||||||
|
project_name: '',
|
||||||
|
points: 0,
|
||||||
|
office: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 分页数据
|
||||||
|
const paginatedNews = computed(() => {
|
||||||
|
const start = (currentPage.value - 1) * pageSize.value
|
||||||
|
const end = start + pageSize.value
|
||||||
|
return newsList.value.slice(start, end)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 处理每页显示数量变化
|
||||||
|
const handleSizeChange = (val) => {
|
||||||
|
pageSize.value = val
|
||||||
|
currentPage.value = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理页码变化
|
||||||
|
const handleCurrentChange = (val) => {
|
||||||
|
currentPage.value = val
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回首页
|
||||||
|
const goToIndex = () => {
|
||||||
|
window.location.href = '/'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取所有喜报数据
|
||||||
|
const fetchNews = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/news')
|
||||||
|
const data = await response.json()
|
||||||
|
newsList.value = data
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取数据失败:', error)
|
||||||
|
ElementPlus.ElMessage.error('获取数据失败')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取代表处列表
|
||||||
|
const fetchOffices = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/offices')
|
||||||
|
const data = await response.json()
|
||||||
|
offices.value = data
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取代表处列表失败:', error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化日期
|
||||||
|
const formatDate = (dateStr) => {
|
||||||
|
const date = new Date(dateStr)
|
||||||
|
return date.toLocaleString('zh-CN', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示上传对话框
|
||||||
|
const showUploadDialog = () => {
|
||||||
|
uploadDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示编辑对话框
|
||||||
|
const showEditDialog = (row) => {
|
||||||
|
editForm.value = { ...row }
|
||||||
|
editDialogVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理上传成功
|
||||||
|
const handleUploadSuccess = () => {
|
||||||
|
ElementPlus.ElMessage.success('上传成功')
|
||||||
|
uploadDialogVisible.value = false
|
||||||
|
fetchNews()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理上传失败
|
||||||
|
const handleUploadError = () => {
|
||||||
|
ElementPlus.ElMessage.error('上传失败')
|
||||||
|
uploadDialogVisible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理编辑
|
||||||
|
const handleEdit = async () => {
|
||||||
|
if (!editForm.value.id) {
|
||||||
|
ElementPlus.ElMessage.error('无效的记录ID')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/news/${editForm.value.id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(editForm.value)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
ElementPlus.ElMessage.success('更新成功')
|
||||||
|
editDialogVisible.value = false
|
||||||
|
fetchNews()
|
||||||
|
} else {
|
||||||
|
ElementPlus.ElMessage.error('更新失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('更新失败:', error)
|
||||||
|
ElementPlus.ElMessage.error('更新失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理删除
|
||||||
|
const handleDelete = async (row) => {
|
||||||
|
if (!row || !row.id) {
|
||||||
|
ElementPlus.ElMessage.error('无效的记录ID')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await ElementPlus.ElMessageBox.confirm(
|
||||||
|
'确定要删除这条记录吗?',
|
||||||
|
'警告',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const response = await fetch(`/api/news/${row.id}`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
ElementPlus.ElMessage.success('删除成功')
|
||||||
|
fetchNews()
|
||||||
|
} else {
|
||||||
|
ElementPlus.ElMessage.error('删除失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
console.error('删除失败:', error)
|
||||||
|
ElementPlus.ElMessage.error('删除失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化
|
||||||
|
fetchNews()
|
||||||
|
fetchOffices()
|
||||||
|
|
||||||
|
return {
|
||||||
|
newsList,
|
||||||
|
paginatedNews,
|
||||||
|
offices,
|
||||||
|
uploadDialogVisible,
|
||||||
|
editDialogVisible,
|
||||||
|
editForm,
|
||||||
|
loading,
|
||||||
|
currentPage,
|
||||||
|
pageSize,
|
||||||
|
formatDate,
|
||||||
|
showUploadDialog,
|
||||||
|
showEditDialog,
|
||||||
|
handleUploadSuccess,
|
||||||
|
handleUploadError,
|
||||||
|
handleEdit,
|
||||||
|
handleDelete,
|
||||||
|
handleSizeChange,
|
||||||
|
handleCurrentChange,
|
||||||
|
goToIndex,
|
||||||
|
UploadFilled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
||||||
|
app.component(key, component)
|
||||||
|
}
|
||||||
|
app.use(ElementPlus)
|
||||||
|
app.mount('#app')
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
After Width: | Height: | Size: 548 KiB |
After Width: | Height: | Size: 551 KiB |
After Width: | Height: | Size: 551 KiB |
After Width: | Height: | Size: 164 KiB |
After Width: | Height: | Size: 559 KiB |
After Width: | Height: | Size: 559 KiB |
After Width: | Height: | Size: 164 KiB |
After Width: | Height: | Size: 548 KiB |
After Width: | Height: | Size: 548 KiB |
After Width: | Height: | Size: 551 KiB |
After Width: | Height: | Size: 164 KiB |
After Width: | Height: | Size: 548 KiB |
After Width: | Height: | Size: 559 KiB |
After Width: | Height: | Size: 559 KiB |