1.0.1
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"recommendations": [
|
||||
"pomdtr.excalidraw-editor",
|
||||
"editorconfig.editorconfig",
|
||||
"lokalise.i18n-ally"
|
||||
]
|
||||
}
|
|
@ -1,3 +1,3 @@
|
|||
# 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 |