This commit is contained in:
wangchen147 2024-09-13 17:37:02 +08:00
parent ecc7c620c1
commit 3dd2b3e0a8
801 changed files with 6948 additions and 11583 deletions

View File

@ -55,4 +55,4 @@
"vite": "4.1.0",
"vue-tsc": "1.0.24"
}
}
}

View File

@ -184,6 +184,15 @@ export function unlockUser(uid: number) {
return request.put(`site/user/unlock/${uid}`)
}
/**
*
*
* @param uid
*/
export function deleteUser(uid: number) {
return request.delete(`site/user/${uid}`)
}
/***************************************************** 操作日志 **************************************************/
/**

View File

@ -39,6 +39,14 @@ export function deleteUser(uid: number) {
return request.delete(`user/user/${uid}`, { showSuccessMessage: true })
}
/**
*
* @param uid
*/
export function editUser(params: Record<string, any>) {
return request.put(`user/user/${params.uid}`, params, { showSuccessMessage: true })
}
/**
*
* @param params

View File

@ -18,7 +18,7 @@
<div class="mt-[20px]" v-for="(item, index) in upgradeContent.version_list" :key="index">
<div class="font-bold text-lg">{{ item.version_no }}</div>
<div class="mt-[5px]" v-if="item.release_time">{{ item.release_time }}</div>
<div class="mt-[10px] p-[10px] rounded bg-[#f4f4f5] whitespace-pre" v-if="item.upgrade_log" v-html="item.upgrade_log"></div>
<div class="mt-[10px] p-[10px] rounded bg-[#f4f4f5] whitespace-pre-wrap !break-all" v-if="item.upgrade_log" v-html="item.upgrade_log"></div>
</div>
</el-scrollbar>
</div>

View File

@ -28,5 +28,6 @@
"manager": "用户",
"managerPlaceholder": "请选择用户",
"managerTips": "选择或者新增用户作为管理员",
"newAddManager": "新增用户"
}
"newAddManager": "新增用户",
"userDeleteTips": "是否要删除该管理员?"
}

View File

@ -19,5 +19,6 @@
"dataValuePlaceholder":"请输入数据值",
"sortPlaceholder":"数值越大越排前",
"momePlaceholder":"请输入备注",
"createTime":"创建时间"
}
"createTime":"创建时间",
"keyFormatTips": "关键字只允许输入字母和下划线"
}

View File

@ -1,4 +1,7 @@
{
"title": "插件名称",
"key": "插件标识"
"key": "插件标识",
"type": "插件类型",
"app": "应用",
"addon": "插件"
}

View File

@ -1,48 +1,50 @@
{
"orderNo":"订单编号",
"orderStatus":"订单状态",
"orderNoPlaceholder":"请输入订单编号",
"createTime":"创建时间",
"rechargeMoney":"充值金额",
"totalTransfered":"累计提现(元)",
"totalCashOuting":"提现中(元)",
"transfered":"累计提现",
"cashOuting":"提现中",
"orderMoney":"订单金额",
"member":"买家",
"orderFromName":"订单来源",
"payTypeName":"支付方式",
"startDate":"开始时间",
"endDate":"结束时间",
"namePlaceholder":"请选择",
"applyTime":"申请时间",
"cashOutStatus":"提现状态",
"actualTransferAmount":"实际转账金额",
"cashOutCommission":"提现手续费",
"applicationForWithdrawalAmount":"申请提现金额",
"cashOutMethod":"提现方式",
"cashOutAccountType":"会员账户",
"memberInfo":"会员信息",
"toBeReviewed":"待审核",
"toBeTransferred":"待转账",
"transferred":"已转账",
"turnDown":"拒绝",
"transfer": "转账",
"detail": "详情",
"auditFailure": "审核失败",
"successfulAudit": "审核成功",
"rejectionAudit": "拒绝审核",
"reasonsRefusal": "拒绝理由",
"reasonsRefusalPlaceholder": "请输入拒绝理由",
"isTransfer": "是否确认转账",
"nickname":"会员名称",
"headimg":"会员头像",
"cashOutDetail":"提现详情",
"cashOutMoney": "转账金额",
"orderNo": "订单编号",
"orderStatus": "订单状态",
"orderNoPlaceholder": "请输入订单编号",
"createTime": "创建时间",
"rechargeMoney": "充值金额",
"totalTransfered": "累计提现(元)",
"totalCashOuting": "提现中(元)",
"transfered": "累计提现",
"cashOuting": "提现中",
"orderMoney": "订单金额",
"member": "买家",
"orderFromName": "订单来源",
"payTypeName": "支付方式",
"startDate": "开始时间",
"endDate": "结束时间",
"namePlaceholder": "请选择",
"applyTime": "申请时间",
"cashOutStatus": "提现状态",
"actualTransferAmount": "实际转账金额",
"cashOutCommission": "提现手续费",
"applicationForWithdrawalAmount": "申请提现金额",
"cashOutMethod": "提现方式",
"cashOutAccountType": "会员账户",
"memberInfo": "会员信息",
"toBeReviewed": "待审核",
"toBeTransferred": "待转账",
"transferred": "已转账",
"turnDown": "拒绝",
"transfer": "转账",
"detail": "详情",
"auditFailure": "审核失败",
"successfulAudit": "审核成功",
"rejectionAudit": "拒绝审核",
"reasonsRefusal": "拒绝理由",
"reasonsRefusalPlaceholder": "请输入拒绝理由",
"isTransfer": "是否确认转账",
"nickname": "会员名称",
"headimg": "会员头像",
"cashOutDetail": "提现详情",
"cashOutMoney": "转账金额",
"auditTime": "审核时间",
"transferTime": "转账时间",
"memberInfoPlaceholder":"请输入会员名称/会员昵称/手机号",
"memberInfoPlaceholder": "请输入会员名称/会员昵称/手机号",
"cashOutNumber": "提现单号",
"cashOutNumberPlaceholder": "请输入提现单号"
"cashOutNumberPlaceholder": "请输入提现单号",
"alipayAccount": "支付宝账号",
"bankName": "银行名称",
"bankAccount": "银行卡号"
}

View File

@ -7,6 +7,7 @@
"calendarSign": "日历签到",
"periodSign": "周期签到",
"daySignAward": "日签奖励",
"daySignAwardPlaceholder": "请选择日签奖励",
"continueSignAward": "连签奖励",
"calendarSignTip": "用户根据日期进行打卡,连续签到一定天数可即可获得连签奖励。",
"periodSignTip": "用户在规定的周期内完成签到可以获得奖励;一个周期结束后将进入下一个循环周期。",

View File

@ -7,7 +7,7 @@
"businessHours":"营业时间",
"createTime":"创建时间",
"expireTime":"到期时间",
"siteNamePlaceholder":"请输入站点名称",
"siteNamePlaceholder":"请输入站点名称/编号",
"createTimePlaceholder":"请输入创建时间",
"addSite":"添加站点",
"editSite":"编辑站点",
@ -52,5 +52,6 @@
"siteDomainTipsTwo": "需要将域名配置到您的服务器,同时域名需要解析您的服务器才可生效",
"siteDomainTipsThree": "站点域名不需要加http或者https末尾不需要加/",
"toSite": "访问站点",
"noPermission": "您没有该站点的管理权限"
"noPermission": "您没有该站点的管理权限",
"closeSiteTips": "是否要停止该站点?"
}

View File

@ -9,6 +9,7 @@
"startDate": "开始时间",
"loginTime": "最后登录时间",
"addUser": "添加用户",
"updateUser": "编辑用户",
"username": "账号",
"passwordPlaceholder": "请输入用户密码",
"usernamePlaceholder": "请输入用户账号",

View File

@ -29,5 +29,5 @@
"addCron": "添加任务",
"cronTimeTips": "任务周期时间不能为空",
"cronTipsOne": "启动计划任务方式:",
"cronTipsTwo": "1、使用命令启动php think cron:schedule 如果更改了任务周期、状态、删除任务等操作后,需要重新启动下 php think cron:schedule 确保生效"
}
"cronTipsTwo": "1、使用命令启动php think workerman 如果更改了任务周期、状态、删除任务等操作后,需要重新启动下 php think workerman 确保生效"
}

View File

@ -89,7 +89,7 @@
<div class="text-page-title mb-[20px]">历史版本</div>
<el-timeline>
<el-timeline-item :timestamp="item['release_time'] + ' 版本:' + item['version_no']" v-for="(item,index) in frameworkVersionList" type="primary" :hollow="true" placement="top" :key="index">
<div class="mt-[10px] p-[20px] bg-overlay rounded-md timeline-log-wrap whitespace-pre" v-if="item['upgrade_log']">
<div class="mt-[10px] p-[20px] bg-overlay rounded-md timeline-log-wrap whitespace-pre-wrap" v-if="item['upgrade_log']">
<div v-html="item['upgrade_log']"></div>
</div>
</el-timeline-item>

View File

@ -12,7 +12,7 @@
<el-image class="w-[40px] h-[40px] mr-[10px]" :src="img(item.icon)" fit="contain">
<template #error>
<div class="image-slot">
<img class="w-[50px] h-[50px]" src="@/app/assets/images/index/app_default.png" />
<img class="w-[40px] h-[40px]" src="@/app/assets/images/index/app_default.png" />
</div>
</template>
</el-image>

View File

@ -68,6 +68,7 @@
<el-button type="primary" link @click="editEvent(row)">{{ t('edit') }}</el-button>
<el-button type="primary" link @click="lockEvent(row.uid)" v-if="row.status">{{ t('lock') }}</el-button>
<el-button type="primary" link @click="unlockEvent(row.uid)" v-else>{{ t('unlock') }}</el-button>
<el-button type="primary" link @click="deleteEvent(row.uid)">{{ t('delete') }}</el-button>
</div>
<div v-else>
<el-button link disabled>{{ t('adminDisabled') }}</el-button>
@ -91,7 +92,7 @@
<script lang="ts" setup>
import { reactive, ref } from 'vue'
import { t } from '@/lang'
import { getUserList, lockUser, unlockUser } from '@/app/api/site'
import { getUserList, lockUser, unlockUser, deleteUser } from '@/app/api/site'
import EditUser from '@/app/views/auth/components/edit-user.vue'
import { img } from '@/utils/common'
import { ElMessageBox } from 'element-plus'
@ -198,6 +199,21 @@ const unlockEvent = (id: number) => {
})
}
const deleteEvent = (uid: number) => {
ElMessageBox.confirm(t('userDeleteTips'), t('warning'),
{
confirmButtonText: t('confirm'),
cancelButtonText: t('cancel'),
type: 'warning'
}
).then(() => {
deleteUser(uid).then(() => {
loadUserList()
}).catch(() => {
})
})
}
</script>
<style lang="scss" scoped></style>

View File

@ -75,7 +75,7 @@
</el-dialog>
<el-dialog v-model="failReasonDialogVisible" :title="t('failReason')" width="60%">
<el-scrollbar class="h-[60vh] w-full whitespace-pre p-[20px]">
<el-scrollbar class="h-[60vh] w-full whitespace-pre-wrap p-[20px]">
<div v-html="failReason"></div>
</el-scrollbar>
</el-dialog>

View File

@ -6,6 +6,7 @@
</el-form-item>
<el-form-item :label="t('key')" prop="key">
<el-input v-model.trim="formData.key" clearable maxlength="40" show-word-limit :placeholder="t('keyPlaceholder')" class="input-width" />
<p class="form-tip">{{ t('keyFormatTips') }}</p>
</el-form-item>
<el-form-item :label="t('memo')">
<el-input v-model="formData.memo" type="textarea" clearable :placeholder="t('memoPlaceholder')" class="input-width" />
@ -50,7 +51,17 @@ const formRules = computed(() => {
{ required: true, message: t('namePlaceholder'), trigger: 'blur' }
],
key: [
{ required: true, message: t('keyPlaceholder'), trigger: 'blur' }
{ required: true, message: t('keyPlaceholder'), trigger: 'blur' },
{
validator: (rule: any, value: any, callback: any) => {
if (/^[a-zA-Z_]+$/.test(value)) {
callback()
} else {
callback(new Error(t('keyFormatTips')))
}
},
trigger: 'blur'
}
],
data: [
{ required: true, message: t('dataPlaceholder'), trigger: 'blur' }

View File

@ -87,15 +87,15 @@
<el-input v-model.trim="item.title.text" :placeholder="t('activeCubeTitlePlaceholder')" clearable maxlength="4" show-word-limit/>
</el-form-item>
<el-form-item :label="t('activeCubeSubTitleTextColor')" v-show="selectBlockStyle.value == 'style-3'">
<el-form-item :label="t('activeCubeSubTitleTextColor')" v-show="diyStore.editComponent.blockStyle.value == 'style-3'">
<el-color-picker v-model="item.title.textColor" show-alpha :predefine="diyStore.predefineColors" />
</el-form-item>
<el-form-item :label="t('activeCubeSubTitle')" v-if="selectBlockStyle.value != 'style-3'">
<el-input v-model.trim="item.subTitle.text" :placeholder="t('activeCubeSubTitlePlaceholder')" clearable :maxlength="(selectBlockStyle.value != 'style-4' ? '6' : '4')" show-word-limit/>
<el-form-item :label="t('activeCubeSubTitle')" v-if="diyStore.editComponent.blockStyle.value != 'style-3'">
<el-input v-model.trim="item.subTitle.text" :placeholder="t('activeCubeSubTitlePlaceholder')" clearable :maxlength="(diyStore.editComponent.blockStyle.value != 'style-4' ? '6' : '4')" show-word-limit/>
</el-form-item>
<div v-show="selectBlockStyle.value == 'style-4'">
<div v-show="diyStore.editComponent.blockStyle.value == 'style-4'">
<el-form-item :label="t('activeCubeSubTitleTextColor')">
<el-color-picker v-model="item.subTitle.textColor" show-alpha :predefine="diyStore.predefineColors" />
</el-form-item>
@ -111,7 +111,7 @@
</el-form-item>
</div>
<div v-show="selectBlockStyle.value != 'style-4' && selectBlockStyle.value != 'style-3'">
<div v-show="diyStore.editComponent.blockStyle.value != 'style-4' && diyStore.editComponent.blockStyle.value != 'style-3'">
<el-form-item :label="t('activeCubeButton')">
<el-input v-model.trim="item.moreTitle.text" :placeholder="t('activeCubeButtonPlaceholder')" clearable maxlength="3" show-word-limit/>
</el-form-item>

View File

@ -70,14 +70,14 @@
</el-card>
<!--添加页面-->
<el-dialog v-model="dialogVisible" :title="t('addPageTips')" width="25%">
<el-dialog v-model="dialogVisible" :title="t('addPageTips')" width="350px">
<el-form :model="formData" label-width="90px" ref="formRef" :rules="formRules">
<el-form-item :label="t('title')" prop="title">
<el-input v-model="formData.title" :placeholder="t('titlePlaceholder')" clearable maxlength="12" show-word-limit class="w-full" />
</el-form-item>
<el-form-item :label="t('typeName')" prop="type">
<el-select v-model="formData.type" :placeholder="t('pageTypePlaceholder')" class="w-full">
<el-select v-model="formData.type" :placeholder="t('pageTypePlaceholder')" class="!w-full">
<el-option v-for="(item, key) in pageType" :label="item.title" :value="key" :key="key"/>
</el-select>
</el-form-item>

View File

@ -17,7 +17,13 @@
</template>
</el-table-column>
<el-table-column prop="key" :label="t('key')" min-width="80"/>
<el-table-column prop="key" :label="t('key')" min-width="120"/>
<el-table-column :label="t('type')" min-width="120">
<template #default="{ row }">
<span>{{ row.info.type === 'app' ? t('app') : t('addon') }}</span>
</template>
</el-table-column>
<el-table-column :label="t('operation')" fixed="right" align="right" min-width="160">
<template #default="{ row }">

View File

@ -129,7 +129,7 @@
</template>
</el-table-column>
<el-table-column :label="t('operation')" align="right" fixed="right" width="230">
<el-table-column :label="t('operation')" align="right" fixed="right" width="120">
<template #default="{ row }">
<el-button v-for="(item, index) in operationBtn[row.status.toString()].value" :key="index + 'a'"
@click="fnProcessing(operationBtn[row.status.toString()].clickArr[index], row)"
@ -158,6 +158,19 @@
<el-form-item :label="t('cashOutMethod')">
<div class="input-width"> {{ Transfertype[cashOutInfo.transfer_type].name }} </div>
</el-form-item>
<template v-if="cashOutInfo.transfer_type == 'alipay'">
<el-form-item :label="t('alipayAccount')">
<div class="input-width"> {{ cashOutInfo.transfer_account }} </div>
</el-form-item>
</template>
<template v-if="cashOutInfo.transfer_type == 'bank'">
<el-form-item :label="t('bankName')">
<div class="input-width"> {{ cashOutInfo.transfer_bank }} </div>
</el-form-item>
<el-form-item :label="t('bankAccount')">
<div class="input-width"> {{ cashOutInfo.transfer_account }} </div>
</el-form-item>
</template>
<el-form-item :label="t('applicationForWithdrawalAmount')">
<div class="input-width"> {{ cashOutInfo.apply_money }} </div>
</el-form-item>

View File

@ -24,7 +24,7 @@
<span :class="['px-[10px] cursor-pointer h-[35px] leading-[35px] inline-block', {'text-[var(--el-color-primary)]': params.app == item.key}]" @click="cutAppFn(item.key)" v-for="(item,index) in addonList" :key="index">{{item.title}}</span>
</el-scrollbar>
</div>
<el-input v-model="params.keywords" class="!w-[300px] !h-[34px]" placeholder="请输入要搜索的站点名称" @keyup.enter.native="getHomeSiteFn()">
<el-input v-model="params.keywords" class="!w-[300px] !h-[34px]" placeholder="请输入要搜索的站点名称/编号" @keyup.enter.native="getHomeSiteFn()">
<template #suffix>
<el-icon @click.stop="getHomeSiteFn()" class="cursor-pointer">
<Search />
@ -95,7 +95,7 @@
:class="{'bg-[#F6F7FF] border-[#466CEA]': createSiteData.formData.group_id == item.group_id ,'ml-[20px]': index > 0, ' ml-[10px]': index == 0, 'mr-[10px]': (siteGroup.length-1) == index }"
@click="createSiteData.formData.group_id = item.group_id"
>
<div class="w-[140px] h-[40px] truncate text-white text-[16px] text-center leading-[40px] creatBg relative -left-[1px] -top-[2px]">
<div class="w-[140px] h-[40px] px-[15px] truncate text-white text-[16px] text-center leading-[40px] creatBg relative -left-[1px] -top-[2px]">
{{ item.site_group.group_name }}
</div>
<el-scrollbar class="flex pb-[20px] pt-[4px] box-border !h-[260px]">

View File

@ -27,7 +27,7 @@
{{ t('buyLabel') }}
</div>
</div>
<el-button type="primary" round @click="handleCloudBuild" :loading="cloudBuildRef?.loading">{{ t('cloudBuild') }}</el-button>
<el-button type="primary" round @click="handleCloudBuild" :loading="cloudBuildRef?.loading" :disabled="authLoading">{{ t('cloudBuild') }}</el-button>
</div>
<div>
@ -42,12 +42,15 @@
</div>
</template>
</el-image>
<div class="flex flex-col justify-center pl-[20px] font-500 text-[13px]">
<div class="flex-1 w-0 flex flex-col justify-center pl-[20px] font-500 text-[13px]">
<div class="w-[236px] truncate leading-[18px]">{{ row.title }}</div>
<div class="w-[236px] truncate leading-[18px] mt-[6px]" v-if="row.install_info && Object.keys(row.install_info)?.length">{{ row.install_info.version }}</div>
<div class="w-[236px] truncate leading-[18px] mt-[6px]" v-else>{{ row.version }}</div>
<div class="mt-[3px]" v-if="row.install_info && Object.keys(row.install_info)?.length && row.install_info.version != row.version">
<el-tag type="danger" size="small">{{ t('newVersion') }}{{ row.version }}</el-tag>
<div class="mt-[3px] flex flex-nowrap">
<el-tag type="danger" size="small" v-if="row.install_info && Object.keys(row.install_info)?.length && row.install_info.version != row.version">{{ t('newVersion') }}{{ row.version }}</el-tag>
<el-tooltip v-if="versionJudge(row)" effect="dark" content="该插件与框架版本不兼容,可能存在未知问题" placement="top-start" >
<el-tag type="info" size="small" class="ml-[3px]">该插件与框架版本不兼容可能存在未知问题</el-tag>
</el-tooltip>
</div>
</div>
</div>
@ -376,6 +379,7 @@ import { t } from '@/lang'
import { getAddonLocal, uninstallAddon, installAddon, preInstallCheck, cloudInstallAddon, getAddonInstalltask, getAddonCloudInstallLog, preUninstallCheck, cancelInstall } from '@/app/api/addon'
import { deleteAddonDevelop } from '@/app/api/tools'
import { downloadVersion, getAuthInfo, setAuthInfo } from '@/app/api/module'
import { getVersions } from '@/app/api/auth'
import { ElMessage, ElMessageBox, ElNotification, FormInstance, FormRules } from 'element-plus'
import 'vue-web-terminal/lib/theme/dark.css'
import { Terminal, TerminalFlash } from 'vue-web-terminal'
@ -397,6 +401,11 @@ const installAfterTips = ref<string[]>([])
const userStore = useUserStore()
const unloadHintDialog = ref(false)
const terminalRef = ref(null)
const frameworkVersion = ref('')
getVersions().then(res => {
frameworkVersion.value = res.data.version.version
})
const currDownData = ref()
const downEventHintFn = () => {
@ -902,6 +911,14 @@ const deleteAddonFn = (key: string) => {
})
}).catch(() => { })
}
const versionJudge = (row: any) => {
if (!row.support_version) return true
const supportVersionApp = row.support_version.split('.')
const frameworkVersionArr = frameworkVersion.value.split('.')
if (parseFloat(`${supportVersionApp[0]}.${supportVersionApp[1]}`) < parseFloat(`${frameworkVersionArr[0]}.${frameworkVersionArr[1]}`)) return true
return false
}
</script>
<style lang="scss" scoped>

View File

@ -1,10 +1,10 @@
<template>
<el-form :model="formData" :rules="formRules" class="page-form" ref="formRef">
<el-form-item :label="t('continueSign')" prop="continue_sign">
<el-input class="input-width" v-model.trim="formData.continue_sign" clearable /><span class="ml-[10px]">{{ t('day') }}</span>
<el-input class="input-width" v-model.trim="formData.continue_sign" :maxlength="5" clearable /><span class="ml-[10px]">{{ t('day') }}</span>
</el-form-item>
<el-form-item :label="t('continueSign')" >
<div>
<div class="flex-1">
<div v-for="(item,index) in gifts" :key="index" class="mb-[15px]">
<component :is="item.component" v-model="formData[item.key]" ref="giftRefs" v-if="item.component" />
</div>
@ -15,7 +15,7 @@
<el-radio class="mb-[15px]" v-model="formData.receive_limit" :label="1" @change="radioChange($event, 1)">{{ t('noLimit') }}</el-radio>
<div class="flex">
<el-radio class="!mr-[15px]" v-model="formData.receive_limit" :label="2" @change="radioChange($event, 2)">{{ t('everyOneLimit') }}</el-radio>
<el-input class="input-width" v-model="formData.receive_num" clearable /><span class="ml-[10px]">{{ t('time') }}</span>
<el-input class="input-width" v-model="formData.receive_num" :maxlength="5" clearable /><span class="ml-[10px]">{{ t('time') }}</span>
</div>
</div>
</el-form-item>
@ -76,18 +76,26 @@ getGiftDict().then(({ data }) => {
})
const formRef = ref(null)
//
const regExp = {
required: /[\S]+/,
number: /^\d{0,10}$/,
digit: /^\d{0,10}(.?\d{0,2})$/,
special: /^\d{0,10}(.?\d{0,3})$/
}
//
const formRules = reactive<FormRules>({
continue_sign: [
{ required: true, message: t('continueSignPlaceholder'), trigger: 'blur' },
{
validator: (rule: any, value: any, callback: Function) => {
if (!Test.digits(formData.value.continue_sign)) {
callback('连续签到格式错误')
} else if (formData.value.continue_sign <= 0) {
callback('连续签到不能小于等于0')
} else {
callback()
validator: (rule: any, value: any, callback: any) => {
if (isNaN(value) || !regExp.number.test(value)) {
callback('连续签到天数格式错误')
} else if (value <=0) {
callback('连续签到天数不能小于等于0')
} else{
callback();
}
},
trigger: 'blur'
@ -101,7 +109,7 @@ const formRules = reactive<FormRules>({
if (Test.empty(formData.value.receive_num)) {
callback('请输入限领次数')
}
if (!Test.digits(formData.value.receive_num)) {
if (isNaN(formData.value.receive_num) || !regExp.number.test(formData.value.receive_num)) {
callback('限领次数格式错误')
}
if (formData.value.receive_num <= 0) {

View File

@ -11,10 +11,10 @@
</el-form-item>
<el-form-item :label="t('signPeriod')" v-if="formData.is_use">
<el-input-number v-model="formData.sign_period" clearable class="input-width" controls-position="right" /><span class="ml-[10px]"></span>
<el-input-number v-model="formData.sign_period" :min="0" :precision="0" clearable class="input-width" controls-position="right" /><span class="ml-[10px]"></span>
</el-form-item>
<el-form-item :label="t('daySignAward')" prop="formData.day_award" v-if="formData.is_use">
<el-form-item :label="t('daySignAward')" prop="day_award" v-if="formData.is_use">
<div v-for="(item, index) in daySignAwardText" :key="index">
<span v-if="item.is_use == '1'">{{ item.content }}&nbsp;&nbsp;</span>
</div>
@ -27,7 +27,7 @@
<div class="form-tip">{{ t('daySignAwardTip') }}</div>
</el-form-item>
<el-form-item :label="t('continueSignAward')" prop="formData.continue_award" v-if="formData.is_use">
<el-form-item :label="t('continueSignAward')" prop="continue_award" v-if="formData.is_use">
<div>
<div class="form-tip">{{ t('continueSignAwardTipTop') }}</div>
<div class="mt-[10px]">
@ -68,9 +68,9 @@
</div>
</el-form-item>
<el-form-item :label="t('ruleExplain')" prop="formData.rule_explain" v-if="formData.is_use">
<el-form-item :label="t('ruleExplain')" prop="rule_explain" v-if="formData.is_use">
<div class="flex">
<el-input v-model="formData.rule_explain" :placeholder="t('ruleExplainTip')" type="textarea" rows="5" class="textarea-width" clearable />
<el-input v-model="formData.rule_explain" :placeholder="t('ruleExplainTip')" type="textarea" maxlength="500" show-word-limit rows="5" class="textarea-width" clearable />
<el-button class="ml-[20px]" type="primary" @click="defaultExplainEvent()" plain>{{ t('useDefaultExplain') }}</el-button>
</div>
</el-form-item>
@ -89,7 +89,7 @@
</el-dialog>
<!-- 连签奖励 -->
<el-dialog v-model="continueSignDialog" :title="t('continueSignTitle')" width="1200px" :destroy-on-close="true" v-if="formData.is_use">
<el-dialog v-model="continueSignDialog" :title="t('continueSignTitle')" width="800px" :destroy-on-close="true" v-if="formData.is_use">
<sign-continue ref="continueRef" v-model="continue_award" />
<template #footer>
<span class="dialog-footer">
@ -134,7 +134,10 @@ let editIndex = 0 // 连签奖励修改下标
const formRules = reactive<FormRules>({
sign_period: [
{ required: true, message: t('signPeriodTip'), trigger: 'blur' }
]
],
day_award: [
{ required: true, message: t('daySignAwardPlaceholder'), trigger: 'change' }
],
})
/**

View File

@ -5,7 +5,7 @@
<el-checkbox v-model="formData.is_use" :true-label="1" :false-label="0" label="" size="large" />
<span class="ml-[10px] el-form-item__label"></span>
<div class="w-[70px]">
<el-input v-model.trim="formData.money" clearable />
<el-input v-model.trim="formData.money" :maxlength="5" clearable />
</div>
<span class="ml-[15px] el-form-item__label">元红包</span>
</div>
@ -33,26 +33,31 @@ const formData = ref({
money: ''
})
const formRef = ref(null)
//
const regExp = {
required: /[\S]+/,
number: /^\d{0,10}$/,
digit: /^\d{0,10}(.?\d{0,2})$/,
special: /^\d{0,10}(.?\d{0,3})$/
}
const formRules = reactive<FormRules>({
money: [
{
validator: (rule: any, value: any, callback: any) => {
if (formData.value.is_use) {
if (Test.empty(formData.value.money)) {
if (Test.empty(value)) {
callback('请输入红包金额')
}
if (!Test.amount(formData.value.money)) {
}else if (isNaN(value) || !regExp.digit.test(value)) {
callback('红包金额格式错误')
}
if (formData.value.money <= 0) {
}else if (value <= 0) {
callback('红包金额不能小于等于0')
}
callback()
} else {
callback()
}
}
},
trigger: 'blur'
}
]
})

View File

@ -32,26 +32,32 @@ const formData = ref({
})
const formRef = ref(null)
//
const regExp = {
required: /[\S]+/,
number: /^\d{0,10}$/,
digit: /^\d{0,10}(.?\d{0,2})$/,
special: /^\d{0,10}(.?\d{0,3})$/
}
const formRules = reactive<FormRules>({
num: [
{
validator: (rule: any, value: any, callback: Function) => {
validator: (rule: any, value: any, callback: any) => {
if (formData.value.is_use) {
if (Test.empty(formData.value.num)) {
callback('请输入发放积分数量')
}
if (!Test.digits(formData.value.num)) {
if (value.length == 0) {
callback('请输入积分数量')
} else if (isNaN(value) || !regExp.number.test(value)) {
callback('积分数量格式错误')
}
if (formData.value.num <= 0) {
} else if (value <=0) {
callback('积分数量不能小于等于0')
} else{
callback();
}
callback()
} else {
callback()
}
}
},
trigger: 'blur'
}
]
})

View File

@ -66,14 +66,14 @@
</el-card>
<!--添加海报-->
<el-dialog v-model="dialogVisible" :title="t('addPosterTitle')" width="25%">
<el-dialog v-model="dialogVisible" :title="t('addPosterTitle')" width="350px">
<el-form :model="formData" label-width="90px" ref="formRef" :rules="formRules">
<el-form-item :label="t('posterName')" prop="name">
<el-input v-model="formData.name" :placeholder="t('posterNamePlaceholder')" clearable maxlength="12" show-word-limit class="w-full" />
</el-form-item>
<el-form-item :label="t('posterType')" prop="type">
<el-select v-model="formData.type" :placeholder="t('posterTypePlaceholder')" class="w-full">
<el-select v-model="formData.type" :placeholder="t('posterTypePlaceholder')" class="!w-full">
<el-option v-for="item in posterType" :label="item.name" :value="item.type" :key="item.type"/>
</el-select>
</el-form-item>

View File

@ -116,4 +116,8 @@ const back = () => {
}
</script>
<style lang="scss" scoped></style>
<style lang="scss">
.edui-default .edui-editor{
z-index: 1 !important;
}
</style>

View File

@ -1,21 +1,29 @@
<template>
<el-dialog v-model="showDialog" :title="t('addUser')" width="750px" :destroy-on-close="true">
<el-dialog v-model="showDialog" :title="formData.uid ? t('updateUser') : t('addUser')" width="750px" :destroy-on-close="true">
<el-scrollbar>
<div class="max-h-[60vh]">
<el-form :model="formData" label-width="120px" ref="formRef" :rules="formRules" class="page-form" autocomplete="off" v-loading="loading">
<el-form-item :label="t('username')" prop="username">
<el-input v-model="formData.username" clearable :placeholder="t('usernamePlaceholder')" class="input-width" :readonly="real_name_input" @click="real_name_input = false" @blur="real_name_input = true" />
<el-input v-model="formData.username" clearable :placeholder="t('usernamePlaceholder')" class="input-width" :readonly="formData.uid" :disabled="formData.uid" @click="realnameInput = false" @blur="realnameInput = true" />
</el-form-item>
<el-form-item :label="t('headImg')">
<upload-image v-model="formData.head_img" />
</el-form-item>
<el-form-item :label="t('userRealName')" prop="real_name">
<el-input v-model.trim="formData.real_name" :placeholder="t('userRealNamePlaceholder')" :readonly="realnameInput" @click="realnameInput = false" @blur="realnameInput = true" clearable class="input-width" maxlength="10" show-word-limit />
</el-form-item>
<el-form-item :label="t('password')" prop="password">
<el-input v-model="formData.password" clearable :placeholder="t('passwordPlaceholder')" class="input-width" :show-password="true" type="password" :readonly="password_input" @click="password_input = false" @blur="password_input = true" />
<el-input v-model="formData.password" clearable :placeholder="t('passwordPlaceholder')" class="input-width" :show-password="true" type="password" :readonly="passwordInput" @click="passwordInput = false" @blur="passwordInput = true" />
</el-form-item>
<el-form-item :label="t('confirmPassword')" prop="confirm_password">
<el-input v-model="formData.confirm_password" :placeholder="t('confirmPasswordPlaceholder')" type="password" :show-password="true" clearable class="input-width" :readonly="confirm_password_input" @click="confirm_password_input = false" @blur="confirm_password_input = true" />
<el-input v-model="formData.confirm_password" :placeholder="t('confirmPasswordPlaceholder')" type="password" :show-password="true" clearable class="input-width" :readonly="confirmPasswordInput" @click="confirmPasswordInput = false" @blur="confirmPasswordInput = true" />
</el-form-item>
<el-form-item :label="t('userCreateSiteLimit')" v-if="Object.keys(siteGroup).length" prop="create_site_limit">
<el-form-item :label="t('userCreateSiteLimit')" v-if="!formData.uid && Object.keys(siteGroup).length" prop="create_site_limit">
<div>
<div>{{ t('siteGroup') }}</div>
<el-checkbox-group v-model="formData.group_ids" @change="groupSelect">
@ -60,75 +68,81 @@
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import {computed, ref} from 'vue'
import { t } from '@/lang'
import { FormInstance } from 'element-plus'
import { getSiteGroupAll } from '@/app/api/site'
import { addUser } from '@/app/api/user'
import { addUser, getUserInfo, editUser } from '@/app/api/user'
import Test from '@/utils/test'
const showDialog = ref(false)
const loading = ref(true)
const formData = ref({
uid: 0,
username: '',
password: '',
head_img: '',
real_name: '',
confirm_password: '',
create_site_limit: [],
group_ids: []
})
const siteGroup = ref({})
const formRef = ref<FormInstance>()
const formRules = ref({
username: [
{ required: true, message: t('usernamePlaceholder'), trigger: 'blur' }
],
password: [
{ required: true, message: t('passwordPlaceholder'), trigger: 'blur' }
],
real_name: [
{ required: true, message: t('userRealNamePlaceholder'), trigger: 'blur' }
],
confirm_password: [
{ required: true, message: t('confirmPasswordPlaceholder'), trigger: 'blur' },
{
validator: (rule: any, value: string, callback: any) => {
if (value != formData.value.password) callback(new Error(t('confirmPasswordError')))
else callback()
},
trigger: 'blur'
}
],
create_site_limit: [
{
validator: (rule: any, value: string, callback: any) => {
let verify = true
for (let i = 0; i < formData.value.create_site_limit.length; i++) {
const item = formData.value.create_site_limit[i]
if (Test.empty(item.num)) {
callback(t('siteNumPlaceholder'))
verify = false
break
}
if (item.num < 1) {
callback(t('siteNumCannotLtOne'))
verify = false
break
}
if (Test.empty(item.month)) {
callback(t('siteMonthPlaceholder'))
verify = false
break
}
if (item.month < 0) {
callback(t('siteMonthCannotLtOne'))
verify = false
break
}
}
if (verify) callback()
const formRules = computed(() => {
return {
username: [
{ required: true, message: t('usernamePlaceholder'), trigger: 'blur' }
],
password: [
{ required: formData.value.uid == 0, message: t('passwordPlaceholder'), trigger: 'blur' }
],
real_name: [
{ required: true, message: t('userRealNamePlaceholder'), trigger: 'blur' }
],
confirm_password: [
{ required: formData.value.uid == 0, message: t('confirmPasswordPlaceholder'), trigger: 'blur' },
{
validator: (rule: any, value: string, callback: any) => {
if (value != formData.value.password) callback(new Error(t('confirmPasswordError')))
else callback()
},
trigger: 'blur'
}
}
]
],
create_site_limit: [
{
validator: (rule: any, value: string, callback: any) => {
if (formData.value.uid) callback()
let verify = true
for (let i = 0; i < formData.value.create_site_limit.length; i++) {
const item = formData.value.create_site_limit[i]
if (Test.empty(item.num)) {
callback(t('siteNumPlaceholder'))
verify = false
break
}
if (item.num < 1) {
callback(t('siteNumCannotLtOne'))
verify = false
break
}
if (Test.empty(item.month)) {
callback(t('siteMonthPlaceholder'))
verify = false
break
}
if (item.month < 0) {
callback(t('siteMonthCannotLtOne'))
verify = false
break
}
}
if (verify) callback()
}
}
]
}
})
getSiteGroupAll().then(({ data }) => {
@ -141,7 +155,14 @@ getSiteGroupAll().then(({ data }) => {
const setFormData = (uid: number = 0) => {
if (uid) {
getUserInfo(uid).then(({ data }) => {
formData.value.uid = data.uid
formData.value.username = data.username
formData.value.real_name = data.real_name
formData.value.head_img = data.head_img
loading.value = false
showDialog.value = true
})
} else {
loading.value = false
showDialog.value = true
@ -172,7 +193,7 @@ const confirm = async (formEl: FormInstance | undefined) => {
await formEl.validate(async (valid) => {
if (valid) {
loading.value = true
const save = addUser
const save = formData.value.uid ? editUser : addUser
save(formData.value).then(() => {
loading.value = false
@ -185,9 +206,9 @@ const confirm = async (formEl: FormInstance | undefined) => {
})
}
const real_name_input = ref(true)
const password_input = ref(true)
const confirm_password_input = ref(true)
const realnameInput = ref(true)
const passwordInput = ref(true)
const confirmPasswordInput = ref(true)
defineExpose({
showDialog,

View File

@ -81,7 +81,7 @@
<div class="fixed-footer-wrap">
<div class="fixed-footer">
<el-button type="primary" @click="confirm(formRef)" v-loading="saveLoading">{{ t('save') }}</el-button>
<el-button type="primary" @click="confirm(formRef)">{{ t('save') }}</el-button>
<el-button @click="back()">{{ t('cancel') }}</el-button>
</div>
</div>

View File

@ -17,7 +17,7 @@
<el-card class="box-card !border-none my-[10px] table-search-wrap" shadow="never">
<el-form :inline="true" :model="siteTableData.searchParam" ref="searchFormRef">
<el-form-item :label="t('siteName')" prop="keywords">
<el-form-item :label="t('siteInfo')" prop="keywords">
<el-input v-model="siteTableData.searchParam.keywords" :placeholder="t('siteNamePlaceholder')" />
</el-form-item>
@ -157,6 +157,7 @@ import { useRouter, useRoute } from 'vue-router'
import EditSite from '@/app/views/site/components/edit-site.vue'
import { getInstalledAddonList } from '@/app/api/addon'
import useUserStore from '@/stores/modules/user'
import {deleteUser} from "@/app/api/user";
const route = useRoute()
const pageName = route.meta.title
@ -308,8 +309,16 @@ const toSiteLink = (siteId:number = 0) => {
const openClose = (i, site_id) => {
if (i == 1) {
closeSite({ site_id }).then(res => {
loadSiteList()
ElMessageBox.confirm(t('closeSiteTips'), t('warning'),
{
confirmButtonText: t('confirm'),
cancelButtonText: t('cancel'),
type: 'warning'
}
).then(() => {
closeSite({ site_id }).then(res => {
loadSiteList()
})
})
}
if (i == 3) {

View File

@ -65,10 +65,11 @@
{{ row.last_ip || '' }}
</template>
</el-table-column>
<el-table-column :label="t('operation')" align="right" fixed="right" width="200">
<el-table-column :label="t('operation')" align="right" fixed="right" width="180">
<template #default="{ row }">
<el-button type="primary" link @click="detailEvent(row.uid)">{{ t('detail') }}</el-button>
<template v-if="!row.is_super_admin">
<el-button type="primary" link @click="editEvent(row.uid)" >{{ t('edit') }}</el-button>
<el-button type="primary" link @click="detailEvent(row.uid, 'userCreateSiteLimit')" >{{ t('userCreateSiteLimit') }}</el-button>
<el-button type="primary" link @click="deleteEvent(row.uid)" >{{ t('delete') }}</el-button>
</template>
@ -157,6 +158,14 @@ const detailEvent = (uid: number, tab: string = '') => {
router.push({ path: '/admin/site/user_info', query: { uid, tab } })
}
/**
* 编辑用户
* @param uid
*/
const editEvent = (uid: number) => {
userEditRef.value.setFormData(uid)
}
/**
* 删除用户
*/

