1
0
mirror of https://github.com/fumiama/paper-manager.git synced 2026-06-04 23:40:24 +08:00

front: finish docx & back: init

This commit is contained in:
源文雨
2023-03-16 14:03:58 +08:00
parent 6c823457b9
commit a72afdbb5e
26 changed files with 453 additions and 31 deletions

2
.gitignore vendored
View File

@@ -16,3 +16,5 @@
# MacOS storage file
.DS_Store
/data

15
backend/api/main.go Normal file
View File

@@ -0,0 +1,15 @@
package api
import (
"net/http"
"github.com/fumiama/paper-manager/backend/utils"
)
// Handler serves all backend /api call
func Handler(w http.ResponseWriter, r *http.Request) {
if !utils.IsMethod("GET", w, r) {
return
}
http.Error(w, "404 Not Found", http.StatusNotFound)
}

26
backend/file/provider.go Normal file
View File

@@ -0,0 +1,26 @@
package file
import (
"net/http"
"strings"
"github.com/fumiama/paper-manager/backend/global"
"github.com/fumiama/paper-manager/backend/utils"
"github.com/sirupsen/logrus"
)
// Handler serves contents in global.FileFolder
func Handler(w http.ResponseWriter, r *http.Request) {
if !utils.IsMethod("GET", w, r) {
return
}
i := strings.LastIndex(r.URL.Path, "/")
fn := r.URL.Path[i+1:]
if fn == "" {
http.Error(w, "400 Bad Request: empty path", http.StatusBadRequest)
return
}
name := global.FileFolder + fn
logrus.Infoln("[file.Handler]\t serve", name)
http.ServeFile(w, r, name)
}

36
backend/global/base.go Normal file
View File

@@ -0,0 +1,36 @@
package global
import (
"os"
"runtime"
"github.com/sirupsen/logrus"
)
const (
// DataFolder stores all backend data in
DataFolder = "./data/"
// FileFolder stores all blob files
FileFolder = DataFolder + "file/"
)
func init() {
initdir(DataFolder)
initdir(FileFolder)
}
func initdir(folder string) {
err := os.MkdirAll(folder, 0755)
if err != nil {
logrus.Errorln("[os.MkdirAll]\t", err)
os.Exit(line())
}
}
func line() int {
_, _, fileLine, ok := runtime.Caller(2)
if ok {
return fileLine
}
return -1
}

27
backend/utils/method.go Normal file
View File

@@ -0,0 +1,27 @@
package utils
import (
"net/http"
"github.com/sirupsen/logrus"
)
// IP gets ip from r.Header's X-FORWARDED-FOR or r.RemoteAddr
func IP(r *http.Request) string {
forwarded := r.Header.Get("X-FORWARDED-FOR")
if forwarded != "" {
return forwarded
}
return r.RemoteAddr
}
// IsMethod check if the method meets the requirement
// and response 405 Method Not Allowed if not matched
func IsMethod(m string, w http.ResponseWriter, r *http.Request) bool {
logrus.Infoln("[utils.IsMethod]\t accept", IP(r), r.Method, r.URL)
if r.Method != m {
http.Error(w, "405 Method Not Allowed", http.StatusMethodNotAllowed)
return false
}
return true
}

View File

@@ -6,14 +6,14 @@ VITE_PUBLIC_PATH = /
# Cross-domain proxy, you can configure multiple
# Please note that no line breaks
VITE_PROXY = [["/basic-api","http://localhost:3000"],["/upload","http://localhost:3000/upload"]]
VITE_PROXY = [["/api","http://localhost:3000/api"],["/file","http://localhost:3000/file"],["/upload","http://localhost:3000/upload"]]
# VITE_PROXY=[["/api","https://vvbin.cn/test"]]
# Delete console
VITE_DROP_CONSOLE = false
# Basic interface address SPA
VITE_GLOB_API_URL=/basic-api
VITE_GLOB_API_URL=/api
# File upload address optional
VITE_GLOB_UPLOAD_URL=/upload

View File

@@ -16,7 +16,7 @@ VITE_BUILD_COMPRESS = 'none'
VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE = false
# Basic interface address SPA
VITE_GLOB_API_URL=/basic-api
VITE_GLOB_API_URL=/api
# File upload address optional
# It can be forwarded by nginx or write the actual address directly

View File

