feat(导出): 新增分销商筛选功能及任务列表页面
- 添加分销商API接口及前端筛选组件 - 实现导出任务分页列表功能 - 优化导出任务状态展示和进度显示 - 增加EXPLAIN评分阈值检查 - 默认设置导出时间为当年范围
This commit is contained in:
parent
24891fa208
commit
6fa4abdcf5
|
|
@ -3,276 +3,348 @@ package api
|
|||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"marketing-system-data-tool/server/internal/exporter"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"marketing-system-data-tool/server/internal/exporter"
|
||||
)
|
||||
|
||||
type ExportsAPI struct{
|
||||
meta *sql.DB
|
||||
marketing *sql.DB
|
||||
type ExportsAPI struct {
|
||||
meta *sql.DB
|
||||
marketing *sql.DB
|
||||
}
|
||||
|
||||
func ExportsHandler(meta, marketing *sql.DB) http.Handler {
|
||||
api := &ExportsAPI{meta: meta, marketing: marketing}
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
p := strings.TrimPrefix(r.URL.Path, "/api/exports")
|
||||
if r.Method == http.MethodPost && p == "" {
|
||||
api.create(w, r)
|
||||
return
|
||||
}
|
||||
if strings.HasPrefix(p, "/") {
|
||||
id := strings.TrimPrefix(p, "/")
|
||||
if r.Method == http.MethodGet && !strings.HasSuffix(p, "/download") {
|
||||
api.get(w, r, id)
|
||||
return
|
||||
}
|
||||
if r.Method == http.MethodGet && strings.HasSuffix(p, "/download") {
|
||||
id = strings.TrimSuffix(id, "/download")
|
||||
api.download(w, r, id)
|
||||
return
|
||||
}
|
||||
if r.Method == http.MethodPost && strings.HasSuffix(p, "/cancel") {
|
||||
id = strings.TrimSuffix(id, "/cancel")
|
||||
api.cancel(w, r, id)
|
||||
return
|
||||
}
|
||||
}
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
})
|
||||
api := &ExportsAPI{meta: meta, marketing: marketing}
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
p := strings.TrimPrefix(r.URL.Path, "/api/exports")
|
||||
if r.Method == http.MethodPost && p == "" {
|
||||
api.create(w, r)
|
||||
return
|
||||
}
|
||||
if r.Method == http.MethodGet && p == "" {
|
||||
api.list(w, r)
|
||||
return
|
||||
}
|
||||
if strings.HasPrefix(p, "/") {
|
||||
id := strings.TrimPrefix(p, "/")
|
||||
if r.Method == http.MethodGet && !strings.HasSuffix(p, "/download") {
|
||||
api.get(w, r, id)
|
||||
return
|
||||
}
|
||||
if r.Method == http.MethodGet && strings.HasSuffix(p, "/download") {
|
||||
id = strings.TrimSuffix(id, "/download")
|
||||
api.download(w, r, id)
|
||||
return
|
||||
}
|
||||
if r.Method == http.MethodPost && strings.HasSuffix(p, "/cancel") {
|
||||
id = strings.TrimSuffix(id, "/cancel")
|
||||
api.cancel(w, r, id)
|
||||
return
|
||||
}
|
||||
}
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
})
|
||||
}
|
||||
|
||||
type ExportPayload struct{
|
||||
TemplateID uint64 `json:"template_id"`
|
||||
RequestedBy uint64 `json:"requested_by"`
|
||||
Permission map[string]interface{} `json:"permission"`
|
||||
Options map[string]interface{} `json:"options"`
|
||||
FileFormat string `json:"file_format"`
|
||||
Filters map[string]interface{} `json:"filters"`
|
||||
Datasource string `json:"datasource"`
|
||||
type ExportPayload struct {
|
||||
TemplateID uint64 `json:"template_id"`
|
||||
RequestedBy uint64 `json:"requested_by"`
|
||||
Permission map[string]interface{} `json:"permission"`
|
||||
Options map[string]interface{} `json:"options"`
|
||||
FileFormat string `json:"file_format"`
|
||||
Filters map[string]interface{} `json:"filters"`
|
||||
Datasource string `json:"datasource"`
|
||||
}
|
||||
|
||||
func (a *ExportsAPI) create(w http.ResponseWriter, r *http.Request) {
|
||||
b, _ := io.ReadAll(r.Body)
|
||||
var p ExportPayload
|
||||
json.Unmarshal(b, &p)
|
||||
r = WithPayload(r, p)
|
||||
var main string
|
||||
var ds string
|
||||
var fields []byte
|
||||
log.Printf("trace_id=%s sql=%s args=%v", TraceIDFrom(r), "SELECT datasource, main_table, fields_json FROM export_templates WHERE id= ?", []interface{}{p.TemplateID})
|
||||
row := a.meta.QueryRow("SELECT datasource, main_table, fields_json FROM export_templates WHERE id= ?", p.TemplateID)
|
||||
err := row.Scan(&ds, &main, &fields)
|
||||
if err != nil {
|
||||
fail(w, r, http.StatusBadRequest, "invalid template")
|
||||
return
|
||||
}
|
||||
if p.Datasource != "" { ds = p.Datasource }
|
||||
var fs []string
|
||||
json.Unmarshal(fields, &fs)
|
||||
wl := whitelist()
|
||||
req := exporter.BuildRequest{MainTable: main, Fields: fs, Filters: p.Filters}
|
||||
q, args, err := exporter.BuildSQL(req, wl)
|
||||
if err != nil {
|
||||
r = WithSQL(r, q)
|
||||
fail(w, r, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
r = WithSQL(r, q)
|
||||
dataDB := a.selectDataDB(ds)
|
||||
b, _ := io.ReadAll(r.Body)
|
||||
var p ExportPayload
|
||||
json.Unmarshal(b, &p)
|
||||
r = WithPayload(r, p)
|
||||
var main string
|
||||
var ds string
|
||||
var fields []byte
|
||||
log.Printf("trace_id=%s sql=%s args=%v", TraceIDFrom(r), "SELECT datasource, main_table, fields_json FROM export_templates WHERE id= ?", []interface{}{p.TemplateID})
|
||||
row := a.meta.QueryRow("SELECT datasource, main_table, fields_json FROM export_templates WHERE id= ?", p.TemplateID)
|
||||
err := row.Scan(&ds, &main, &fields)
|
||||
if err != nil {
|
||||
fail(w, r, http.StatusBadRequest, "invalid template")
|
||||
return
|
||||
}
|
||||
if p.Datasource != "" {
|
||||
ds = p.Datasource
|
||||
}
|
||||
var fs []string
|
||||
json.Unmarshal(fields, &fs)
|
||||
wl := whitelist()
|
||||
req := exporter.BuildRequest{MainTable: main, Fields: fs, Filters: p.Filters}
|
||||
q, args, err := exporter.BuildSQL(req, wl)
|
||||
if err != nil {
|
||||
r = WithSQL(r, q)
|
||||
fail(w, r, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
r = WithSQL(r, q)
|
||||
dataDB := a.selectDataDB(ds)
|
||||
expRows, score, err := exporter.RunExplain(dataDB, q, args)
|
||||
if err != nil {
|
||||
fail(w, r, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
ejSQL := "INSERT INTO export_jobs (template_id, status, requested_by, permission_scope_json, filters_json, options_json, explain_json, explain_score, file_format, created_at) VALUES (?,?,?,?,?,?,?,?,?,?)"
|
||||
ejArgs := []interface{}{p.TemplateID, "queued", p.RequestedBy, toJSON(p.Permission), toJSON(p.Filters), toJSON(p.Options), toJSON(expRows), score, p.FileFormat, time.Now()}
|
||||
log.Printf("trace_id=%s sql=%s args=%v", TraceIDFrom(r), ejSQL, ejArgs)
|
||||
res, err := a.meta.Exec(ejSQL, ejArgs...)
|
||||
const passThreshold = 60
|
||||
if score < passThreshold {
|
||||
fail(w, r, http.StatusBadRequest, fmt.Sprintf("EXPLAIN 未通过:评分=%d,请优化索引或缩小查询范围", score))
|
||||
return
|
||||
}
|
||||
var estimate int64
|
||||
for _, r := range expRows {
|
||||
if r.Table.Valid && r.Table.String == "order" && r.Rows.Valid { estimate = r.Rows.Int64; break }
|
||||
if r.Rows.Valid { estimate += r.Rows.Int64 }
|
||||
}
|
||||
ejSQL := "INSERT INTO export_jobs (template_id, status, requested_by, permission_scope_json, filters_json, options_json, explain_json, explain_score, row_estimate, file_format, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?)"
|
||||
ejArgs := []interface{}{p.TemplateID, "queued", p.RequestedBy, toJSON(p.Permission), toJSON(p.Filters), toJSON(p.Options), toJSON(expRows), score, estimate, p.FileFormat, time.Now(), time.Now()}
|
||||
log.Printf("trace_id=%s sql=%s args=%v", TraceIDFrom(r), ejSQL, ejArgs)
|
||||
res, err := a.meta.Exec(ejSQL, ejArgs...)
|
||||
if err != nil {
|
||||
fail(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
id, _ := res.LastInsertId()
|
||||
go a.runJob(uint64(id), dataDB, q, args, fs, p.FileFormat)
|
||||
ok(w, r, map[string]interface{}{"id": id})
|
||||
}
|
||||
|
||||
func (a *ExportsAPI) runJob(id uint64, db *sql.DB, q string, args []interface{}, cols []string, fmt string) {
|
||||
log.Printf("job_id=%d sql=%s args=%v", id, "UPDATE export_jobs SET status=?, started_at=? WHERE id= ?", []interface{}{"running", time.Now(), id})
|
||||
a.meta.Exec("UPDATE export_jobs SET status=?, started_at=?, updated_at=? WHERE id= ?", "running", time.Now(), time.Now(), id)
|
||||
if fmt == "csv" {
|
||||
w, err := exporter.NewCSVWriter("storage", "export")
|
||||
if err != nil {
|
||||
a.meta.Exec("UPDATE export_jobs SET status=?, finished_at=? WHERE id= ?", "failed", time.Now(), id)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(cols)
|
||||
log.Printf("job_id=%d sql=%s args=%v", id, q, args)
|
||||
rows, err := db.Query(q, args...)
|
||||
if err != nil {
|
||||
a.meta.Exec("UPDATE export_jobs SET status=?, finished_at=? WHERE id= ?", "failed", time.Now(), id)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
out := make([]interface{}, len(cols))
|
||||
dest := make([]interface{}, len(cols))
|
||||
for i := range out {
|
||||
dest[i] = &out[i]
|
||||
}
|
||||
var count int64
|
||||
var tick int64
|
||||
for rows.Next() {
|
||||
if err := rows.Scan(dest...); err != nil {
|
||||
a.meta.Exec("UPDATE export_jobs SET status=?, finished_at=? WHERE id=?", "failed", time.Now(), id)
|
||||
return
|
||||
}
|
||||
vals := make([]string, len(cols))
|
||||
for i := range out {
|
||||
if b, ok := out[i].([]byte); ok {
|
||||
vals[i] = string(b)
|
||||
} else if out[i] == nil {
|
||||
vals[i] = ""
|
||||
} else {
|
||||
vals[i] = toString(out[i])
|
||||
}
|
||||
}
|
||||
w.WriteRow(vals)
|
||||
count++
|
||||
tick++
|
||||
if tick%500 == 0 { a.meta.Exec("UPDATE export_jobs SET total_rows=?, updated_at=? WHERE id= ?", count, time.Now(), id) }
|
||||
}
|
||||
path, size, _ := w.Close()
|
||||
log.Printf("job_id=%d sql=%s args=%v", id, "INSERT INTO export_job_files (job_id, storage_uri, row_count, size_bytes, created_at, updated_at) VALUES (?,?,?,?,?,?)", []interface{}{id, path, count, size, time.Now(), time.Now()})
|
||||
a.meta.Exec("INSERT INTO export_job_files (job_id, storage_uri, row_count, size_bytes, created_at, updated_at) VALUES (?,?,?,?,?,?)", id, path, count, size, time.Now(), time.Now())
|
||||
log.Printf("job_id=%d sql=%s args=%v", id, "UPDATE export_jobs SET status=?, finished_at=?, total_rows=?, updated_at=? WHERE id= ?", []interface{}{"completed", time.Now(), count, time.Now(), id})
|
||||
a.meta.Exec("UPDATE export_jobs SET status=?, finished_at=?, total_rows=?, updated_at=? WHERE id= ?", "completed", time.Now(), count, time.Now(), id)
|
||||
return
|
||||
}
|
||||
if fmt == "xlsx" {
|
||||
x, path, err := exporter.NewXLSXWriter("storage", "export", "Sheet1")
|
||||
if err != nil {
|
||||
a.meta.Exec("UPDATE export_jobs SET status=?, finished_at=? WHERE id= ?", "failed", time.Now(), id)
|
||||
return
|
||||
}
|
||||
x.WriteHeader(cols)
|
||||
log.Printf("job_id=%d sql=%s args=%v", id, q, args)
|
||||
rows, err := db.Query(q, args...)
|
||||
if err != nil {
|
||||
a.meta.Exec("UPDATE export_jobs SET status=?, finished_at=? WHERE id= ?", "failed", time.Now(), id)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
out := make([]interface{}, len(cols))
|
||||
dest := make([]interface{}, len(cols))
|
||||
for i := range out {
|
||||
dest[i] = &out[i]
|
||||
}
|
||||
var count int64
|
||||
for rows.Next() {
|
||||
if err := rows.Scan(dest...); err != nil {
|
||||
a.meta.Exec("UPDATE export_jobs SET status=?, finished_at=? WHERE id=?", "failed", time.Now(), id)
|
||||
return
|
||||
}
|
||||
vals := make([]string, len(cols))
|
||||
for i := range out {
|
||||
if b, ok := out[i].([]byte); ok {
|
||||
vals[i] = string(b)
|
||||
} else if out[i] == nil {
|
||||
vals[i] = ""
|
||||
} else {
|
||||
vals[i] = toString(out[i])
|
||||
}
|
||||
}
|
||||
x.WriteRow(vals)
|
||||
count++
|
||||
}
|
||||
p, size, _ := x.Close(path)
|
||||
log.Printf("job_id=%d sql=%s args=%v", id, "INSERT INTO export_job_files (job_id, storage_uri, row_count, size_bytes, created_at, updated_at) VALUES (?,?,?,?,?,?)", []interface{}{id, p, count, size, time.Now(), time.Now()})
|
||||
a.meta.Exec("INSERT INTO export_job_files (job_id, storage_uri, row_count, size_bytes, created_at, updated_at) VALUES (?,?,?,?,?,?)", id, p, count, size, time.Now(), time.Now())
|
||||
log.Printf("job_id=%d sql=%s args=%v", id, "UPDATE export_jobs SET status=?, finished_at=?, total_rows=?, updated_at=? WHERE id= ?", []interface{}{"completed", time.Now(), count, time.Now(), id})
|
||||
a.meta.Exec("UPDATE export_jobs SET status=?, finished_at=?, total_rows=?, updated_at=? WHERE id= ?", "completed", time.Now(), count, time.Now(), id)
|
||||
return
|
||||
}
|
||||
a.meta.Exec("UPDATE export_jobs SET status=?, finished_at=?, updated_at=? WHERE id= ?", "failed", time.Now(), time.Now(), id)
|
||||
}
|
||||
|
||||
func (a *ExportsAPI) selectDataDB(ds string) *sql.DB {
|
||||
if ds == "ymt" {
|
||||
return a.meta
|
||||
}
|
||||
return a.marketing
|
||||
}
|
||||
|
||||
func (a *ExportsAPI) get(w http.ResponseWriter, r *http.Request, id string) {
|
||||
row := a.meta.QueryRow("SELECT id, template_id, status, requested_by, total_rows, file_format, started_at, finished_at, created_at, updated_at FROM export_jobs WHERE id=?", id)
|
||||
var m = map[string]interface{}{}
|
||||
var jid uint64
|
||||
var templateID uint64
|
||||
var status string
|
||||
var requestedBy uint64
|
||||
var totalRows sql.NullInt64
|
||||
var fileFormat string
|
||||
var startedAt, finishedAt sql.NullTime
|
||||
var createdAt, updatedAt time.Time
|
||||
err := row.Scan(&jid, &templateID, &status, &requestedBy, &totalRows, &fileFormat, &startedAt, &finishedAt, &createdAt, &updatedAt)
|
||||
if err != nil {
|
||||
fail(w, r, http.StatusNotFound, "not found")
|
||||
return
|
||||
}
|
||||
m["id"] = jid
|
||||
m["template_id"] = templateID
|
||||
m["status"] = status
|
||||
m["requested_by"] = requestedBy
|
||||
m["file_format"] = fileFormat
|
||||
m["total_rows"] = totalRows.Int64
|
||||
m["started_at"] = startedAt.Time
|
||||
m["finished_at"] = finishedAt.Time
|
||||
m["created_at"] = createdAt
|
||||
m["updated_at"] = updatedAt
|
||||
rows, _ := a.meta.Query("SELECT storage_uri, sheet_name, row_count, size_bytes FROM export_job_files WHERE job_id=?", id)
|
||||
files := []map[string]interface{}{}
|
||||
for rows.Next() {
|
||||
var uri, sheet sql.NullString
|
||||
var rc, sz sql.NullInt64
|
||||
rows.Scan(&uri, &sheet, &rc, &sz)
|
||||
files = append(files, map[string]interface{}{"storage_uri": uri.String, "sheet_name": sheet.String, "row_count": rc.Int64, "size_bytes": sz.Int64})
|
||||
}
|
||||
rows.Close()
|
||||
m["files"] = files
|
||||
ok(w, r, m)
|
||||
}
|
||||
|
||||
func (a *ExportsAPI) download(w http.ResponseWriter, r *http.Request, id string) {
|
||||
row := a.meta.QueryRow("SELECT storage_uri FROM export_job_files WHERE job_id=? ORDER BY id DESC LIMIT 1", id)
|
||||
var uri string
|
||||
err := row.Scan(&uri)
|
||||
if err != nil {
|
||||
fail(w, r, http.StatusNotFound, "not found")
|
||||
return
|
||||
}
|
||||
http.ServeFile(w, r, uri)
|
||||
}
|
||||
|
||||
func (a *ExportsAPI) cancel(w http.ResponseWriter, r *http.Request, id string) {
|
||||
a.meta.Exec("UPDATE export_jobs SET status=?, updated_at=? WHERE id=? AND status IN ('queued','running')", "canceled", time.Now(), id)
|
||||
w.Write([]byte("ok"))
|
||||
}
|
||||
|
||||
func toString(v interface{}) string {
|
||||
switch t := v.(type) {
|
||||
case []byte:
|
||||
return string(t)
|
||||
case string:
|
||||
return t
|
||||
case int64:
|
||||
return strconv.FormatInt(t, 10)
|
||||
case int:
|
||||
return strconv.Itoa(t)
|
||||
case float64:
|
||||
return strconv.FormatFloat(t, 'f', -1, 64)
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
func (a *ExportsAPI) list(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query()
|
||||
page := 1
|
||||
size := 15
|
||||
if p := q.Get("page"); p != "" {
|
||||
if n, err := strconv.Atoi(p); err == nil && n > 0 { page = n }
|
||||
}
|
||||
if s := q.Get("page_size"); s != "" {
|
||||
if n, err := strconv.Atoi(s); err == nil && n > 0 && n <= 100 { size = n }
|
||||
}
|
||||
tplIDStr := q.Get("template_id")
|
||||
var tplID uint64
|
||||
if tplIDStr != "" {
|
||||
if n, err := strconv.ParseUint(tplIDStr, 10, 64); err == nil { tplID = n }
|
||||
}
|
||||
offset := (page - 1) * size
|
||||
var totalCount int64
|
||||
if tplID > 0 {
|
||||
row := a.meta.QueryRow("SELECT COUNT(1) FROM export_jobs WHERE template_id = ?", tplID)
|
||||
_ = row.Scan(&totalCount)
|
||||
} else {
|
||||
row := a.meta.QueryRow("SELECT COUNT(1) FROM export_jobs")
|
||||
_ = row.Scan(&totalCount)
|
||||
}
|
||||
var rows *sql.Rows
|
||||
var err error
|
||||
if tplID > 0 {
|
||||
rows, err = a.meta.Query("SELECT id, template_id, status, requested_by, row_estimate, total_rows, file_format, created_at, updated_at, explain_score FROM export_jobs WHERE template_id = ? ORDER BY id DESC LIMIT ? OFFSET ?", tplID, size, offset)
|
||||
} else {
|
||||
rows, err = a.meta.Query("SELECT id, template_id, status, requested_by, row_estimate, total_rows, file_format, created_at, updated_at, explain_score FROM export_jobs ORDER BY id DESC LIMIT ? OFFSET ?", size, offset)
|
||||
}
|
||||
if err != nil {
|
||||
fail(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
id, _ := res.LastInsertId()
|
||||
go a.runJob(uint64(id), dataDB, q, args, fs, p.FileFormat)
|
||||
ok(w, r, map[string]interface{}{"id": id})
|
||||
}
|
||||
|
||||
func (a *ExportsAPI) runJob(id uint64, db *sql.DB, q string, args []interface{}, cols []string, fmt string) {
|
||||
log.Printf("job_id=%d sql=%s args=%v", id, "UPDATE export_jobs SET status=?, started_at=? WHERE id= ?", []interface{}{"running", time.Now(), id})
|
||||
a.meta.Exec("UPDATE export_jobs SET status=?, started_at=? WHERE id= ?", "running", time.Now(), id)
|
||||
if fmt == "csv" {
|
||||
w, err := exporter.NewCSVWriter("storage", "export")
|
||||
if err != nil {
|
||||
a.meta.Exec("UPDATE export_jobs SET status=?, finished_at=? WHERE id= ?", "failed", time.Now(), id)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(cols)
|
||||
log.Printf("job_id=%d sql=%s args=%v", id, q, args)
|
||||
rows, err := db.Query(q, args...)
|
||||
if err != nil {
|
||||
a.meta.Exec("UPDATE export_jobs SET status=?, finished_at=? WHERE id= ?", "failed", time.Now(), id)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
out := make([]interface{}, len(cols))
|
||||
dest := make([]interface{}, len(cols))
|
||||
for i := range out {
|
||||
dest[i] = &out[i]
|
||||
}
|
||||
var count int64
|
||||
for rows.Next() {
|
||||
if err := rows.Scan(dest...); err != nil {
|
||||
a.meta.Exec("UPDATE export_jobs SET status=?, finished_at=? WHERE id=?", "failed", time.Now(), id)
|
||||
return
|
||||
}
|
||||
vals := make([]string, len(cols))
|
||||
for i := range out {
|
||||
if b, ok := out[i].([]byte); ok {
|
||||
vals[i] = string(b)
|
||||
} else if out[i] == nil {
|
||||
vals[i] = ""
|
||||
} else {
|
||||
vals[i] = toString(out[i])
|
||||
}
|
||||
}
|
||||
w.WriteRow(vals)
|
||||
count++
|
||||
}
|
||||
path, size, _ := w.Close()
|
||||
log.Printf("job_id=%d sql=%s args=%v", id, "INSERT INTO export_job_files (job_id, storage_uri, row_count, size_bytes, created_at) VALUES (?,?,?,?,?)", []interface{}{id, path, count, size, time.Now()})
|
||||
a.meta.Exec("INSERT INTO export_job_files (job_id, storage_uri, row_count, size_bytes, created_at) VALUES (?,?,?,?,?)", id, path, count, size, time.Now())
|
||||
log.Printf("job_id=%d sql=%s args=%v", id, "UPDATE export_jobs SET status=?, finished_at=?, total_rows=? WHERE id= ?", []interface{}{"completed", time.Now(), count, id})
|
||||
a.meta.Exec("UPDATE export_jobs SET status=?, finished_at=?, total_rows=? WHERE id=?", "completed", time.Now(), count, id)
|
||||
return
|
||||
}
|
||||
if fmt == "xlsx" {
|
||||
x, path, err := exporter.NewXLSXWriter("storage", "export", "Sheet1")
|
||||
if err != nil {
|
||||
a.meta.Exec("UPDATE export_jobs SET status=?, finished_at=? WHERE id= ?", "failed", time.Now(), id)
|
||||
return
|
||||
}
|
||||
x.WriteHeader(cols)
|
||||
log.Printf("job_id=%d sql=%s args=%v", id, q, args)
|
||||
rows, err := db.Query(q, args...)
|
||||
if err != nil {
|
||||
a.meta.Exec("UPDATE export_jobs SET status=?, finished_at=? WHERE id= ?", "failed", time.Now(), id)
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
out := make([]interface{}, len(cols))
|
||||
dest := make([]interface{}, len(cols))
|
||||
for i := range out {
|
||||
dest[i] = &out[i]
|
||||
}
|
||||
var count int64
|
||||
for rows.Next() {
|
||||
if err := rows.Scan(dest...); err != nil {
|
||||
a.meta.Exec("UPDATE export_jobs SET status=?, finished_at=? WHERE id=?", "failed", time.Now(), id)
|
||||
return
|
||||
}
|
||||
vals := make([]string, len(cols))
|
||||
for i := range out {
|
||||
if b, ok := out[i].([]byte); ok {
|
||||
vals[i] = string(b)
|
||||
} else if out[i] == nil {
|
||||
vals[i] = ""
|
||||
} else {
|
||||
vals[i] = toString(out[i])
|
||||
}
|
||||
}
|
||||
x.WriteRow(vals)
|
||||
count++
|
||||
}
|
||||
p, size, _ := x.Close(path)
|
||||
log.Printf("job_id=%d sql=%s args=%v", id, "INSERT INTO export_job_files (job_id, storage_uri, row_count, size_bytes, created_at) VALUES (?,?,?,?,?)", []interface{}{id, p, count, size, time.Now()})
|
||||
a.meta.Exec("INSERT INTO export_job_files (job_id, storage_uri, row_count, size_bytes, created_at) VALUES (?,?,?,?,?)", id, p, count, size, time.Now())
|
||||
log.Printf("job_id=%d sql=%s args=%v", id, "UPDATE export_jobs SET status=?, finished_at=?, total_rows=? WHERE id= ?", []interface{}{"completed", time.Now(), count, id})
|
||||
a.meta.Exec("UPDATE export_jobs SET status=?, finished_at=?, total_rows=? WHERE id=?", "completed", time.Now(), count, id)
|
||||
return
|
||||
}
|
||||
a.meta.Exec("UPDATE export_jobs SET status=?, finished_at=? WHERE id= ?", "failed", time.Now(), id)
|
||||
}
|
||||
|
||||
func (a *ExportsAPI) selectDataDB(ds string) *sql.DB {
|
||||
if ds == "ymt" {
|
||||
return a.meta
|
||||
}
|
||||
return a.marketing
|
||||
}
|
||||
|
||||
func (a *ExportsAPI) get(w http.ResponseWriter, r *http.Request, id string) {
|
||||
row := a.meta.QueryRow("SELECT id, template_id, status, requested_by, total_rows, file_format, started_at, finished_at, created_at, updated_at FROM export_jobs WHERE id=?", id)
|
||||
var m = map[string]interface{}{}
|
||||
var jid uint64
|
||||
var templateID uint64
|
||||
var status string
|
||||
var requestedBy uint64
|
||||
var totalRows sql.NullInt64
|
||||
var fileFormat string
|
||||
var startedAt, finishedAt sql.NullTime
|
||||
var createdAt, updatedAt time.Time
|
||||
err := row.Scan(&jid, &templateID, &status, &requestedBy, &totalRows, &fileFormat, &startedAt, &finishedAt, &createdAt, &updatedAt)
|
||||
if err != nil {
|
||||
fail(w, r, http.StatusNotFound, "not found")
|
||||
return
|
||||
}
|
||||
m["id"] = jid
|
||||
m["template_id"] = templateID
|
||||
m["status"] = status
|
||||
m["requested_by"] = requestedBy
|
||||
m["file_format"] = fileFormat
|
||||
m["total_rows"] = totalRows.Int64
|
||||
m["started_at"] = startedAt.Time
|
||||
m["finished_at"] = finishedAt.Time
|
||||
m["created_at"] = createdAt
|
||||
m["updated_at"] = updatedAt
|
||||
rows, _ := a.meta.Query("SELECT storage_uri, sheet_name, row_count, size_bytes FROM export_job_files WHERE job_id=?", id)
|
||||
files := []map[string]interface{}{}
|
||||
defer rows.Close()
|
||||
items := []map[string]interface{}{}
|
||||
for rows.Next() {
|
||||
var uri, sheet sql.NullString
|
||||
var rc, sz sql.NullInt64
|
||||
rows.Scan(&uri, &sheet, &rc, &sz)
|
||||
files = append(files, map[string]interface{}{"storage_uri": uri.String, "sheet_name": sheet.String, "row_count": rc.Int64, "size_bytes": sz.Int64})
|
||||
}
|
||||
rows.Close()
|
||||
m["files"] = files
|
||||
ok(w, r, m)
|
||||
}
|
||||
|
||||
func (a *ExportsAPI) download(w http.ResponseWriter, r *http.Request, id string) {
|
||||
row := a.meta.QueryRow("SELECT storage_uri FROM export_job_files WHERE job_id=? ORDER BY id DESC LIMIT 1", id)
|
||||
var uri string
|
||||
err := row.Scan(&uri)
|
||||
if err != nil {
|
||||
fail(w, r, http.StatusNotFound, "not found")
|
||||
return
|
||||
}
|
||||
http.ServeFile(w, r, uri)
|
||||
}
|
||||
|
||||
func (a *ExportsAPI) cancel(w http.ResponseWriter, r *http.Request, id string) {
|
||||
a.meta.Exec("UPDATE export_jobs SET status=? WHERE id=? AND status IN ('queued','running')", "canceled", id)
|
||||
w.Write([]byte("ok"))
|
||||
}
|
||||
|
||||
func toString(v interface{}) string {
|
||||
switch t := v.(type) {
|
||||
case []byte:
|
||||
return string(t)
|
||||
case string:
|
||||
return t
|
||||
case int64:
|
||||
return strconv.FormatInt(t, 10)
|
||||
case int:
|
||||
return strconv.Itoa(t)
|
||||
case float64:
|
||||
return strconv.FormatFloat(t, 'f', -1, 64)
|
||||
default:
|
||||
return ""
|
||||
var id, tid, req uint64
|
||||
var status, fmtstr string
|
||||
var estimate, total sql.NullInt64
|
||||
var createdAt, updatedAt sql.NullTime
|
||||
var score sql.NullInt64
|
||||
if err := rows.Scan(&id, &tid, &status, &req, &estimate, &total, &fmtstr, &createdAt, &updatedAt, &score); err != nil { continue }
|
||||
evalStatus := "通过"
|
||||
if score.Int64 < 60 { evalStatus = "禁止" }
|
||||
desc := fmt.Sprintf("评分:%d,估算行数:%d;%s", score.Int64, estimate.Int64, map[bool]string{true:"允许执行", false:"禁止执行"}[score.Int64>=60])
|
||||
m := map[string]interface{}{"id": id, "template_id": tid, "status": status, "requested_by": req, "row_estimate": estimate.Int64, "total_rows": total.Int64, "file_format": fmtstr, "created_at": createdAt.Time, "updated_at": updatedAt.Time, "eval_status": evalStatus, "eval_desc": desc}
|
||||
items = append(items, m)
|
||||
}
|
||||
ok(w, r, map[string]interface{}{"items": items, "total": totalCount, "page": page, "page_size": size})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,72 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ResellersAPI struct {
|
||||
marketing *sql.DB
|
||||
}
|
||||
|
||||
func ResellersHandler(marketing *sql.DB) http.Handler {
|
||||
api := &ResellersAPI{marketing: marketing}
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
p := strings.TrimPrefix(r.URL.Path, "/api/resellers")
|
||||
if r.Method == http.MethodGet && p == "" {
|
||||
api.list(w, r)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
})
|
||||
}
|
||||
|
||||
func (a *ResellersAPI) list(w http.ResponseWriter, r *http.Request) {
|
||||
creatorsParam := r.URL.Query().Get("creator")
|
||||
q := r.URL.Query().Get("q")
|
||||
limitStr := r.URL.Query().Get("limit")
|
||||
limit := 2000
|
||||
if limitStr != "" {
|
||||
if n, err := strconv.Atoi(limitStr); err == nil && n > 0 && n <= 10000 { limit = n }
|
||||
}
|
||||
creators := []string{}
|
||||
for _, s := range strings.Split(creatorsParam, ",") {
|
||||
s = strings.TrimSpace(s)
|
||||
if s != "" { creators = append(creators, s) }
|
||||
}
|
||||
if len(creators) == 0 {
|
||||
ok(w, r, []map[string]interface{}{})
|
||||
return
|
||||
}
|
||||
ph := strings.Repeat("?,", len(creators))
|
||||
ph = strings.TrimSuffix(ph, ",")
|
||||
sql1 := "SELECT DISTINCT reseller_id, COALESCE(reseller_name,'') AS name FROM plan WHERE reseller_id IS NOT NULL AND creator IN (" + ph + ")"
|
||||
args := []interface{}{}
|
||||
for _, c := range creators { args = append(args, c) }
|
||||
if q != "" {
|
||||
sql1 += " AND (CAST(reseller_id AS CHAR) LIKE ? OR reseller_name LIKE ?)"
|
||||
like := "%" + q + "%"
|
||||
args = append(args, like, like)
|
||||
}
|
||||
sql1 += " ORDER BY reseller_id ASC LIMIT ?"
|
||||
args = append(args, limit)
|
||||
rows, err := a.marketing.Query(sql1, args...)
|
||||
if err != nil {
|
||||
fail(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
out := []map[string]interface{}{}
|
||||
for rows.Next() {
|
||||
var id sql.NullInt64
|
||||
var name sql.NullString
|
||||
if err := rows.Scan(&id, &name); err != nil { continue }
|
||||
if !id.Valid { continue }
|
||||
m := map[string]interface{}{"id": id.Int64, "name": name.String}
|
||||
out = append(out, m)
|
||||
}
|
||||
ok(w, r, out)
|
||||
}
|
||||
|
||||
|
|
@ -14,6 +14,8 @@ func NewRouter(metaDB *sql.DB, marketingDB *sql.DB) http.Handler {
|
|||
mux.Handle("/api/exports/", withAccess(withTrace(ExportsHandler(metaDB, marketingDB))))
|
||||
mux.Handle("/api/creators", withAccess(withTrace(CreatorsHandler(marketingDB))))
|
||||
mux.Handle("/api/creators/", withAccess(withTrace(CreatorsHandler(marketingDB))))
|
||||
mux.Handle("/api/resellers", withAccess(withTrace(ResellersHandler(marketingDB))))
|
||||
mux.Handle("/api/resellers/", withAccess(withTrace(ResellersHandler(marketingDB))))
|
||||
sd := staticDir()
|
||||
mux.Handle("/", http.FileServer(http.Dir(sd)))
|
||||
return mux
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
|
|
@ -38,21 +38,38 @@
|
|||
<el-table-column prop="exec_count" label="执行次数" width="120"></el-table-column>
|
||||
<el-table-column label="操作" width="260">
|
||||
<template #default="scope">
|
||||
<el-button size="small" type="primary" @click="openExport(scope.row)">执行</el-button>
|
||||
<el-button size="small" @click="openEdit(scope.row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" @click="removeTemplate(scope.row.id)">删除</el-button>
|
||||
<el-button size="small" type="primary" @click="openExport(scope.row)">执行</el-button>
|
||||
<el-button size="small" @click="openJobs(scope.row)">任务</el-button>
|
||||
<el-button size="small" @click="openEdit(scope.row)">编辑</el-button>
|
||||
<el-button size="small" type="danger" @click="removeTemplate(scope.row.id)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-card header="导出任务">
|
||||
<div v-if="job.id" class="job">
|
||||
任务 {{ job.id }} 状态:<b>{{ job.status }}</b> 行数:{{ job.total_rows || '' }}
|
||||
<el-button v-if="job.files && job.files.length" type="success" size="small" @click="download(job.id)">下载</el-button>
|
||||
<el-card v-if="jobsVisible" :header="'导出任务(模板 '+ (jobsTplId||'') +')'">
|
||||
<el-table :data="jobs" size="small" stripe>
|
||||
<el-table-column prop="id" label="ID"></el-table-column>
|
||||
<el-table-column prop="eval_status" label="校验状态"></el-table-column>
|
||||
<el-table-column label="进度">
|
||||
<template #default="scope">{{ jobPercent(scope.row) }}</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="eval_desc" label="评估描述"></el-table-column>
|
||||
<el-table-column prop="total_rows" label="行数"></el-table-column>
|
||||
<el-table-column prop="file_format" label="格式"></el-table-column>
|
||||
<el-table-column prop="created_at" label="创建时间"></el-table-column>
|
||||
<el-table-column label="操作" width="140">
|
||||
<template #default="scope">
|
||||
<el-button size="small" type="success" @click="download(scope.row.id)">下载</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div style="display:flex;justify-content:space-between;margin-top:8px">
|
||||
<div><el-button size="small" @click="closeJobs">关闭</el-button></div>
|
||||
<div><el-pagination background layout="prev, pager, next, total" :total="jobsTotal" :page-size="jobsPageSize" :current-page="jobsPage" @current-change="(p)=>loadJobs(p)" /></div>
|
||||
</div>
|
||||
<div v-else>暂无任务</div>
|
||||
<div v-if="!jobs || !jobs.length" style="padding:8px 0;color:#999">暂无任务</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
|
@ -143,13 +160,13 @@
|
|||
<el-button type="primary" @click="saveEdit">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<el-dialog v-model="exportVisible" :title="exportTitle" width="700px">
|
||||
<el-dialog v-model="exportVisible" :title="exportTitle" width="1100px">
|
||||
<el-form ref="exportFormRef" :model="exportForm" :rules="exportRules" label-width="110px" status-icon>
|
||||
<el-divider content-position="left">筛选条件</el-divider>
|
||||
<el-row :gutter="8">
|
||||
<el-col :span="12">
|
||||
<el-col :span="24">
|
||||
<el-form-item label="时间范围" required show-message prop="dateRange">
|
||||
<el-date-picker v-model="exportForm.dateRange" type="datetimerange" start-placeholder="开始时间" end-placeholder="结束时间" value-format="YYYY-MM-DD HH:mm:ss" />
|
||||
<el-date-picker v-model="exportForm.dateRange" type="datetimerange" start-placeholder="开始时间" end-placeholder="结束时间" value-format="YYYY-MM-DD HH:mm:ss" style="width:100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
|
@ -164,7 +181,9 @@
|
|||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="分销商ID" prop="resellerId">
|
||||
<el-input v-model.number="exportForm.resellerId" :disabled="!hasCreators" placeholder="reseller_id" />
|
||||
<el-select v-model.number="exportForm.resellerId" filterable :teleported="false" placeholder="请选择分销商" style="width:100%" :disabled="!hasCreators">
|
||||
<el-option v-for="opt in resellerOptions" :key="opt.value" :label="opt.label||String(opt.value)" :value="opt.value" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
|
|
|||
58
web/main.js
58
web/main.js
|
|
@ -3,6 +3,12 @@ const { createApp, reactive } = Vue;
|
|||
setup(){
|
||||
const state = reactive({
|
||||
templates: [],
|
||||
jobs: [],
|
||||
jobsVisible: false,
|
||||
jobsTplId: null,
|
||||
jobsPage: 1,
|
||||
jobsPageSize: 15,
|
||||
jobsTotal: 0,
|
||||
job: {},
|
||||
form: {
|
||||
name: '',
|
||||
|
|
@ -345,6 +351,7 @@ const { createApp, reactive } = Vue;
|
|||
return v || ''
|
||||
}
|
||||
const creatorOptions = Vue.ref([])
|
||||
const resellerOptions = Vue.ref([])
|
||||
const hasCreators = Vue.computed(()=> Array.isArray(state.exportForm.creatorIds) && state.exportForm.creatorIds.length>0 )
|
||||
const hasReseller = Vue.computed(()=> !!state.exportForm.resellerId)
|
||||
const hasPlan = Vue.computed(()=> !!state.exportForm.planId)
|
||||
|
|
@ -358,6 +365,16 @@ const { createApp, reactive } = Vue;
|
|||
creatorOptions.value = arr.map(it=>({label: it.name || String(it.id), value: Number(it.id)}))
|
||||
}catch(_e){ creatorOptions.value = [] }
|
||||
}
|
||||
const loadResellers = async ()=>{
|
||||
const ids = Array.isArray(state.exportForm.creatorIds) ? state.exportForm.creatorIds : []
|
||||
if(!ids.length){ resellerOptions.value = []; return }
|
||||
try{
|
||||
const res = await fetch(API_BASE + '/api/resellers?creator=' + ids.join(','))
|
||||
const data = await res.json()
|
||||
const arr = Array.isArray(data?.data) ? data.data : (Array.isArray(data) ? data : [])
|
||||
resellerOptions.value = arr.map(it=>({label: (it.name||'') + (it.name?'':'') , value: Number(it.id)}))
|
||||
}catch(_e){ resellerOptions.value = [] }
|
||||
}
|
||||
const exportType = Vue.computed(()=>{
|
||||
const f = state.exportTpl && state.exportTpl.filters
|
||||
if(!f) return null
|
||||
|
|
@ -404,6 +421,12 @@ const { createApp, reactive } = Vue;
|
|||
const pad=(n)=>String(n).padStart(2,'0');
|
||||
return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
||||
}
|
||||
const yearRange = ()=>{
|
||||
const now = new Date()
|
||||
const start = new Date(now.getFullYear(), 0, 1, 0, 0, 0)
|
||||
const end = new Date(now.getFullYear(), 11, 31, 23, 59, 59)
|
||||
return [ fmtDT(start), fmtDT(end) ]
|
||||
}
|
||||
const loadTemplates = async ()=>{
|
||||
try{
|
||||
const res = await fetch(API_BASE + '/api/templates');
|
||||
|
|
@ -420,6 +443,33 @@ const { createApp, reactive } = Vue;
|
|||
state.templates = []
|
||||
}
|
||||
}
|
||||
const loadJobs = async (page)=>{
|
||||
if(!page) page = state.jobsPage
|
||||
try{
|
||||
const qs = new URLSearchParams()
|
||||
qs.set('page', String(page))
|
||||
qs.set('page_size', String(state.jobsPageSize))
|
||||
if(state.jobsTplId){ qs.set('template_id', String(state.jobsTplId)) }
|
||||
const res = await fetch(API_BASE + '/api/exports?' + qs.toString());
|
||||
if(!res.ok){ state.jobs = []; return }
|
||||
const data = await res.json();
|
||||
const payload = data?.data || data || {}
|
||||
const arr = Array.isArray(payload.items) ? payload.items : (Array.isArray(payload) ? payload : [])
|
||||
state.jobs = arr
|
||||
state.jobsTotal = Number(payload.total || 0)
|
||||
state.jobsPage = Number(payload.page || page)
|
||||
}catch(_e){ state.jobs = [] }
|
||||
}
|
||||
const openJobs = (row)=>{ state.jobsTplId = row.id; state.jobsVisible = true; loadJobs(1) }
|
||||
const closeJobs = ()=>{ state.jobsVisible = false }
|
||||
const jobPercent = (row)=>{
|
||||
const est = Number(row.row_estimate || 0)
|
||||
const done = Number(row.total_rows || 0)
|
||||
if(row.status==='completed') return '100%'
|
||||
if(row.status==='queued') return '0%'
|
||||
if(est>0 && done>=0){ const p = Math.max(0, Math.min(100, Math.floor(done*100/est))); return p + '%' }
|
||||
return row.status || ''
|
||||
}
|
||||
const createTemplate = async ()=>{
|
||||
const formRef = createFormRef.value
|
||||
const ok = formRef ? await formRef.validate().catch(()=>false) : true
|
||||
|
|
@ -463,6 +513,7 @@ const { createApp, reactive } = Vue;
|
|||
state.exportForm.datasource = state.exportTpl.datasource || row.datasource || 'marketing'
|
||||
state.exportForm.file_format = state.exportTpl.file_format || row.file_format || 'xlsx'
|
||||
if(state.exportForm.datasource==='marketing'){ loadCreators() }
|
||||
if(!Array.isArray(state.exportForm.dateRange) || state.exportForm.dateRange.length!==2){ state.exportForm.dateRange = yearRange() }
|
||||
state.exportVisible = true
|
||||
}
|
||||
const loadTemplateDetail = async (id)=>{
|
||||
|
|
@ -501,9 +552,9 @@ const { createApp, reactive } = Vue;
|
|||
const j=await r.json();
|
||||
const jid = j?.data?.id ?? j?.id
|
||||
state.exportVisible=false
|
||||
if(jid){ loadJob(jid) } else { msg('任务创建返回异常','error') }
|
||||
if(jid){ loadJob(jid); loadJobs() } else { msg('任务创建返回异常','error') }
|
||||
}
|
||||
Vue.watch(()=>state.exportForm.creatorIds, ()=>{ state.exportForm.resellerId=null; state.exportForm.planId=null; state.exportForm.keyBatchId=null; state.exportForm.codeBatchId=null; state.exportForm.productId=null })
|
||||
Vue.watch(()=>state.exportForm.creatorIds, ()=>{ state.exportForm.resellerId=null; state.exportForm.planId=null; state.exportForm.keyBatchId=null; state.exportForm.codeBatchId=null; state.exportForm.productId=null; loadResellers() })
|
||||
Vue.watch(()=>state.exportForm.resellerId, ()=>{ state.exportForm.planId=null; state.exportForm.keyBatchId=null; state.exportForm.codeBatchId=null; state.exportForm.productId=null })
|
||||
Vue.watch(()=>state.exportForm.planId, ()=>{ state.exportForm.keyBatchId=null; state.exportForm.codeBatchId=null; state.exportForm.productId=null })
|
||||
Vue.watch(()=>state.exportForm.keyBatchId, ()=>{ state.exportForm.codeBatchId=null; state.exportForm.productId=null })
|
||||
|
|
@ -562,7 +613,8 @@ const { createApp, reactive } = Vue;
|
|||
}
|
||||
const download = (id)=>{ window.open(API_BASE + '/api/exports/'+id+'/download','_blank') }
|
||||
loadTemplates()
|
||||
return { ...Vue.toRefs(state), visibilityOptions, formatOptions, datasourceOptions, fieldOptions, loadTemplates, createTemplate, openExport, submitExport, loadJob, download, openEdit, saveEdit, removeTemplate, resizeDialog, createRules, exportRules, editRules, createFormRef, exportFormRef, editFormRef, dsLabel, exportType, isOrder, exportTitle }
|
||||
|
||||
return { ...Vue.toRefs(state), visibilityOptions, formatOptions, datasourceOptions, fieldOptions, loadTemplates, createTemplate, openExport, submitExport, loadJob, loadJobs, openJobs, closeJobs, download, openEdit, saveEdit, removeTemplate, resizeDialog, createRules, exportRules, editRules, createFormRef, exportFormRef, editFormRef, dsLabel, exportType, isOrder, exportTitle, creatorOptions, resellerOptions, hasCreators, hasReseller, hasPlan, hasKeyBatch, hasCodeBatch, jobPercent }
|
||||
}
|
||||
})
|
||||
app.use(ElementPlus)
|
||||
|
|
|
|||
Loading…
Reference in New Issue