refactor(web): 重构前端资源引用和API请求路径

将CDN资源改为本地引用,并统一API请求基础路径
修复导出表单变量名冲突问题
添加错误处理逻辑增强稳定性
This commit is contained in:
zhouyonggao 2025-11-25 10:51:11 +08:00
parent d0e1488815
commit 26e8685ac4
9 changed files with 166 additions and 31 deletions

13
server/.env.example Normal file
View File

@ -0,0 +1,13 @@
PORT=8080
# Marketing MySQL
MARKETING_DB_HOST=127.0.0.1
MARKETING_DB_PORT=3306
MARKETING_DB_USER=root
MARKETING_DB_PASSWORD=
MARKETING_DB_NAME=market
# YMT MySQL
YMT_DB_HOST=127.0.0.1
YMT_DB_PORT=3306
YMT_DB_USER=root
YMT_DB_PASSWORD=
YMT_DB_NAME=market

15
server/.env.local Normal file
View File

@ -0,0 +1,15 @@
PORT=8077
MARKETING_DB_HOST=192.168.6.92
MARKETING_DB_PORT=3306
MARKETING_DB_USER=root
MARKETING_DB_PASSWORD=lansexiongdi
MARKETING_DB_NAME=market
YMT_DB_HOST=47.97.27.195
YMT_DB_PORT=3306
YMT_DB_USER=root
YMT_DB_PASSWORD=lansexiongdi6,
YMT_DB_NAME=merketing
MARKETING_REDIS_HOST=192.168.6.75
MARKETING_REDIS_PORT=6379
MARKETING_REDIS_PASSWORD=lansexiongdi
MARKETING_REDIS_DB=0

View File

@ -41,7 +41,7 @@ func main() {
log.Println("skip migrations: YMT DSN missing")
}
r := api.NewRouter(ymt, marketing)
addr := ":" + func() string { s := os.Getenv("PORT"); if s == "" { return "8080" }; return s }()
addr := ":" + func() string { s := os.Getenv("PORT"); if s == "" { return "8077" }; return s }()
srv := &http.Server{Addr: addr, Handler: r, ReadTimeout: 15 * time.Second, WriteTimeout: 60 * time.Second}
log.Println("server listening on ", addr)
log.Fatal(srv.ListenAndServe())

Binary file not shown.

View File

@ -4,8 +4,8 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>MarketingSystemDataTool</title>
<link rel="stylesheet" href="https://unpkg.com/element-plus/dist/index.css">
<link rel="stylesheet" href="/styles.css">
<link rel="stylesheet" href="./vendor/element-plus.min.css">
<link rel="stylesheet" href="./styles.css">
</head>
<body>
<div id="app">
@ -143,26 +143,26 @@
</template>
</el-dialog>
<el-dialog v-model="exportVisible" title="执行导出" width="700px">
<el-form :model="export" label-width="110px">
<el-form :model="exportForm" label-width="110px">
<el-form-item label="订单类型">
<el-radio-group v-model="export.orderType">
<el-radio-group v-model="exportForm.orderType">
<el-radio :label="1">直充卡密</el-radio>
<el-radio :label="2">立减金</el-radio>
<el-radio :label="3">红包</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="数据权限">
<el-select v-model="export.permissionMode" style="width:160px">
<el-select v-model="exportForm.permissionMode" style="width:160px">
<el-option label="所有" value="all" />
<el-option label="按创建者" value="creator" />
</el-select>
</el-form-item>
<el-form-item v-if="export.permissionMode==='creator'" label="创建者列表"><el-input v-model="export.creatorRaw" placeholder="如1,2,3" /></el-form-item>
<el-form-item v-if="exportForm.permissionMode==='creator'" label="创建者列表"><el-input v-model="exportForm.creatorRaw" placeholder="如1,2,3" /></el-form-item>
<el-form-item label="时间范围">
<el-date-picker v-model="export.timeRange" type="datetimerange" range-separator="至" start-placeholder="开始" end-placeholder="结束" />
<el-date-picker v-model="exportForm.timeRange" type="datetimerange" range-separator="至" start-placeholder="开始" end-placeholder="结束" />
</el-form-item>
<el-form-item label="输出格式">
<el-select v-model="export.file_format" :teleported="false" placeholder="请选择" style="width:160px">
<el-select v-model="exportForm.file_format" :teleported="false" placeholder="请选择" style="width:160px">
<el-option v-for="opt in formatOptions" :key="opt.value" :label="opt.label" :value="opt.value" />
</el-select>
</el-form-item>
@ -173,8 +173,8 @@
</template>
</el-dialog>
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<script src="https://unpkg.com/element-plus/dist/index.full.min.js"></script>
<script src="/main.js"></script>
<script src="./vendor/vue.global.prod.js"></script>
<script src="./vendor/element-plus.full.min.js"></script>
<script src="./main.js"></script>
</body>
</html>

View File