View File

@ -22,6 +22,30 @@
</template>
</el-alert>
<el-card class="box-card !border-none mb-[10px] table-search-wrap" shadow="never">
<div class="flex justify-between">
<el-form :inline="true" :model="cronTableData.searchParam" ref="searchFormRef">
<el-form-item :label="t('title')" prop="key">
<el-select v-model="cronTableData.searchParam.key" placeholder="全部" filterable remote clearable :remote-method="getAddonDevelopFn">
<el-option label="全部" value="all" />
<el-option v-for="item in templateList" :key="item.key" :label="item.name" :value="item.key" />
</el-select>
</el-form-item>
<el-form-item :label="t('status')" prop="status">
<el-select v-model="cronTableData.searchParam.status" placeholder="全部" filterable remote clearable :remote-method="getAddonDevelopFn">
<el-option label="全部" value="all" />
<el-option label="启用" value="1" />
<el-option label="关闭" value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="loadCronList()">{{ t('search') }}</el-button>
<el-button @click="resetForm(searchFormRef)">{{ t('reset') }}</el-button>
</el-form-item>
</el-form>
</div>
</el-card>
<div class="mt-[20px]">
<el-table :data="cronTableData.data" size="large" v-loading="cronTableData.loading">
<template #empty>
@ -124,9 +148,8 @@ const cronTableData = reactive({
loading: true,
data: [],
searchParam: {
title: '',
type: '',
last_time: ''
status: 'all'
}
})
const templateList = ref([])
@ -134,6 +157,12 @@ const date_type = ref([])
const week_list = ref([])
const searchFormRef = ref<FormInstance>()
const resetForm = (formEl: FormInstance | undefined) => {
if (!formEl) return
formEl.resetFields()
loadCronList()
}
const setTypeList = async () => {
templateList.value = await (await getCronTemplate()).data
date_type.value = await (await getCronDateType()).data

View File

@ -40,7 +40,7 @@
</el-dialog>
<el-dialog v-model="failReasonDialogShow" :title="t('failReason')" width="60%">
<el-scrollbar class="h-[60vh] w-full whitespace-pre p-[20px]">
<el-scrollbar class="h-[60vh] w-full whitespace-pre-wrap p-[20px]">
{{ failReason }}
</el-scrollbar>
</el-dialog>

View File

@ -1,5 +1,5 @@
<template>
<upload-attachment type="image" ref="imageRef" :limit="10" @confirm="imageSelect" />
<upload-attachment type="image" ref="imageRef" limit="" @confirm="imageSelect" />
<upload-attachment type="video" ref="videoRef" @confirm="videoSelect" />
<vue-ueditor-wrap v-model="content" :config="editorConfig" :editorDependencies="['ueditor.config.js','ueditor.all.js']" ref="editorRef"></vue-ueditor-wrap>
</template>

View File

@ -1,5 +1,5 @@
<template>
<el-dialog v-model="status" :title="t('exportTip')" width="300px" :close-on-click-modal="false" :close-on-press-escape="false" :show-close="false">
<el-dialog v-model="status" :title="t('exportTip')" width="300px" :close-on-click-modal="true" :close-on-press-escape="false" :show-close="false">
<span>{{ t('exportPlaceholder') }}</span>
<template #footer>
<span class="dialog-footer">
@ -52,23 +52,25 @@ const router = useRouter()
* 导出报表并跳转到下载页
*/
const detectionExportFn = () => {
loading.value = true
loading.value = true
const url = router.resolve({
path: '/site/setting/export'
})
exportDataCheck(prop.type, { page: 1, limit: 1, ...prop.searchParam }).then((res: any) => {
if (res.data) {
exportData(prop.type, prop.searchParam).then(() => {
loading.value = false
loading.value = false
emit('close', false)
setTimeout(() => {
window.open(url.href)
}, 100)
})
} else {
emit('close', false)
loading.value = false
ElMessage.error(res.msg)
}
}).catch(() => {
loading.value = false
})
}
//

View File

@ -113,7 +113,7 @@
<div class="attachment-item mr-[10px] w-[120px]" v-for="(item, index) in attachment.data" :key="index">
<div class="attachment-wrap w-full rounded cursor-pointer overflow-hidden relative flex items-center justify-center h-[120px]">
<el-image :src="img(item.url)" fit="contain" v-if="type == 'image'" :preview-src-list="item.image_list"/>
<video :src="img(item.url)" v-else-if="type == 'video'"></video>
<video :src="img(item.url)" v-else-if="type == 'video'" @click="previewVideo(index)"></video>
<icon :name="item.url" size="24px" v-else-if="type == 'icon'"></icon>
</div>
<div class="flex items-center">
@ -197,7 +197,7 @@ import {
getIconList
} from '@/app/api/sys'
import { debounce, img, getToken } from '@/utils/common'
import { ElMessage, UploadFile, UploadFiles, ElMessageBox } from 'element-plus'
import { ElMessage, UploadFile, UploadFiles, ElMessageBox, MessageParams } from 'element-plus'
import storage from '@/utils/storage'
const attachmentCategoryName = ref('')
@ -382,12 +382,24 @@ const upload = computed(() => {
uploadRef.value?.handleRemove(uploadFile)
} else {
uploadFile.status = 'fail'
ElMessage({ message: response.msg, type: 'error' })
showElMessage({ message: response.msg, type: 'error' })
}
}
}
})
const messageCache = new Map()
const showElMessage = (options: MessageParams) => {
const cacheKey = options.message
const cachedMessage = messageCache.get(cacheKey)
if (!cachedMessage || Date.now() - cachedMessage.timestamp > 5000) { // 5
messageCache.set(cacheKey, { timestamp: Date.now() })
ElMessage(options)
}
}
//
const selectAll = ref(false)
watch(selectAll, () => {
@ -431,7 +443,7 @@ const selectFile = (data: any) => {
if (prop.limit == 1 && length == prop.limit) {
delete selectedFile[keys[0]]
selectedFileIndex.splice(selectedFileIndex.indexOf(keys[0]),1);
} else if (length >= prop.limit) {
} else if (prop.limit && length >= prop.limit) {
ElMessage.info(t('upload.triggerUpperLimit'))
return
}

View File

@ -18,6 +18,17 @@
<el-color-picker v-model="theme" />
</div>
</div>
<!-- 布局风格 -->
<div class="setting-item mb-[10px]">
<div class="title text-base text-tx-secondary">{{ t('layout.layoutStyle') }}</div>
<div class="flex mt-[10px] layout-style flex-wrap">
<div class="relative w-[125px] h-[100px] border mr-[10px] mb-[10px] hover:border-primary"
:class="{ 'border-primary': currLayout == item.key }" v-for="(item, index) in layouts"
@click="handleSetLayout(item.key)">
<img :src="item.image" alt="" class="w-full h-full">
</div>
</div>
</div>
</el-scrollbar>
</el-drawer>
</div>
@ -27,8 +38,15 @@
import { ref, computed } from 'vue'
import useSystemStore from '@/stores/modules/system'
import { useDark, useToggle } from '@vueuse/core'
import { setThemeColor } from '@/utils/common'
import { setThemeColor, img } from '@/utils/common'
import { t } from '@/lang'
import Storage from '@/utils/storage'
const layouts = ref([
{ key: 'admin', image: img('static/resource/images/system/layout_bussiness.png') },
{ key: 'admin_simplicity', image: img('static/resource/images/system/layout_darkside.png') }
])
const currLayout = ref(Storage.get('admin_layout') || 'admin')
const drawer = ref(false)
const systemStore = useSystemStore()
@ -66,6 +84,11 @@ const theme = computed({
setThemeColor(systemStore.theme, systemStore.dark ? 'dark' : 'light')
}
})
const handleSetLayout = (key: string) => {
Storage.set({ key: 'admin_layout', data: key })
location.reload()
}
</script>
<style lang="scss" scoped>

View File

@ -0,0 +1,65 @@
<template>
<el-aside class="layout-aside dark w-auto">
<side class="hidden-xs-only slide" />
</el-aside>
</template>
<script lang="ts" setup>
import { watch, computed } from 'vue'
import { useRoute } from 'vue-router'
import side from './side.vue'
import useSystemStore from '@/stores/modules/system'
const systemStore = useSystemStore()
const dark = computed(() => {
return systemStore.dark
})
const route = useRoute()
watch(route, () => {
systemStore.$patch(state => {
state.menuDrawer = false
})
})
</script>
<style lang="scss">
.layout-aside {
//--side-dark-color: #141414;
//background-color: var(--side-dark-color, var(--el-bg-color));
&.bright {
background-color: #F5F7F9;
li {
background-color: #F5F7F9;
&.is-active:not(.is-opened) {
position: relative;
color: #333;
background-color: #fff;
&::after {
content: "";
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: 2px;
//background-color: var(--el-menu-active-color);
}
}
}
}
.slide {
border-right: 1px solid var(--el-border-color-extra-light);
}
}
.aside-drawer {
.el-drawer__body {
padding: 0px !important;
}
}
</style>

View File

@ -0,0 +1,88 @@
<template>
<template v-if="meta.show">
<el-sub-menu v-if="meta.type == 0 && routes.children" :index="String(routes.name)">
<template #title>
<div class="w-[16px] h-[16px] relative flex items-center" v-if="props.level == 1">
<icon v-if="meta.icon" :name="meta.icon" class="absolute !w-auto" />
</div>
<span class="ml-[10px]">{{ meta.title }}</span>
</template>
<menu-item v-for="(route, index) in routes.children" :routes="route" :key="index" :level="props.level + 1" />
</el-sub-menu>
<template v-else>
<el-menu-item :index="String(routes.name)" @click="router.push({ name: routes.name })">
<div class="w-[16px] h-[16px] relative flex items-center" v-if="props.level == 1">
<icon v-if="meta.icon" :name="meta.icon" class="absolute !w-auto" />
</div>
<template #title>
<span class="ml-[10px]">{{ meta.title }}</span>
</template>
</el-menu-item>
</template>
</template>
</template>
<script lang="ts" setup>
import { useRouter, useRoute } from 'vue-router'
import { ref, computed, watch } from 'vue'
import { img } from '@/utils/common'
import menuItem from './menu-item.vue'
import useSystemStore from '@/stores/modules/system'
import useUserStore from '@/stores/modules/user'
const router = useRouter()
const route = useRoute()
const userStore = useUserStore()
const routers = useUserStore().routers
const props = defineProps({
routes: {
type: Object,
required: true
},
level: {
type: Number,
default: 1
}
})
const systemStore = useSystemStore()
const meta = computed(() => props.routes.meta)
const addons = computed(() => {
const addons:Record<string, any> = {}
userStore.siteInfo?.apps.forEach((item: any) => { addons[item.key] = item })
userStore.siteInfo?.site_addons.forEach((item: any) => { addons[item.key] = item })
return addons
})
const systemAddonKeys = computed(() => {
return userStore.siteInfo?.site_addons.map((item: any) => item.key)
})
const addonRouters: Record<string, any> = {}
routers.forEach(item => {
item.original_name = item.name
if (item.meta.addon) {
addonRouters[item.meta.addon] = item
}
})
const addonsMenus = ref(null)
watch(route, () => {
if (props.routes.name != 'addon_list') return
if (systemAddonKeys.value.includes(route.meta.addon) && addonRouters[route.meta.addon]) {
addonsMenus.value = addonRouters[route.meta.addon]
} else {
addonsMenus.value = null
}
}, { immediate: true })
</script>
<style lang="scss">
.el-sub-menu{
.el-icon{
width: auto;
}
}
</style>

View File

@ -0,0 +1,117 @@
<template>
<el-container class="h-screen flex flex-col">
<el-main class="menu-wrap">
<el-header class="logo-wrap flex items-center justify-center h-[64px] w-[var(--aside-width)]">
<div class="flex justify-center items-center h-[64px] w-full px-[10px]" v-if="Object.keys(website).length">
<el-image :src="img(website.icon)" class="w-[44px] h-[44px] rounded-[50%]" @error="website.icon = img('static/resource/images/niucloud_icon.jpg')"></el-image>
<div class="flex-1 w-0 overflow-text truncate ml-[10px] text-white" v-if="!systemStore.menuIsCollapse">
<el-tooltip
effect="dark"
:content="website.site_name"
placement="top"
>
{{ website.site_name }}
</el-tooltip>
</div>
</div>
</el-header>
<el-scrollbar class="menu-scrollbar flex-1 h-0">
<el-menu :default-active="route.name" :router="true" :unique-opened="false" :collapse="systemStore.menuIsCollapse" background-color="#545c64" text-color="#fff" active-text-color="#ffd04b">
<menu-item v-for="(route, index) in menuData" :routes="route" :key="index" />
</el-menu>
<div class="h-[48px]"></div>
</el-scrollbar>
</el-main>
</el-container>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue'
import { useRoute } from 'vue-router'
import useSystemStore from '@/stores/modules/system'
import useUserStore from '@/stores/modules/user'
import menuItem from './menu-item.vue'
import { img, isUrl } from '@/utils/common'
import { findFirstValidRoute } from '@/router/routers'
const systemStore = useSystemStore()
const userStore = useUserStore()
const route = useRoute()
const siteInfo = userStore.siteInfo
const routers = userStore.routers
const addonIndexRoute = userStore.addonIndexRoute
const menuData = ref<Record<string, any>[]>([])
const addonRouters: Record<string, any> = {}
const website = computed(() => {
return systemStore.website
})
routers.forEach(item => {
item.original_name = item.name
if (item.meta.addon == '') {
if (item.children && item.children.length) {
item.name = findFirstValidRoute(item.children)
}
menuData.value.push(item)
} else if (item.meta.addon != '' && siteInfo?.apps.length == 1 && siteInfo?.apps[0].key == item.meta.addon) {
if (item.children) {
item.children.forEach((citem: Record<string, any>) => {
citem.original_name = citem.name
if (citem.children && citem.children.length) {
citem.name = findFirstValidRoute(citem.children)
}
})
menuData.value.unshift(...item.children)
} else {
menuData.value.unshift(item)
}
} else {
addonRouters[item.meta.addon] = item
}
})
//
if (siteInfo?.apps.length > 1) {
const routers:Record<string, any>[] = []
siteInfo?.apps.forEach((item: Record<string, any>) => {
if (addonRouters[item.key]) {
addonRouters[item.key].name = addonIndexRoute[item.key]
routers.push(addonRouters[item.key])
}
})
menuData.value.unshift(...routers)
}
</script>
<style lang="scss">
.logo-wrap {
background: #545c64;
transition: transform getCssVar('transition-duration');
}
:root{
--aside-width: 200px;
}
.menu-wrap {
padding: 0!important;
background: #545c64;
display: flex;
flex-direction: column;
.el-menu {
border-right: 0!important;
&:not(.el-menu--collapse) {
width: var(--aside-width);
}
.el-menu-item, .el-sub-menu__title {
--el-menu-item-height: 40px;
}
.el-sub-menu .el-menu-item {
--el-menu-sub-item-height: 40px;
}
}
}
</style>

View File

@ -0,0 +1,140 @@
<template>
<el-container class="h-[64px] layout-admin flex items-center justify-between px-[15px]" >
<!-- :class="['h-full px-[10px]',{'layout-header border-b border-color': !dark}]" -->
<div class="left-panel flex items-center text-[14px] leading-[1]">
<div class="navbar-item flex items-center h-full cursor-pointer" @click="toggleMenuCollapse">
<icon name="element Expand" v-if="systemStore.menuIsCollapse" />
<icon name="element Fold" v-else />
</div>
<!-- 刷新当前页 -->
<div class="navbar-item flex items-center h-full cursor-pointer" @click="refreshRouter">
<icon name="element Refresh" />
</div>
<!-- 面包屑导航 -->
<div class="flex items-center h-full pl-[10px] hidden-xs-only">
<el-breadcrumb separator="/">
<el-breadcrumb-item v-for="(route, index) in breadcrumb" :key="index">{{route.meta.title }}</el-breadcrumb-item>
</el-breadcrumb>
</div>
</div>
<div class="right-panel h-full flex items-center justify-end">
<div class="navbar-item flex items-center h-full cursor-pointer">
<layout-setting />
</div>
<!-- 用户信息 -->
<div class="navbar-item flex items-center h-full cursor-pointer">
<user-info />
</div>
</div>
<input type="hidden" v-model="comparisonToken">
<input type="hidden" v-model="comparisonSiteId">
<el-dialog v-model="detectionLoginDialog" :title="t('layout.detectionLoginTip')" width="30%" :close-on-click-modal="false" :close-on-press-escape="false" :show-close="false">
<span>{{ t('layout.detectionLoginContent') }}</span>
<template #footer>
<span class="dialog-footer">
<el-button @click="detectionLoginFn">{{ t('layout.detectionLoginOperation') }}</el-button>
</span>
</template>
</el-dialog>
<el-dialog v-model="showDialog" :title="t('indexTemplate')" width="550px" :destroy-on-close="true" >
<div class="flex flex-wrap">
<div v-for="(items, index) in indexList" :key="index" v-if="index_path == ''">
<div @click="index_path = items.view_path" class="index-item py-[5px] px-[10px] mr-[10px] rounded-[3px] cursor-pointer" :class="items.is_use == 1 ? 'bg-primary text-[#fff]' : '' ">
<span >{{ items.name }}</span>
</div>
</div>
<div v-for="(itemTo, indexTo) in indexList" :key="indexTo" v-else>
<div @click="index_path = itemTo.view_path" class="index-item py-[5px] px-[10px] mr-[10px] rounded-[3px] cursor-pointer" :class="index_path == itemTo.view_path ? 'bg-primary text-[#fff]' : '' ">
<span >{{ itemTo.name }}</span>
</div>
</div>
</div>
<template #footer>
<span class="dialog-footer">
<el-button type="primary" @click="submitIndex">{{ t('confirm') }}</el-button>
</span>
</template>
</el-dialog>
</el-container>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue'
import useUserStore from '@/stores/modules/user'
import useAppStore from '@/stores/modules/app'
import { useRoute } from 'vue-router'
import { t } from '@/lang'
import storage from '@/utils/storage'
import userInfo from './user-info.vue'
import layoutSetting from './layout-setting.vue'
import useSystemStore from "@/stores/modules/system";
const route = useRoute()
const appStore = useAppStore()
const userStore = useUserStore()
const systemStore = useSystemStore()
// start
const detectionLoginDialog = ref(false)
const comparisonToken = ref('')
const comparisonSiteId = ref('')
if (storage.get('comparisonTokenStorage')) {
comparisonToken.value = storage.get('comparisonTokenStorage')
}
if (storage.get('comparisonSiteIdStorage')) {
comparisonSiteId.value = storage.get('comparisonSiteIdStorage')
}
//
document.addEventListener('visibilitychange', e => {
if (document.visibilityState === 'visible' && (comparisonSiteId.value != storage.get('siteId') || comparisonToken.value != storage.get('token'))) {
detectionLoginDialog.value = true
}
})
systemStore.toggleMenuCollapse(storage.get('menuiscollapse') || false)
const detectionLoginFn = () => {
detectionLoginDialog.value = false
location.reload()
}
// end
//
const refreshRouter = () => {
if (!appStore.routeRefreshTag) return
appStore.refreshRouterView()
}
//
const breadcrumb = computed(() => {
const matched = route.matched.filter(item => { return item.meta.title })
if (matched[0] && matched[0].path == '/') matched.splice(0, 1)
return matched
})
storage.set({ key: 'currHeadMenuName', data: "" })
const toggleMenuCollapse = () => {
systemStore.toggleMenuCollapse(!systemStore.menuIsCollapse)
}
</script>
<style lang="scss" scoped>
.layout-header{
position: relative;
z-index: 5;
border-bottom: 1px solid #e8e9eb;
}
.navbar-item {
padding: 0 8px;
}
.index-item {
border: 1px solid;
border-color: var(--el-color-primary);
&:hover {
color: #fff;
background-color: var(--el-color-primary);
}
}
</style>

View File

@ -0,0 +1,104 @@
<template>
<div class="flex">
<icon name="element Setting" @click="drawer = true" />
<el-drawer v-model="drawer" :title="t('layout.layoutSetting')" size="300px">
<el-scrollbar>
<!-- 黑暗模式 -->
<div class="setting-item flex items-center justify-between mb-[10px]">
<div class="title text-base text-tx-secondary">{{ t('layout.darkMode') }}</div>
<div>
<el-switch v-model="dark" :active-value="true" :inactive-value="false" />
</div>
</div>
<!-- 主题颜色 -->
<div class="setting-item flex items-center justify-between mb-[10px]">
<div class="title text-base text-tx-secondary">{{ t('layout.themeColor') }}</div>
<div>
<el-color-picker v-model="theme" />
</div>
</div>
<!-- 布局风格 -->
<div class="setting-item mb-[10px]">
<div class="title text-base text-tx-secondary">{{ t('layout.layoutStyle') }}</div>
<div class="flex mt-[10px] layout-style flex-wrap">
<div class="relative w-[125px] h-[100px] border mr-[10px] mb-[10px] hover:border-primary"
:class="{ 'border-primary': currLayout == item.key }" v-for="(item, index) in layouts"
@click="handleSetLayout(item.key)">
<img :src="item.image" alt="" class="w-full h-full">
</div>
</div>
</div>
</el-scrollbar>
</el-drawer>
</div>
</template>
<script lang="ts" setup>
import { ref, computed } from 'vue'
import useSystemStore from '@/stores/modules/system'
import { useDark, useToggle } from '@vueuse/core'
import { setThemeColor, img } from '@/utils/common'
import { t } from '@/lang'
import Storage from '@/utils/storage'
const layouts = ref([
{ key: 'admin', image: img('static/resource/images/system/layout_bussiness.png') },
{ key: 'admin_simplicity', image: img('static/resource/images/system/layout_darkside.png') }
])
const currLayout = ref(Storage.get('admin_layout') || 'admin')
const drawer = ref(false)
const systemStore = useSystemStore()
const isDark = useDark()
const toggleDark = useToggle(isDark)
const dark = computed({
get() {
return systemStore.dark
},
set(val) {
systemStore.setTheme('dark', val)
toggleDark(val)
setThemeColor(systemStore.theme, systemStore.dark ? 'dark' : 'light')
}
})
const sidebar = computed({
get() {
return systemStore.sidebar
},
set(val) {
systemStore.setTheme('sidebar', val)
setThemeColor(systemStore.theme, systemStore.dark ? 'dark' : 'light')
}
})
const theme = computed({
get() {
return systemStore.theme
},
set(val) {
systemStore.setTheme('theme', val)
setThemeColor(systemStore.theme, systemStore.dark ? 'dark' : 'light')
}
})
const handleSetLayout = (key: string) => {
Storage.set({ key: 'admin_layout', data: key })
location.reload()
}
</script>
<style lang="scss" scoped>
:deep(.el-drawer__header) {
margin-bottom: 0 !important;
}
.layout-style {
&>div:nth-child(2n+2) {
margin-right: 0;
}
}
</style>

View File

@ -0,0 +1,32 @@
<template>
<el-dropdown @command="switchLang" :tabindex="1">
<icon name="iconfont iconfanyi" />
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="zh-cn" :disabled="systemStore.lang == 'zh-cn'">简体中文</el-dropdown-item>
<el-dropdown-item command="en" :disabled="systemStore.lang == 'en'">English</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<script lang="ts" setup>
import useSystemStore from '@/stores/modules/system'
import { language } from '@/lang'
import { useRoute } from 'vue-router'
import storage from '@/utils/storage'
const route = useRoute()
const systemStore = useSystemStore()
const switchLang = (command: string) => {
systemStore.$patch((state) => {
state.lang = command
storage.set({ key: 'lang', data: command })
})
language.loadLocaleMessages(route.path, systemStore.lang)
location.reload()
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,147 @@
<template>
<div>
<el-dropdown @command="clickEvent" :tabindex="1">
<div class="userinfo flex h-full items-center">
<el-avatar v-if="userStore.userInfo.head_img" :size="25" :icon="UserFilled" :src="img(userStore.userInfo.head_img)"/>
<img v-else src="@/app/assets/images/member_head.png" class="w-[25px] rounded-full" />
<div class="user-name pl-[8px]">{{ userStore.userInfo.username }}</div>
<icon name="element ArrowDown" class="ml-[5px]" />
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="getUserInfoFn">
<!-- <router-link to="/user/center"> -->
<div class="flex items-center leading-[1] py-[5px]">
<span class="iconfont iconshezhi1 ml-[4px] !text-[14px] mr-[10px]"></span>
<span class="text-[14px]">账号设置</span>
</div>
<!-- </router-link> -->
</el-dropdown-item>
<el-dropdown-item>
<router-link to="/tools/authorize">
<div class="flex items-center leading-[1] py-[5px]">
<span class="iconfont iconshouquanxinxi2 ml-[4px] !text-[14px] mr-[10px]"></span>
<span class="text-[14px]">授权信息</span>
</div>
</router-link>
</el-dropdown-item>
<el-dropdown-item @click="changePasswordDialog=true">
<div class="flex items-center leading-[1] py-[5px]">
<span class="iconfont iconxiugai ml-[4px] !text-[14px] mr-[10px]"></span>
<span class="text-[14px]">修改密码</span>
</div>
</el-dropdown-item>
<el-dropdown-item @click="logout">
<div class="flex items-center leading-[1] py-[5px]">
<span class="iconfont icontuichudenglu ml-[4px] !text-[14px] mr-[10px]"></span>
<span class="text-[14px]">退出登录</span>
</div>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<el-dialog v-model="changePasswordDialog" width="450px" title="修改密码">
<div>
<el-form :model="saveInfo" label-width="90px" ref="formRef" :rules="formRules" class="page-form">
<el-form-item :label="t('originalPassword')" prop="original_password">
<el-input v-model="saveInfo.original_password" type="password" :placeholder="t('originalPasswordPlaceholder')" clearable class="input-width" />
</el-form-item>
<el-form-item :label="t('newPassword')" prop="password">
<el-input v-model="saveInfo.password" type="password" :placeholder="t('passwordPlaceholder')" clearable class="input-width" />
<div class="form-tip">{{t('passwordTip')}}</div>
</el-form-item>
<el-form-item :label="t('passwordCopy')" prop="password_copy">
<el-input v-model="saveInfo.password_copy" type="password" :placeholder="t('passwordPlaceholder')" clearable class="input-width" />
</el-form-item>
</el-form>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="changePasswordDialog = false">{{t('cancel')}}</el-button>
<el-button type="primary" @click="submitForm(formRef)">{{t('save')}}</el-button>
</span>
</template>
</el-dialog>
<user-info-edit ref="userInfoEditRef" />
</div>
</template>
<script lang="ts" setup>
import { UserFilled } from '@element-plus/icons-vue'
import { reactive, ref } from 'vue'
import type { FormInstance, FormRules, ElNotification } from 'element-plus'
import useUserStore from '@/stores/modules/user'
import { setUserInfo } from '@/app/api/personal'
import { t } from '@/lang'
import { img } from '@/utils/common'
import userInfoEdit from '@/app/components/user-info-edit/index.vue'
const userStore = useUserStore()
const clickEvent = (command: string) => {
switch (command) {
case 'logout':
userStore.logout()
break
}
}
const logout = () => {
userStore.logout();
}
const userInfoEditRef = ref(null)
const getUserInfoFn = ()=>{
userInfoEditRef.value?.open()
}
// --- start
const changePasswordDialog = ref(false)
const formRef = ref<FormInstance>();
//
const saveInfo = reactive({
original_password: '',
password: '',
password_copy: ''
});
//
const formRules = reactive<FormRules>({
original_password: [
{ required: true, message: t("originalPasswordPlaceholder"), trigger: "blur" },
],
password: [
{ required: true, message: t("passwordPlaceholder"), trigger: "blur" },
],
password_copy: [
{ required: true, message: t("passwordPlaceholder"), trigger: "blur" },
]
});
const submitForm = (formEl: FormInstance | undefined) => {
if (!formEl) return
formEl.validate((valid) => {
if (valid) {
let msg = "";
if (saveInfo.password && !saveInfo.original_password) msg = t('originalPasswordHint');
if (saveInfo.password && saveInfo.original_password && !saveInfo.password_copy) msg = t('newPasswordHint');
if (saveInfo.password && saveInfo.original_password && saveInfo.password_copy && saveInfo.password != saveInfo.password_copy) msg = t('doubleCipherHint');
if (msg) {
ElNotification({
type: 'error',
message: msg,
})
return;
}
setUserInfo(saveInfo).then((res: any) => {
changePasswordDialog.value = false;
})
} else {
return false
}
});
}
</script>
<style lang="scss" scoped>
.el-popper .el-dropdown-menu{
width: 150px;
}
</style>

View File

@ -0,0 +1,136 @@
<template>
<div class="tab-wrap w-full px-[16px]">
<el-tabs :closable="tabbarStore.tabLength > 1" :model-value="route.path" @tab-click="tabClick" @tab-remove="removeTab">
<el-tab-pane v-for="(tab, key, index) in tabbarStore.tabs" :name="tab.path" :key="index">
<template #label>
<el-dropdown trigger="contextmenu" placement="bottom-start">
<span :class="{ 'text-primary': route.path == tab.path }" class="tab-name">{{ tab.title }}</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item icon="Back" :disabled="index == 0" @click="closeLeft(tab.path)">{{t('tabs.closeLeft') }}</el-dropdown-item>
<el-dropdown-item icon="Right" :disabled="index == (tabbarStore.tabLength - 1)" @click="closeRight(tab.path)">{{t('tabs.closeRight') }}</el-dropdown-item>
<el-dropdown-item icon="Close" :disabled="tabbarStore.tabLength == 1" @click="closeOther(tab.path)">{{t('tabs.closeOther') }}</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script lang="ts" setup>
import { watch, onMounted } from 'vue'
import useTabbarStore from '@/stores/modules/tabbar'
import { useRoute, useRouter } from 'vue-router'
import { t } from '@/lang'
const tabbarStore = useTabbarStore()
const route = useRoute()
const router = useRouter()
onMounted(() => {
tabbarStore.addTab(route)
})
watch(route, (nval: any) => {
tabbarStore.addTab(nval)
})
/**
* 添加tab
* @param content
*/
const tabClick = (content: any) => {
const tabRoute = tabbarStore.tabs[content.props.name]
router.push({ path: tabRoute.path, query: tabRoute.query })
}
/**
* 移除tab
* @param content
*/
const removeTab = (content: any) => {
if (route.path == content) {
const tabs = Object.keys(tabbarStore.tabs)
router.push({ path: tabs[tabs.indexOf(content) - 1] })
}
tabbarStore.removeTab(content)
}
/**
* 关闭左侧
* @param path
*/
const closeLeft = (path: string) => {
const tabs = Object.keys(tabbarStore.tabs)
for (let i = tabs.indexOf(path) - 1; i >= 0; i--) {
delete tabbarStore.tabs[tabs[i]]
}
router.push({ path })
}
/**
* 关闭右侧
* @param path
*/
const closeRight = (path: string) => {
const tabs = Object.keys(tabbarStore.tabs)
for (let i = tabs.indexOf(path) + 1; i < tabs.length; i++) {
delete tabbarStore.tabs[tabs[i]]
}
router.push({ path })
}
/**
* 关闭其他
* @param path
*/
const closeOther = (path: string) => {
const tabs = Object.keys(tabbarStore.tabs)
tabs.forEach((key: string) => { key != path && delete tabbarStore.tabs[key] })
router.push({ path })
}
</script>
<style lang="scss" scoped>
:deep(.el-tabs) {
.el-tabs--border-card {
border: none;
}
.el-tabs__header {
margin: 0;
}
.el-tabs__nav-wrap {
margin-bottom: 0;
&::after {
display: none;
}
}
.el-tabs__content {
display: none;
}
.el-tabs__item {
display: inline-flex !important;
padding: 0 20px !important;
align-items: center;
.tab-name:focus {
outline: none !important;
}
}
.el-tabs__active-bar {
display: none;
}
.el-tabs__item.is-active {
background-color: var(--el-color-primary-light-9);
}
}
</style>

View File

@ -0,0 +1,41 @@
<template>
<div class="flex w-full h-screen">
<!-- 左侧边栏 -->
<layout-aside></layout-aside>
<!-- 左侧边栏 end -->
<el-container>
<!-- 顶部 -->
<el-header>
<layout-header></layout-header>
</el-header>
<!-- 顶部 end -->
<!-- 主体 -->
<el-main class="h-full p-0 bg-page">
<el-scrollbar>
<div class="p-[15px]">
<router-view v-slot="{ Component, route }" v-if="appStore.routeRefreshTag">
<keep-alive :include="tabbarStore.tabNames">
<component :is="Component" :key="route.fullPath" />
</keep-alive>
</router-view>
</div>
</el-scrollbar>
</el-main>
<!-- 主体 end -->
</el-container>
</div>
</template>
<script lang="ts" setup>
import layoutHeader from './components/header/index.vue'
import layoutAside from './components/aside/index.vue'
import useAppStore from '@/stores/modules/app'
import useTabbarStore from '@/stores/modules/tabbar'
const appStore = useAppStore()
const tabbarStore = useTabbarStore()
</script>
<style lang="scss" scoped></style>

View File

@ -7,6 +7,7 @@ import { ref, markRaw, defineAsyncComponent, provide } from 'vue'
import { getAppType } from '@/utils/common'
import useUserStore from '@/stores/modules/user'
import useSystemStore from '@/stores/modules/system'
import Storage from '@/utils/storage'
const sysLayout = import.meta.glob('./*/index.vue')
const addonLayout = import.meta.glob('@/addon/**/layout/index.vue')
@ -15,7 +16,7 @@ const modules = Object.assign(sysLayout, addonLayout)
let siteLayout = 'default'
switch (getAppType()) {
case 'admin':
siteLayout = 'admin'
siteLayout = Storage.get('admin_layout') || 'admin'
break
default:
const siteInfo = useUserStore().siteInfo

View File

@ -90,7 +90,7 @@ html, body {
.fixed-footer {
position: absolute;
z-index: 1000;
z-index: 4;
right: 15px;
bottom: 0;
left: 15px;

View File

@ -1 +1 @@
/* addon-iconfont.css */
/* addon iconfont */

View File

View File

@ -106,7 +106,7 @@ class User extends BaseAdminController
*/
public function del($uid)
{
(new UserService())->del($uid);
(new SiteUserService())->del($uid);
return success('DELETE_SUCCESS');
}

View File

@ -29,7 +29,7 @@ class Schedule extends BaseAdminController
{
$data = $this->request->params([
['key', ''],
['status', ''],
['status', 'all'],
]);
return success(data: (new ScheduleService())->getPage($data));

View File

@ -86,6 +86,21 @@ class User extends BaseAdminController
return success();
}
/**
* 编辑用户
* @return Response
* @throws \Exception
*/
public function edit($uid) {
$data = $this->request->params([
['password', ''],
['real_name', ''],
['head_img', ''],
]);
(new UserService())->edit($uid, $data);
return success();
}
/**
* 删除用户
* @param $uid

View File

@ -67,6 +67,8 @@ Route::group('site', function () {
Route::put('user/:uid', 'site.User/edit');
//站点修改用户属性
Route::put('user/:uid/:field', 'site.User/modify');
//站点删除用户
Route::delete('user/:uid', 'site.User/del');
/***************************************************** 操作日志 **************************************************/
//操作日志列表
Route::get('log', 'site.UserLog/lists');

View File

@ -34,6 +34,8 @@ Route::group('user', function () {
Route::get('isexist', 'user.User/checkUserIsExist');
//添加用户
Route::post('user', 'user.User/add');
// 编辑用户
Route::put('user/:uid', 'user.User/edit');
// 获取用户站点创建限制
Route::get('user/create_site_limit/:uid', 'user.User/getUserCreateSiteLimit');
// 获取用户站点创建限制

View File

@ -43,18 +43,18 @@ class ApiCheckToken
try {
$token = $request->apiToken();
$token_info = ( new LoginService() )->parseToken($token);
if (!empty($token_info)) {
$request->memberId($token_info[ 'member_id' ]);
}
//校验会员和站点
( new AuthService() )->checkSiteAuth($request);
// 校验渠道
( new AuthService() )->checkChannel($request);
} catch (AuthException $e) {
//是否将登录错误抛出
if ($is_throw_exception)
return fail($e->getMessage(), [], $e->getCode());
}
if (!empty($token_info)) {
$request->memberId($token_info[ 'member_id' ]);
}
//校验会员和站点
( new AuthService() )->checkSiteAuth($request);
// 校验渠道
( new AuthService() )->checkChannel($request);
return $next($request);
}
}

View File

@ -0,0 +1,29 @@
<?php
declare (strict_types = 1);
namespace app\command;
use app\service\admin\auth\LoginService;
use app\service\admin\install\InstallSystemService;
use app\service\core\menu\CoreMenuService;
use think\console\Command;
use think\console\Input;
use think\console\input\Option;
use think\console\Output;
class Resetpassword extends Command
{
protected function configure()
{
// 指令配置
$this->setName('reset')
->setDescription('the reset administrator password command');
}
protected function execute(Input $input, Output $output)
{
LoginService::resetAdministratorPassword();
// 指令输出
$output->writeln('password reset success');
}
}

View File

@ -69,10 +69,10 @@ function get_lang($str)
function list_to_tree($list, $pk = 'id', $pid = 'pid', $child = 'child', $root = 0)
{
// 创建Tree
$tree = array ();
$tree = array();
if (is_array($list)) {
// 创建基于主键的数组引用
$refer = array ();
$refer = array();
foreach ($list as $key => $data) {
$refer[ $data[ $pk ] ] =& $list[ $key ];
}
@ -132,7 +132,7 @@ function array_keys_search($array, $keys, $index = '', $is_sort = true)
return [];
if (!empty($index) && count($array) != count($array, COUNT_RECURSIVE))
$array = array_column($array, null, $index);
$list = array ();
$list = array();
foreach ($keys as $key) {
if (isset($array[ $key ])) {
@ -502,7 +502,7 @@ function array_merge2(array $array1, array $array2)
function get_files_by_dir($dir)
{
$dh = @opendir($dir); //打开目录,返回一个目录流
$return = array ();
$return = array();
while ($file = @readdir($dh)) { //循环读取目录下的文件
if ($file != '.' and $file != '..') {
$path = $dir . DIRECTORY_SEPARATOR . $file; //设置目录,用于含有子目录的情况
@ -947,3 +947,18 @@ function str_sub($str, $length = 10, $is_need_apostrophe = true)
{
return mb_substr($str, 0, $length, 'UTF-8') . ( $is_need_apostrophe ? '...' : '' );
}
/**
* 使用正则表达式匹配特殊字符
* @param $str
* @return bool
*/
function is_special_character($str)
{
$pattern = '/[!@#$%^&*()\[\]{}<>\|?:;"]/';
if (preg_match($pattern, $str)) {
return true;
} else {
return false;
}
}

View File

@ -12,7 +12,7 @@ class CommonDict
public const UNKNOWN = 0;
public const MAN = 1;
public const WOMAN = 2;
public const ENCRYPT_STR = '*****************************';
/**
* 性别
@ -26,4 +26,4 @@ class CommonDict
self::WOMAN => get_lang('dict_sex.woman'),//女
];
}
}
}

View File

@ -233,6 +233,20 @@ return [
'sort' => '100',
'status' => '1',
'is_show' => '1',
],
[
'menu_name' => '删除管理员',
'menu_key' => 'delete_site_user',
'menu_short_name' => '删除管理员',
'menu_type' => '2',
'icon' => '',
'api_url' => 'site/user/<uid>',
'router_path' => '',
'view_path' => '',
'methods' => 'delete',
'sort' => '100',
'status' => '1',
'is_show' => '1',
]
]
],
@ -828,7 +842,7 @@ return [
],
],
[
'menu_name' => '用户',
'menu_name' => '用户管理',
'menu_key' => 'site_user_list',
'menu_short_name' => '用户',
'menu_type' => '1',
@ -1043,7 +1057,7 @@ return [
]
],
[
'menu_name' => '开发',
'menu_name' => '开发管理',
'menu_key' => 'app_manage_tool',
'menu_short_name' => '开发',
'menu_type' => '1',

View File

@ -1618,6 +1618,20 @@ return [
'sort' => '100',
'status' => '1',
'is_show' => '1',
],
[
'menu_name' => '删除管理员',
'menu_key' => 'delete_site_user',
'menu_short_name' => '删除管理员',
'menu_type' => '2',
'icon' => '',
'api_url' => 'site/user/<uid>',
'router_path' => '',
'view_path' => '',
'methods' => 'delete',
'sort' => '100',
'status' => '1',
'is_show' => '1',
]
]
],

View File

@ -57,19 +57,22 @@ class PayDict
'name' => get_lang('dict_pay.type_wechatpay'),
'key' => self::WECHATPAY,
'icon' => self::WECHATPAY_ICON,
'setting_component' => '/src/app/views/setting/components/pay-wechatpay.vue'
'setting_component' => '/src/app/views/setting/components/pay-wechatpay.vue',
'encrypt_params' => ['mch_public_cert_path', 'mch_secret_cert', 'mch_secret_key'],
],//微信支付
self::ALIPAY => [
'name' => get_lang('dict_pay.type_alipay'),
'key' => self::ALIPAY,
'icon' => self::ALIPAY_ICON,
'setting_component' => '/src/app/views/setting/components/pay-alipay.vue'
'setting_component' => '/src/app/views/setting/components/pay-alipay.vue',
'encrypt_params' => ['app_secret_cert', 'app_public_cert_path', 'alipay_public_cert_path', 'alipay_root_cert_path'],
],//支付宝支付
self::BALANCEPAY => [
'name' => get_lang('dict_pay.type_balancepay'),
'key' => self::BALANCEPAY,
'icon' => self::BALANCEPAY_ICON,
'setting_component' => ''
'setting_component' => '',
'encrypt_params' => ['secret_key'],
],//微信支付
];

View File

@ -47,6 +47,7 @@ class SmsDict
'app_key' => 'APP_KEY',
'secret_key' => 'SECRET_KEY'
],
'encrypt_params' => ['secret_key'],
'component' => '/src/app/views/setting/components/sms-ali.vue',
],
self::TENCENTSMS => [
@ -58,6 +59,7 @@ class SmsDict
'secret_id' => 'SECRET_ID',
'secret_key' => 'SECRET_KEY'
],
'encrypt_params' => ['secret_key'],
'component' => '/src/app/views/setting/components/sms-tencent.vue',
],
];

View File

@ -54,6 +54,7 @@ class StorageDict
'secret_key' => 'SECRET_KEY',
'domain' => '空间域名'
],
'encrypt_params' => ['secret_key'],
'component' => '/src/app/views/setting/components/storage-qiniu.vue',
],
@ -67,6 +68,7 @@ class StorageDict
'endpoint' => 'Endpoint',
'domain' => '空间域名'
],
'encrypt_params' => ['secret_key'],
'component' => '/src/app/views/setting/components/storage-ali.vue',
],
@ -80,6 +82,7 @@ class StorageDict
'secret_key' => 'SECRET_KEY',
'domain' => '空间域名'
],
'encrypt_params' => ['secret_key'],
'component' => '/src/app/views/setting/components/storage-tencent.vue',
],

View File

@ -923,6 +923,7 @@ CREATE TABLE `sys_user_role` (
`create_time` int(11) NOT NULL DEFAULT 0 COMMENT '添加时间',
`is_admin` int(11) NOT NULL DEFAULT 0 COMMENT '是否是超级管理员',
`status` int(11) NOT NULL DEFAULT 1 COMMENT '状态',
`delete_time` INT(11) NOT NULL DEFAULT 0 COMMENT '删除时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户权限表' ROW_FORMAT = Dynamic;
@ -1036,7 +1037,7 @@ UPDATE `site` SET site_id = 0 WHERE site_id = 1;
INSERT INTO `sys_user` VALUES ('1', '', '', '', '', '', '0', '0', '0', '1', '0', '0', '0');
INSERT INTO `sys_user_role` VALUES ('1', '1', '0', '', '0', '1', '1');
INSERT INTO `sys_user_role` VALUES ('1', '1', '0', '', '0', '1', '1', '0');
INSERT INTO `sys_area` VALUES
(110000, 0, '北京市', '北京', '116.40529', '39.904987', 1, 0, 1),

View File

@ -84,6 +84,7 @@ return [
//插件安装相关
'REPEAT_INSTALL' => '当前插件已安装,不能重复安装',
'NOT_UNINSTALL' => '当前插件未安装,不能进行卸载操作',
'ADDON_INFO_FILE_NOT_EXIST' => '未找到插件的info.json文件',
//菜单管理
'MENU_NOT_EXIST' => '菜单不存在',
@ -183,6 +184,7 @@ return [
'KEYWORDS_NOT_EXIST' => '关键词回复不存在',
'WECHAT_EMPOWER_NOT_EXIST' => '微信授权信息不存在',
'SCAN_SUCCESS' => '扫码成功',
'WECHAT_SNAPSHOUTUSER' => '返回的是虚拟账号',
//小程序
'WEAPP_NOT_EXIST' => '微信小程序未配置完善',
'WEAPP_EMPOWER_NOT_EXIST' => '微信小程序授信信息不存在',

View File

@ -73,7 +73,7 @@ class Site extends BaseModel
public function searchKeywordsAttr($query, $value, $data)
{
if ($value != '') {
$query->where('site_name|keywords', 'like', '%' . $this->handelSpecialCharacter($value) . '%');
$query->where('site_id|site_name|keywords', 'like', '%' . $this->handelSpecialCharacter($value) . '%');
}
}

View File

@ -79,7 +79,7 @@ class SysSchedule extends BaseModel
*/
public function searchStatusAttr(Query $query, $value, $data)
{
if ($value) {
if ($value != 'all') {
$query->where('status', $value);
}
}

View File

@ -14,6 +14,7 @@ namespace app\model\sys;
use app\dict\sys\UserDict;
use app\model\site\Site;
use core\base\BaseModel;
use think\model\concern\SoftDelete;
use think\model\relation\HasOne;
/**
@ -24,6 +25,8 @@ use think\model\relation\HasOne;
class SysUserRole extends BaseModel
{
use SoftDelete;
/**
* 数据表主键
* @var string
@ -41,6 +44,18 @@ class SysUserRole extends BaseModel
// 设置JSON数据返回数组
protected $jsonAssoc = true;
/**
* 定义软删除标记字段
* @var string
*/
protected $deleteTime = 'delete_time';
/**
* 定义软删除字段的默认值
* @var int
*/
protected $defaultSoftDelete = 0;
/**
* 关联查询用户信息
* @return HasOne

View File

@ -38,49 +38,50 @@ class AuthService extends BaseAdminService
* @param Request $request
* @return true
*/
public function checkSiteAuth(Request $request){
public function checkSiteAuth(Request $request)
{
$site_id = $request->adminSiteId();
//todo 将站点编号转化为站点id
$site_info = (new CoreSiteService())->getSiteCache($site_id);
$site_info = ( new CoreSiteService() )->getSiteCache($site_id);
//站点不存在
if(empty($site_info)) throw new AuthException('SITE_NOT_EXIST');
if (empty($site_info)) throw new AuthException('SITE_NOT_EXIST');
//没有当前站点的信息
if (!AuthService::isSuperAdmin()) {
if(!$this->getAuthRole($site_id)) throw new AuthException('NO_SITE_PERMISSION');
if (!$this->getAuthRole($site_id)) throw new AuthException('NO_SITE_PERMISSION');
}
$request->siteId($site_id);
$request->appType($site_info['app_type']);
$request->appType($site_info[ 'app_type' ]);
return true;
}
/**
* 校验权限
* @param Request $request
* @return bool
* @throws Exception
*/
public function checkRole(Request $request){
public function checkRole(Request $request)
{
$rule = strtolower(trim($request->rule()->getRule()));
$method = strtolower(trim($request->method()));
$site_info = (new AuthSiteService())->getSiteInfo();
if($method != 'get'){
if($site_info['status'] == SiteDict::EXPIRE) throw new AuthException('SITE_EXPIRE_NOT_ALLOW');
if($site_info['status'] == SiteDict::CLOSE) throw new AuthException('SITE_CLOSE_NOT_ALLOW');
$site_info = ( new AuthSiteService() )->getSiteInfo();
if ($method != 'get') {
if ($site_info[ 'status' ] == SiteDict::EXPIRE) throw new AuthException('SITE_EXPIRE_NOT_ALLOW');
if ($site_info[ 'status' ] == SiteDict::CLOSE) throw new AuthException('SITE_CLOSE_NOT_ALLOW');
}
$menu_service = new MenuService();
$all_menu_list = $menu_service->getAllApiList($this->app_type);
//先判断当前访问的接口是否收到权限的限制
$method_menu_list = $all_menu_list[$method] ?? [];
if(!in_array($rule, $method_menu_list)) {
$method_menu_list = $all_menu_list[ $method ] ?? [];
if (!in_array($rule, $method_menu_list)) {
$other_menu_list = $menu_service->getAllApiList($this->app_type == AppTypeDict::ADMIN ? AppTypeDict::SITE : AppTypeDict::ADMIN);
$method_menu_list = $other_menu_list[$method] ?? [];
if(!in_array($rule, $method_menu_list)) {
$method_menu_list = $other_menu_list[ $method ] ?? [];
if (!in_array($rule, $method_menu_list)) {
return true;
} else {
throw new AuthException('NO_PERMISSION');
@ -88,7 +89,7 @@ class AuthService extends BaseAdminService
}
$auth_role_list = $this->getAuthApiList();
if(!empty($auth_role_list[$method]) && in_array($rule, $auth_role_list[$method]))
if (!empty($auth_role_list[ $method ]) && in_array($rule, $auth_role_list[ $method ]))
return true;
throw new AuthException('NO_PERMISSION');
@ -99,7 +100,8 @@ class AuthService extends BaseAdminService
* 获取授权用户的权限信息
* @return mixed
*/
public function getAuthRole(int $site_id){
public function getAuthRole(int $site_id)
{
$user_role_service = new UserRoleService();
return $user_role_service->getUserRole($site_id, $this->uid);
}
@ -108,7 +110,8 @@ class AuthService extends BaseAdminService
* 当前授权用户接口权限
* @return array
*/
public function getAuthApiList(){
public function getAuthApiList()
{
if (AuthService::isSuperAdmin()) {
$is_admin = 1;
} else {
@ -116,15 +119,15 @@ class AuthService extends BaseAdminService
if (empty($user_role_info))
return [];
$is_admin = $user_role_info['is_admin'];//是否是超级管理员组
$is_admin = $user_role_info[ 'is_admin' ];//是否是超级管理员组
}
$menu_service = new MenuService();
if($is_admin){//查询全部启用的权限
if ($is_admin) {//查询全部启用的权限
//获取站点信息
return (new AuthSiteService())->getApiList(1);
}else{
$user_role_ids = $user_role_info['role_ids'];
return ( new AuthSiteService() )->getApiList(1);
} else {
$user_role_ids = $user_role_info[ 'role_ids' ];
$role_service = new RoleService();
$menu_keys = $role_service->getMenuIdsByRoleIds($this->site_id, $user_role_ids);
@ -137,21 +140,23 @@ class AuthService extends BaseAdminService
* 当前授权用户菜单权限
* @return array
*/
public function getAuthMenuList(int $is_tree = 0, $addon = 'all'){
public function getAuthMenuList(int $is_tree = 0, $addon = 'all')
{
if (AuthService::isSuperAdmin()) {
$is_admin = 1;
} else {
$user_role_info = $this->getAuthRole($this->site_id);
if(empty($user_role_info))
if (empty($user_role_info))
return [];
$is_admin = $user_role_info['is_admin'];//是否是超级管理员组
$is_admin = $user_role_info[ 'is_admin' ];//是否是超级管理员组
}
$menu_service = new MenuService();
if($is_admin){//查询全部启用的权限
if ($is_admin) {
// 查询全部启用的权限
return ( new MenuService() )->getAllMenuList($this->app_type, 1, $is_tree, 1);
}else{
$user_role_ids = $user_role_info['role_ids'];
} else {
$user_role_ids = $user_role_info[ 'role_ids' ];
$role_service = new RoleService();
$menu_keys = $role_service->getMenuIdsByRoleIds($this->site_id, $user_role_ids);
return $menu_service->getMenuListByMenuKeys($this->site_id, $menu_keys, $this->app_type, $is_tree, $addon);
@ -161,8 +166,9 @@ class AuthService extends BaseAdminService
/**
* 获取授权用户信息
*/
public function getAuthInfo(){
return (new SiteUserService())->getInfo($this->uid);
public function getAuthInfo()
{
return ( new SiteUserService() )->getInfo($this->uid);
}
/**
@ -171,8 +177,9 @@ class AuthService extends BaseAdminService
* @param $data
* @return bool
*/
public function modifyAuth(string $field, $data){
return (new SiteUserService())->modify($this->uid, $field, $data);
public function modifyAuth(string $field, $data)
{
return ( new SiteUserService() )->modify($this->uid, $field, $data);
}
/**
@ -180,33 +187,35 @@ class AuthService extends BaseAdminService
* @param array $data
* @return true
*/
public function editAuth(array $data){
if(!empty($data['password'])){
public function editAuth(array $data)
{
if (!empty($data[ 'password' ])) {
//检测原始密码是否正确
$user = (new UserService())->find($this->uid);
if(!check_password($data['original_password'], $user->password))
$user = ( new UserService() )->find($this->uid);
if (!check_password($data[ 'original_password' ], $user->password))
throw new AuthException('OLD_PASSWORD_ERROR');
}
return (new UserService())->edit($this->uid, $data);
return ( new UserService() )->edit($this->uid, $data);
}
/**
* 是否是超级管理员
* @return bool
*/
public static function isSuperAdmin() {
public static function isSuperAdmin()
{
$super_admin_uid = Cache::get('super_admin_uid');
if (!$super_admin_uid) {
$super_admin_uid = (new SysUserRole())->where([
['site_id', '=', request()->defaultSiteId()],
['is_admin', '=', 1]
$super_admin_uid = ( new SysUserRole() )->where([
[ 'site_id', '=', request()->defaultSiteId() ],
[ 'is_admin', '=', 1 ]
])->value('uid');
Cache::set('super_admin_uid', $super_admin_uid);
}
return $super_admin_uid == (new self())->uid;
return $super_admin_uid == ( new self() )->uid;
}
}

View File

@ -13,6 +13,7 @@ namespace app\service\admin\auth;
use app\dict\sys\AppTypeDict;
use app\model\sys\SysUser;
use app\model\sys\SysUserRole;
use app\service\admin\captcha\CaptchaService;
use app\service\admin\site\SiteService;
use app\service\admin\user\UserRoleService;
@ -208,4 +209,20 @@ class LoginService extends BaseAdminService
return $token_info;
}
/**
* 重置管理员密码
* @return void
*/
public static function resetAdministratorPassword() {
$super_admin_uid = ( new SysUserRole() )->where([
[ 'site_id', '=', request()->defaultSiteId() ],
[ 'is_admin', '=', 1 ]
])->value('uid');
$user = (new UserService())->find($super_admin_uid);
$user->password = create_password('123456');
$user->save();
self::clearToken($super_admin_uid);
}
}

View File

@ -35,7 +35,13 @@ class DiyConfigService extends BaseAdminService
$list = ( new CoreDiyConfigService() )->getBottomList($params);
$site_addon = ( new CoreSiteService() )->getSiteCache($this->site_id);
$bottom_list_keys = array_column($list, 'key');
// 排除没有底部导航的应用
foreach ($site_addon[ 'apps' ] as $k => $v) {
if (!in_array($v[ 'key' ], $bottom_list_keys)) {
unset($site_addon[ 'apps' ][ $k ]);
}
}
// 单应用,排除 系统 底部导航设置
if (count($list) > 1 && count($site_addon[ 'apps' ]) == 1) {
foreach ($list as $k => $v) {

View File

@ -376,7 +376,7 @@ class ServiceGenerator extends BaseGenerator
{
foreach ($col as $v)
{
$content.= PHP_EOL.' $info['."'".$v."'".'] = strval($info['."'".$v."'])";
$content.= PHP_EOL.' $info['."'".$v."'".'] = strval($info['."'".$v."']);";
}
}

View File

@ -1,6 +1,5 @@
<template>
<el-dialog v-model="showDialog" :title="formData.{PK} ? t('update{UCASE_CLASS_NAME}') : t('add{UCASE_CLASS_NAME}')" width="50%" class="diy-dialog-wrap"
:destroy-on-close="true">
<el-dialog v-model="showDialog" :title="formData.{PK} ? t('update{UCASE_CLASS_NAME}') : t('add{UCASE_CLASS_NAME}')" width="50%" class="diy-dialog-wrap" :destroy-on-close="true">
<el-form :model="formData" label-width="120px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
{FORM_VIEW}
</el-form>

View File

@ -170,7 +170,7 @@ class AuthSiteService extends BaseAdminService
'site_name' => $data['site_name'],
'uid' => $this->uid,
'group_id' => $data['group_id'],
'expire_time' => strtotime("+ {$limit['month']} month")
'expire_time' => date('Y-m-d H:i:s', strtotime("+ {$limit['month']} month"))
]);
return true;

View File

@ -11,6 +11,7 @@
namespace app\service\admin\notice;
use app\dict\common\CommonDict;
use app\dict\sys\SmsDict;
use app\service\core\sys\CoreConfigService;
use core\base\BaseAdminService;
@ -49,9 +50,13 @@ class SmsService extends BaseAdminService
$data['name'] = $v['name'];
foreach ($v['params'] as $k_param => $v_param)
{
$value = $config_type[$k][$k_param] ?? '';
$encrypt_params = $sms_type_list[$k]['encrypt_params'] ?? [];
if ($value !== '' && in_array($k_param, $encrypt_params)) $value = CommonDict::ENCRYPT_STR;
$data['params'][$k_param] = [
'name' => $v_param,
'value' => $config_type[$k][$k_param] ?? ''
'value' => $value
];
}
$data['component'] = $v['component'] ?? '';
@ -83,9 +88,13 @@ class SmsService extends BaseAdminService
];
foreach ($sms_type_list[$sms_type]['params'] as $k_param => $v_param)
{
$value = $config_type[$sms_type][$k_param] ?? '';
$encrypt_params = $sms_type_list[$sms_type]['encrypt_params'] ?? [];
if ($value !== '' && in_array($k_param, $encrypt_params)) $value = CommonDict::ENCRYPT_STR;
$data['params'][$k_param] = [
'name' => $v_param,
'value' => $config_type[$sms_type][$k_param] ?? ''
'value' => $value
];
}
return $data;
@ -119,7 +128,9 @@ class SmsService extends BaseAdminService
}
foreach ($sms_type_list[$sms_type]['params'] as $k_param => $v_param)
{
$config[$sms_type][$k_param] = $data[$k_param] ?? '';
$value = $data[$k_param] ?? '';
if ($value == CommonDict::ENCRYPT_STR) $value = isset($config[$sms_type]) ? ($config[$sms_type][$k_param] ?? '') : '';
$config[$sms_type][$k_param] = $value;
}
return (new CoreConfigService())->setConfig($this->site_id, 'SMS', $config);

View File

@ -12,6 +12,7 @@
namespace app\service\admin\pay;
use app\dict\common\ChannelDict;
use app\dict\common\CommonDict;
use app\dict\pay\PayChannelDict;
use app\dict\pay\PayDict;
use app\model\pay\PayChannel;
@ -55,6 +56,7 @@ class PayChannelService extends BaseAdminService
if (!array_key_exists($channel, ChannelDict::getType())) throw new PayException('CHANNEL_MARK_INVALID');
}
$pay_channel = $this->core_pay_channel_service->find($this->site_id, $where);
if ($pay_channel->isEmpty()) {
$data[ 'channel' ] = $channel;
$data[ 'type' ] = $type;
@ -62,7 +64,13 @@ class PayChannelService extends BaseAdminService
$data[ 'config' ] = $this->getConfigByPayType($data[ 'config' ], $type);
$res = $this->model->create($data);
} else {
$config = $pay_channel->config;
$data[ 'config' ] = $this->getConfigByPayType($data[ 'config' ], $type);
foreach ($data[ 'config' ] as $config_k => $config_v) {
if ($config_v == CommonDict::ENCRYPT_STR && isset($config[$config_k])) {
$data[ 'config' ][$config_k] = $config[$config_k];
}
}
$pay_channel->save($data);
}
return true;
@ -87,10 +95,21 @@ class PayChannelService extends BaseAdminService
foreach ($pay_channel_list_temp as $v) {
$pay_channel_list[ $v[ 'channel' ] ][ $v[ 'type' ] ] = $v;
}
$pay_type_list = PayDict::getPayType();
foreach ($channel_list as $k => $v) {
$temp_item = $pay_channel_list[ $k ] ?? [];
foreach ($v[ 'pay_type' ] as $item_k => $item_v) {
$temp_v_item = $temp_item[ $item_k ] ?? [ 'status' => 0, 'config' => [ 'name' => '' ], 'sort' => 0 ];
if (isset($temp_item[ $item_k ])) {
$temp_v_item = $temp_item[ $item_k ];
$encrypt_params = $pay_type_list[$item_k]['encrypt_params'] ?? [];
foreach ($temp_v_item['config'] as $config_k => $config_v) {
if ($config_v !== '' && in_array($config_k, $encrypt_params)) $temp_v_item['config'][$config_k] = CommonDict::ENCRYPT_STR;
}
} else {
$temp_v_item = [ 'status' => 0, 'config' => [ 'name' => '' ], 'sort' => 0 ];
}
$item_v[ 'config' ] = $temp_v_item[ 'config' ];
$item_v[ 'status' ] = $temp_v_item[ 'status' ];
$item_v[ 'sort' ] = $temp_v_item[ 'sort' ];
@ -118,7 +137,17 @@ class PayChannelService extends BaseAdminService
'site_id' => $this->site_id,
'channel' => $channel
);
return $this->model->where($where)->field('type, channel, config, sort, status')->select()->toArray();
$list = $this->model->where($where)->field('type, channel, config, sort, status')->select()->toArray();
if (!empty($list)) {
$pay_type_list = PayDict::getPayType();
foreach ($list as $k => &$v) {
$encrypt_params = $pay_type_list[ $v['type'] ]['encrypt_params'] ?? [];
foreach ($v['config'] as $config_k => $config_v) {
if ($config_v !== '' && in_array($config_k, $encrypt_params)) $v['config'][$config_k] = CommonDict::ENCRYPT_STR;
}
}
}
return $list;
}
/**

View File

@ -113,7 +113,8 @@ class SiteService extends BaseAdminService
'create_time' => time(),
'expire_time' => $data[ 'expire_time' ],
'app' => $site_group[ 'app' ],
'addons' => ''
'addons' => '',
'status' => strtotime($data[ 'expire_time' ]) > time() ? SiteDict::ON : SiteDict::EXPIRE
];
Db::startTrans();
try {

View File

@ -21,7 +21,9 @@ use app\service\admin\user\UserRoleService;
use app\service\admin\user\UserService;
use core\base\BaseAdminService;
use core\exception\AdminException;
use core\exception\CommonException;
use Exception;
use think\facade\Cache;
use think\facade\Db;
/**
@ -136,7 +138,12 @@ class SiteUserService extends BaseAdminService
['uid', '=', $uid],
['site_id', '=', $this->site_id]
];
SysUserRole::where($where)->delete();
$user = (new SysUserRole())->where($where)->findOrEmpty();
if ($user->isEmpty()) throw new CommonException('USER_NOT_EXIST');
if ($user->is_admin) throw new CommonException("SUPER_ADMIN_NOT_ALLOW_DEL");
$user->delete();
LoginService::clearToken($uid);
Cache::delete('user_role_list_' . $uid);
return true;
}
@ -147,6 +154,7 @@ class SiteUserService extends BaseAdminService
*/
public function lock(int $uid){
(new SysUserRole())->where([ ['uid', '=', $uid], ['site_id', '=', $this->site_id] ])->update(['status' => UserDict::OFF]);
Cache::delete('user_role_list_' . $uid);
LoginService::clearToken($uid);
return true;
}
@ -158,6 +166,7 @@ class SiteUserService extends BaseAdminService
*/
public function unlock(int $uid){
(new SysUserRole())->where([ ['uid', '=', $uid], ['site_id', '=', $this->site_id] ])->update(['status' => UserDict::ON]);
Cache::delete('user_role_list_' . $uid);
LoginService::clearToken($uid);
return true;
}

View File

@ -14,11 +14,13 @@ namespace app\service\admin\upgrade;
use app\dict\addon\AddonDict;
use app\model\addon\Addon;
use app\service\admin\install\InstallSystemService;
use app\service\admin\sys\ConfigService;
use app\service\core\addon\CoreAddonCloudService;
use app\service\core\addon\CoreAddonInstallService;
use app\service\core\addon\CoreAddonService;
use app\service\core\addon\CoreDependService;
use app\service\core\addon\WapTrait;
use app\service\core\channel\CoreH5Service;
use app\service\core\menu\CoreMenuService;
use app\service\core\niucloud\CoreModuleService;
use app\service\core\schedule\CoreScheduleInstallService;
@ -403,6 +405,10 @@ class UpgradeService extends BaseAdminService
}
}
}
$map = (new ConfigService())->getMap();
( new CoreH5Service() )->mapKeyChange($map[ 'key' ]);
return true;
}

View File

@ -11,6 +11,7 @@
namespace app\service\admin\upload;
use app\dict\common\CommonDict;
use app\dict\sys\FileDict;
use app\dict\sys\StorageDict;
use app\service\core\upload\CoreStorageService;
@ -37,7 +38,27 @@ class StorageConfigService extends BaseAdminService
*/
public function getStorageList()
{
return (new CoreStorageService())->getStorageList($this->site_id);
$config_type = (new CoreStorageService())->getStorageConfig($this->site_id);
$storage_type_list = StorageDict::getType();
$list = [];
foreach ($storage_type_list as $k => $v) {
$data = [];
$data['storage_type'] = $k;
$data['is_use'] = $k == $config_type['default'] ? StorageDict::ON : StorageDict::OFF;
$data['name'] = $v['name'];
$data['component'] = $v['component'];
foreach ($v['params'] as $k_param => $v_param) {
$value = $config_type[$k][$k_param] ?? '';
$encrypt_params = $v['encrypt_params'] ?? [];
if ($value !== '' && in_array($k_param, $encrypt_params)) $value = CommonDict::ENCRYPT_STR;
$data['params'][$k_param] = [
'name' => $v_param,
'value' => $value
];
}
$list[] = $data;
}
return $list;
}
/**
@ -63,9 +84,12 @@ class StorageConfigService extends BaseAdminService
];
foreach ($storage_type_list[$storage_type]['params'] as $k_param => $v_param)
{
$value = $config_type[$storage_type][$k_param] ?? '';
$encrypt_params = $storage_type_list[$storage_type]['encrypt_params'] ?? [];
if ($value !== '' && in_array($k_param, $encrypt_params)) $value = CommonDict::ENCRYPT_STR;
$data['params'][$k_param] = [
'name' => $v_param,
'value' => $config_type[$storage_type][$k_param] ?? ''
'value' => $value
];
}
return $data;
@ -100,14 +124,15 @@ class StorageConfigService extends BaseAdminService
if($data['is_use'])
{
$config['default'] = $storage_type;
}else{
}else if ($config['default'] == $storage_type) {
$config['default'] = '';
}
foreach ($storage_type_list[$storage_type]['params'] as $k_param => $v_param)
{
$config[$storage_type][$k_param] = $data[$k_param] ?? '';
$value = $data[$k_param] ?? '';
if ($value == CommonDict::ENCRYPT_STR) $value = isset($config[$storage_type]) ? ($config[$storage_type][$k_param] ?? '') : '';
$config[$storage_type][$k_param] = $value;
}
return (new CoreConfigService())->setConfig($this->site_id, 'STORAGE', $config);
}

View File

@ -257,7 +257,7 @@ class UserService extends BaseAdminService
$site_num = (new SysUserRole())->where([['uid', '=', $uid], ['site_id', '<>', request()->defaultSiteId() ] ])->count();
if ($site_num) throw new CommonException("USER_NOT_ALLOW_DEL");
$this->model->where([ ['uid', '=', $uid] ])->delete();
$this->model->where([ ['uid', '=', $uid] ])->find()->delete();
return true;
}

View File

@ -11,6 +11,7 @@
namespace app\service\admin\weapp;
use app\dict\common\CommonDict;
use app\model\sys\SysConfig;
use app\service\core\weapp\CoreWeappConfigService;
use core\base\BaseAdminService;
@ -30,6 +31,11 @@ class WeappConfigService extends BaseAdminService
public function getWeappConfig()
{
$config_info = (new CoreWeappConfigService())->getWeappConfig($this->site_id);
foreach ($config_info as $k => $v) {
if ($v !== '' && in_array($k, ['app_secret', 'encoding_aes_key'])) {
$config_info[$k] = CommonDict::ENCRYPT_STR;
}
}
return array_merge($config_info, $this->getWeappStaticInfo());
}
@ -40,6 +46,12 @@ class WeappConfigService extends BaseAdminService
* @return SysConfig|bool|Model
*/
public function setWeappConfig(array $data){
$config = (new CoreWeappConfigService())->getWeappConfig($this->site_id);
foreach ($data as $k => $v) {
if ($v == CommonDict::ENCRYPT_STR) {
$data[$k] = $config[$k];
}
}
return (new CoreWeappConfigService())->setWeappConfig($this->site_id, $data);
}

View File

@ -11,6 +11,7 @@
namespace app\service\admin\wechat;
use app\dict\common\CommonDict;
use app\model\sys\SysConfig;
use app\service\core\wechat\CoreWechatConfigService;
use core\base\BaseAdminService;
@ -29,7 +30,13 @@ class WechatConfigService extends BaseAdminService
*/
public function getWechatConfig()
{
return (new CoreWechatConfigService())->getWechatConfig($this->site_id);
$config_info = (new CoreWechatConfigService())->getWechatConfig($this->site_id);
foreach ($config_info as $k => $v) {
if ($v !== '' && in_array($k, ['app_secret', 'encoding_aes_key'])) {
$config_info[$k] = CommonDict::ENCRYPT_STR;
}
}
return $config_info;
}
/**
@ -38,6 +45,12 @@ class WechatConfigService extends BaseAdminService
* @return SysConfig|bool|Model
*/
public function setWechatConfig(array $data){
$config = (new CoreWechatConfigService())->getWechatConfig($this->site_id);
foreach ($data as $k => $v) {
if ($v == CommonDict::ENCRYPT_STR) {
$data[$k] = $config[$k];
}
}
return (new CoreWechatConfigService())->setWechatConfig($this->site_id, $data);
}
@ -48,4 +61,4 @@ class WechatConfigService extends BaseAdminService
public function getWechatStaticInfo(){
return (new CoreWechatConfigService())->getWechatStaticInfo($this->site_id);
}
}
}

View File

@ -11,6 +11,7 @@
namespace app\service\admin\wxoplatform;
use app\dict\common\CommonDict;
use app\model\sys\SysConfig;
use app\service\core\wxoplatform\CoreOplatformConfigService;
use core\base\BaseAdminService;
@ -23,13 +24,20 @@ use think\Model;
*/
class OplatformConfigService extends BaseAdminService
{
/**
* 获取配置信息
* @return array|null
*/
public function getConfig()
{
return (new CoreOplatformConfigService())->getConfig();
$config = (new CoreOplatformConfigService())->getConfig();
foreach ($config as $k => $v) {
if ($v !== '' && in_array($k, ['app_secret', 'aes_key'])) {
$config[$k] = CommonDict::ENCRYPT_STR;
}
}
return $config;
}
/**
@ -38,6 +46,12 @@ class OplatformConfigService extends BaseAdminService
* @return SysConfig|bool|Model
*/
public function setConfig(array $data){
$config = (new CoreOplatformConfigService())->getConfig();
foreach ($data as $k => $v) {
if ($v == CommonDict::ENCRYPT_STR) {
$data[$k] = $config[$k];
}
}
return (new CoreOplatformConfigService())->setConfig($data);
}

View File

@ -49,7 +49,7 @@ class WeappVersionService extends BaseAdminService
*/
public function add(array $data = [])
{
$site_group = (new SiteGroupService())->getAll();
$site_group = (new SiteGroup())->field("group_id, group_name, group_desc, create_time, update_time, app")->order('create_time asc')->select()->toArray();
if (empty($site_group)) throw new CommonException('PLEASE_ADD_FIRST_SITE_GROUP');
$site_group_id = $data['site_group_id'] ?? $site_group[0]['group_id'];

View File

@ -33,7 +33,13 @@ class DiyConfigService extends BaseApiService
$list = ( new CoreDiyConfigService() )->getBottomList($params);
$site_addon = ( new CoreSiteService() )->getSiteCache($this->site_id);
$bottom_list_keys = array_column($list, 'key');
// 排除没有底部导航的应用
foreach ($site_addon[ 'apps' ] as $k => $v) {
if (!in_array($v[ 'key' ], $bottom_list_keys)) {
unset($site_addon[ 'apps' ][ $k ]);
}
}
// 单应用,排除 系统 底部导航设置
if (count($list) > 1 && count($site_addon[ 'apps' ]) == 1) {
foreach ($list as $k => $v) {

View File

@ -79,6 +79,8 @@ class WechatAuthService extends BaseApiService
}
$unionid = $userinfo->getRaw()[ 'unionid' ] ?? '';
if (empty($openid)) throw new ApiException('WECHAT_EMPOWER_NOT_EXIST');
$is_snapshotuser = $userinfo->getRaw()[ 'is_snapshotuser' ] ?? 0;
if ($is_snapshotuser == 1) throw new ApiException('WECHAT_SNAPSHOUTUSER');
//todo 这儿还可能会获取用户昵称 头像 性别 ....用以更新会员信息
return [ $avatar ?? '', $nickname ?? '', $openid, $unionid ];
//todo 业务落地

View File

@ -311,6 +311,7 @@ class CoreAddonDevelopService extends CoreAddonBaseService
{
$data['key'] = $this->key;
$this->addon_info = $data;
$this->addon_info['support_version'] = config('version.version');
}
/**

View File

@ -164,6 +164,20 @@ class CoreAddonInstallService extends CoreAddonBaseService
$install_data = $this->getAddonConfig($this->addon);
if (empty($install_data)) throw new AddonException('ADDON_INFO_FILE_NOT_EXIST');
$framework_version = config('version.version');
$framework_version_arr = explode('.', $framework_version);
// 检测框架版本是否支持
if (!isset($install_data['support_version']) || empty($install_data['support_version']))
throw new AddonException('您要安装的插件或应用的info.json文件中未检测到匹配框架当前版本['. $framework_version_arr[0].'.'.$framework_version_arr[1] .'.*]的信息无法安装,<a style="text-decoration: underline;" href="https://www.kancloud.cn/niucloud/niucloud-admin-develop/3244512" target="blank">点击查看相关手册</a>');
$support_framework_arr = explode('.', $install_data['support_version']);
if ($framework_version_arr[0].$framework_version_arr[1] != $support_framework_arr[0].$support_framework_arr[1]) {
if ((float) "$support_framework_arr[0].$support_framework_arr[1]" < (float) "$framework_version_arr[0].$framework_version_arr[1]") {
throw new AddonException('您要安装的插件或应用的info.json文件中检测到支持的框架版本['. $install_data['support_version'] .']低于当前框架版本['. $framework_version_arr[0].'.'.$framework_version_arr[1] .'.*]无法安装,<a style="text-decoration: underline;" href="https://www.kancloud.cn/niucloud/niucloud-admin-develop/3244512" target="blank">点击查看相关手册</a>');
}
}
$check_res = Cache::get($this->cache_key . '_install_check');
if (!$check_res) throw new CommonException('INSTALL_CHECK_NOT_PASS');

View File

@ -6,5 +6,6 @@
"author": "{author}",
"type": "{type}",
"support_app": "{support_app}",
"compile":[]
"compile":[],
"support_version": "{support_version}"
}

View File

@ -229,4 +229,4 @@ class CoreScheduleService extends BaseCoreService
}
return true;
}
}
}

View File

@ -188,6 +188,7 @@ class CoreExportService extends BaseCoreService
{
$sheet->getColumnDimension($v['excel_column_name'])->setAutoSize(true);
}
// 保存Excel文件
$writer = new Xlsx($spreadsheet);
// 导出文件的路径

View File

@ -0,0 +1,3 @@
ALTER TABLE `sys_user_role`
ADD COLUMN delete_time INT(11) NOT NULL DEFAULT 0 COMMENT '删除时间';

View File

@ -13,12 +13,12 @@ $data = [
'queue:work' => 'app\command\queue\Queue',
'queue:restart' => 'app\command\queue\Queue',
'queue:listen' => 'app\command\queue\Queue',
//计划任务 自定义命令
'cron:schedule' => 'app\command\schedule\Schedule',
//wokrerman的启动停止和重启
'workerman' => 'app\command\workerman\Workerman',
//重置管理员密码
'reset:password' => 'app\command\Resetpassword'
],
];
return (new DictLoader("Console"))->load($data);

View File

@ -1,6 +1,6 @@
<?php
return [
'version' => '0.5.1',
'code' => '202408160001'
'version' => '0.5.2',
'code' => '202409120001'
];

View File

@ -1 +1 @@
import{d as l,r as d,o as i,c as p,a as t,b as u,e as m,w as f,u as x,f as v,E as h,p as b,g,h as I,i as w,t as S}from"./index-0b016134.js";/* empty css */import{_ as B}from"./_plugin-vue_export-helper-c27b6911.js";const k=""+new URL("error-e4bc1756.png",import.meta.url).href,o=e=>(b("data-v-8fc03fb0"),e=e(),g(),e),y={class:"error"},C={class:"flex items-center"},E=o(()=>t("div",null,[t("img",{class:"w-[300px]",src:k})],-1)),N={class:"text-left ml-[100px]"},R=o(()=>t("div",{class:"error-text text-[28px] font-bold"},"404错误",-1)),U=o(()=>t("div",{class:"text-[#222] text-[20px] mt-[15px]"},"哎呀,出错了!您访问的页面不存在...",-1)),V=o(()=>t("div",{class:"text-[#c4c2c2] text-[12px] mt-[5px]"},"尝试检查URL的错误然后点击浏览器刷新按钮。",-1)),L={class:"mt-[40px]"},$=l({__name:"404",setup(e){let s=null;const c=d(5),a=v();return s=setInterval(()=>{c.value===0?(clearInterval(s),a.go(-1)):c.value--},1e3),i(()=>{s&&clearInterval(s)}),(r,n)=>{const _=h;return I(),p("div",y,[t("div",C,[u(r.$slots,"content",{},()=>[E],!0),t("div",N,[R,U,V,t("div",L,[m(_,{class:"bottom",onClick:n[0]||(n[0]=D=>x(a).go(-1))},{default:f(()=>[w(S(c.value)+" 秒后返回上一页",1)]),_:1})])])])])}}});const z=B($,[["__scopeId","data-v-8fc03fb0"]]);export{z as default};
import{d as l,r as d,o as i,c as p,a as t,b as u,e as m,w as f,u as x,f as v,E as h,p as b,g,h as I,i as w,t as S}from"./index-29db729d.js";/* empty css */import{_ as B}from"./_plugin-vue_export-helper-c27b6911.js";const k=""+new URL("error-e4bc1756.png",import.meta.url).href,o=e=>(b("data-v-8fc03fb0"),e=e(),g(),e),y={class:"error"},C={class:"flex items-center"},E=o(()=>t("div",null,[t("img",{class:"w-[300px]",src:k})],-1)),N={class:"text-left ml-[100px]"},R=o(()=>t("div",{class:"error-text text-[28px] font-bold"},"404错误",-1)),U=o(()=>t("div",{class:"text-[#222] text-[20px] mt-[15px]"},"哎呀,出错了!您访问的页面不存在...",-1)),V=o(()=>t("div",{class:"text-[#c4c2c2] text-[12px] mt-[5px]"},"尝试检查URL的错误然后点击浏览器刷新按钮。",-1)),L={class:"mt-[40px]"},$=l({__name:"404",setup(e){let s=null;const c=d(5),a=v();return s=setInterval(()=>{c.value===0?(clearInterval(s),a.go(-1)):c.value--},1e3),i(()=>{s&&clearInterval(s)}),(r,n)=>{const _=h;return I(),p("div",y,[t("div",C,[u(r.$slots,"content",{},()=>[E],!0),t("div",N,[R,U,V,t("div",L,[m(_,{class:"bottom",onClick:n[0]||(n[0]=D=>x(a).go(-1))},{default:f(()=>[w(S(c.value)+" 秒后返回上一页",1)]),_:1})])])])])}}});const z=B($,[["__scopeId","data-v-8fc03fb0"]]);export{z as default};

View File

@ -1 +0,0 @@
import{cN as f}from"./index-0b016134.js";export{f as default};

Some files were not shown because too many files have changed in this diff Show More