@@ -2,35 +2,24 @@
import { computed , onBeforeUnmount , onMounted , ref , watch } from 'vue'
import { ElMessage , ElMessageBox } from 'element-plus'
import { fetchSystemConfig , updateSystemConfig , executeScheduleNow } from '../api/system'
import { fetchSystemConfig , updateSystemConfig } from '../api/system'
import { fetchKdocsQr , fetchKdocsStatus , clearKdocsLogin } from '../api/kdocs'
import { fetchProxyConfig , testProxy , updateProxyConfig } from '../api/proxy'
const loading = ref ( false )
// 并发
const maxConcurrentGlobal = ref ( 2 )
const maxConcurrentPerAccount = ref ( 1 )
const maxScreenshotConcurrent = ref ( 3 )
// 定时
const scheduleEnabled = ref ( false )
const scheduleTime = ref ( '02:00' )
const scheduleBrowseType = ref ( '应读' )
const scheduleWeekdays = ref ( [ '1' , '2' , '3' , '4' , '5' , '6' , '7' ] )
const scheduleScreenshotEnabled = ref ( true )
// 代理
const proxyEnabled = ref ( false )
const proxyApiUrl = ref ( '' )
const proxyExpireMinutes = ref ( 3 )
// 自动审核
const autoApproveEnabled = ref ( false )
const autoApproveHourlyLimit = ref ( 10 )
const autoApproveVipDays = ref ( 7 )
// 金山文档上传
const kdocsEnabled = ref ( false )
const kdocsDocUrl = ref ( '' )
const kdocsDefaultUnit = ref ( '' )
@@ -42,6 +31,7 @@ const kdocsRowStart = ref(0)
const kdocsRowEnd = ref ( 0 )
const kdocsAdminNotifyEnabled = ref ( false )
const kdocsAdminNotifyEmail = ref ( '' )
const kdocsStatus = ref ( { } )
const kdocsQrOpen = ref ( false )
const kdocsQrImage = ref ( '' )
@@ -52,40 +42,10 @@ const kdocsClearLoading = ref(false)
const kdocsActionHint = ref ( '' )
let kdocsPollingTimer = null
const weekdaysOptions = [
{ label : '周一' , value : '1' } ,
{ label : '周二' , value : '2' } ,
{ label : '周三' , value : '3' } ,
{ label : '周四' , value : '4' } ,
{ label : '周五' , value : '5' } ,
{ label : '周六' , value : '6' } ,
{ label : '周日' , value : '7' } ,
]
const weekdayNames = {
1 : '周一' ,
2 : '周二' ,
3 : '周三' ,
4 : '周四' ,
5 : '周五' ,
6 : '周六' ,
7 : '周日' ,
}
const scheduleWeekdayDisplay = computed ( ( ) =>
( scheduleWeekdays . value || [ ] )
. map ( ( d ) => weekdayNames [ Number ( d ) ] || d )
. join ( '、' ) ,
)
const kdocsActionBusy = computed (
( ) => kdocsStatusLoading . value || kdocsQrLoading . value || kdocsClearLoading . value ,
)
function normalizeBrowseType ( value ) {
if ( String ( value ) === '注册前未读' ) return '注册前未读'
return '应读'
}
function setKdocsHint ( message ) {
if ( ! message ) {
kdocsActionHint . value = ''
@@ -108,17 +68,6 @@ async function loadAll() {
maxConcurrentPerAccount . value = system . max _concurrent _per _account ? ? 1
maxScreenshotConcurrent . value = system . max _screenshot _concurrent ? ? 3
scheduleEnabled . value = ( system . schedule _enabled ? ? 0 ) === 1
scheduleTime . value = system . schedule _time || '02:00'
scheduleBrowseType . value = normalizeBrowseType ( system . schedule _browse _type )
const weekdays = String ( system . schedule _weekdays || '1,2,3,4,5,6,7' )
. split ( ',' )
. map ( ( x ) => x . trim ( ) )
. filter ( Boolean )
scheduleWeekdays . value = weekdays . length ? weekdays : [ '1' , '2' , '3' , '4' , '5' , '6' , '7' ]
scheduleScreenshotEnabled . value = ( system . enable _screenshot ? ? 1 ) === 1
autoApproveEnabled . value = ( system . auto _approve _enabled ? ? 0 ) === 1
autoApproveHourlyLimit . value = system . auto _approve _hourly _limit ? ? 10
autoApproveVipDays . value = system . auto _approve _vip _days ? ? 7
@@ -171,63 +120,6 @@ async function saveConcurrency() {
}
}
async function saveSchedule ( ) {
if ( scheduleEnabled . value && ( ! scheduleWeekdays . value || scheduleWeekdays . value . length === 0 ) ) {
ElMessage . error ( '请至少选择一个执行日期' )
return
}
const payload = {
schedule _enabled : scheduleEnabled . value ? 1 : 0 ,
schedule _time : scheduleTime . value ,
schedule _browse _type : scheduleBrowseType . value ,
schedule _weekdays : ( scheduleWeekdays . value || [ ] ) . join ( ',' ) ,
enable _screenshot : scheduleScreenshotEnabled . value ? 1 : 0 ,
}
const screenshotText = scheduleScreenshotEnabled . value ? '截图' : '不截图'
const message = scheduleEnabled . value
? ` 确定启用定时任务吗? \ n \ n执行时间: 每天 ${ payload . schedule _time } \ n执行日期: ${ scheduleWeekdayDisplay . value } \ n浏览类型: ${ payload . schedule _browse _type } \ n截图: ${ screenshotText } \ n \ n系统将自动执行所有账号的浏览任务 `
: '确定关闭定时任务吗?'
try {
await ElMessageBox . confirm ( message , '保存定时任务' , {
confirmButtonText : '确认' ,
cancelButtonText : '取消' ,
type : 'warning' ,
} )
} catch {
return
}
try {
const res = await updateSystemConfig ( payload )
ElMessage . success ( res ? . message || ( scheduleEnabled . value ? '定时任务已启用' : '定时任务已关闭' ) )
} catch {
// handled by interceptor
}
}
async function runScheduleNow ( ) {
const msg = ` 确定要立即执行定时任务吗? \ n \ n这将执行所有账号的浏览任务 \ n浏览类型: ${ scheduleBrowseType . value } \ n \ n注意: 无视定时时间和执行日期配置, 立即开始执行! `
try {
await ElMessageBox . confirm ( msg , '立即执行' , {
confirmButtonText : '立即执行' ,
cancelButtonText : '取消' ,
type : 'warning' ,
} )
} catch {
return
}
try {
const res = await executeScheduleNow ( )
ElMessage . success ( res ? . message || '定时任务已开始执行' )
} catch {
// handled by interceptor
}
}
async function saveProxy ( ) {
if ( proxyEnabled . value && ! proxyApiUrl . value . trim ( ) ) {
ElMessage . error ( '启用代理时, API地址不能为空' )
@@ -248,6 +140,47 @@ async function saveProxy() {
}
}
async function onTestProxy ( ) {
if ( ! proxyApiUrl . value . trim ( ) ) {
ElMessage . error ( '请先输入代理API地址' )
return
}
try {
const res = await testProxy ( { api _url : proxyApiUrl . value . trim ( ) } )
await ElMessageBox . alert ( res ? . message || '测试完成' , '代理测试' , { confirmButtonText : '知道了' } )
} catch {
// handled by interceptor
}
}
async function saveAutoApprove ( ) {
const hourly = Number ( autoApproveHourlyLimit . value )
const vipDays = Number ( autoApproveVipDays . value )
if ( ! Number . isFinite ( hourly ) || hourly < 1 ) {
ElMessage . error ( '每小时注册限制必须大于0' )
return
}
if ( ! Number . isFinite ( vipDays ) || vipDays < 0 ) {
ElMessage . error ( 'VIP天数不能为负数' )
return
}
const payload = {
auto _approve _enabled : autoApproveEnabled . value ? 1 : 0 ,
auto _approve _hourly _limit : hourly ,
auto _approve _vip _days : vipDays ,
}
try {
const res = await updateSystemConfig ( payload )
ElMessage . success ( res ? . message || '注册设置已保存' )
} catch {
// handled by interceptor
}
}
async function saveKdocsConfig ( ) {
const payload = {
kdocs _enabled : kdocsEnabled . value ? 1 : 0 ,
@@ -375,47 +308,6 @@ onBeforeUnmount(() => {
stopKdocsPolling ( )
} )
async function onTestProxy ( ) {
if ( ! proxyApiUrl . value . trim ( ) ) {
ElMessage . error ( '请先输入代理API地址' )
return
}
try {
const res = await testProxy ( { api _url : proxyApiUrl . value . trim ( ) } )
await ElMessageBox . alert ( res ? . message || '测试完成' , '代理测试' , { confirmButtonText : '知道了' } )
} catch {
// handled by interceptor
}
}
async function saveAutoApprove ( ) {
const hourly = Number ( autoApproveHourlyLimit . value )
const vipDays = Number ( autoApproveVipDays . value )
if ( ! Number . isFinite ( hourly ) || hourly < 1 ) {
ElMessage . error ( '每小时注册限制必须大于0' )
return
}
if ( ! Number . isFinite ( vipDays ) || vipDays < 0 ) {
ElMessage . error ( 'VIP天数不能为负数' )
return
}
const payload = {
auto _approve _enabled : autoApproveEnabled . value ? 1 : 0 ,
auto _approve _hourly _limit : hourly ,
auto _approve _vip _days : vipDays ,
}
try {
const res = await updateSystemConfig ( payload )
ElMessage . success ( res ? . message || '注册设置已保存' )
} catch {
// handled by interceptor
}
}
onMounted ( loadAll )
< / script >
@@ -423,124 +315,102 @@ onMounted(loadAll)
< div class = "page-stack" v-loading = "loading" >
< div class = "app-page-title" >
< h2 > 系统配置 < / h2 >
< div >
< div class = "toolbar" >
< el-button @click ="loadAll" > 刷新 < / el -button >
< / div >
< / div >
< el-card shadow = "never" : body -style = " { padding : ' 16px ' } " class = "car d" >
< h3 class = "section-title " > 系统并发配置 < / h3 >
< div class = "config-gri d" >
< el-card shadow = "never" : body -style = " { padding : ' 16px ' } " class = "card section-card " >
< h3 class = "section-title" > 并发配置 < / h3 >
< div class = "section-sub app-muted" > 控制任务与截图的并发资源上限 < / div >
< el-form label -width = " 130 px " >
< el-form-item label = "全局最大并发数" >
< el-input-number v-model = "maxConcurrentGlobal" :min="1" :max="200" / >
< div class = "help" > 同时最多运行的 账号数量 ( 浏览任务使用 API 方式 , 资源占用较低 ) 。 < / div >
< / el-form-item >
< el-form label -width = " 122 px " >
< el-form-item label = "全局最大并发数" >
< el-input-number v-model = "maxConcurrentGlobal" :min="1" :max="200" / >
< div class = "help" > 同时最多运行账号数 ( 浏览任务 API 执行 , 资源占用较低 ) 。 < / div >
< / el-form-item >
< el-form-item label = "单账号最大并发数" >
< el-input-number v-model = "maxConcurrentPerAccount" :min="1" :max="50" / >
< div class = "help" > 单个账号同时最多运行的任务数量 ( 建议设为 1 ) 。 < / div >
< / el-form-item >
< el-form-item label = "单账号最大并发数" >
< el-input-number v-model = "maxConcurrentPerAccount" :min="1" :max="50" / >
< div class = "help" > 建议保持为 1 , 避免同账号任务抢占 。 < / div >
< / el-form-item >
< el-form-item label = "截图最大并发数" >
< el-input-number v-model = "maxScreenshotConcurrent" :min="1" :max="50" / >
< div class = "help" > 同时进行截图的最大数量 ( wkhtmltoimage 资源占用较低, 可按需提高 ) 。 < / div >
< / el-form-item >
< / el-form >
< el-form-item label = "截图最大并发数" >
< el-input-number v-model = "maxScreenshotConcurrent" :min="1" :max="50" / >
< div class = "help" > 截图 资源占用较低, 可按机器性能逐步提高 。 < / div >
< / el-form-item >
< / el-form >
< el-button type = "primary" @click ="saveConcurrency" > 保存并发配置 < / el -button >
< / el-card >
< div class = "row-actions" >
< el-button type = "primary" @click ="saveConcurrency" > 保存并发配置 < / el -button >
< / div >
< / el-card >
< el-card shadow = "never" : body -style = " { padding : ' 16px ' } " class = "card" >
< h3 class = "section-title" > 定时任务配 置< / h3 >
< el-card shadow = "never" : body -style = " { padding : ' 16px ' } " class = "card section-card " >
< h3 class = "section-title" > 代理设 置< / h3 >
< div class = "section-sub app-muted" > 用于任务出网代理与连接有效期管理 < / div >
< el-form label -width = " 130 px " >
< el-form-item label = "启用定时任务 " >
< el-switch v-model = "schedule Enabled" / >
< div class = "help" > 开启后 , 系统会按计划自动执行 浏览任务。 < / div >
< / el-form-item >
< el-form label -width = " 122 px " >
< el-form-item label = "启用 IP 代理 " >
< el-switch v-model = "proxy Enabled" / >
< div class = "help" > 开启后 , 浏览任务通过代理访问 , 失败自动重试 。 < / div >
< / el-form-item >
< el-form-item v-if = "scheduleEnabled" label="执行时间" >
< el-time -picker v-model = "scheduleTime" value-format="HH:mm" format="HH:mm " / >
< / el-form-item >
< el-form-item label = "代理 API 地址" >
< el-input v-model = "proxyApiUrl" placeholder="http://api.xxx/Tools/IP.ashx?... " / >
< div class = "help" > API 应返回 ` IP:PORT ` ( 例 : 123.45 .67 .89 : 8888 ) 。 < / div >
< / el-form-item >
< el-form-item v-if = "scheduleEnabled" label="浏览类型" >
< el-select v-model = "scheduleBrowseType" style="width: 220px" >
< el -option label = "注册前未读" value = "注册前未读" / >
< el-option label = "应读" value = "应读" / >
< / el-select >
< / el-form-item >
< el-form-item label = "有效期(分钟)" >
< el-input-number v-model = "proxyExpireMinutes" :min="1" :max="60" / >
< / el-form-item >
< / el-form >
< el-form-item v-if = "scheduleEnabled" label="执行日期" >
< el-checkbox -group v-model = "scheduleWeekdays" >
< el-checkbox v-for = "w in weekdaysOptions" :key="w.value" :label="w.value" >
{{ w.label }}
< / el-checkbox >
< / el-checkbox-group >
< / el-form-item >
< div class = "row-actions" >
< el-button type = "primary" @click ="saveProxy" > 保存代理配置 < / el -button >
< el-button @click ="onTestProxy" > 测试代理 < / el -button >
< / div >
< / el-card >
< el-form-item v-if = "scheduleEnabled" label="定时任务截图" >
< el -switch v-model = "scheduleScreenshotEnab led " / >
< div class = "help" > 开启后 , 定时任务执行时会生成截图 。 < / div >
< / el-form-item >
< / el-form >
< el-card shadow = "never" : body -style = " { padding : ' 16px ' } " class = "card section-card" >
< h3 class = "section-tit le"> 注册设置 < / h3 >
< div class = "section-sub app-muted" > 控制注册节流与新用户赠送 VIP < / div >
< div c lass = "row-actions ">
< el-button type = "primary" @click ="saveSchedule" > 保存定时任务配置 < / el -button >
< el-button type = "success" plain @click ="runScheduleNow" > 立即执行 < / el -button >
< el-form label -width = " 122px " >
< el-form-item label = "注册赠送 VIP" >
< el-switch v-model = "autoApproveEnabled" / >
< div class = "help" > 开启后 , 新用户注册成功自动赠送下方设定的 VIP 天数 。 < / div >
< / el-form-item >
< el-form-item label = "每小时注册限制" >
< el-input-number v-model = "autoApproveHourlyLimit" :min="1" :max="10000" / >
< / el-form-item >
< el-form-item label = "赠送 VIP 天数" >
< el-input-number v-model = "autoApproveVipDays" :min="0" :max="999999" / >
< / el-form-item >
< / el-form >
< div class = "row-actions" >
< el-button type = "primary" @click ="saveAutoApprove" > 保存注册设置 < / el -button >
< / div >
< / el-card >
< / div >
< el-card shadow = "never" : body -style = " { padding : ' 16px ' } " class = "card kdocs-card" >
< div class = "section-head" >
< h3 class = "section-title" > 金山文档上传 < / h3 >
< div class = "status-inline app-muted" >
< span > 登录状态 : < / span >
< span v-if = "kdocsStatus.last_login_ok === true" > 已登录 < / span >
< span v-else-if = "kdocsStatus.login_required" > 需要扫码 < / span >
< span v-else > 未知 < / span >
< span > · 待上传 { { kdocsStatus . queue _size || 0 } } < / span >
< / div >
< / div >
< / el-card >
< el-card shadow = "never" : body -style = " { padding : ' 16px ' } " class = "card " >
< h3 class = "section-title" > 代理设置 < / h3 >
< el-form label -width = " 130px " >
< el-form-item label = "启用IP代理" >
< el-switch v-model = "proxyEnabled" / >
< div class = "help" > 开启后 , 所有浏览任务将通过代理IP访问 ( 失败自动重试3次 ) 。 < / div >
< / el-form-item >
< el-form-item label = "代理API地址" >
< el-input v-model = "proxyApiUrl" placeholder="http://api.xxx/Tools/IP.ashx?..." / >
< div class = "help" > API 应返回 : IP : PORT ( 例如 123.45 .67 .89 : 8888 ) 。 < / div >
< / el-form-item >
< el-form-item label = "代理有效期(分钟)" >
< el-input-number v-model = "proxyExpireMinutes" :min="1" :max="60" / >
< / el-form-item >
< / el-form >
< div class = "row-actions" >
< el-button type = "primary" @click ="saveProxy" > 保存代理配置 < / el -button >
< el-button @click ="onTestProxy" > 测试代理 < / el -button >
< / div >
< / el-card >
< el-card shadow = "never" : body -style = " { padding : ' 16px ' } " class = "card" >
< h3 class = "section-title" > 注册设置 < / h3 >
< el-form label -width = " 130px " >
< el-form-item label = "注册赠送VIP" >
< el-switch v-model = "autoApproveEnabled" / >
< div class = "help" > 开启后 , 新用户注册成功后将赠送下方设置的VIP天数 ( 注册已默认无需审核 ) 。 < / div >
< / el-form-item >
< el-form-item label = "每小时注册限制" >
< el-input-number v-model = "autoApproveHourlyLimit" :min="1" :max="10000" / >
< / el-form-item >
< el-form-item label = "注册赠送VIP天数" >
< el-input-number v-model = "autoApproveVipDays" :min="0" :max="999999" / >
< / el-form-item >
< / el-form >
< el-button type = "primary" @click ="saveAutoApprove" > 保存注册设置 < / el -button >
< / el-card >
< el-card shadow = "never" : body -style = " { padding : ' 16px ' } " class = "card" >
< h3 class = "section-title" > 金山文档上传 < / h3 >
< el-form label -width = " 130px " >
< el-form label -width = " 118px " class = "kdocs-form " >
< el-form-item label = "启用上传" >
< el-switch v-model = "kdocsEnabled" / >
< div class = "help" > 表格结构变化时可先关闭 , 避免错误上传 。 < / div >
@@ -554,30 +424,29 @@ onMounted(loadAll)
< el-input v-model = "kdocsDefaultUnit" placeholder="如:道县(用户可覆盖)" / >
< / el-form-item >
< el-form-item label = "Sheet名称" >
< el-input v-model = "kdocsSheetName" placeholder="留空使用第一个Sheet" / >
< el-form-item label = "Sheet 名称" >
< el-input v-model = "kdocsSheetName" placeholder="留空使用第一个 Sheet" / >
< / el-form-item >
< el-form-item label = "Sheet序号" >
< el-form-item label = "Sheet 序号" >
< el-input-number v-model = "kdocsSheetIndex" :min="0" :max="50" / >
< div class = "help" > 0 表示第一个Sheet 。 < / div >
< div class = "help" > 0 表示第一个 Sheet 。 < / div >
< / el-form-item >
< el-form-item label = "县区 列" >
< el-input v-model = "kdocsUnitColumn" placeholder="A" style="max-width: 120px" / >
< / el-form-item >
< el-form-item label = "图片列" >
< el-input v-model = "kdocsImageColumn" placeholder="D" style="max-width: 120px" / >
< el-form-item label = "列配置 " >
< div class = "kdocs-inline" >
< el-input v-model = "kdocsUnitColumn" placeholder="县区列,如 A" / >
< el-input v-model = "kdocsImageColumn" placeholder="图片列,如 D" / >
< / div >
< / el-form-item >
< el-form-item label = "有效行范围" >
< div style = "display: flex; align-items: center; gap: 8px; " >
< el-input-number v-model = "kdocsRowStart" :min="0" :max="10000" placeholder="起始行" style="width: 12 0px" / >
< span > 至 < / span >
< el-input-number v-model = "kdocsRowEnd" :min="0" :max="10000" placeholder="结束行" style="width: 12 0px" / >
< div class = "kdocs-range " >
< el-input-number v-model = "kdocsRowStart" :min="0" :max="10000" placeholder="起始行" style="width: 14 0px" / >
< span class = "app-muted" > 至 < / span >
< el-input-number v-model = "kdocsRowEnd" :min="0" :max="10000" placeholder="结束行" style="width: 14 0px" / >
< / div >
< div class = "help" > 限制上传的行范围 ( 如 50 - 100 ) , 0 表示不限制 。 用于防止重名导致误传到其他县区 。 < / div >
< div class = "help" > 用于 限制上传区间 ( 如 50 - 100 ) , 0 表示不限制 。 < / div >
< / el-form-item >
< el-form-item label = "管理员通知" >
@@ -618,14 +487,7 @@ onMounted(loadAll)
< / el-button >
< / div >
< div class = "help" >
登录状态 :
< span v-if = "kdocsStatus.last_login_ok === true" > 已登录 < / span >
< span v-else-if = "kdocsStatus.login_required" > 需要扫码 < / span >
< span v-else > 未知 < / span >
· 待上传 {{ kdocsStatus.queue_size | | 0 }}
< span v-if = "kdocsStatus.last_error" > · 最近错误 : {{ kdocsStatus.last_error }} < / span >
< / div >
< div v-if = "kdocsStatus.last_error" class="help" > 最近错误 : {{ kdocsStatus.last_error }} < / div >
< div v-if = "kdocsActionHint" class="help" > 操作提示 : {{ kdocsActionHint }} < / div >
< / el -card >
@@ -646,6 +508,12 @@ onMounted(loadAll)
min - width : 0 ;
}
. config - grid {
display : grid ;
grid - template - columns : repeat ( 3 , minmax ( 0 , 1 fr ) ) ;
gap : 14 px ;
}
. card {
border - radius : var ( -- app - radius ) ;
border : 1 px solid var ( -- app - border ) ;
@@ -653,13 +521,54 @@ onMounted(loadAll)
box - shadow : var ( -- app - shadow - soft ) ;
}
. section - card {
min - width : 0 ;
}
. section - title {
margin : 0 0 12 px ;
margin : 0 ;
font - size : 15 px ;
font - weight : 800 ;
letter - spacing : 0.2 px ;
}
. section - sub {
margin - top : 6 px ;
margin - bottom : 10 px ;
font - size : 12 px ;
}
. section - head {
display : flex ;
align - items : flex - start ;
justify - content : space - between ;
gap : 12 px ;
flex - wrap : wrap ;
margin - bottom : 10 px ;
}
. status - inline {
font - size : 12 px ;
}
. kdocs - form {
margin - top : 6 px ;
}
. kdocs - inline {
display : grid ;
grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr ) ) ;
gap : 10 px ;
width : 100 % ;
}
. kdocs - range {
display : flex ;
align - items : center ;
gap : 8 px ;
flex - wrap : wrap ;
}
. kdocs - qr {
display : flex ;
flex - direction : column ;
@@ -687,4 +596,24 @@ onMounted(loadAll)
flex - wrap : wrap ;
gap : 10 px ;
}
@ media ( max - width : 1200 px ) {
. config - grid {
grid - template - columns : repeat ( 2 , minmax ( 0 , 1 fr ) ) ;
}
}
@ media ( max - width : 768 px ) {
. config - grid {
grid - template - columns : 1 fr ;
}
. kdocs - inline {
grid - template - columns : 1 fr ;
}
. kdocs - range {
align - items : stretch ;
}
}
< / style >