@@ -45,7 +45,7 @@ const userInfo = {
export default [
{
url: '/basic-api/account/getAccountInfo',
url: '/api/account/getAccountInfo',
timeout: 1000,
method: 'get',
response: () => {
@@ -53,7 +53,7 @@ export default [
},
},
{
url: '/basic-api/user/sessionTimeout',
url: '/api/user/sessionTimeout',
method: 'post',
statusCode: 401,
response: () => {
@@ -61,7 +61,7 @@ export default [
},
},
{
url: '/basic-api/user/tokenExpired',
url: '/api/user/tokenExpired',
method: 'post',
statusCode: 200,
response: () => {

View File

@@ -311,7 +311,7 @@ const areaList: any[] = [
]
export default [
{
url: '/basic-api/cascader/getAreaRecord',
url: '/api/cascader/getAreaRecord',
timeout: 1000,
method: 'post',
response: ({ body }) => {

View File

@@ -16,7 +16,7 @@ const demoList = (keyword: string, count = 20) => {
export default [
{
url: '/basic-api/select/getDemoOptions',
url: '/api/select/getDemoOptions',
timeout: 1000,
method: 'get',
response: ({ query }) => {

View File

@@ -136,7 +136,7 @@ const menuList = (() => {
export default [
{
url: '/basic-api/system/getAccountList',
url: '/api/system/getAccountList',
timeout: 100,
method: 'get',
response: ({ query }) => {
@@ -145,7 +145,7 @@ export default [
},
},
{
url: '/basic-api/system/getRoleListByPage',
url: '/api/system/getRoleListByPage',
timeout: 100,
method: 'get',
response: ({ query }) => {
@@ -154,7 +154,7 @@ export default [
},
},
{
url: '/basic-api/system/setRoleStatus',
url: '/api/system/setRoleStatus',
timeout: 500,
method: 'post',
response: ({ query }) => {
@@ -163,7 +163,7 @@ export default [
},
},
{
url: '/basic-api/system/getAllRoleList',
url: '/api/system/getAllRoleList',
timeout: 100,
method: 'get',
response: () => {
@@ -171,7 +171,7 @@ export default [
},
},
{
url: '/basic-api/system/getDeptList',
url: '/api/system/getDeptList',
timeout: 100,
method: 'get',
response: () => {
@@ -179,7 +179,7 @@ export default [
},
},
{
url: '/basic-api/system/getMenuList',
url: '/api/system/getMenuList',
timeout: 100,
method: 'get',
response: () => {
@@ -187,7 +187,7 @@ export default [
},
},
{
url: '/basic-api/system/accountExist',
url: '/api/system/accountExist',
timeout: 500,
method: 'post',
response: ({ body }) => {

View File

@@ -41,7 +41,7 @@ const demoList = (() => {
export default [
{
url: '/basic-api/table/getDemoList',
url: '/api/table/getDemoList',
timeout: 100,
method: 'get',
response: ({ query }) => {

View File

@@ -26,7 +26,7 @@ const demoTreeList = (keyword: string) => {
export default [
{
url: '/basic-api/tree/getDemoOptions',
url: '/api/tree/getDemoOptions',
timeout: 1000,
method: 'get',
response: ({ query }) => {

View File

@@ -0,0 +1,19 @@
import { MockMethod } from 'vite-plugin-mock'
import { resultError, resultSuccess, getRequestToken, requestParams } from '../_util'
export default [
{
url: '/api/dlFile',
timeout: 1000,
method: 'get',
response: (request: requestParams) => {
const token = getRequestToken(request)
if (!token) return resultError('Invalid token')
const id = Number(request.query.id)
if (!id || id < 0) return resultError('Invalid id')
return resultSuccess({
url: '/file/' + id + '.docx',
})
},
},
] as MockMethod[]

View File

@@ -26,7 +26,7 @@ function createFileList() {
export default [
// mock get filelist
{
url: '/basic-api/getFileList',
url: '/api/getFileList',
timeout: 200,
method: 'get',
response: (request: requestParams) => {
@@ -42,7 +42,7 @@ export default [
},
},
{
url: '/basic-api/getFilePercent',
url: '/api/getFilePercent',
timeout: 200,
method: 'get',
response: (request: requestParams) => {
@@ -71,7 +71,7 @@ export default [
},
},
{
url: '/basic-api/delFile',
url: '/api/delFile',
timeout: 200,
method: 'get',
response: (request: requestParams) => {
@@ -86,7 +86,7 @@ export default [
},
},
{
url: '/basic-api/analyzeFile',
url: '/api/analyzeFile',
timeout: 1000,
method: 'get',
response: (request: requestParams) => {

View File

@@ -227,7 +227,7 @@ const linkRoute = {
export default [
{
url: '/basic-api/getMenuList',
url: '/api/getMenuList',
timeout: 1000,
method: 'get',
response: (request: requestParams) => {

View File

@@ -62,7 +62,7 @@ const fakeCodeList: any = {
export default [
// mock user login
{
url: '/basic-api/login',
url: '/api/login',
timeout: 200,
method: 'post',
response: ({ body }) => {
@@ -86,7 +86,7 @@ export default [
},
// mock reset password
{
url: '/basic-api/resetPassword',
url: '/api/resetPassword',
timeout: 200,
method: 'post',
response: ({ body }) => {
@@ -98,7 +98,7 @@ export default [
},
// mock register
{
url: '/basic-api/register',
url: '/api/register',
timeout: 200,
method: 'post',
response: ({ body }) => {
@@ -109,7 +109,7 @@ export default [
},
},
{
url: '/basic-api/getUserInfo',
url: '/api/getUserInfo',
method: 'get',
response: (request: requestParams) => {
const token = getRequestToken(request)
@@ -122,7 +122,7 @@ export default [
},
},
{
url: '/basic-api/getPermCode',
url: '/api/getPermCode',
timeout: 200,
method: 'get',
response: (request: requestParams) => {
@@ -138,7 +138,7 @@ export default [
},
},
{
url: '/basic-api/logout',
url: '/api/logout',
timeout: 200,
method: 'get',
response: (request: requestParams) => {
@@ -152,7 +152,7 @@ export default [
},
},
{
url: '/basic-api/testRetry',
url: '/api/testRetry',
statusCode: 405,
method: 'get',
response: () => {

View File

@@ -49,6 +49,7 @@
"cropperjs": "^1.5.12",
"crypto-js": "^4.1.1",
"dayjs": "^1.11.1",
"docx-preview": "^0.1.15",
"echarts": "^5.3.2",
"intro.js": "^5.1.0",
"lodash-es": "^4.17.21",

View File

@@ -1,11 +1,13 @@
import { defHttp } from '/@/utils/http/axios'
import { getFileListModel, FilePercent, DelFile, AnalyzeFile } from './model/fileListModel'
import { DownloadFile } from './model/fileModel'
enum Api {
GetFileList = '/getFileList',
GetFilePercent = '/getFilePercent',
DelFile = '/delFile',
AnalyzeFile = '/analyzeFile',
DlFile = '/dlFile',
}
/**
@@ -35,3 +37,10 @@ export const delFile = (id: number) => {
export const analyzeFile = (id: number) => {
return defHttp.get<AnalyzeFile>({ url: Api.AnalyzeFile, params: { id: id } })
}
/**
* @description: Download file
*/
export const downloadFile = (id: number) => {
return defHttp.get<DownloadFile>({ url: Api.DlFile, params: { id: id } })
}

View File

@@ -0,0 +1,3 @@
export interface DownloadFile {
url: string
}

View File

@@ -1,3 +1,4 @@
export default {
name: '试卷库',
file: '试卷解析',
}

View File

@@ -1,8 +1,10 @@
import type { AppRouteModule } from '/@/router/types'
import { ExceptionEnum } from '/@/enums/exceptionEnum'
import { LAYOUT } from '/@/router/constant'
import { t } from '/@/hooks/web/useI18n'
const ExceptionPage = () => import('/@/views/sys/exception/Exception.vue')
const filelist: AppRouteModule = {
path: '/filelist',
name: 'FileList',
@@ -25,6 +27,28 @@ const filelist: AppRouteModule = {
hideMenu: true,
},
},
{
path: 'file/:id',
name: 'FilePage',
component: () => import('/@/views/page/file/index.vue'),
meta: {
title: t('routes.filelist.file'),
carryParam: true,
icon: 'bi:filetype-docx',
hideMenu: true,
},
},
{
path: '404',
name: 'PageNotFound',
component: ExceptionPage,
props: {
status: ExceptionEnum.PAGE_NOT_FOUND,
},
meta: {
title: '404',
},
},
],
}

View File

@@ -0,0 +1,206 @@
<template>
<PageWrapper title="带参数标签页" content="支持带参数多tab缓存">
<div ref="chartRef" :style="{ height, width }"></div>
<div class="docxWrap" :style="{ width }">
<div ref="docxRef"></div>
</div>
</PageWrapper>
</template>
<script lang="ts">
import { computed, defineComponent, unref, PropType, ref, Ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { PageWrapper } from '/@/components/Page'
import { useECharts } from '/@/hooks/web/useECharts'
import { renderAsync } from 'docx-preview'
import { downloadFile } from '/@/api/page'
import { DownloadFile } from '/@/api/page/model/fileModel'
import { router } from '/@/router'
import { PageEnum } from '/@/enums/pageEnum'
import axios from 'axios'
let docxRef = ref(null)
function loadDocx(file: Blob) {
renderAsync(file, docxRef.value as unknown as HTMLElement, undefined, {
className: 'docx', // 默认和文档样式类的类名/前缀
inWrapper: false, // 启用围绕文档内容渲染包装器
ignoreWidth: false, // 禁止页面渲染宽度
ignoreHeight: false, // 禁止页面渲染高度
ignoreFonts: false, // 禁止字体渲染
breakPages: false, // 在分页符上启用分页
ignoreLastRenderedPageBreak: true, //禁用lastRenderedPageBreak元素的分页
experimental: true, // 启用实验性功能(制表符停止计算)
trimXmlDeclaration: true, // 如果为真xml声明将在解析之前从xml文档中删除
debug: false, // 启用额外的日志记录
})
}
export default defineComponent({
name: 'TestTab',
components: { PageWrapper },
props: {
width: {
type: String as PropType<string>,
default: '100%',
},
height: {
type: String as PropType<string>,
default: 'calc(100vh - 78px)',
},
},
setup() {
const { currentRoute } = useRouter()
const params = computed(() => {
return unref(currentRoute).params
})
if (!params.value || !params.value.id) {
router.push(PageEnum.ERROR_PAGE)
}
downloadFile(Number(params.value.id)).then((file: DownloadFile) => {
if (file && file.url) {
axios({
method: 'get',
responseType: 'blob',
url: file.url,
}).then(({ data }) => {
loadDocx(data)
})
}
})
const chartRef = ref<HTMLDivElement | null>(null)
const { setOptions } = useECharts(chartRef as Ref<HTMLDivElement>)
const dataAll = [389, 259, 262, 324, 232, 176, 196, 214, 133, 370]
const yAxisData = [
'原因1',
'原因2',
'原因3',
'原因4',
'原因5',
'原因6',
'原因7',
'原因8',
'原因9',
'原因10',
]
onMounted(() => {
setOptions({
title: [
{
text: '题量占比',
left: '2%',
top: '1%',
textStyle: {
fontSize: 36,
},
},
{
text: '重复率前十',
left: '40%',
top: '1%',
textStyle: {
fontSize: 36,
},
},
{
text: '分数占比',
left: '2%',
top: '50%',
textStyle: {
fontSize: 36,
},
},
],
grid: [{ left: '50%', top: '7%', width: '45%', height: '90%' }],
tooltip: {
formatter: '{b} ({c})',
},
xAxis: [
{
gridIndex: 0,
axisTick: { show: false },
axisLabel: { show: false },
splitLine: { show: false },
axisLine: { show: false },
},
],
yAxis: [
{
gridIndex: 0,
interval: 0,
data: yAxisData.reverse(),
axisTick: { show: false },
axisLabel: { show: true },
splitLine: { show: false },
axisLine: { show: true },
},
],
series: [
{
name: '各渠道投诉占比',
type: 'pie',
radius: '30%',
center: ['22%', '25%'],
data: [
{ value: 335, name: '客服电话' },
{ value: 310, name: '奥迪官网' },
{ value: 234, name: '媒体曝光' },
{ value: 135, name: '质检总局' },
{ value: 105, name: '其他' },
],
labelLine: { show: false },
label: {
show: true,
formatter: '{b} \n ({d}%)',
},
},
{
name: '各级别投诉占比',
type: 'pie',
radius: '30%',
center: ['22%', '75%'],
labelLine: { show: false },
data: [
{ value: 335, name: 'A级' },
{ value: 310, name: 'B级' },
{ value: 234, name: 'C级' },
{ value: 135, name: 'D级' },
],
label: {
show: true,
formatter: '{b} \n ({d}%)',
},
},
{
name: '投诉原因TOP10',
type: 'bar',
xAxisIndex: 0,
yAxisIndex: 0,
barWidth: '45%',
itemStyle: { color: '#86c9f4' },
label: { show: true, position: 'right' },
data: dataAll.sort(),
},
],
})
})
return {
chartRef,
docxRef,
}
},
})
</script>
<style lang="less" scoped>
.docxWrap {
padding-top: 0px;
margin: 0 auto;
overflow-x: auto;
display: grid;
align-items: center;
justify-items: center;
}
</style>

4
go.mod
View File

@@ -1,3 +1,7 @@
module github.com/fumiama/paper-manager
go 1.20
require github.com/sirupsen/logrus v1.9.0
require golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 // indirect

15
go.sum Normal file
View File

@@ -0,0 +1,15 @@
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

36
main.go
View File

@@ -1,5 +1,39 @@
package main
func main() {
import (
"flag"
"net"
"net/http"
"os"
"runtime"
"github.com/sirupsen/logrus"
"github.com/fumiama/paper-manager/backend/api"
"github.com/fumiama/paper-manager/backend/file"
)
func line() int {
_, _, fileLine, ok := runtime.Caller(1)
if ok {
return fileLine
}
return -1
}
func main() {
addr := flag.String("l", "[::]:3000", "listen addr")
flag.Parse()
l, err := net.Listen("tcp", *addr)
if err != nil {
logrus.Errorln("[net.Listen]\t", err)
os.Exit(line())
}
http.HandleFunc("/api/", api.Handler)
http.HandleFunc("/file/", file.Handler)
logrus.Infoln("[http.Serve]\t start at", l.Addr())
logrus.Errorln("[http.Serve]\t", http.Serve(l, nil))
os.Exit(line())
}