@ -24,8 +24,9 @@ const app = createApp({
createWidth: (localStorage.getItem('tplDialogWidth') || '900px'),
editWidth: (localStorage.getItem('tplEditDialogWidth') || '600px'),
edit: { id: null, name: '', visibility: 'private', file_format: 'csv' },
export: { tplId: null, orderType: 1, permissionMode: 'all', creatorRaw: '', timeRange: [], file_format: 'xlsx' }
exportForm: { tplId: null, orderType: 1, permissionMode: 'all', creatorRaw: '', timeRange: [], file_format: 'xlsx' }
})
const API_BASE = 'http://localhost:8077'
const FIELDS_MAP = {
marketing: {
order: [
@ -207,8 +208,19 @@ const app = createApp({
return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
}
const loadTemplates = async ()=>{
const res = await fetch('/api/templates');
state.templates = await res.json();
try{
const res = await fetch(API_BASE + '/api/templates');
if(!res.ok){
msg('加载模板失败','error');
state.templates = []
return
}
const data = await res.json();
state.templates = Array.isArray(data)?data:[]
}catch(e){
msg('加载模板异常','error');
state.templates = []
}
}
const createTemplate = async ()=>{
let fields = []
@ -233,26 +245,26 @@ const app = createApp({
owner_id: Number(state.form.owner_id),
visibility: state.form.visibility
}
const res = await fetch('/api/templates',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});
const res = await fetch(API_BASE + '/api/templates',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});
if(res.ok){ msg('创建成功'); state.createVisible=false; loadTemplates() } else { msg(await res.text(),'error') }
}
const openExport = (row)=>{
state.export.tplId = row.id
state.export.file_format = row.file_format || 'xlsx'
state.exportForm.tplId = row.id
state.exportForm.file_format = row.file_format || 'xlsx'
state.exportVisible = true
}
const submitExport = async ()=>{
const id = state.export.tplId
const id = state.exportForm.tplId
const filters = {}
if(state.export.permissionMode==='creator' && state.export.creatorRaw){
filters.creator_in = state.export.creatorRaw.split(',').map(s=>Number(s.trim())).filter(x=>!isNaN(x))
if(state.exportForm.permissionMode==='creator' && state.exportForm.creatorRaw){
filters.creator_in = state.exportForm.creatorRaw.split(',').map(s=>Number(s.trim())).filter(x=>!isNaN(x))
}
if(state.export.orderType){ filters.type_eq = Number(state.export.orderType) }
if(state.export.timeRange && state.export.timeRange.length===2){
filters.create_time_between = [fmtDT(new Date(state.export.timeRange[0])), fmtDT(new Date(state.export.timeRange[1]))]
if(state.exportForm.orderType){ filters.type_eq = Number(state.exportForm.orderType) }
if(state.exportForm.timeRange && state.exportForm.timeRange.length===2){
filters.create_time_between = [fmtDT(new Date(state.exportForm.timeRange[0])), fmtDT(new Date(state.exportForm.timeRange[1]))]
}
const payload={template_id:Number(id),requested_by:1,permission:{},options:{},filters, file_format: state.export.file_format};
const r=await fetch('/api/exports',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});
const payload={template_id:Number(id),requested_by:1,permission:{},options:{},filters, file_format: state.exportForm.file_format};
const r=await fetch(API_BASE + '/api/exports',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});
const j=await r.json();
state.exportVisible=false
loadJob(j.id);
@ -284,18 +296,28 @@ const app = createApp({
const saveEdit = async ()=>{
const id = state.edit.id
const payload = { name: state.edit.name, visibility: state.edit.visibility, file_format: state.edit.file_format }
const res = await fetch('/api/templates/'+id,{method:'PATCH',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)})
const res = await fetch(API_BASE + '/api/templates/'+id,{method:'PATCH',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)})
if(res.ok){ msg('保存成功'); state.editVisible=false; loadTemplates() } else { msg(await res.text(),'error') }
}
const removeTemplate = async (id)=>{
const r = await fetch('/api/templates/'+id,{method:'DELETE'})
const r = await fetch(API_BASE + '/api/templates/'+id,{method:'DELETE'})
if(r.ok){ msg('删除成功'); loadTemplates() } else { msg(await r.text(),'error') }
}
const loadJob = async (id)=>{
const res=await fetch('/api/exports/'+id);
state.job = await res.json();
try{
const res=await fetch(API_BASE + '/api/exports/'+id);
if(!res.ok){
msg('加载任务失败','error');
state.job = {}
return
}
state.job = await res.json();
}catch(e){
msg('加载任务异常','error');
state.job = {}
}
}
const download = (id)=>{ window.open('/api/exports/'+id+'/download','_blank') }
const download = (id)=>{ window.open(API_BASE + '/api/exports/'+id+'/download','_blank') }
loadTemplates()
return { ...Vue.toRefs(state), visibilityOptions, formatOptions, fieldOptions, loadTemplates, createTemplate, openExport, submitExport, loadJob, download, openEdit, saveEdit, removeTemplate, resizeDialog }
}

72
web/vendor/element-plus.full.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
web/vendor/element-plus.min.css vendored Normal file

File diff suppressed because one or more lines are too long

12
web/vendor/vue.global.prod.js vendored Normal file

File diff suppressed because one or more lines are too long