mirror of
https://github.com/fumiama/paper-manager.git
synced 2026-06-13 05:03:16 +08:00
implement genfile
This commit is contained in:
408
backend/global/analyze.go
Normal file
408
backend/global/analyze.go
Normal file
@@ -0,0 +1,408 @@
|
|||||||
|
package global
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/binary"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"image"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
sql "github.com/FloatTech/sqlite"
|
||||||
|
"github.com/corona10/goimagehash"
|
||||||
|
base14 "github.com/fumiama/go-base16384"
|
||||||
|
"github.com/fumiama/go-docx"
|
||||||
|
"github.com/fumiama/paper-manager/backend/utils"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AddFile from lst and copy it to analyzed path.
|
||||||
|
// The para reg must belong to a valid user
|
||||||
|
func (f *FileDatabase) AddFile(lstid int, reg *Regex, istemp bool, progress func(uint)) error {
|
||||||
|
user, err := UserDB.GetUserByID(reg.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !user.IsFileManager() && !istemp {
|
||||||
|
return ErrInvalidRole
|
||||||
|
}
|
||||||
|
progress(1)
|
||||||
|
f.mu.RLock()
|
||||||
|
lst, err := sql.Find[List](&f.db, FileTableList, "WHERE ID="+strconv.Itoa(lstid))
|
||||||
|
f.mu.RUnlock()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if lst.Path == "" || strings.Contains(lst.Path, "..") {
|
||||||
|
return os.ErrNotExist
|
||||||
|
}
|
||||||
|
tempath := lst.Path
|
||||||
|
docf, err := os.Open(tempath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer docf.Close()
|
||||||
|
progress(2)
|
||||||
|
h := md5.New()
|
||||||
|
_, err = io.Copy(h, docf)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var buf [md5.Size]byte
|
||||||
|
id := int64(binary.LittleEndian.Uint64(h.Sum(buf[:0])))
|
||||||
|
_, err = docf.Seek(0, io.SeekStart)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
stat, err := docf.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
sz := stat.Size()
|
||||||
|
progress(3)
|
||||||
|
doc, err := docx.Parse(docf, sz)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
progress(5)
|
||||||
|
doc.Document.Body.DropDrawingOf("NilPicture")
|
||||||
|
majorre, err := regexp.Compile(reg.Major)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
docs := doc.SplitByParagraph(docx.SplitDocxByPlainTextRegex(majorre))
|
||||||
|
if len(docs) < 2 {
|
||||||
|
return ErrMajorSplitsTooShort
|
||||||
|
}
|
||||||
|
progress(9)
|
||||||
|
// filling File struct
|
||||||
|
file := &File{
|
||||||
|
ID: id,
|
||||||
|
ListID: *lst.ID,
|
||||||
|
}
|
||||||
|
titlere, err := regexp.Compile(reg.Title)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
classre, err := regexp.Compile(reg.Class)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
opclre, err := regexp.Compile(reg.OpenCl)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
datere, err := regexp.Compile(reg.Date)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
timere, err := regexp.Compile(reg.Time)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ratere, err := regexp.Compile(reg.Rate)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
progress(10)
|
||||||
|
for _, it := range docs[0].Document.Body.Items {
|
||||||
|
if p, ok := it.(*docx.Paragraph); ok {
|
||||||
|
text := p.String()
|
||||||
|
title := titlere.FindStringSubmatch(text)
|
||||||
|
if len(title) >= 5 {
|
||||||
|
years, semesters, mfs, abs := title[1], title[2], title[3], title[4]
|
||||||
|
y, err := strconv.Atoi(years)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
file.Year = StudyYear(y)
|
||||||
|
if len(semesters) > 0 {
|
||||||
|
file.Type = file.Type.SetFirstSecond(semesters[0])
|
||||||
|
}
|
||||||
|
file.Type = file.Type.SetMiddleFinal(mfs)
|
||||||
|
if len(abs) > 0 {
|
||||||
|
file.Type = file.Type.SetAB(abs[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
class := classre.FindStringSubmatch(text)
|
||||||
|
if len(class) >= 3 {
|
||||||
|
file.Class = class[2]
|
||||||
|
}
|
||||||
|
opcl := opclre.FindStringSubmatch(text)
|
||||||
|
if len(opcl) >= 2 {
|
||||||
|
file.Type = file.Type.SetOpenClose(opcl[1])
|
||||||
|
}
|
||||||
|
date := datere.FindStringSubmatch(text)
|
||||||
|
if len(date) >= 4 {
|
||||||
|
y, m, d := date[1], date[2], date[3]
|
||||||
|
if y != "" && m != "" {
|
||||||
|
if d == "" {
|
||||||
|
d = "1"
|
||||||
|
}
|
||||||
|
yyyy, err := strconv.ParseUint(y, 10, 64)
|
||||||
|
if err == nil && yyyy > 1600 {
|
||||||
|
mm, err := strconv.ParseUint(m, 10, 64)
|
||||||
|
if err == nil && mm >= 1 && mm <= 12 {
|
||||||
|
dd, err := strconv.ParseUint(d, 10, 64)
|
||||||
|
if err == nil && dd >= 1 && dd <= 31 {
|
||||||
|
file.Date = uint32(yyyy*10000 + mm*100 + dd)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
times := timere.FindStringSubmatch(text)
|
||||||
|
if len(times) >= 2 {
|
||||||
|
min, err := strconv.Atoi(times[1])
|
||||||
|
if err == nil && min > 0 {
|
||||||
|
file.Time = time.Minute * time.Duration(min)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rate := ratere.FindStringSubmatch(text)
|
||||||
|
if len(rate) >= 3 {
|
||||||
|
file.Rate = rate[2]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
progress(19)
|
||||||
|
if file.Class == "" || strings.Contains(file.Class, "..") || strings.ContainsAny(file.Class, `/\`) {
|
||||||
|
return ErrEmptyClass
|
||||||
|
}
|
||||||
|
filebasepath := ""
|
||||||
|
if istemp {
|
||||||
|
filebasepath = PaperFolder + "temp/" + strconv.Itoa(*user.ID) + "/" + file.Class + "/"
|
||||||
|
} else {
|
||||||
|
filebasepath = fmt.Sprintf(
|
||||||
|
PaperFolder+file.Class+"/%v/%v/%v/%c/",
|
||||||
|
file.Year, file.Type.FirstSecond(), file.Type.MiddleFinal(), file.Type.AB(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
questionpath := filebasepath + "questions/"
|
||||||
|
err = os.MkdirAll(questionpath, 0755)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
docs = docs[1:]
|
||||||
|
// parse questions
|
||||||
|
subre, err := regexp.Compile(reg.Sub)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
filequestions := make([]QuestionJSON, 0, len(docs))
|
||||||
|
lst.QuesC = 0
|
||||||
|
progress(20)
|
||||||
|
p := uint(20)
|
||||||
|
delta := uint(70 / len(docs))
|
||||||
|
if delta == 0 {
|
||||||
|
delta = 1
|
||||||
|
}
|
||||||
|
for _, majordoc := range docs {
|
||||||
|
p += delta
|
||||||
|
if p > 90 {
|
||||||
|
p = 90
|
||||||
|
}
|
||||||
|
progress(p)
|
||||||
|
majorq := QuestionJSON{}
|
||||||
|
for _, it := range majordoc.Document.Body.Items {
|
||||||
|
if p, ok := it.(*docx.Paragraph); ok {
|
||||||
|
text := p.String()
|
||||||
|
majorinfo := majorre.FindStringSubmatch(text)
|
||||||
|
if len(majorinfo) >= 6 {
|
||||||
|
name, points := majorinfo[2], majorinfo[5]
|
||||||
|
majorq.Name = name
|
||||||
|
majorq.Points, _ = strconv.Atoi(points)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
subdocs := majordoc.SplitByParagraph(docx.SplitDocxByPlainTextRegex(subre))
|
||||||
|
if len(subdocs) < 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
subdocs = subdocs[1:]
|
||||||
|
majorq.Sub = make([]QuestionJSON, 0, len(subdocs))
|
||||||
|
for _, subdoc := range subdocs {
|
||||||
|
sb := bytes.NewBuffer(make([]byte, 0, 4096))
|
||||||
|
for _, it := range subdoc.Document.Body.Items {
|
||||||
|
sb.WriteString(fmt.Sprint(it))
|
||||||
|
}
|
||||||
|
m := md5.Sum(sb.Bytes())
|
||||||
|
que := &Question{
|
||||||
|
ID: int64(binary.LittleEndian.Uint64(m[:8])),
|
||||||
|
FileID: file.ID,
|
||||||
|
Plain: base14.BytesToString(sb.Bytes()),
|
||||||
|
Images: func() []byte {
|
||||||
|
m := make(map[string]string)
|
||||||
|
_ = subdoc.RangeRelationships(func(r *docx.Relationship) error {
|
||||||
|
if r.Type != docx.REL_IMAGE {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if r.Target == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
i := strings.LastIndex(r.Target, "/")
|
||||||
|
if i < 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
name := r.Target[i+1:]
|
||||||
|
if name == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
md := subdoc.Media(name)
|
||||||
|
if md == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
img, _, err := image.Decode(bytes.NewReader(md.Data))
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
dh, err := goimagehash.DifferenceHash(img)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var buf [8]byte
|
||||||
|
binary.LittleEndian.PutUint64(buf[:], dh.GetHash())
|
||||||
|
m[name] = hex.EncodeToString(buf[:])
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if len(m) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
data, err := json.Marshal(m)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}(),
|
||||||
|
Vector: func() []byte {
|
||||||
|
words := utils.Segmenter.Cut(base14.BytesToString(sb.Bytes()), true)
|
||||||
|
if len(words) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
v := make(map[string]uint8, len(words)*2)
|
||||||
|
for _, word := range words {
|
||||||
|
if word != "" && !strings.Contains("\n 。,、的是使,.()()1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ", word) {
|
||||||
|
if v[word] == 0 { // 二值化
|
||||||
|
v[word] = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data, err := json.Marshal(v)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
}(),
|
||||||
|
}
|
||||||
|
var q Question
|
||||||
|
dupmap := make(map[string]float64, 64)
|
||||||
|
f.mu.RLock()
|
||||||
|
err = f.db.FindFor(FileTableQuestion, &q, "", func() error {
|
||||||
|
r, err := q.GetDuplicateRate(que)
|
||||||
|
if err != nil {
|
||||||
|
logrus.Warnln("[global.AddFile] GetDuplicateRate err:", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if r < 0.5 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var buf [8]byte
|
||||||
|
binary.LittleEndian.PutUint64(buf[:], uint64(q.ID))
|
||||||
|
dupmap[hex.EncodeToString(buf[:])] = r
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
f.mu.RUnlock()
|
||||||
|
if err == nil && len(dupmap) > 0 {
|
||||||
|
que.Dup, _ = json.Marshal(dupmap)
|
||||||
|
}
|
||||||
|
w := bytes.NewBuffer(make([]byte, 0, 65536))
|
||||||
|
_, err = subdoc.WriteTo(w)
|
||||||
|
var buf [8]byte
|
||||||
|
binary.LittleEndian.PutUint64(buf[:], uint64(que.ID))
|
||||||
|
queidstr := hex.EncodeToString(buf[:])
|
||||||
|
if err == nil {
|
||||||
|
m5 := md5.Sum(w.Bytes())
|
||||||
|
quepath := questionpath + hex.EncodeToString(m5[:]) + ".docx"
|
||||||
|
qf, err := os.Create(quepath)
|
||||||
|
if err == nil {
|
||||||
|
_, _ = io.Copy(qf, w)
|
||||||
|
_ = qf.Close()
|
||||||
|
}
|
||||||
|
que.Path = quepath
|
||||||
|
if istemp {
|
||||||
|
f.mu.Lock()
|
||||||
|
_ = f.db.Insert(FileTableTempQuestion, que)
|
||||||
|
f.mu.Unlock()
|
||||||
|
} else {
|
||||||
|
f.mu.Lock()
|
||||||
|
for k, v := range dupmap {
|
||||||
|
err = f.db.Find(FileTableQuestion, &q, "WHERE ID=0x"+k)
|
||||||
|
if err == nil {
|
||||||
|
thismap := make(map[string]float64, 64)
|
||||||
|
err := json.Unmarshal(q.Dup, &thismap)
|
||||||
|
if err == nil {
|
||||||
|
thismap[queidstr] = v
|
||||||
|
q.Dup, err = json.Marshal(thismap)
|
||||||
|
if err == nil {
|
||||||
|
_ = f.db.Insert(FileTableQuestion, &q)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = f.db.Insert(FileTableQuestion, que)
|
||||||
|
f.mu.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r := 0.0
|
||||||
|
for _, v := range dupmap {
|
||||||
|
if v > r {
|
||||||
|
r = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
majorq.Sub = append(majorq.Sub, QuestionJSON{
|
||||||
|
Name: queidstr, //TODO: fill sub points
|
||||||
|
})
|
||||||
|
}
|
||||||
|
filequestions = append(filequestions, majorq)
|
||||||
|
lst.QuesC += len(majorq.Sub)
|
||||||
|
}
|
||||||
|
progress(90)
|
||||||
|
file.Questions, _ = json.Marshal(filequestions)
|
||||||
|
_, err = docf.Seek(0, io.SeekStart)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
lst.Path = filebasepath + file.Class + ".docx"
|
||||||
|
lst.HasntAnalyzed = false
|
||||||
|
lst.Desc = fmt.Sprintf("%s%v%v%v%c卷",
|
||||||
|
file.Class, file.Year, file.Type.FirstSecond(), file.Type.MiddleFinal(), file.Type.AB(),
|
||||||
|
)
|
||||||
|
dstf, err := os.Create(lst.Path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer dstf.Close()
|
||||||
|
_, err = io.Copy(dstf, docf)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
progress(95)
|
||||||
|
f.mu.Lock()
|
||||||
|
if istemp {
|
||||||
|
err = f.db.Insert(FileTableTempFile, file)
|
||||||
|
lst.IsTemp = true
|
||||||
|
} else {
|
||||||
|
err = f.db.Insert(FileTableFile, file)
|
||||||
|
lst.IsTemp = false
|
||||||
|
}
|
||||||
|
_ = f.db.Insert(FileTableList, &lst)
|
||||||
|
f.mu.Unlock()
|
||||||
|
progress(100)
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -1,17 +1,9 @@
|
|||||||
package global
|
package global
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"crypto/md5"
|
|
||||||
"encoding/binary"
|
|
||||||
"encoding/hex"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"image"
|
|
||||||
"io"
|
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -22,10 +14,6 @@ import (
|
|||||||
_ "golang.org/x/image/webp"
|
_ "golang.org/x/image/webp"
|
||||||
|
|
||||||
sql "github.com/FloatTech/sqlite"
|
sql "github.com/FloatTech/sqlite"
|
||||||
"github.com/corona10/goimagehash"
|
|
||||||
base14 "github.com/fumiama/go-base16384"
|
|
||||||
"github.com/fumiama/go-docx"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
|
|
||||||
"github.com/fumiama/paper-manager/backend/utils"
|
"github.com/fumiama/paper-manager/backend/utils"
|
||||||
)
|
)
|
||||||
@@ -64,7 +52,9 @@ func init() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
err = FileDB.db.Create(FileTableQuestion, &Question{})
|
err = FileDB.db.Create(FileTableQuestion, &Question{},
|
||||||
|
"FOREIGN KEY(FileID) REFERENCES "+FileTableFile+"(ID)",
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -108,389 +98,6 @@ func (sy StudyYear) String() string {
|
|||||||
return strconv.Itoa(int(sy)) + "-" + strconv.Itoa(int(next)) + "学年"
|
return strconv.Itoa(int(sy)) + "-" + strconv.Itoa(int(next)) + "学年"
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddFile from lst and copy it to analyzed path.
|
|
||||||
// The para reg must belong to a valid user
|
|
||||||
func (f *FileDatabase) AddFile(lstid int, reg *Regex, istemp bool, progress func(uint)) error {
|
|
||||||
user, err := UserDB.GetUserByID(reg.ID)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !user.IsFileManager() && !istemp {
|
|
||||||
return ErrInvalidRole
|
|
||||||
}
|
|
||||||
progress(1)
|
|
||||||
f.mu.RLock()
|
|
||||||
lst, err := sql.Find[List](&f.db, FileTableList, "WHERE ID="+strconv.Itoa(lstid))
|
|
||||||
f.mu.RUnlock()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if lst.Path == "" || strings.Contains(lst.Path, "..") {
|
|
||||||
return os.ErrNotExist
|
|
||||||
}
|
|
||||||
tempath := lst.Path
|
|
||||||
docf, err := os.Open(tempath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer docf.Close()
|
|
||||||
progress(2)
|
|
||||||
h := md5.New()
|
|
||||||
_, err = io.Copy(h, docf)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
var buf [md5.Size]byte
|
|
||||||
id := int64(binary.LittleEndian.Uint64(h.Sum(buf[:0])))
|
|
||||||
_, err = docf.Seek(0, io.SeekStart)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
stat, err := docf.Stat()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
sz := stat.Size()
|
|
||||||
progress(3)
|
|
||||||
doc, err := docx.Parse(docf, sz)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
progress(5)
|
|
||||||
doc.Document.Body.DropDrawingOf("NilPicture")
|
|
||||||
majorre, err := regexp.Compile(reg.Major)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
docs := doc.SplitByParagraph(docx.SplitDocxByPlainTextRegex(majorre))
|
|
||||||
if len(docs) < 2 {
|
|
||||||
return ErrMajorSplitsTooShort
|
|
||||||
}
|
|
||||||
progress(9)
|
|
||||||
// filling File struct
|
|
||||||
file := &File{
|
|
||||||
ID: id,
|
|
||||||
ListID: *lst.ID,
|
|
||||||
}
|
|
||||||
titlere, err := regexp.Compile(reg.Title)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
classre, err := regexp.Compile(reg.Class)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
opclre, err := regexp.Compile(reg.OpenCl)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
datere, err := regexp.Compile(reg.Date)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
timere, err := regexp.Compile(reg.Time)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
ratere, err := regexp.Compile(reg.Rate)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
progress(10)
|
|
||||||
for _, it := range docs[0].Document.Body.Items {
|
|
||||||
if p, ok := it.(*docx.Paragraph); ok {
|
|
||||||
text := p.String()
|
|
||||||
title := titlere.FindStringSubmatch(text)
|
|
||||||
if len(title) >= 5 {
|
|
||||||
years, semesters, mfs, abs := title[1], title[2], title[3], title[4]
|
|
||||||
y, err := strconv.Atoi(years)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
file.Year = StudyYear(y)
|
|
||||||
if len(semesters) > 0 {
|
|
||||||
file.Type = file.Type.SetFirstSecond(semesters[0])
|
|
||||||
}
|
|
||||||
file.Type = file.Type.SetMiddleFinal(mfs)
|
|
||||||
if len(abs) > 0 {
|
|
||||||
file.Type = file.Type.SetAB(abs[0])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
class := classre.FindStringSubmatch(text)
|
|
||||||
if len(class) >= 3 {
|
|
||||||
file.Class = class[2]
|
|
||||||
}
|
|
||||||
opcl := opclre.FindStringSubmatch(text)
|
|
||||||
if len(opcl) >= 2 {
|
|
||||||
file.Type = file.Type.SetOpenClose(opcl[1])
|
|
||||||
}
|
|
||||||
date := datere.FindStringSubmatch(text)
|
|
||||||
if len(date) >= 4 {
|
|
||||||
y, m, d := date[1], date[2], date[3]
|
|
||||||
if y != "" && m != "" {
|
|
||||||
if d == "" {
|
|
||||||
d = "1"
|
|
||||||
}
|
|
||||||
yyyy, err := strconv.ParseUint(y, 10, 64)
|
|
||||||
if err == nil && yyyy > 1600 {
|
|
||||||
mm, err := strconv.ParseUint(m, 10, 64)
|
|
||||||
if err == nil && mm >= 1 && mm <= 12 {
|
|
||||||
dd, err := strconv.ParseUint(d, 10, 64)
|
|
||||||
if err == nil && dd >= 1 && dd <= 31 {
|
|
||||||
file.Date = uint32(yyyy*10000 + mm*100 + dd)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
times := timere.FindStringSubmatch(text)
|
|
||||||
if len(times) >= 2 {
|
|
||||||
min, err := strconv.Atoi(times[1])
|
|
||||||
if err == nil && min > 0 {
|
|
||||||
file.Time = time.Minute * time.Duration(min)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
rate := ratere.FindStringSubmatch(text)
|
|
||||||
if len(rate) >= 3 {
|
|
||||||
file.Rate = rate[2]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
progress(19)
|
|
||||||
if file.Class == "" || strings.Contains(file.Class, "..") || strings.ContainsAny(file.Class, `/\`) {
|
|
||||||
return ErrEmptyClass
|
|
||||||
}
|
|
||||||
filebasepath := ""
|
|
||||||
if istemp {
|
|
||||||
filebasepath = PaperFolder + "temp/" + strconv.Itoa(*user.ID) + "/" + file.Class + "/"
|
|
||||||
} else {
|
|
||||||
filebasepath = fmt.Sprintf(
|
|
||||||
PaperFolder+file.Class+"/%v/%v/%v/%c/",
|
|
||||||
file.Year, file.Type.FirstSecond(), file.Type.MiddleFinal(), file.Type.AB(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
questionpath := filebasepath + "questions/"
|
|
||||||
err = os.MkdirAll(questionpath, 0755)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
docs = docs[1:]
|
|
||||||
// parse questions
|
|
||||||
subre, err := regexp.Compile(reg.Sub)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
filequestions := make([]QuestionJSON, 0, len(docs))
|
|
||||||
lst.QuesC = 0
|
|
||||||
progress(20)
|
|
||||||
p := uint(20)
|
|
||||||
delta := uint(70 / len(docs))
|
|
||||||
if delta == 0 {
|
|
||||||
delta = 1
|
|
||||||
}
|
|
||||||
for _, majordoc := range docs {
|
|
||||||
p += delta
|
|
||||||
if p > 90 {
|
|
||||||
p = 90
|
|
||||||
}
|
|
||||||
progress(p)
|
|
||||||
majorq := QuestionJSON{}
|
|
||||||
for _, it := range majordoc.Document.Body.Items {
|
|
||||||
if p, ok := it.(*docx.Paragraph); ok {
|
|
||||||
text := p.String()
|
|
||||||
majorinfo := majorre.FindStringSubmatch(text)
|
|
||||||
if len(majorinfo) >= 6 {
|
|
||||||
name, points := majorinfo[2], majorinfo[5]
|
|
||||||
majorq.Name = name
|
|
||||||
majorq.Points, _ = strconv.Atoi(points)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
subdocs := majordoc.SplitByParagraph(docx.SplitDocxByPlainTextRegex(subre))
|
|
||||||
if len(subdocs) < 2 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
subdocs = subdocs[1:]
|
|
||||||
majorq.Sub = make([]QuestionJSON, 0, len(subdocs))
|
|
||||||
for _, subdoc := range subdocs {
|
|
||||||
sb := bytes.NewBuffer(make([]byte, 0, 4096))
|
|
||||||
for _, it := range subdoc.Document.Body.Items {
|
|
||||||
sb.WriteString(fmt.Sprint(it))
|
|
||||||
}
|
|
||||||
m := md5.Sum(sb.Bytes())
|
|
||||||
que := &Question{
|
|
||||||
ID: int64(binary.LittleEndian.Uint64(m[:8])),
|
|
||||||
Plain: base14.BytesToString(sb.Bytes()),
|
|
||||||
Images: func() []byte {
|
|
||||||
m := make(map[string]string)
|
|
||||||
_ = subdoc.RangeRelationships(func(r *docx.Relationship) error {
|
|
||||||
if r.Type != docx.REL_IMAGE {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if r.Target == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
i := strings.LastIndex(r.Target, "/")
|
|
||||||
if i < 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
name := r.Target[i+1:]
|
|
||||||
if name == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
md := subdoc.Media(name)
|
|
||||||
if md == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
img, _, err := image.Decode(bytes.NewReader(md.Data))
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
dh, err := goimagehash.DifferenceHash(img)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
var buf [8]byte
|
|
||||||
binary.LittleEndian.PutUint64(buf[:], dh.GetHash())
|
|
||||||
m[name] = hex.EncodeToString(buf[:])
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if len(m) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
data, err := json.Marshal(m)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return data
|
|
||||||
}(),
|
|
||||||
Vector: func() []byte {
|
|
||||||
words := utils.Segmenter.Cut(base14.BytesToString(sb.Bytes()), true)
|
|
||||||
if len(words) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
v := make(map[string]uint8, len(words)*2)
|
|
||||||
for _, word := range words {
|
|
||||||
if word != "" && !strings.Contains("\n 。,、的是使,.()()1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ", word) {
|
|
||||||
if v[word] == 0 { // 二值化
|
|
||||||
v[word] = 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
data, err := json.Marshal(v)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return data
|
|
||||||
}(),
|
|
||||||
}
|
|
||||||
var q Question
|
|
||||||
dupmap := make(map[string]float64, 64)
|
|
||||||
f.mu.RLock()
|
|
||||||
err = f.db.FindFor(FileTableQuestion, &q, "", func() error {
|
|
||||||
r, err := q.GetDuplicateRate(que)
|
|
||||||
if err != nil {
|
|
||||||
logrus.Warnln("[global.AddFile] GetDuplicateRate err:", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if r < 0.5 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
var buf [8]byte
|
|
||||||
binary.LittleEndian.PutUint64(buf[:], uint64(q.ID))
|
|
||||||
dupmap[hex.EncodeToString(buf[:])] = r
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
f.mu.RUnlock()
|
|
||||||
if err == nil && len(dupmap) > 0 {
|
|
||||||
que.Dup, _ = json.Marshal(dupmap)
|
|
||||||
}
|
|
||||||
w := bytes.NewBuffer(make([]byte, 0, 65536))
|
|
||||||
_, err = subdoc.WriteTo(w)
|
|
||||||
var buf [8]byte
|
|
||||||
binary.LittleEndian.PutUint64(buf[:], uint64(que.ID))
|
|
||||||
queidstr := hex.EncodeToString(buf[:])
|
|
||||||
if err == nil {
|
|
||||||
m5 := md5.Sum(w.Bytes())
|
|
||||||
quepath := questionpath + hex.EncodeToString(m5[:]) + ".docx"
|
|
||||||
qf, err := os.Create(quepath)
|
|
||||||
if err == nil {
|
|
||||||
_, _ = io.Copy(qf, w)
|
|
||||||
_ = qf.Close()
|
|
||||||
}
|
|
||||||
que.Path = quepath
|
|
||||||
if istemp {
|
|
||||||
f.mu.Lock()
|
|
||||||
_ = f.db.Insert(FileTableTempQuestion, que)
|
|
||||||
f.mu.Unlock()
|
|
||||||
} else {
|
|
||||||
f.mu.Lock()
|
|
||||||
for k, v := range dupmap {
|
|
||||||
err = f.db.Find(FileTableQuestion, &q, "WHERE ID=0x"+k)
|
|
||||||
if err == nil {
|
|
||||||
thismap := make(map[string]float64, 64)
|
|
||||||
err := json.Unmarshal(q.Dup, &thismap)
|
|
||||||
if err == nil {
|
|
||||||
thismap[queidstr] = v
|
|
||||||
q.Dup, err = json.Marshal(thismap)
|
|
||||||
if err == nil {
|
|
||||||
_ = f.db.Insert(FileTableQuestion, &q)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ = f.db.Insert(FileTableQuestion, que)
|
|
||||||
f.mu.Unlock()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
r := 0.0
|
|
||||||
for _, v := range dupmap {
|
|
||||||
if v > r {
|
|
||||||
r = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
majorq.Sub = append(majorq.Sub, QuestionJSON{
|
|
||||||
Name: queidstr, //TODO: fill sub points
|
|
||||||
})
|
|
||||||
}
|
|
||||||
filequestions = append(filequestions, majorq)
|
|
||||||
lst.QuesC += len(majorq.Sub)
|
|
||||||
}
|
|
||||||
progress(90)
|
|
||||||
file.Questions, _ = json.Marshal(filequestions)
|
|
||||||
_, err = docf.Seek(0, io.SeekStart)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
lst.Path = filebasepath + file.Class + ".docx"
|
|
||||||
lst.HasntAnalyzed = false
|
|
||||||
lst.Desc = fmt.Sprintf("%s%v%v%v%c卷",
|
|
||||||
file.Class, file.Year, file.Type.FirstSecond(), file.Type.MiddleFinal(), file.Type.AB(),
|
|
||||||
)
|
|
||||||
dstf, err := os.Create(lst.Path)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer dstf.Close()
|
|
||||||
_, err = io.Copy(dstf, docf)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
progress(95)
|
|
||||||
f.mu.Lock()
|
|
||||||
if istemp {
|
|
||||||
err = f.db.Insert(FileTableTempFile, file)
|
|
||||||
lst.IsTemp = true
|
|
||||||
} else {
|
|
||||||
err = f.db.Insert(FileTableFile, file)
|
|
||||||
lst.IsTemp = false
|
|
||||||
}
|
|
||||||
_ = f.db.Insert(FileTableList, &lst)
|
|
||||||
f.mu.Unlock()
|
|
||||||
progress(100)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// DelFile by listid
|
// DelFile by listid
|
||||||
func (f *FileDatabase) DelFile(lstid, uid int, istemp bool) error {
|
func (f *FileDatabase) DelFile(lstid, uid int, istemp bool) error {
|
||||||
user, err := UserDB.GetUserByID(uid)
|
user, err := UserDB.GetUserByID(uid)
|
||||||
|
|||||||
24
backend/global/generate.go
Normal file
24
backend/global/generate.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package global
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidGenerateConfig = errors.New("invalid generate config")
|
||||||
|
)
|
||||||
|
|
||||||
|
// GenerateConfig 试卷生成配置
|
||||||
|
type GenerateConfig struct {
|
||||||
|
Distribution map[string]uint // Distribution is map[majorname]subcount
|
||||||
|
RateLimit float64 // RateLimit 重复率上限
|
||||||
|
YearStart StudyYear // YearStart 起始年份(空则直到最旧)
|
||||||
|
YearEnd StudyYear // YearEnd 截止年份(空则直到最新)
|
||||||
|
TypeMask PaperType // TypeMask & File.Type != 0 则匹配
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateFile 用一些限定条件生成新试卷, 云端不保存
|
||||||
|
func (f *FileDatabase) GenerateFile(config *GenerateConfig) ([]byte, error) {
|
||||||
|
if config == nil || config.Distribution == nil {
|
||||||
|
return nil, ErrInvalidGenerateConfig
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
@@ -79,7 +79,7 @@ func (pt PaperType) OpenClose() string {
|
|||||||
return "开卷"
|
return "开卷"
|
||||||
case 2:
|
case 2:
|
||||||
return "一页纸开卷"
|
return "一页纸开卷"
|
||||||
case 3:
|
case 4:
|
||||||
return "闭卷"
|
return "闭卷"
|
||||||
default:
|
default:
|
||||||
return "闭卷"
|
return "闭卷"
|
||||||
@@ -94,7 +94,7 @@ func (pt PaperType) SetOpenClose(x string) PaperType {
|
|||||||
case "一页纸开卷":
|
case "一页纸开卷":
|
||||||
n = 2 << 12
|
n = 2 << 12
|
||||||
case "闭卷":
|
case "闭卷":
|
||||||
n = 3 << 12
|
n = 4 << 12
|
||||||
}
|
}
|
||||||
return pt | n
|
return pt | n
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ func (f *FileDatabase) DelQuestion(id int64, istemp bool) error {
|
|||||||
|
|
||||||
type Question struct {
|
type Question struct {
|
||||||
ID int64 // ID is the first 8 bytes of the Plain's md5
|
ID int64 // ID is the first 8 bytes of the Plain's md5
|
||||||
|
FileID int64 // FileID is fk to File(ID)
|
||||||
Path string // Path is the question's docx position
|
Path string // Path is the question's docx position
|
||||||
Plain string // Plain is the plain text of the question (like markdown format)
|
Plain string // Plain is the plain text of the question (like markdown format)
|
||||||
Images []byte // Images is json of the image dhash in XML, ex. ['rId1': '1234567890abcdef', ...]
|
Images []byte // Images is json of the image dhash in XML, ex. ['rId1': '1234567890abcdef', ...]
|
||||||
|
|||||||
3
frontend/vben/src/locales/lang/zh-CN/routes/genfile.ts
Normal file
3
frontend/vben/src/locales/lang/zh-CN/routes/genfile.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export default {
|
||||||
|
name: '试卷生成',
|
||||||
|
}
|
||||||
17
frontend/vben/src/router/menus/modules/genfile.ts
Normal file
17
frontend/vben/src/router/menus/modules/genfile.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import type { MenuModule } from '/@/router/types'
|
||||||
|
import { t } from '/@/hooks/web/useI18n'
|
||||||
|
const menu: MenuModule = {
|
||||||
|
orderNo: 40,
|
||||||
|
menu: {
|
||||||
|
name: t('routes.genfile.name'),
|
||||||
|
path: '/genfile',
|
||||||
|
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'index',
|
||||||
|
name: t('routes.genfile.name'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
export default menu
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { MenuModule } from '/@/router/types'
|
import type { MenuModule } from '/@/router/types'
|
||||||
import { t } from '/@/hooks/web/useI18n'
|
import { t } from '/@/hooks/web/useI18n'
|
||||||
const menu: MenuModule = {
|
const menu: MenuModule = {
|
||||||
orderNo: 20,
|
orderNo: 30,
|
||||||
menu: {
|
menu: {
|
||||||
name: t('routes.templist.name'),
|
name: t('routes.templist.name'),
|
||||||
path: '/templist',
|
path: '/templist',
|
||||||
|
|||||||
30
frontend/vben/src/router/routes/modules/genfile.ts
Normal file
30
frontend/vben/src/router/routes/modules/genfile.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import type { AppRouteModule } from '/@/router/types'
|
||||||
|
import { LAYOUT } from '/@/router/constant'
|
||||||
|
import { t } from '/@/hooks/web/useI18n'
|
||||||
|
|
||||||
|
const genfile: AppRouteModule = {
|
||||||
|
path: '/genfile',
|
||||||
|
name: 'GenFile',
|
||||||
|
component: LAYOUT,
|
||||||
|
redirect: '/genfile/index',
|
||||||
|
meta: {
|
||||||
|
hideChildrenInMenu: true,
|
||||||
|
icon: 'ion:balloon-outline',
|
||||||
|
title: t('routes.genfile.name'),
|
||||||
|
orderNo: 40,
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'index',
|
||||||
|
name: 'GenFilePage',
|
||||||
|
component: () => import('/@/views/page/genfile/index.vue'),
|
||||||
|
meta: {
|
||||||
|
title: t('routes.genfile.name'),
|
||||||
|
icon: 'ion:balloon-outline',
|
||||||
|
hideMenu: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
export default genfile
|
||||||
@@ -11,7 +11,7 @@ const templist: AppRouteModule = {
|
|||||||
hideChildrenInMenu: true,
|
hideChildrenInMenu: true,
|
||||||
icon: 'ion:ios-analytics',
|
icon: 'ion:ios-analytics',
|
||||||
title: t('routes.templist.name'),
|
title: t('routes.templist.name'),
|
||||||
orderNo: 20,
|
orderNo: 30,
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -25,15 +25,15 @@
|
|||||||
const { createMessage } = useMessage()
|
const { createMessage } = useMessage()
|
||||||
const [register, { validate, setProps }] = useForm({
|
const [register, { validate, setProps }] = useForm({
|
||||||
labelCol: {
|
labelCol: {
|
||||||
span: 8,
|
span: 5,
|
||||||
},
|
},
|
||||||
wrapperCol: {
|
wrapperCol: {
|
||||||
span: 15,
|
span: 16,
|
||||||
},
|
},
|
||||||
schemas: schemas,
|
schemas: schemas,
|
||||||
actionColOptions: {
|
actionColOptions: {
|
||||||
offset: 8,
|
offset: 3,
|
||||||
span: 23,
|
span: 16,
|
||||||
},
|
},
|
||||||
submitButtonOptions: {
|
submitButtonOptions: {
|
||||||
text: '提交',
|
text: '提交',
|
||||||
|
|||||||
103
frontend/vben/src/views/page/genfile/Step1.vue
Normal file
103
frontend/vben/src/views/page/genfile/Step1.vue
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
<template>
|
||||||
|
<div class="step1">
|
||||||
|
<div class="step1-form">
|
||||||
|
<BasicForm @register="register">
|
||||||
|
<template #fac="{ model, field }">
|
||||||
|
<a-input-group compact>
|
||||||
|
<a-select v-model:value="model['pay']" class="pay-select">
|
||||||
|
<a-select-option value="zfb"> 支付宝 </a-select-option>
|
||||||
|
<a-select-option value="yl"> 银联 </a-select-option>
|
||||||
|
</a-select>
|
||||||
|
<a-input class="pay-input" v-model:value="model[field]" />
|
||||||
|
</a-input-group>
|
||||||
|
</template>
|
||||||
|
</BasicForm>
|
||||||
|
</div>
|
||||||
|
<a-divider />
|
||||||
|
<h3>说明</h3>
|
||||||
|
<h4>转账到支付宝账户</h4>
|
||||||
|
<p>
|
||||||
|
如果需要,这里可以放一些关于产品的常见问题说明。如果需要,这里可以放一些关于产品的常见问题说明。如果需要,这里可以放一些关于产品的常见问题说明。
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h4>转账到银行卡</h4>
|
||||||
|
<p>
|
||||||
|
如果需要,这里可以放一些关于产品的常见问题说明。如果需要,这里可以放一些关于产品的常见问题说明。如果需要,这里可以放一些关于产品的常见问题说明。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue'
|
||||||
|
import { BasicForm, useForm } from '/@/components/Form'
|
||||||
|
import { step1Schemas } from './data'
|
||||||
|
|
||||||
|
import { Select, Input, Divider } from 'ant-design-vue'
|
||||||
|
export default defineComponent({
|
||||||
|
components: {
|
||||||
|
BasicForm,
|
||||||
|
[Select.name]: Select,
|
||||||
|
ASelectOption: Select.Option,
|
||||||
|
[Input.name]: Input,
|
||||||
|
[Input.Group.name]: Input.Group,
|
||||||
|
[Divider.name]: Divider,
|
||||||
|
},
|
||||||
|
emits: ['next'],
|
||||||
|
setup(_, { emit }) {
|
||||||
|
const [register, { validate }] = useForm({
|
||||||
|
labelWidth: 100,
|
||||||
|
schemas: step1Schemas,
|
||||||
|
actionColOptions: {
|
||||||
|
span: 14,
|
||||||
|
},
|
||||||
|
showResetButton: false,
|
||||||
|
submitButtonOptions: {
|
||||||
|
text: '下一步',
|
||||||
|
},
|
||||||
|
submitFunc: customSubmitFunc,
|
||||||
|
})
|
||||||
|
|
||||||
|
async function customSubmitFunc() {
|
||||||
|
try {
|
||||||
|
const values = await validate()
|
||||||
|
emit('next', values)
|
||||||
|
} catch (error) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { register }
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.step1 {
|
||||||
|
&-form {
|
||||||
|
width: 450px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: 0 0 12px;
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 32px;
|
||||||
|
color: @text-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin: 0 0 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 22px;
|
||||||
|
color: @text-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
color: @text-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.pay-select {
|
||||||
|
width: 20%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pay-input {
|
||||||
|
width: 70%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
78
frontend/vben/src/views/page/genfile/Step2.vue
Normal file
78
frontend/vben/src/views/page/genfile/Step2.vue
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
<template>
|
||||||
|
<div class="step2">
|
||||||
|
<a-alert message="确认转账后,资金将直接打入对方账户,无法退回。" show-icon />
|
||||||
|
<a-descriptions :column="1" class="mt-5">
|
||||||
|
<a-descriptions-item label="付款账户"> ant-design@alipay.com </a-descriptions-item>
|
||||||
|
<a-descriptions-item label="收款账户"> test@example.com </a-descriptions-item>
|
||||||
|
<a-descriptions-item label="收款人姓名"> Vben </a-descriptions-item>
|
||||||
|
<a-descriptions-item label="转账金额"> 500元 </a-descriptions-item>
|
||||||
|
</a-descriptions>
|
||||||
|
<a-divider />
|
||||||
|
<BasicForm @register="register" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue'
|
||||||
|
import { BasicForm, useForm } from '/@/components/Form'
|
||||||
|
import { step2Schemas } from './data'
|
||||||
|
import { Alert, Divider, Descriptions } from 'ant-design-vue'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: {
|
||||||
|
BasicForm,
|
||||||
|
[Alert.name]: Alert,
|
||||||
|
[Divider.name]: Divider,
|
||||||
|
[Descriptions.name]: Descriptions,
|
||||||
|
[Descriptions.Item.name]: Descriptions.Item,
|
||||||
|
},
|
||||||
|
emits: ['next', 'prev'],
|
||||||
|
setup(_, { emit }) {
|
||||||
|
const [register, { validate, setProps }] = useForm({
|
||||||
|
labelWidth: 80,
|
||||||
|
schemas: step2Schemas,
|
||||||
|
actionColOptions: {
|
||||||
|
span: 14,
|
||||||
|
},
|
||||||
|
resetButtonOptions: {
|
||||||
|
text: '上一步',
|
||||||
|
},
|
||||||
|
submitButtonOptions: {
|
||||||
|
text: '提交',
|
||||||
|
},
|
||||||
|
resetFunc: customResetFunc,
|
||||||
|
submitFunc: customSubmitFunc,
|
||||||
|
})
|
||||||
|
|
||||||
|
async function customResetFunc() {
|
||||||
|
emit('prev')
|
||||||
|
}
|
||||||
|
|
||||||
|
async function customSubmitFunc() {
|
||||||
|
try {
|
||||||
|
const values = await validate()
|
||||||
|
setProps({
|
||||||
|
submitButtonOptions: {
|
||||||
|
loading: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
setTimeout(() => {
|
||||||
|
setProps({
|
||||||
|
submitButtonOptions: {
|
||||||
|
loading: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
emit('next', values)
|
||||||
|
}, 1500)
|
||||||
|
} catch (error) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { register }
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.step2 {
|
||||||
|
width: 450px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
49
frontend/vben/src/views/page/genfile/Step3.vue
Normal file
49
frontend/vben/src/views/page/genfile/Step3.vue
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<template>
|
||||||
|
<div class="step3">
|
||||||
|
<a-result status="success" title="操作成功" sub-title="预计两小时内到账">
|
||||||
|
<template #extra>
|
||||||
|
<a-button type="primary" @click="redo"> 再转一笔 </a-button>
|
||||||
|
<a-button> 查看账单 </a-button>
|
||||||
|
</template>
|
||||||
|
</a-result>
|
||||||
|
<div class="desc-wrap">
|
||||||
|
<a-descriptions :column="1" class="mt-5">
|
||||||
|
<a-descriptions-item label="付款账户"> ant-design@alipay.com </a-descriptions-item>
|
||||||
|
<a-descriptions-item label="收款账户"> test@example.com </a-descriptions-item>
|
||||||
|
<a-descriptions-item label="收款人姓名"> Vben </a-descriptions-item>
|
||||||
|
<a-descriptions-item label="转账金额"> 500元 </a-descriptions-item>
|
||||||
|
</a-descriptions>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent } from 'vue'
|
||||||
|
import { Result, Descriptions } from 'ant-design-vue'
|
||||||
|
export default defineComponent({
|
||||||
|
components: {
|
||||||
|
[Result.name]: Result,
|
||||||
|
[Descriptions.name]: Descriptions,
|
||||||
|
[Descriptions.Item.name]: Descriptions.Item,
|
||||||
|
},
|
||||||
|
emits: ['redo'],
|
||||||
|
setup(_, { emit }) {
|
||||||
|
return {
|
||||||
|
redo: () => {
|
||||||
|
emit('redo')
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.step3 {
|
||||||
|
width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.desc-wrap {
|
||||||
|
padding: 24px 40px;
|
||||||
|
margin-top: 24px;
|
||||||
|
background-color: @background-color-light;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
78
frontend/vben/src/views/page/genfile/data.tsx
Normal file
78
frontend/vben/src/views/page/genfile/data.tsx
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import { FormSchema } from '/@/components/Form'
|
||||||
|
|
||||||
|
export const step1Schemas: FormSchema[] = [
|
||||||
|
{
|
||||||
|
field: 'account',
|
||||||
|
component: 'Select',
|
||||||
|
label: '付款账户',
|
||||||
|
required: true,
|
||||||
|
defaultValue: '1',
|
||||||
|
componentProps: {
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: 'anncwb@126.com',
|
||||||
|
value: '1',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
colProps: {
|
||||||
|
span: 24,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'fac',
|
||||||
|
component: 'InputGroup',
|
||||||
|
label: '收款账户',
|
||||||
|
required: true,
|
||||||
|
defaultValue: 'test@example.com',
|
||||||
|
slot: 'fac',
|
||||||
|
colProps: {
|
||||||
|
span: 24,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'pay',
|
||||||
|
component: 'Input',
|
||||||
|
label: '',
|
||||||
|
defaultValue: 'zfb',
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'payeeName',
|
||||||
|
component: 'Input',
|
||||||
|
label: '收款人姓名',
|
||||||
|
defaultValue: 'Vben',
|
||||||
|
required: true,
|
||||||
|
colProps: {
|
||||||
|
span: 24,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'money',
|
||||||
|
component: 'Input',
|
||||||
|
label: '转账金额',
|
||||||
|
defaultValue: '500',
|
||||||
|
required: true,
|
||||||
|
renderComponentContent: () => {
|
||||||
|
return {
|
||||||
|
prefix: () => '¥',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colProps: {
|
||||||
|
span: 24,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
export const step2Schemas: FormSchema[] = [
|
||||||
|
{
|
||||||
|
field: 'pwd',
|
||||||
|
component: 'InputPassword',
|
||||||
|
label: '支付密码',
|
||||||
|
required: true,
|
||||||
|
defaultValue: '123456',
|
||||||
|
colProps: {
|
||||||
|
span: 24,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
96
frontend/vben/src/views/page/genfile/index.vue
Normal file
96
frontend/vben/src/views/page/genfile/index.vue
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
<template>
|
||||||
|
<PageWrapper
|
||||||
|
title="分步表单"
|
||||||
|
contentBackground
|
||||||
|
content=" 将一个冗长或用户不熟悉的表单任务分成多个步骤,指导用户完成。"
|
||||||
|
contentClass="p-4"
|
||||||
|
>
|
||||||
|
<div class="step-form-form">
|
||||||
|
<a-steps :current="current">
|
||||||
|
<a-step title="填写转账信息" />
|
||||||
|
<a-step title="确认转账信息" />
|
||||||
|
<a-step title="完成" />
|
||||||
|
</a-steps>
|
||||||
|
</div>
|
||||||
|
<div class="mt-5">
|
||||||
|
<Step1 @next="handleStep1Next" v-show="current === 0" />
|
||||||
|
<Step2
|
||||||
|
@prev="handleStepPrev"
|
||||||
|
@next="handleStep2Next"
|
||||||
|
v-show="current === 1"
|
||||||
|
v-if="initSetp2"
|
||||||
|
/>
|
||||||
|
<Step3 v-show="current === 2" @redo="handleRedo" v-if="initSetp3" />
|
||||||
|
</div>
|
||||||
|
</PageWrapper>
|
||||||
|
</template>
|
||||||
|
<script lang="ts">
|
||||||
|
import { defineComponent, ref, reactive, toRefs } from 'vue'
|
||||||
|
import Step1 from './Step1.vue'
|
||||||
|
import Step2 from './Step2.vue'
|
||||||
|
import Step3 from './Step3.vue'
|
||||||
|
import { PageWrapper } from '/@/components/Page'
|
||||||
|
import { Steps } from 'ant-design-vue'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'FormStepPage',
|
||||||
|
components: {
|
||||||
|
Step1,
|
||||||
|
Step2,
|
||||||
|
Step3,
|
||||||
|
PageWrapper,
|
||||||
|
[Steps.name]: Steps,
|
||||||
|
[Steps.Step.name]: Steps.Step,
|
||||||
|
},
|
||||||
|
setup() {
|
||||||
|
const current = ref(0)
|
||||||
|
|
||||||
|
const state = reactive({
|
||||||
|
initSetp2: false,
|
||||||
|
initSetp3: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
function handleStep1Next(step1Values: any) {
|
||||||
|
current.value++
|
||||||
|
state.initSetp2 = true
|
||||||
|
console.log(step1Values)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleStepPrev() {
|
||||||
|
current.value--
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleStep2Next(step2Values: any) {
|
||||||
|
current.value++
|
||||||
|
state.initSetp3 = true
|
||||||
|
console.log(step2Values)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRedo() {
|
||||||
|
current.value = 0
|
||||||
|
state.initSetp2 = false
|
||||||
|
state.initSetp3 = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
current,
|
||||||
|
handleStep1Next,
|
||||||
|
handleStep2Next,
|
||||||
|
handleRedo,
|
||||||
|
handleStepPrev,
|
||||||
|
...toRefs(state),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.step-form-content {
|
||||||
|
padding: 24px;
|
||||||
|
background-color: @component-background;
|
||||||
|
}
|
||||||
|
|
||||||
|
.step-form-form {
|
||||||
|
width: 750px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user