This commit is contained in:
全栈小学生 2025-06-11 09:17:09 +08:00
parent 52b112670f
commit fa49aade7a
57 changed files with 7217 additions and 3292 deletions

View File

@ -98,3 +98,7 @@ export function getInstalledAddonList() {
export function getShowApp() {
return request.get('addon/list/showapp')
}
export function getAddonInit() {
return request.get('addon/init')
}

View File

@ -78,3 +78,220 @@ export function editSms(params: Record<string, any>) {
export function getSmsLog(params: Record<string, any>) {
return request.get(`notice/sms/log`, { params })
}
/**
*
* @param params
*/
export function getAccountIsLogin() {
return request.get(`notice/niusms/config`)
}
/**
*
* @param params
*/
export function loginAccount(params: Record<string, any>) {
return request.post(`notice/niusms/account/login`,params,{ showSuccessMessage: true })
}
/**
*
* @param params
*/
export function registerAccount(params: Record<string, any>) {
return request.post(`notice/niusms/account/register`,params,{ showSuccessMessage: true })
}
/**
*
* @param params
*/
export function getAccountInfo(username: string) {
return request.get(`notice/niusms/account/info/${username}`)
}
/**
*
* @param params
*/
export function getTemplateList(params: Record<string, any>) {
return request.get(`notice/niusms/template/list/${params.sms_type}/${params.username}`,{})
}
/**
*
* @param params
*/
export function getSignList(username: string, params: Record<string, any>) {
return request.get(`notice/niusms/sign/list/${username}`,{params})
}
/**
*
* @param params
*/
export function addSign(username: string, params: Record<string, any>) {
return request.post(`notice/niusms/sign/report/${username}`, params, { showSuccessMessage: true });
}
/**
*
* @param params
*/
export function deleteSign(username: string, params: Record<string, any>) {
return request.post(`notice/niusms/sign/delete/${username}`, params, { showSuccessMessage: true });
}
/**
*
* @param params
*/
export function editAccount(username: string,params: Record<string, any>) {
return request.post(`notice/niusms/account/edit/${username}`, params, { showSuccessMessage: true });
}
/**
*
* @param params
*/
export function getSmsSendList(username: string, params: Record<string, any>) {
return request.get(`notice/niusms/account/send_list/${username}`,{params})
}
/**
*
* @param params
*/
export function getSmsOrdersList(username: string, params: Record<string, any>) {
return request.get(`notice/niusms/order/list/${username}`,{params})
}
/**
*
* @param params
*/
export function getSmsPackagesList() {
return request.get(`notice/niusms/packages`)
}
/**
*
* @param params
*/
export function getSmsCaptcha() {
return request.get(`notice/niusms/captcha`)
}
/**
*
* @param params
*/
export function getSmsSend(params: Record<string, any>) {
return request.post(`notice/niusms/send`,params,{ showSuccessMessage: true })
}
/**
*
* @param params
*/
export function getSmsSignConfig() {
return request.get(`notice/niusms/sign/report/config`)
}
/**
*
* @param params
*/
export function getTemplateReportConfig() {
return request.get(`notice/niusms/template/report/config`)
}
/**
*
* @param params
*/
export function reportTemplate(sms_type: string, username: string, params: Record<string, any>) {
return request.post(`notice/niusms/template/report/${sms_type}/${username}`,params,{ showSuccessMessage: true })
}
/**
*
* @param params
*/
export function getreportTemplateInfo(sms_type: string, username: string,params: Record<string, any>) {
return request.get(`notice/niusms/template/info/${sms_type}/${username}`,{params})
}
/**
*
* @param params
*/
export function smsOrderCreate(username: string, params: Record<string, any>) {
return request.post(`notice/niusms/order/create/${username}`, params)
}
/**
*
* @param params
*/
export function getOrderPayInfo(username: string, params: Record<string, any>) {
return request.get(`notice/niusms/order/pay/${username}`, {params})
}
/**
*
* @param params
*/
export function getOrderInfo(username: string, params: Record<string, any>) {
return request.get(`notice/niusms/order/info/${username}`, {params})
}
/**
*
* @param params
*/
export function getOrderPayStatus(username: string, params: Record<string, any>) {
return request.get(`notice/niusms/order/status/${username}`, {params})
}
/**
*
* @param params
*/
export function calculateOrderPay(username: string, params: Record<string, any>) {
return request.post(`notice/niusms/order/calculate/${username}`, params)
}
/**
*
* @param params
*/
export function enableNiusms(params: Record<string, any>) {
return request.put(`notice/niusms/enable`,params,{ showSuccessMessage: true })
}
/**
*
* @param params
*/
export function templateSync(sms_type: string, username: string) {
return request.get(`notice/niusms/template/sync/${sms_type}/${username}`)
}
/**
*
* @param params
*/
export function resetPassword(username: string,params: Record<string, any>) {
return request.post(`notice/niusms/account/reset/password/${username}`,params,{ showSuccessMessage: true})
}
/**
*
* @param params
*/
export function clearTemplate(username: string,template_id: string) {
return request.delete(`notice/niusms/template/${username}/${template_id}`)
}

View File

@ -258,24 +258,6 @@ export function moveAttachment(params: Record<string, any>) {
return request.put(`sys/attachment/batchmove`, params)
}
/**
*
* @param params
* @returns
*/
export function getIconCategoryList(params: Record<string, any>) {
return request.get(`sys/attachment/icon_category`, { params })
}
/**
*
* @param params
* @returns
*/
export function getIconList(params: Record<string, any>) {
return request.get(`sys/attachment/icon`, { params })
}
/**
* evn
* @returns

View File

@ -62,6 +62,15 @@ export function unlockUser(uid: number) {
return request.put(`user/unlock/${ uid }`, {}, { showSuccessMessage: true })
}
/**
*
*
* @param uid
*/
export function deleteUser(uid: number) {
return request.delete(`user/${ uid }`)
}
/***************************************************** 操作日志 **************************************************/
/**
@ -80,3 +89,11 @@ export function getLogList(params: Record<string, any>) {
export function getLogInfo(id: number) {
return request.get(`user/userlog/${ id }`)
}
/**
*
* @returns
*/
export function logDestroy() {
return request.delete(`user/userlog/destroy`)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -1,68 +1,108 @@
<template>
<el-dialog v-model="showDialog" :title="t('cloudbuild.title')" width="850px" :close-on-click-modal="false" :close-on-press-escape="false" :before-close="dialogClose">
<div v-if="active == 'build'" class="h-[60vh]" v-loading="loading">
<div class="h-[60vh] flex flex-col" v-if="cloudBuildCheck && !cloudBuildTask">
<el-scrollbar>
<div v-show="active == 'build'" class="h-[50vh]" v-loading="loading">
<div class="h-[50vh] flex flex-col" v-if="cloudBuildCheck && !cloudBuildTask">
<!-- <el-scrollbar> -->
<div class="bg-[#fff] my-3" v-if="cloudBuildCheck.dir">
<p class="pt-[20px] pl-[20px] ">{{ t('cloudbuild.dirPermission') }}</p>
<div class="">
<p class="pl-[20px] ">{{ t('cloudbuild.dirPermission') }}</p>
<div class="mt-[10px] mx-[20px] text-[14px] cursor-pointer text-primary flex items-center justify-between bg-[#EFF6FF] rounded-[4px] p-[10px]" @click="cloudBuildCheckDirFn">
<div class="flex items-center">
<el-icon :size="17"><QuestionFilled /></el-icon>
<span class="ml-[5px] leading-[20px]">编译权限错误查看解决方案</span></div>
<div class="border-[1px] border-primary rounded-[3px] w-[72px] h-[26px] leading-[25px] text-center">立即查看</div>
</div>
</div>
<div class="px-[20px] pt-[10px] text-[14px] el-table">
<el-row class="py-[10px] items table-head-bg pl-[15px] mb-[10px]">
<el-col :span="12">
<el-col :span="18">
<span>{{ t('cloudbuild.path') }}</span>
</el-col>
<el-col :span="6">
<el-col :span="3">
<span>{{ t('cloudbuild.demand') }}</span>
</el-col>
<el-col :span="6">
<el-col :span="3">
<span>{{ t('status') }}</span>
</el-col>
</el-row>
<el-row class="pb-[10px] items pl-[15px]" v-for="item in cloudBuildCheck.dir.is_readable">
<el-col :span="12">
<span>{{ item.dir }}</span>
</el-col>
<el-col :span="6">
<span>{{ t('cloudbuild.readable') }}</span>
</el-col>
<el-col :span="6">
<span v-if="item.status"><el-icon color="green"><Select /></el-icon></span>
<span v-else>
<el-scrollbar style="height: calc(300px); overflow: auto">
<el-row class="pb-[10px] items pl-[15px]" v-for="item in cloudBuildCheck.dir.is_readable">
<el-col :span="18">
<span>{{ item.dir }}</span>
</el-col>
<el-col :span="3">
<span>{{ t('cloudbuild.readable') }}</span>
</el-col>
<el-col :span="3">
<span v-if="item.status"><el-icon color="green"><Select /></el-icon></span>
<span v-else>
<el-icon color="red">
<CloseBold />
</el-icon>
</span>
</el-col>
</el-row>
<el-row class="pb-[10px] items pl-[15px]" v-for="item in cloudBuildCheck.dir.is_write">
<el-col :span="18">
<span>{{ item.dir }}</span>
</el-col>
<el-col :span="3">
<span>{{ t('cloudbuild.write') }}</span>
</el-col>
<el-col :span="3" >
<span v-if="item.status"><el-icon color="green"><Select /></el-icon></span>
<span v-else>
<el-icon color="red">
<CloseBold />
</el-icon>
</span>
</el-col>
</el-row>
<el-row class="pb-[10px] items pl-[15px]" v-for="item in cloudBuildCheck.dir.is_write">
<el-col :span="12">
<span>{{ item.dir }}</span>
</el-col>
<el-col :span="6">
<span>{{ t('cloudbuild.write') }}</span>
</el-col>
<el-col :span="6">
<span v-if="item.status"><el-icon color="green"><Select /></el-icon></span>
<span v-else>
<el-icon color="red">
<CloseBold />
</el-icon>
</span>
</el-col>
</el-row>
</el-col>
</el-row>
</el-scrollbar>
</div>
</div>
</el-scrollbar>
<!-- </el-scrollbar> -->
</div>
<div class="h-[60vh]" v-show="cloudBuildTask">
<div class="h-[45vh]" v-show="cloudBuildTask">
<terminal ref="terminalRef" context="" :init-log="null" :show-header="false" :show-log-time="true" @exec-cmd="onExecCmd"/>
</div>
<div class="flex justify-end mt-[20px]" v-show="cloudBuildTask">
<el-button @click="dialogCancel()" class="!w-[90px]">取消</el-button>
<el-button type="primary" :loading="timeloading" class="!w-[140px]">已用时 {{ formattedDuration }}</el-button>
</div>
</div>
<div v-if="active == 'complete'">
<div class="h-[60vh] flex flex-col">
<div class="flex-1 h-0">
<el-result icon="success" :title="t('cloudbuild.cloudbuildSuccess')"></el-result>
<div v-show="active == 'error'">
<div class="h-[50vh] flex flex-col">
<div class="flex-1 h-0 flex justify-center items-center flex-col">
<el-result icon="error" :title="t('编译失败')">
<template #icon>
<img src="@/app/assets/images/error_icon.png" alt="">
</template>
<template #extra>
<el-scrollbar class="max-h-[150px] overflow-auto text-[15px] text-[#4F516D] mb-[15px] mt-[-15px]">
{{errorInfo}}
</el-scrollbar>
<el-button @click="handleReturn" class="!w-[90px]">错误信息</el-button>
<el-button @click="showDialog=false" type="primary" class="!w-[90px]">完成</el-button>
</template>
</el-result>
</div>
</div>
</div>
<div v-show="active == 'complete'">
<div class="h-[50vh] flex flex-col">
<div class="flex-1 h-0 flex justify-center items-center flex-col">
<el-result icon="success" :title="t('cloudbuild.cloudbuildSuccess')" :sub-title="`编译耗时${formattedDuration},成功编译完成。`">
<template #icon>
<img src="@/app/assets/images/success_icon.png" alt="">
</template>
<template #extra>
<el-button @click="handleReturn" class="!w-[90px]">返回</el-button>
<el-button @click="showDialog=false" type="primary" class="!w-[90px]">完成</el-button>
</template>
</el-result>
</div>
</div>
</div>
@ -70,7 +110,7 @@
</template>
<script lang="ts" setup>
import { ref, h, watch } from 'vue'
import { ref, h, watch ,computed} from 'vue'
import { t } from '@/lang'
import { getCloudBuildLog, getCloudBuildTask, cloudBuild, clearCloudBuildTask, preBuildCheck } from '@/app/api/cloud'
import { Terminal, TerminalFlash } from 'vue-web-terminal'
@ -86,6 +126,18 @@ const loading = ref(false)
const terminalRef = ref(null)
let cloudBuildLog = []
//
const buildStartTime = ref<number | null>(null)
const buildDuration = ref<number>(0)
let buildTimer: number | null = null
const formattedDuration = computed(() => {
const seconds = buildDuration.value
const mins = Math.floor(seconds / 60)
const secs = seconds % 60
return mins > 0 ? `${mins}${secs}` : `${secs}`
})
/**
* 查询升级任务
*/
@ -96,22 +148,29 @@ const getCloudBuildTaskFn = () => {
cloudBuildTask.value = data
if (!showDialog.value) {
showElNotification()
// showElNotification()
localStorage.setItem('cloud_build_task', 'true')
}
}).catch()
}
getCloudBuildTaskFn()
const errorInfo = ref('')
const timeloading = ref(false)
const getCloudBuildLogFn = () => {
timeloading.value = true
getCloudBuildLog().then(res => {
if (!res.data) {
if (showDialog.value && cloudBuildLog.length) {
active.value = 'complete'
timeloading.value = false
terminalRef.value.execute('clear')
clearCloudBuildTask()
buildTimer && clearInterval(buildTimer) //
localStorage.removeItem('cloud_build_start_time')
localStorage.removeItem('cloud_build_task')
}
notificationEl && notificationEl.close()
cloudBuildTask.value = null
// cloudBuildTask.value = null
return
}
@ -120,6 +179,22 @@ const getCloudBuildLogFn = () => {
if (data[0] && data[0].length && showDialog.value) {
if (cloudBuildLog.length == 0) {
const storedTime = localStorage.getItem('cloud_build_start_time')
if (storedTime) {
buildStartTime.value = Number(storedTime)
} else {
const now = Date.now()
buildStartTime.value = now
localStorage.setItem('cloud_build_start_time', String(now))
}
buildDuration.value = Math.floor((Date.now() - buildStartTime.value) / 1000)
buildTimer && clearInterval(buildTimer)
buildTimer = setInterval(() => {
if (buildStartTime.value) {
buildDuration.value = Math.floor((Date.now() - buildStartTime.value) / 1000)
}
}, 1000)
terminalRef.value.execute('clear')
terminalRef.value.execute('开始编译')
}
@ -128,10 +203,27 @@ const getCloudBuildLogFn = () => {
if (!cloudBuildLog.includes(item.action)) {
terminalRef.value.pushMessage({ content: `${item.action}` })
cloudBuildLog.push(item.action)
if (item.code == 0) {
error = item.msg
terminalRef.value.pushMessage({ content: item.msg, class: 'error' })
timeloading.value = false
active.value = 'error'
terminalRef.value.execute('clear')
clearCloudBuildTask()
errorInfo.value = item.msg
//
if (buildTimer) {
clearInterval(buildTimer)
buildTimer = null
}
// duration
if (buildStartTime.value) {
buildDuration.value = Math.floor((Date.now() - buildStartTime.value) / 1000)
}
localStorage.removeItem('cloud_build_start_time')
localStorage.removeItem('cloud_build_task')
}
}
})
@ -145,22 +237,29 @@ const getCloudBuildLogFn = () => {
}).catch()
}
const closeType = ref('normal')
const handleReturn = () => {
active.value = 'build'
closeType.value = 'success'
}
let notificationEl : any = null
/**
* 升级中任务提示
*/
const showElNotification = () => {
notificationEl = ElNotification.success({
title: t('warning'),
dangerouslyUseHTMLString: true,
message: h('div', {}, [
t('cloudbuild.executingTips'),
h('span', { class: 'text-primary cursor-pointer', onClick: elNotificationClick }, [t('cloudbuild.clickView')])
]),
duration: 0,
showClose: false
})
}
// const showElNotification = () => {
// notificationEl = ElNotification.success({
// title: t('warning'),
// dangerouslyUseHTMLString: true,
// message: h('div', {}, [
// t('cloudbuild.executingTips'),
// h('span', { class: 'text-primary cursor-pointer', onClick: elNotificationClick }, [t('cloudbuild.clickView')])
// ]),
// duration: 0,
// showClose: false
// })
// }
const elNotificationClick = () => {
showDialog.value = true
@ -171,7 +270,7 @@ const elNotificationClick = () => {
const open = async () => {
loading.value = true
active.value = 'build'
closeType.value = 'normal'
if (cloudBuildTask.value) {
showDialog.value = true
loading.value = false
@ -228,7 +327,7 @@ const makeIterator = (array: string[]) => {
}
const dialogClose = (done: () => {}) => {
if (active.value == 'build' && cloudBuildTask.value) {
if (active.value == 'build' && cloudBuildTask.value && closeType.value == 'normal') {
ElMessageBox.confirm(
t('cloudbuild.showDialogCloseTips'),
t('warning'),
@ -239,19 +338,57 @@ const dialogClose = (done: () => {}) => {
}
).then(() => {
terminalRef.value.execute('clear')
localStorage.removeItem('cloud_build_start_time')
localStorage.removeItem('cloud_build_task')
done()
buildTimer && clearInterval(buildTimer)
buildTimer = null
buildStartTime.value = null
buildDuration.value = 0
}).catch(() => { })
} else {
done()
}
}
const dialogCancel = () => {
if (active.value == 'build' && cloudBuildTask.value && closeType.value == 'normal') {
ElMessageBox.confirm(
t('cloudbuild.showDialogCloseTips'),
t('warning'),
{
confirmButtonText: t('confirm'),
cancelButtonText: t('cancel'),
type: 'warning'
}
).then(() => {
terminalRef.value.execute('clear')
localStorage.removeItem('cloud_build_start_time')
localStorage.removeItem('cloud_build_task')
buildTimer && clearInterval(buildTimer)
buildTimer = null
buildStartTime.value = null
buildDuration.value = 0
showDialog.value = false
}).catch(() => { })
} else {
showDialog.value = false
}
}
const cloudBuildCheckDirFn = () => {
window.open('https://doc.niucloud.com/v6.html?keywords=/chang-jian-wen-ti-chu-li/er-shi-wu-3001-sheng-7ea7-yun-bian-yi-mu-lu-du-xie-quan-xian-zhuang-tai-bu-tong-guo-ru-he-chu-li')
}
watch(() => showDialog.value, () => {
if (!showDialog.value) {
cloudBuildTask.value = null
active.value = 'build'
cloudBuildLog = []
flashInterval && clearInterval(flashInterval)
buildTimer && clearInterval(buildTimer)
buildStartTime.value = null
buildDuration.value = 0
clearCloudBuildTask()
}
})
@ -259,7 +396,8 @@ watch(() => showDialog.value, () => {
defineExpose({
open,
cloudBuildTask,
loading
loading,
elNotificationClick
})
</script>
@ -270,4 +408,30 @@ defineExpose({
:deep(.terminal .t-log-box span) {
white-space: pre-wrap;
}
:deep(.el-result__icon) {
color: unset !important; //
}
:deep(.el-dialog__title){
font-size: 20px;
font-weight: bold;
}
:deep(.el-result__title p){
font-size: 25px;
color: #1D1F3A;
font-weight: 500;
}
:deep(.el-result__subtitle p){
font-size: 15px;
color: #4F516D;
font-weight: 500;
word-break: break-all;
text-overflow: ellipsis;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 5;
-webkit-box-orient: vertical;
}
:deep(.el-result){
margin-top: -80px !important;
}
</style>

View File

@ -60,6 +60,12 @@ const getAppVersionListFn = () => {
})
frameworkVersionList.value = data
if(frameworkVersionList.value.length == 0){
ElMessage.warning('暂无版本更新信息')
return
}else{
dialogVisible.value = true
}
})
}
const getFrameworkVersionListFn = () => {
@ -73,6 +79,7 @@ const getFrameworkVersionListFn = () => {
}
})
frameworkVersionList.value = data
dialogVisible.value = true
})
}
@ -90,7 +97,6 @@ const open = async () => {
} else {
getFrameworkVersionListFn()
}
dialogVisible.value = true
})
}

View File

@ -35,9 +35,9 @@
</template>
</template>
<div v-if="step == 2">
<el-steps :active="numberOfSteps" align-center class="number-of-steps" finish-status="success" process-status="process">
<el-steps :active="numberOfSteps" align-center class="number-of-steps" process-status="process" v-if="!errorDialog && active != 'complete'">
<el-step :title="t('testDirectoryPermissions')" />
<el-step :title="t('backupFiles')" />
<el-step :title="t('upgrade.option')" />
<el-step :title="t('startUpgrade')" />
<el-step :title="t('upgradeEnd')" />
</el-steps>
@ -72,26 +72,26 @@
<div class="bg-[#fff] my-3" v-if="upgradeCheck.dir">
<div class="px-[20px] pt-[10px] text-[14px] el-table">
<el-row class="py-[10px] items table-head-bg pl-[15px] mb-[10px]">
<el-col :span="12">
<el-col :span="18">
<span>{{ t("upgrade.path") }}</span>
</el-col>
<el-col :span="6">
<el-col :span="3">
<span>{{ t("upgrade.demand") }}</span>
</el-col>
<el-col :span="6">
<el-col :span="3">
<span>{{ t("status") }}</span>
</el-col>
</el-row>
<div style="height: calc(300px); overflow: auto">
<el-row class="pb-[10px] items pl-[15px]" v-for="item in upgradeCheck.dir.is_readable">
<el-col :span="12">
<el-col :span="18">
<span>{{ item.dir }}</span>
</el-col>
<el-col :span="6">
<el-col :span="3">
<span>{{ t("upgrade.readable") }}</span>
</el-col>
<el-col :span="6">
<el-col :span="3" >
<span v-if="item.status">
<el-icon color="green">
<Select />
@ -105,13 +105,13 @@
</el-col>
</el-row>
<el-row class="pb-[10px] items pl-[15px]" v-for="item in upgradeCheck.dir.is_write">
<el-col :span="12">
<el-col :span="18">
<span>{{ item.dir }}</span>
</el-col>
<el-col :span="6">
<el-col :span="3">
<span>{{ t("upgrade.write") }}</span>
</el-col>
<el-col :span="6">
<el-col :span="3">
<span v-if="item.status">
<el-icon color="green">
<Select />
@ -129,7 +129,7 @@
</div>
</el-scrollbar>
</div>
<div class="h-[370px] mt-[30px]" v-show="upgradeTask">
<div class="h-[370px] mt-[30px]" v-show="showTerminal && upgradeTask && !errorDialog">
<terminal ref="terminalRef" :context="upgradeTask ? upgradeTask.upgrade.app_key : ''" :init-log="null" :show-header="false" :show-log-time="true" @exec-cmd="onExecCmd" />
</div>
</div>
@ -137,40 +137,50 @@
<div class="flex flex-col" v-show="active == 'backup'">
<el-scrollbar>
<div class="bg-[#fff] my-3">
<div class="px-[20px] pt-[10px] text-[14px] el-table" v-if="!upgradeContent.last_backup">
<el-row class="py-[10px] items table-head-bg pl-[15px] mb-[10px]">
<el-col :span="20">
<span>功能操作</span>
</el-col>
<el-col :span="4">
<span>状态</span>
</el-col>
</el-row>
<el-row class="pb-[10px] items pl-[15px]" v-for="item in excludeSteps">
<el-col :span="20">
<span>{{ item.name }}</span>
</el-col>
<el-col :span="4">
<span>
<el-icon color="green">
<Select />
</el-icon>
</span>
</el-col>
</el-row>
<div class="p-[20px] mt-[50px] mx-[10px] border-[1px] border-[#E6E6E6] rounded-[10px]">
<div class="flex justify-between items-center mt-[-9px]">
<el-checkbox v-model="upgradeOption.is_need_cloudbuild" :label="t('upgrade.isNeedCloudbuild')" :true-value="true" :false-value="false" size="large" ></el-checkbox>
</div>
<div class="text-[14px] text-[#374151] mb-[10px]">{{ t('upgrade.cloudbuildTips') }}</div>
</div>
<div class="pl-[50px] pt-[50px]" v-else>
<el-checkbox v-model="isNeedBackup" :label="t('upgrade.isNeedBackup')" :true-value="true" :false-value="false" size="large" >
</el-checkbox>
<div class="backup">{{ t('upgrade.isNeedBackupTips') }}<el-button link type="primary" @click="toBackupRecord">{{ t('upgrade.isNeedBackupBtn') }}</el-button></div>
<div class="p-[20px] mt-[20px] mx-[10px] border-[1px] border-[#E6E6E6] rounded-[10px]" v-if="upgradeContent.last_backup">
<div class="flex justify-between items-center mt-[-9px]">
<el-checkbox v-model="upgradeOption.is_need_backup" :label="t('upgrade.isNeedBackup')" :true-value="true" :false-value="false" size="large" ></el-checkbox>
<el-button link type="primary" class="!text-[#9699B6]" @click="toBackupRecord">{{ t('upgrade.isNeedBackupBtn') }}</el-button>
</div>
<div class="text-[14px] text-[#374151] mb-[10px]">{{ t('upgrade.isNeedBackupTips') }}</div>
<div class="text-[14px] text-[#9699B6]">{{ t('上次备份时间:') }}{{ upgradeContent.last_backup.complete_time }}</div>
</div>
</div>
</el-scrollbar>
</div>
<div class="mt-[50px]" v-show="errorDialog">
<el-result icon="error" :title="t('升级失败')" :sub-title="errorMsg">
<template #icon>
<img src="@/app/assets/images/error_icon.png" alt="" />
</template>
<template #extra>
<el-button @click="handleBack()" class="!w-[90px]">错误信息</el-button>
<el-button @click="showDialog=false" type="primary" class="!w-[90px]">完成</el-button>
</template>
</el-result>
</div>
<div class="mt-[50px]" v-show="active == 'complete'">
<el-result icon="success" :title="t('upgrade.upgradeSuccess')"></el-result>
<el-alert :title="t('upgrade.upgradeCompleteTips')" type="error" :closable="false" v-show="upgradeTask && upgradeTask.executed && !upgradeTask.executed.includes('cloudBuild')"/>
<el-result icon="success" :title="t('upgrade.upgradeSuccess')">
<template #icon>
<img src="@/app/assets/images/success_icon.png" alt="">
</template>
<template #extra>
<div class="text-[16px] text-[#4F516D] mt-[-5px]" v-show="upgradeTask && upgradeTask.executed && !upgradeTask.executed.includes('cloudBuild')">{{ t('upgrade.upgradeCompleteTips') }}</div>
<div class="text-[16px] text-[#9699B6] mt-[10px]">本次升级用时{{ formatUpgradeDuration }}</div>
<div class="mt-[20px]">
<el-button @click="handleBack()" class="!w-[90px]">返回</el-button>
<el-button @click="showDialog=false" type="primary" class="!w-[90px]">完成</el-button>
</div>
</template>
</el-result>
<!-- <el-alert :title="t('upgrade.upgradeCompleteTips')" type="error" :closable="false" v-show="upgradeTask && upgradeTask.executed && !upgradeTask.executed.includes('cloudBuild')"/> -->
</div>
</div>
</div>
@ -180,7 +190,7 @@
<!-- 查看升级内容 -->
<el-button v-if="step == 1 && upgradeContent.content.length && isAllowUpgrade" @click="step = 2" type="primary">{{ t("upgrade.upgradeButton") }}</el-button>
<template v-if="step == 2">
<template v-if="step == 2 && active != 'complete'">
<!-- <el-button v-if="active == 'content'" @click="showDialog = false">{{ t("return") }}</el-button>-->
<el-button type="primary" :disabled="!is_pass" v-if="active == 'upgrade' && !upgradeTask" @click="() => { active = 'backup'; numberOfSteps = 1 }">{{ t("nextStep") }}</el-button>
<el-button v-if="active == 'backup'" @click="() => { active = 'upgrade'; numberOfSteps = 1 } ">{{ t("prev") }}</el-button>
@ -215,10 +225,10 @@
</template>
<script lang="ts" setup>
import { ref, h, watch } from 'vue'
import { t } from '@/lang'
import { getVersions } from '@/app/api/auth'
import { getFrameworkNewVersion } from '@/app/api/module'
import { ref, h, watch ,computed} from "vue"
import { t } from "@/lang"
import { getVersions } from "@/app/api/auth"
import { getFrameworkNewVersion } from "@/app/api/module"
import {
getUpgradeContent,
getUpgradeTask,
@ -226,12 +236,12 @@ import {
executeUpgrade,
preUpgradeCheck,
clearUpgradeTask, upgradeUserOperate
} from '@/app/api/upgrade'
import { Terminal, TerminalFlash } from 'vue-web-terminal'
import 'vue-web-terminal/lib/theme/dark.css'
import { AnyObject } from '@/types/global'
import { ElNotification, ElMessage, ElMessageBox } from 'element-plus'
import Storage from '@/utils/storage'
} from "@/app/api/upgrade"
import { Terminal, TerminalFlash } from "vue-web-terminal"
import "vue-web-terminal/lib/theme/dark.css"
import { AnyObject } from "@/types/global"
import { ElNotification, ElMessage, ElMessageBox } from "element-plus"
import Storage from "@/utils/storage"
import { useRouter } from 'vue-router'
const router = useRouter()
@ -244,35 +254,49 @@ const step = ref(1)
const upgradeCheck = ref<null | AnyObject>(null)
const loading = ref(false)
const terminalRef: any = ref(null)
const emits = defineEmits(['complete', 'cloudbuild'])
const emits = defineEmits(["complete", "cloudbuild"])
const upgradeTipsShowDialog = ref<boolean>(false)
let upgradeLog: any = []
let errorLog: any = []
const cloudBuildErrorTipsShowDialog = ref<boolean>(false)
const retrySecond = ref(30)
let retrySecondInterval: any = null
const isNeedBackup = ref(true)
const upgradeOption = ref({
is_need_backup: true,
is_need_cloudbuild: true
})
// backupCode backupSql
const excludeSteps: any = ref([
{
name: '备份源码',
code: 'backupCode'
name: "备份源码",
code: "backupCode"
},
{
name: '备份数据库',
code: 'backupSql'
name: "备份数据库",
code: "backupSql"
}
])
/**
* 查询升级任务
*/
const showTerminal = ref(false)
const upgradeStartTime = ref<number | null>(null)
const upgradeDuration = ref(0) //
let upgradeTimer: ReturnType<typeof setInterval> | null = null
const errorDialog = ref(false)
const errorMsg = ref('')
const getUpgradeTaskFn = () => {
getUpgradeTask().then(({ data }) => {
if (!data) return
if (!upgradeContent.value) {
upgradeContent.value = data.upgrade_content
if ( upgradeContent.value || !data.upgrade_content || !Array.isArray(data.upgrade_content.content)) {
return
}
let upgradeCount = 0
let failUpgradeCount = 0
for (let i = 0; i < upgradeContent.value.content.length; i++) {
@ -295,15 +319,22 @@ const getUpgradeTaskFn = () => {
return
}
if (!upgradeTask.value) {
terminalRef.value.execute('clear')
terminalRef.value.execute('开始升级')
showTerminal.value = true
terminalRef.value.execute("clear")
terminalRef.value.execute("开始升级")
upgradeStartTime.value = Date.now()
upgradeDuration.value = 0
if (upgradeTimer) clearInterval(upgradeTimer)
upgradeTimer = setInterval(() => {
upgradeDuration.value++
}, 1000)
}
upgradeTask.value = data
data.log.forEach((item) => {
if (!upgradeLog.includes(item)) {
terminalRef.value.pushMessage({ content: `${item}` })
terminalRef.value.pushMessage({ content: `${ item }` })
upgradeLog.push(item)
}
})
@ -311,32 +342,64 @@ const getUpgradeTaskFn = () => {
if (data.error) {
data.error.forEach((item) => {
if (!errorLog.includes(item)) {
terminalRef.value.pushMessage({ content: item, class: 'error' })
terminalRef.value.pushMessage({ content: item, class: "error" })
errorLog.push(item)
errorMsg.value = item
}
})
errorDialog.value = true
showTerminal.value = false
if (upgradeTimer) {
clearInterval(upgradeTimer)
upgradeTimer = null
}
}
//
if (data.step == 'restoreComplete') {
if (data.step == "restoreComplete") {
flashInterval && clearInterval(flashInterval)
return
}
//
if (data.step == 'upgradeComplete') {
active.value = 'complete'
if (data.step == "upgradeComplete") {
active.value = "complete"
showTerminal.value = false
numberOfSteps.value = 4
notificationEl && notificationEl.close()
emits('complete')
emits("complete")
if (upgradeTimer) {
clearInterval(upgradeTimer)
upgradeTimer = null
}
clearUpgradeTask()
return
}
numberOfSteps.value = 2
active.value = 'upgrade'
active.value = "upgrade"
executeUpgradeFn()
})
}
getUpgradeTaskFn()
const isBack = ref(false)
const handleBack = () => {
active.value = "upgrade"
isBack.value = true
showTerminal.value = true
errorDialog.value = false //
}
const formatUpgradeDuration = computed(() => {
const s = upgradeDuration.value
const h = Math.floor(s / 3600)
const m = Math.floor((s % 3600) / 60)
const sec = s % 60
return [
h > 0 ? `${h}小时` : '',
m > 0 ? `${m}分钟` : '',
`${sec}`
].filter(Boolean).join('')
})
const executeUpgradeFn = () => {
executeUpgrade().then(() => {
@ -363,12 +426,12 @@ let notificationEl: any = null
*/
const showElNotification = () => {
notificationEl = ElNotification.success({
title: t('warning'),
title: t("warning"),
dangerouslyUseHTMLString: true,
message: h('div', {}, [t('upgrade.upgradingTips'), h('span', {
class: 'text-primary cursor-pointer',
message: h("div", {}, [t("upgrade.upgradingTips"), h("span", {
class: "text-primary cursor-pointer",
onClick: elNotificationClick
}, [t('upgrade.clickView')])]),
}, [t("upgrade.clickView")])]),
duration: 0,
showClose: false
})
@ -379,15 +442,15 @@ const elNotificationClick = () => {
getUpgradeTaskFn()
step.value = 2
numberOfSteps.value = 3
active.value = 'upgrade'
active.value = "upgrade"
notificationEl && notificationEl.close()
}
const frameworkVersion = ref('')
const frameworkVersion = ref("")
getVersions().then((res) => {
frameworkVersion.value = res.data.version.version
})
const newFrameworkVersion = ref('')
const newFrameworkVersion = ref("")
getFrameworkNewVersion().then(({ data }) => {
newFrameworkVersion.value = data.last_version
})
@ -399,7 +462,7 @@ const is_pass = ref(false)
const repeat = ref(false)
const readyLoading = ref(false)
const handleUpgrade = async () => {
const handleUpgrade = async() => {
if (repeat.value) return
repeat.value = true
readyLoading.value = true
@ -409,7 +472,7 @@ const handleUpgrade = async () => {
await preUpgradeCheck(appKey).then(async ({ data }) => {
upgradeCheck.value = data
is_pass.value = data.is_pass
active.value = 'upgrade'
active.value = "upgrade"
!upgradeTask.value ? (numberOfSteps.value = 0) : numberOfSteps.value
upgradeTipsShowDialog.value = false
showDialog.value = true
@ -428,20 +491,21 @@ const upgradeAddonFn = () => {
const appKey = upgradeContent.value?.upgrade_apps.join(',') != 'niucloud-admin' ? upgradeContent.value?.upgrade_apps.join(',') : ''
upgradeAddon(appKey, { is_need_backup: isNeedBackup.value }).then(() => {
upgradeAddon(appKey, upgradeOption.value).then(() => {
getUpgradeTaskFn()
}).catch(() => {
loading.value = false
})
}
const open = (addonKey: string = '', callback = null) => {
const open = (addonKey: string = "", callback = null) => {
errorDialog.value = false //
if (upgradeTask.value) {
ElMessage({ message: '已有正在执行中的升级任务', type: 'error' })
ElMessage({ message: "已有正在执行中的升级任务", type: "error" })
showDialog.value = true
step.value = 2
numberOfSteps.value = 3
active.value = 'upgrade'
active.value = "upgrade"
if (callback) callback()
} else {
if (addonKey && frameworkVersion.value != newFrameworkVersion.value) {
@ -468,8 +532,7 @@ const open = (addonKey: string = '', callback = null) => {
} else if (upgradeContent.value.content.length == failUpgradeCount) {
isAllowUpgrade.value = false
}
if (Storage.get('upgradeTipsLock')) {
if (Storage.get("upgradeTipsLock")) {
handleUpgrade()
} else {
upgradeTipsShowDialog.value = true
@ -488,19 +551,19 @@ const open = (addonKey: string = '', callback = null) => {
let flashInterval: any = null
const terminalFlash = new TerminalFlash()
const onExecCmd = (key, command, success, failed, name) => {
if (command == '开始升级') {
if (command == "开始升级") {
success(terminalFlash)
const frames = makeIterator(['/', '——', '\\', '|'])
const frames = makeIterator(["/", "——", "\\", "|"])
flashInterval = setInterval(() => {
terminalFlash.flush('> ' + frames.next().value)
terminalFlash.flush("> " + frames.next().value)
}, 150)
}
}
const makeIterator = (array: string[]) => {
let nextIndex = 0
var nextIndex = 0
return {
next () {
next() {
if (nextIndex + 1 == array.length) {
nextIndex = 0
}
@ -510,11 +573,11 @@ const makeIterator = (array: string[]) => {
}
const dialogClose = (done: () => {}) => {
if (active.value == 'upgrade' && upgradeTask.value && ['upgradeComplete', 'restoreComplete'].includes(upgradeTask.value.step) === false) {
ElMessageBox.confirm(t('upgrade.showDialogCloseTips'), t('warning'), {
confirmButtonText: t('confirm'),
cancelButtonText: t('cancel'),
type: 'warning'
if (active.value == "upgrade" && upgradeTask.value && ['upgradeComplete', 'restoreComplete'].includes(upgradeTask.value.step) === false && !isBack.value) {
ElMessageBox.confirm(t("upgrade.showDialogCloseTips"), t("warning"), {
confirmButtonText: t("confirm"),
cancelButtonText: t("cancel"),
type: "warning"
}).then(() => {
done()
})
@ -528,20 +591,28 @@ watch(
() => {
if (!showDialog.value) {
clearUpgradeTaskFn()
}
}
)
const clearUpgradeTaskFn = () => {
active.value = 'upgrade'
active.value = "upgrade"
loading.value = false
upgradeTask.value = null
isBack.value = false
errorDialog.value = false
errorMsg.value = ''
showTerminal.value = false
upgradeLog = []
errorLog = []
numberOfSteps.value = 0
flashInterval && clearInterval(flashInterval)
retrySecondInterval && clearInterval(retrySecondInterval)
isNeedBackup.value = true
upgradeOption.value = {
is_need_backup: true,
is_need_cloudbuild: true
}
step.value = 1
clearUpgradeTask().then(() => {
})
@ -554,16 +625,16 @@ const clearUpgradeTaskFn = () => {
const cloudBuildError = (event: string) => {
cloudBuildErrorTipsShowDialog.value = false
switch (event) {
case 'local':
case "local":
upgradeUserOperate(event).then(() => {
getUpgradeTaskFn()
})
break
case 'retry':
case "retry":
executeUpgradeFn()
retrySecondInterval && clearInterval(retrySecondInterval)
break
case 'rollback':
case "rollback":
upgradeUserOperate(event).then(() => {
getUpgradeTaskFn()
})
@ -572,13 +643,13 @@ const cloudBuildError = (event: string) => {
}
const timeSplit = (str: string) => {
const [date, time] = str.split(' ')
const [hours, minutes] = time.split(':')
return [date, `${hours}:${minutes}`]
const [date, time] = str.split(" ")
const [hours, minutes] = time.split(":")
return [date, `${ hours }:${ minutes }`]
}
const upgradeTipsConfirm = (isLock: boolean = false) => {
isLock && Storage.set({ key: 'upgradeTipsLock', data: isLock })
isLock && Storage.set({ key: "upgradeTipsLock", data: isLock })
upgradeTipsShowDialog.value = false
!isLock && (showDialog.value = true)
}
@ -599,9 +670,15 @@ defineExpose({
</script>
<style lang="scss" scoped>
:deep(.el-button){
border-radius: 4px !important;
}
.table-head-bg {
background-color: var(--el-table-header-bg-color);
}
:deep(.el-checkbox__label){
color: var(--el-color-primary);
}
:deep(.terminal .t-log-box span) {
white-space: pre-wrap;
@ -623,8 +700,27 @@ defineExpose({
.el-step__icon {
background: var(--el-color-primary);
color: #fff;
// box-shadow: 0 0 0 4px var(--el-color-primary-light-9);
box-shadow: 0 0 0 4px var(--el-color-primary-light-9);
i {
color: #fff;
}
}
.el-step__line {
margin: 0 25px;
background: var(--el-color-primary);
}
}
.is-finish {
color: var(--el-color-primary);
border-color: var(--el-color-primary);
.el-step__icon {
background: var(--el-color-primary)!important;
color: #fff !important;
// box-shadow: 0 0 0 4px var(--el-color-primary-light-9);
i {
color: #fff;
@ -645,7 +741,9 @@ defineExpose({
.el-step__icon {
padding: 10px;
border: 1px solid var(--el-color-primary);
box-shadow: 0 0 0 4px var(--el-color-primary-light-9);
background: var(--el-color-primary)!important;
color: #fff !important;
// box-shadow: 0 0 0 4px var(--el-color-primary-light-9);
}
}
@ -653,6 +751,26 @@ defineExpose({
color: #333;
}
}
:deep(.el-dialog__title){
font-size: 20px;
font-weight: bold;
}
:deep(.el-result__title p){
font-size: 25px;
color: #1D1F3A;
font-weight: 500;
}
:deep(.el-result__subtitle p){
font-size: 15px;
color: #4F516D;
font-weight: 500;
word-break: break-all;
text-overflow: ellipsis;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
</style>
<style scoped>

View File

@ -2,7 +2,7 @@
<el-dialog v-model="dialogVisible" :title="t('accountSettings')" width="500">
<el-form :model="saveInfo" label-width="90px" ref="formRef" class="page-form">
<el-form-item :label="t('headImg')">
<upload-image v-model="saveInfo.head_img" :limit="1" :type="'avatar'" imageFit="cover" />
<upload-image v-model="saveInfo.head_img" :limit="1" imageFit="cover" />
</el-form-item>
<el-form-item :label="t('userName')">
<span>{{saveInfo.username}}</span>

View File

@ -44,10 +44,10 @@
"createTime": "发布时间",
"buyLabel": "已购买",
"recentlyUpdated": "最近更新",
"installTips": "安装后需手动更新插件引用的依赖和编译各个端口的前端源码",
"installTips": "本地安装过程仅对应用和插件的程序代码和数据库进行安装处理,并不会对前端代码进行编译,本地安装之后,必须对各前端端口进行编译,才能正常使用。",
"localInstall": "本地安装",
"cloudInstall": "一键云安装",
"cloudInstallTips": "云安装可实现一键安装,安装后无需手动更新依赖和编译前端源码",
"cloudInstallTips": "云安装可实现一键安装,云安装不仅会把应用和插件的程序代码安装处理,同时,会在云端编译各前端代码。云安装完成之后即可正常使用程序! ",
"installingTips": "有插件正在安装中请等待安装完成之后再进行其他操作,点击查看",
"installPercent": "安装进度",
"downloading": "下载中",
@ -70,7 +70,7 @@
"appIdentification": "应用标识",
"tipText": "标识指开发应用或插件的文件夹名称",
"uninstallTips": "是否要卸载该插件?",
"upgrade": "一键升级",
"upgrade": "升级",
"newVersion": "最新版本",
"cloudBuild": "云编译",
"cloudBuildTips": "是否要进行云编译该操作可能会影响到正在访问的客户是否要继续操作?",

View File

@ -23,6 +23,7 @@
"verifier": "核销员",
"createTime": "添加时间",
"addVerifier": "添加核销员",
"editVerifier":"编辑核销员",
"verifierDeleteTips": "确定要删除该核销员吗?",
"memberInfo": "会员信息",
"memberIdPlaceholder": "请选择会员",

View File

@ -7,7 +7,7 @@
"key":"插件标识",
"keyPlaceholder":"请输入插件标识",
"keyPlaceholderErr":"插件标识格式不正确,只能以字母开头且只能输入字母、数字、下划线",
"keyPlaceholder1":"插件标识指开发插件的文件夹名称,申请之后不能修改(只能包括字母、数字和下划线且只能以字母开头格式如f1111、f11_22)",
"keyPlaceholder1":"插件标识指开发插件的文件夹名称,申请之后不能修改(仅允许使用字母、数字与下划线组合且必须以字母开头同时名称中至少包含一个下划线格式如a11_34、f11_22)",
"keyPlaceholder2":"插件标识设置后建议进行插件标识检测如果当前插件标识已经在niucloud官方市场注册则只能在本地使用无法在官方市场发布销售",
"desc":"插件描述",
"descPlaceholder":"请输入插件描述",

View File

@ -27,7 +27,7 @@
"min": "分",
"cronDeleteTips": "你确定要删除任务吗",
"addCron": "添加任务",
"cronTimeTips": "任务周期时间不能为空",
"cronTimeTips": "任务周期时间不能为空并且大于0",
"cronTipsOne": "启动计划任务方式:",
"cronTipsTwo": "1、使用命令启动php think workerman 如果更改了任务周期、状态、删除任务等操作后,需要重新启动下 php think workerman 确保生效",
"doOne": "执行一次",

View File

@ -1,32 +1,38 @@
<template>
<!--授权信息-->
<div class="main-container">
<el-card class="box-card !border-none" shadow="never" v-if="!loading">
<div>
<div class="text-[#333] text-[18px]">授权信息</div>
<div class="ml-[50px] mt-[40px]">
<div class="flex flex-col">
<div class="flex flex-wrap items-center">
<span class="mr-[6px] text-[14px] text-[#666666] w-[70px] text-right">授权公司</span>
<span class="text-[14px] text-[#333]">{{ authinfo.company_name || "--" }}</span>
<el-card class="box-card !border-none min-h-[300px]" shadow="never" v-loading="loading">
<div v-if="!loading">
<div class="title text-[16px] font-bold text-[#1D1F3A] mb-[30px]">授权信息</div>
<div class="">
<div class="flex items-center">
<div class="w-[92px] h-[92px] rounded-[10px] flex justify-center items-center mr-[20px]">
<img src="@/app/assets/images/tools/authorize.png" class="w-[92px] h-[92px]" />
</div>
<div class="flex flex-wrap items-center mt-[20px]">
<span class="mr-[6px] text-[14px] text-[#666666] w-[70px] text-right">授权域名</span>
<span class="text-[14px] text-[#333]">{{ authinfo.site_address || "--" }}</span>
</div>
<div class="flex flex-wrap items-center mt-[20px]">
<span class="mr-[6px] text-[14px] text-[#666666] w-[70px] text-right">授权码</span>
<span class="text-[14px] text-[#333]">
<span class="mr-[10px]">{{ authinfo.auth_code ? (isCheck ? authinfo.auth_code : hideAuthCode(authinfo.auth_code)) : "--" }}</span>
<el-icon v-if="!isCheck" @click="isCheck = !isCheck" class="text-[12px] cursor-pointer text-[#4383F9]">
<View />
</el-icon>
<el-icon v-else @click="isCheck = !isCheck" class="text-[12px] cursor-pointer text-[#4383F9]"> <Hide /> </el-icon>
</span>
<div class="flex flex-col justify-between font-500">
<div class="flex flex-wrap items-center mb-[12px]">
<span class="mr-[6px] text-[14px] text-[#666666] w-[70px] text-left">授权公司</span>
<span class="text-[14px] text-[#333]">{{ authinfo.company_name || "--" }}</span>
</div>
<div class="flex flex-wrap items-center mb-[12px]">
<span class="mr-[6px] text-[14px] text-[#666666] w-[70px] text-left">授权域名</span>
<span class="text-[14px] text-[#333]">{{ authinfo.site_address || "--" }}</span>
</div>
<div class="flex flex-wrap items-center">
<span class="mr-[6px] text-[14px] text-[#666666] w-[70px] text-left">授权码</span>
<span class="text-[14px] text-[#333]">
<span class="mr-[10px]">{{ authinfo.auth_code ? (isCheck ? authinfo.auth_code : hideAuthCode(authinfo.auth_code)) : "--" }}</span>
<el-icon v-if="!isCheck" @click="isCheck = !isCheck" class="text-[14px] cursor-pointer text-[#9699B6]">
<View />
</el-icon>
<el-icon v-else @click="isCheck = !isCheck" class="text-[14px] cursor-pointer text-[#9699B6]"> <Hide /> </el-icon>
</span>
</div>
</div>
</div>
<div class="mt-[60px] mb-[50px]">
<el-button class="w-[150px] !h-[46px] mt-[8px]" type="primary" @click="authCodeApproveFn">授权码认证</el-button>
<div class="mt-[17px] ml-[110px]">
<el-button class="!w-[140px] !h-[32px] mt-[8px] !rounded-[4px]" type="primary" @click="authCodeApproveFn">授权码认证</el-button>
<el-popover ref="getAuthCodeDialog" placement="bottom-start" :width="478" trigger="click" class="mt-[8px]">
<div class="px-[18px] py-[8px]">
<p class="leading-[32px] text-[14px]">您在官方应用市场购买任意一款应用即可获得授权码输入正确授权码认证通过后即可支持在线升级和其它相关服务</p>
@ -36,7 +42,7 @@
</div>
</div>
<template #reference>
<el-button class="w-[150px] !h-[46px] mt-[8px] !text-[var(--el-color-primary)] hover:!text-[var(--el-color-primary)] !bg-transparent" plain type="primary">如何获取授权码?</el-button>
<el-button class="!w-[140px] !h-[32px] mt-[8px] !rounded-[4px] !text-[var(--el-color-primary)] hover:!text-[var(--el-color-primary)] !bg-transparent" plain type="primary">如何获取授权码?</el-button>
</template>
</el-popover>
</div>
@ -164,4 +170,8 @@ const getVersionsInfo = () => {
getVersionsInfo()
</script>
<style lang="scss" scoped></style>
<style lang="scss" scoped>
:deep(.el-button){
border-radius: 4px !important;
}
</style>

View File

@ -1,22 +1,58 @@
<template>
<!--授权信息-->
<div class="main-container">
<el-card class="box-card !border-none" shadow="never" v-if="newVersion">
<div>
<div class="mx-[20px] my-[20px]">
<div class="title text-[18px]">版本信息</div>
<div class="text-[18px] text-center mb-[7px] mt-[40px]">系统当前版本v{{ version }}{{ versionCode }}</div>
<div class="text-center text-[#666] text-[14px]" v-if="!newVersion || (newVersion && newVersion.version_no == version)">
<span>当前已是最新版本无需升级</span>
<span class="text-[14px] text-primary ml-[10px] cursor-pointer" @click="openUpgrade">更新说明</span>
<el-card class="box-card !border-none min-h-[500px]" shadow="never" v-loading="loadingVersion">
<div v-if="!loadingVersion">
<div class="mb-[30px]" v-if="newVersion">
<div class="title text-[16px] font-bold text-[#1D1F3A] mb-[20px]">版本信息</div>
<div class="text-[14px] text-[#1D1F3A] mb-[20px]"><span>系统当前版本</span><span class="font-bold ">v{{ version }}</span> </div>
<div class="flex">
<div class="w-[92px] h-[92px] rounded-[10px] flex justify-center items-center mr-[20px]">
<img src="@/app/assets/images/tools/upgrade.png" class="w-[92px] h-[92px]" />
</div>
<div class="flex flex-col justify-between items-start">
<div class="text-[14px] text-[#1D1F3A]">系统最新版本为</div>
<div class="text-[14px] text-[#1D1F3A] font-bold">v{{ newVersion.version_no }}{{ versionCode }}</div>
<div class="text-[#9699B6] text-[16px]" v-if="!shouldShowUpgradeButton">
<span>已是最新</span>
</div>
<div v-else>
<el-button class="w-[102px] !h-[32px]" type="primary" :loading="loading" @click="handleUpgrade" v-if="!(!newVersion || (newVersion && newVersion.version_no == version))">一键升级</el-button>
</div>
</div>
</div>
<div class="text-[#666] text-[14px] text-center" v-else>
当前系统最新版本为 <span class="text-[18px] text-[#FF4D01]">v{{ newVersion.version_no }}</span>
<span class="text-[14px] text-primary ml-[10px]" style="cursor: pointer" @click="openUpgrade">更新说明</span>
</div>
<div class="mt-[30px] flex justify-center items-center">
<el-button class="text-[#4C4C4C] w-[150px] !h-[44px]" type="primary" :loading="loading" @click="handleUpgrade" v-if="!(!newVersion || (newVersion && newVersion.version_no == version))">一键升级</el-button>
<el-button class="text-[#4C4C4C] w-[130px] !h-[44px]" @click="upgradeRecord">升级记录</el-button>
</div>
<div class="panel-title bg-[#F4F5F7] border-[#E6E6E6] border-solid border-b-[1px] h-[40px] flex items-center p-[10px]">
<span class="text-[14px] font-500 text-[#1D1F3A]">升级记录</span>
</div>
<div >
<div class="time-dialog" style="overflow: auto">
<el-scrollbar>
<el-timeline style="width: 100%">
<el-timeline-item v-for="(item, index) in frameworkVersionList" :key="index" placement="left" :hollow="true">
<el-collapse v-model="activeName" accordion>
<el-collapse-item :name="index">
<template #title>
<div class="flex justify-between items-start flex-col">
<span class="text-[#1D1F3A] text-[14px] leading-[20px]">版本 v{{ item.version_no }}</span>
<span class="text-[#9699B6] text-[13px] mt-2">{{ item.release_time }}</span>
</div>
</template>
<template #icon="{ isActive }">
<div class="ml-auto text-[#374151] flex items-center">
<span class="text-[#374151] text-[14px]">{{ isActive ? '收起' : '更新内容' }}</span>
<span class="iconfont iconjiantouxia ml-[4px] !text-[10px] transition-transform duration-300" v-if="!isActive"></span>
<span class="iconfont iconjiantoushang ml-[4px] !text-[10px] transition-transform duration-300" v-if="isActive"></span>
</div>
</template>
<div class="px-[20px] py-[20px] bg-overlay timeline-log-wrap whitespace-pre-wrap rounded-[4px] bg-[#F9F9FB] text-[#4F516D]" v-if="item['upgrade_log']">
<div v-html="item['upgrade_log']"></div>
</div>
</el-collapse-item>
</el-collapse>
</el-timeline-item>
</el-timeline>
</el-scrollbar>
</div>
</div>
</div>
@ -66,7 +102,7 @@ const upgradeRef = ref<any>(null)
const upgradeLogRef = ref<any>(null)
const authCodeApproveDialog = ref(false)
const frameworkVersionList = ref([])
const activeName = ref(0)
const checkVersion = ref(false)
const formData = reactive<Record<string, string>>({
@ -100,10 +136,12 @@ const save = async(formEl: FormInstance | undefined) => {
}
})
}
const loadingVersion = ref(false)
const getFrameworkVersionListFn = () => {
loadingVersion.value = true
getFrameworkVersionList().then(({ data }) => {
frameworkVersionList.value = data
loadingVersion.value = false
if (checkVersion.value) {
if (!newVersion.value || (newVersion.value && newVersion.value.version_no == version.value)) {
ElMessage({
@ -114,10 +152,24 @@ const getFrameworkVersionListFn = () => {
} else {
checkVersion.value = true
}
}).catch(() => {
loadingVersion.value = false
})
}
getFrameworkVersionListFn()
const shouldShowUpgradeButton = computed(() => {
if (!newVersion.value || newVersion.value.version_no === version.value) {
return false;
}
//
const currentVersionStr = String(version.value);
const latestVersionStr = String(newVersion.value.version_no);
//
const currentVersionNum = parseInt(currentVersionStr.replace(/\./g, ''), 10);
const latestVersionNum = parseInt(latestVersionStr.replace(/\./g, ''), 10);
return latestVersionNum > currentVersionNum;
});
const newVersion: any = computed(() => {
return frameworkVersionList.value.length ? frameworkVersionList.value[0] : null
})
@ -136,7 +188,11 @@ const getVersionsInfo = () => {
})
}
getVersionsInfo()
const timeSplit = (str: string) => {
const [date, time] = str.split(" ")
const [hours, minutes] = time.split(":")
return [date, `${ hours }:${ minutes }`]
}
interface AuthInfo {
company_name: string
site_address: string
@ -191,5 +247,32 @@ const openUpgrade = () => {
upgradeLogRef.value?.open()
}
</script>
<style lang="scss" scoped></style>
<style lang="scss" scoped>
:deep(.el-timeline-item__node--normal){
width: 16px !important;
height: 16px !important;
}
:deep(.el-timeline-item__tail){
left: 6px !important;
}
:deep(.el-timeline-item__node.is-hollow){
background: #9699B6 !important;
border-width: 3px !important;
}
:deep(.time-dialog .el-timeline-item__wrapper) {
top: -2px !important;
}
:deep(.el-collapse-item__header){
background: #F9F9FB !important;
border: 1px solid #F1F1F8 !important;
padding: 0 20px !important;
line-height: normal !important;
height: 70px !important;
}
:deep(.el-collapse-item__content){
padding-bottom: 0 !important;
}
:deep(.el-button){
border-radius: 4px !important;
}
</style>

View File

@ -7,7 +7,7 @@
<span class="text-page-title">{{ pageName }}</span>
</div>
<div class="flex justify-between items-center mt-[20px]">
<div class="flex justify-between items-start mt-[20px]">
<el-form :inline="true" :model="sysUserLogTableData.searchParam" ref="searchFormRef">
<el-form-item :label="t('ip')" prop="ip">
<el-input v-model.trim="sysUserLogTableData.searchParam.ip" :placeholder="t('ipPlaceholder')" />
@ -25,6 +25,11 @@
<el-button @click="resetForm(searchFormRef)">{{ t('reset') }}</el-button>
</el-form-item>
</el-form>
<div class="flex justify-end items-center w-[20%]">
<div>
<el-button type="primary" class="w-[100px]" @click="clearEvent()">{{ t('清空日志') }}</el-button>
</div>
</div>
</div>
<div>
@ -61,7 +66,7 @@
<script lang="ts" setup>
import { reactive, ref } from 'vue'
import { t } from '@/lang'
import { getLogList } from '@/app/api/user'
import { getLogList ,logDestroy} from '@/app/api/user'
import UserLogDetail from '@/app/views/auth/components/user-log-detail.vue'
import { FormInstance } from 'element-plus'
import { useRoute } from 'vue-router'
@ -117,6 +122,20 @@ const detailEvent = (data: any) => {
userLogDetailDialog.value.setFormData(data)
userLogDetailDialog.value.showDialog = true
}
const clearEvent = () => {
ElMessageBox.confirm(t('确定要全部清空操作日志吗?'), t('提示'), {
confirmButtonText: t('confirm'),
cancelButtonText: t('cancel'),
type: 'warning'
}).then(() => {
logDestroy().then(() => {
loadSysUserLogList()
})
}).catch(() => {
})
}
</script>
<style lang="scss" scoped></style>

View File

@ -34,7 +34,7 @@
<div v-else-if="row.menu_type == 2">{{ t('menuTypeButton') }}</div>
</template>
</el-table-column>
<el-table-column prop="api_url" :label="t('authId')" min-width="150" align="center" />
<el-table-column prop="api_url" :label="t('authId')" min-width="150" align="left" />
<el-table-column :label="t('status')" min-width="120" align="center">
<template #default="{ row }">
<el-tag class="ml-2" type="success" v-if="row.status == 1">{{ t('statusNormal') }}</el-tag>

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/user'
import { getUserList, lockUser, unlockUser,deleteUser } from '@/app/api/user'
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

@ -215,6 +215,7 @@
<script lang="ts" setup>
import { ref, reactive, toRaw, watch, inject } from 'vue'
import { t } from '@/lang'
import { ArrowLeft } from "@element-plus/icons-vue"
import { img } from '@/utils/common'
import { getDiyTemplatePages, addDiyPage, editDiyPage, initPage } from '@/app/api/diy'
import { useRoute, useRouter } from 'vue-router'

View File

@ -48,7 +48,7 @@
<el-input class="!w-[215px]" v-model.trim="item.text" :placeholder="t('titleContent')" maxlength="5" show-word-limit />
</el-form-item>
<el-form-item :label="t('navLinkOne')">
<diy-link v-model="item.link" @confirm="diyLinkFn" />
<diy-link v-model="item.link" :ignore="['DIY_JUMP_OTHER_APPLET']" @confirm="diyLinkFn" />
</el-form-item>
<el-icon class="close-icon cursor-pointer -top-[11px] -right-[8px]" @click="deleteNav(index)">
<CircleCloseFilled />

View File

@ -370,6 +370,7 @@
<script lang="ts" setup>
import { ref, reactive, toRaw, watch, inject, computed } from 'vue'
import { t } from '@/lang'
import { ArrowLeft } from "@element-plus/icons-vue"
import { img } from '@/utils/common'
import { getDiyTemplatePages, addDiyForm, editDiyForm, initPage } from '@/app/api/diy_form'
import { useRoute, useRouter } from 'vue-router'

View File

@ -1,6 +1,6 @@
<template>
<el-drawer v-model="showDialog" :title="t('dataAndStatistics')" direction="rtl" size="70%" :before-close="handleClose" class="member-detail-drawer">
<el-tabs v-model="activeName" class="demo-tabs" @tab-click="handleClick">
<el-tabs v-model="activeName" class="demo-tabs">
<el-tab-pane :label="t('detailData')" name="detail_data">
<el-card class="box-card !border-none my-[10px] table-search-wrap" shadow="never">
<el-form :inline="true" :model="formData.searchParam" ref="searchFormDiyFormRef">

View File

@ -28,11 +28,11 @@
import { reactive, ref } from 'vue'
import { t } from '@/lang'
import type { FormInstance } from 'element-plus'
import { getUserInfo, setUserInfo } from '@/app/api/personal'
import { useRouter } from 'vue-router'
import { deepClone } from "@/utils/common";
import useUserStore from '@/stores/modules/user'
const router = useRouter()
const userStore = useUserStore()
//
const saveInfo = reactive({
head_img: '',
@ -68,6 +68,9 @@ const submitForm = (formEl: FormInstance | undefined) => {
setUserInfo(saveInfo).then((res: any) => {
loading.value = false
let data: any = deepClone(userStore.userInfo)
data.head_img = saveInfo.head_img
userStore.setUserInfo(data)
}).catch(() => {
loading.value = false
})

View File

@ -4,42 +4,59 @@
<el-card class="box-card !border-none" shadow="never">
<div class="flex justify-between items-center">
<span class="text-page-title">{{ t("localAppText") }}</span>
<el-input class="!w-[250px]" :placeholder="t('search')" v-model.trim="search_name" @keyup.enter="query">
<template #suffix>
<el-icon class="el-input__icon cursor-pointer" size="14px" @click="query">
<search />
</el-icon>
</template>
</el-input>
</div>
<div class="flex justify-between items-center my-[20px]">
<div class="flex">
<div :class="['flex items-center text-[14px] h-[32px] border-[1px] border-solid my-[3px] border-[var(--el-color-info-light-8)] rounded-full px-[20px] mr-[24px] cursor-pointer hover:bg-[var(--el-color-info-light-8)]', { '!text-[#fff] !bg-[#000] !border-[#000]': activeName === 'installed' }]" @click="activeNameTabFn('installed')">{{ t("installLabel") }}</div>
<div :class="['flex items-center text-[14px] h-[32px] border-[1px] border-solid my-[3px] border-[var(--el-color-info-light-8)] rounded-full px-[20px] mr-[24px] cursor-pointer hover:bg-[var(--el-color-info-light-8)]', { '!text-[#fff] !bg-[#000] !border-[#000]': activeName === 'uninstalled' }]" @click="activeNameTabFn('uninstalled')">{{ t("uninstalledLabel") }}</div>
<div :class="['flex items-center text-[14px] h-[32px] border-[1px] border-solid my-[3px] border-[var(--el-color-info-light-8)] rounded-full px-[20px] mr-[24px] cursor-pointer hover:bg-[var(--el-color-info-light-8)]', { '!text-[#fff] !bg-[#000] !border-[#000]': activeName === 'all' }]" @click="activeNameTabFn('all')">{{ t("buyLabel") }}</div>
<div :class="['relative flex items-center text-[14px] h-[32px] border-[1px] border-solid my-[3px] border-[var(--el-color-info-light-8)] rounded-full px-[20px] mr-[24px] cursor-pointer hover:bg-[var(--el-color-info-light-8)]', { '!text-[#fff] !bg-[#000] !border-[#000]': activeName === 'recentlyUpdated' }]" @click="activeNameTabFn('recentlyUpdated')">
<span v-if="localList['recentlyUpdated'].length > 0" class="w-[9px] h-[9px] bg-[#FF0000]" style="position: absolute; border-radius: 50%; right: 5px; top: -5px"></span>
<span>{{ t('recentlyUpdated') }}</span>
</div>
<el-tabs v-model="activeName" class="mt-[10px]">
<el-tab-pane :label="t('installLabel')" name="installed"></el-tab-pane>
<el-tab-pane :label="t('uninstalledLabel')" name="uninstalled"></el-tab-pane>
<el-tab-pane :label="t('buyLabel')" name="all"></el-tab-pane>
<el-tab-pane :label="t('recentlyUpdated')" name="recentlyUpdated">
<template #label>
<span class="custom-tabs-label">
<span>{{ t('recentlyUpdated') }}</span>
<span v-if="localList['recentlyUpdated'].length > 0" class="w-[15px] h-[15px] bg-[#DA203E] absolute text-[#fff] text-[11px] flex items-center justify-center rounded-full top-[3px] right-[-12px]">{{ localList['recentlyUpdated'].length }}</span>
</span>
</template>
</el-tab-pane>
</el-tabs>
<div class="flex justify-between my-[10px]">
<div class="flex items-center search-form">
<el-input class="!w-[192px] !h-[32px] rounded-[4px]" :placeholder="t('search')" v-model.trim="search_name" @keyup.enter="query">
<template #suffix>
<el-icon class="el-input__icon cursor-pointer" size="14px" @click="query">
<search />
</el-icon>
</template>
</el-input>
<el-select v-model="search_type" placeholder="请选择类型" class="!w-[192px] !h-[32px] rounded-[4px] ml-[20px] " >
<el-option :label="t('全部')" value="" />
<el-option v-for="(label, value) in typeList" :key="value" :label="label" :value="value"></el-option>
</el-select>
<el-button type="primary" @click="query" class="ml-[20px]">{{ t("搜索") }}</el-button>
</div>
<div>
<el-button type="primary" v-show="activeName === 'recentlyUpdated'" round @click="batchUpgrade" :loading="upgradeRef?.loading" :disabled="authLoading">{{ t("batchUpgrade") }}</el-button>
<el-button type="primary" round @click="handleCloudBuild" :loading="cloudBuildRef?.loading" :disabled="authLoading">{{ t("cloudBuild") }}</el-button>
<el-button type="primary" v-show="activeName === 'recentlyUpdated'" @click="batchUpgrade" :loading="upgradeRef?.loading" :disabled="authLoading">{{ t("batchUpgrade") }}</el-button>
<!-- <el-button type="primary" @click="handleCloudBuild" :loading="cloudBuildRef?.loading" :disabled="authLoading">{{ t("cloudBuild") }}</el-button> -->
</div>
</div>
<div>
<el-table v-if="localList[activeName].length && !loading" :data="info[activeName]" size="large" class="pt-[5px]" @selection-change="handleSelectionChange">
<el-table-column type="selection" v-if="activeName === 'recentlyUpdated'" />
<el-table-column :label="t('appName')" align="left" width="450">
</div>
<div class="relative">
<el-table v-if="localList[activeName].length && !loading" :tree-props="{ children: 'children' }" :default-expand-all="true" :data="info[activeName]" row-key="key" size="large" @selection-change="handleSelectionChange">
<el-table-column width="24">
<template #default="{ row }">
<div class="flex items-center cursor-pointer">
<div class="tree-child-cell" :class="{ 'is-tree-parent': row.children?.length, 'is-tree-child': typeof row.support_app === 'string' && row.support_app !== '' && visibleRowKeys.has(row.support_app)}">
<span style="opacity: 0;">.</span>
</div>
</template>
</el-table-column>
<el-table-column type="selection" v-if="activeName === 'recentlyUpdated'" />
<el-table-column :label="t('appName')" align="left" width="500">
<template #default="{ row }">
<div class="flex items-center cursor-pointer relative left-[-10px]">
<el-image class="w-[54px] h-[54px]" :src="row.icon" fit="contain">
<template #error>
<div class="flex items-center w-full h-full">
<img class="max-w-full max-h-full" src="@/app/assets/images/icon-addon.png" alt="" />
<img class="max-w-full max-h-full" src="@/app/assets/images/icon-addon.png" alt="" />
</div>
</template>
</el-image>
@ -49,22 +66,23 @@
<div class="w-[236px] truncate leading-[18px] mt-[6px]" v-else>{{ row.version }}</div>
<div class="mt-[3px] flex flex-nowrap">
<el-tag type="danger" size="small" v-if="activeName == 'recentlyUpdated' && 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 v-if="versionJudge(row)" effect="dark" :content="`该插件适配框架版本为${ row.support_version },与已安装框架版本${frameworkVersion}不完全兼容`" placement="top-start">
<el-tag type="warning" size="small" class="ml-[3px]">该插件适配框架版本为{{ row.support_version }}与已安装框架版本{{frameworkVersion}}不完全兼容</el-tag>
</el-tooltip>
</div>
</div>
</div>
</template>
</el-table-column>
<el-table-column align="left" min-width="150">
<template #header>
<div class="flex items-center">
<span class="font-500 text-[13px] mr-[5px]">{{ t("appIdentification") }}</span>
<el-tooltip class="box-item" effect="light" :content="t('tipText')" placement="bottom">
<el-icon class="cursor-pointer text-[16px] text-[#a9a9a9]">
<QuestionFilled />
</el-icon>
<el-icon class="cursor-pointer text-[16px] text-[#a9a9a9]">
<QuestionFilled />
</el-icon>
</el-tooltip>
</div>
</template>
@ -72,21 +90,25 @@
<span class="font-500 text-[13px]">{{ row.key }}</span>
</template>
</el-table-column>
<el-table-column prop="" :label="t('introduction')" align="left" min-width="200">
<el-table-column :label="t('introduction')" align="left" min-width="250">
<template #default="{ row }">
<span class="font-500 text-[13px] multi-hidden">{{ row.desc }}</span>
</template>
</el-table-column>
<el-table-column :label="t('type')" align="left" min-width="80">
<template #default="{ row }">
<span class="font-500 text-[13px]">{{ row.type === "app" ? t("app") : t("addon") }}</span>
<span class="font-500 text-[13px] multi-hidden">{{ row.type === "app" ? t("app") : t("addon") }}</span>
</template>
</el-table-column>
<el-table-column prop="" :label="t('author')" align="left" min-width="80">
<el-table-column :label="t('author')" align="left" min-width="80">
<template #default="{ row }">
<span class="font-500 text-[13px]">{{ row.author }}</span>
</template>
</el-table-column>
<el-table-column :label="t('operation')" fixed="right" align="right" width="250">
<template #default="{ row }">
<el-button class="!text-[13px]" v-if="activeName == 'recentlyUpdated' && row.install_info && Object.keys(row.install_info)?.length && row.install_info.version != row.version" type="primary" link @click="upgradeAddonFn(row.key)">{{ t("upgrade") }}</el-button>
@ -235,72 +257,80 @@
<!-- 安装弹窗 -->
<el-dialog v-model="installShowDialog" :title="t('addonInstall')" width="850px" :close-on-click-modal="false" :close-on-press-escape="false" :before-close="installShowDialogClose">
<el-steps :space="200" :active="installStep" finish-status="success" align-center>
<el-steps :space="200" :active="installStep" class="number-of-steps" process-status="process" align-center v-if="installStep != 2 && !errorDialog ">
<el-step :title="t('envCheck')" class="flex-1" />
<el-step :title="t('installProgress')" class="flex-1" />
<el-step :title="t('installComplete')" class="flex-1" />
</el-steps>
<div v-show="installStep == 1" v-loading="!installCheckResult.dir">
<el-scrollbar max-height="50vh">
<div v-show="installStep == 0" v-loading="!installCheckResult.dir">
<!-- <el-scrollbar max-height="50vh"> -->
<div class="min-h-[150px]">
<div class="my-3" v-if="installCheckResult.dir">
<p class="pt-[20px] pl-[20px]">{{ t("dirPermission") }}</p>
<div v-if="!installCheckResult.is_pass" class="mt-[10px] mx-[20px] text-[14px] cursor-pointer text-primary flex items-center justify-between bg-[#EFF6FF] rounded-[4px] p-[10px]" @click="cloudBuildCheckDirFn">
<div class="flex items-center">
<el-icon :size="17"><QuestionFilled /></el-icon>
<span class="ml-[5px] leading-[20px]">编译权限错误查看解决方案</span></div>
<div class="border-[1px] border-primary rounded-[3px] w-[72px] h-[26px] leading-[25px] text-center">立即查看</div>
</div>
<div class="px-[20px] pt-[10px] text-[14px]">
<el-row class="py-[10px] items table-head-bg pl-[15px] mb-[10px]">
<el-col :span="12">
<el-col :span="18">
<span>{{ t("path") }}</span>
</el-col>
<el-col :span="6">
<el-col :span="3">
<span>{{ t("demand") }}</span>
</el-col>
<el-col :span="6">
<el-col :span="3">
<span>{{ t("status") }}</span>
</el-col>
</el-row>
<el-row class="pb-[10px] items pl-[15px]" v-for="(item, index) in installCheckResult.dir.is_readable" :key="index">
<el-col :span="12">
<span>{{ item.dir }}</span>
</el-col>
<el-col :span="6">
<span>{{ t("readable") }}</span>
</el-col>
<el-col :span="6">
<span v-if="item.status">
<el-icon color="green">
<Select />
</el-icon>
</span>
<span v-else>
<el-icon color="red">
<CloseBold />
</el-icon>
</span>
</el-col>
</el-row>
<el-row class="pb-[10px] items pl-[15px]" v-for="(item, index) in installCheckResult.dir.is_write" :key="index">
<el-col :span="12">
<span>{{ item.dir }}</span>
</el-col>
<el-col :span="6">
<span>{{ t("write") }}</span>
</el-col>
<el-col :span="6">
<span v-if="item.status">
<el-icon color="green">
<Select />
</el-icon>
</span>
<span v-else>
<el-icon color="red">
<CloseBold />
</el-icon>
</span>
</el-col>
</el-row>
<el-scrollbar style="height: calc(300px); overflow: auto">
<el-row class="pb-[10px] items pl-[15px]" v-for="(item, index) in installCheckResult.dir.is_readable" :key="index">
<el-col :span="18">
<span>{{ item.dir }}</span>
</el-col>
<el-col :span="3">
<span>{{ t("readable") }}</span>
</el-col>
<el-col :span="3" >
<span v-if="item.status">
<el-icon color="green">
<Select />
</el-icon>
</span>
<span v-else>
<el-icon color="red">
<CloseBold />
</el-icon>
</span>
</el-col>
</el-row>
<el-row class="pb-[10px] items pl-[15px]" v-for="(item, index) in installCheckResult.dir.is_write" :key="index">
<el-col :span="18">
<span>{{ item.dir }}</span>
</el-col>
<el-col :span="3">
<span>{{ t("write") }}</span>
</el-col>
<el-col :span="3">
<span v-if="item.status" class="text-right">
<el-icon color="green">
<Select />
</el-icon>
</span>
<span v-else>
<el-icon color="red">
<CloseBold />
</el-icon>
</span>
</el-col>
</el-row>
</el-scrollbar>
</div>
</div>
</div>
</el-scrollbar>
<!-- </el-scrollbar> -->
<div class="flex justify-end">
<el-tooltip effect="dark" :content="t('installTips')" placement="top">
<el-button :disabled="!installCheckResult.is_pass || cloudInstalling" :loading="localInstalling" @click="handleInstall">{{ t("localInstall") }}</el-button>
@ -310,15 +340,41 @@
</el-tooltip>
</div>
</div>
<div v-show="installStep == 2" class="h-[50vh] mt-[20px]">
<div v-show="installStep == 1 && !errorDialog" class="h-[50vh] mt-[20px]">
<terminal ref="terminalRef" :context="currAddon" :init-log="null" :show-header="false" :show-log-time="true" @exec-cmd="onExecCmd" />
</div>
<div v-show="installStep == 3" class="h-[50vh] mt-[20px] flex flex-col">
<el-result icon="success" :title="t('addonInstallSuccess')"></el-result>
<div v-show="installStep == 2" class="h-[50vh] mt-[20px] flex flex-col">
<!-- <el-result icon="success" :title="t('addonInstallSuccess')"></el-result> -->
<!-- 提示信息 -->
<div v-for="(item, index) in installAfterTips" class="mb-[10px]" :key="index">
<!-- <div v-for="(item, index) in installAfterTips" class="mb-[10px]" :key="index">
<el-alert :title="item" type="error" :closable="false" />
</div>
</div> -->
<el-result icon="success" :title="t('addonInstallSuccess')">
<template #icon>
<img src="@/app/assets/images/success_icon.png" alt="">
</template>
<template #extra>
<div v-for="(item, index) in installAfterTips" class="mb-[10px]" :key="index">
<div class="text-[16px] text-[#4F516D] mt-[5px]">{{ item }}</div>
</div>
<div class="text-[16px] text-[#9699B6] mt-[10px]" v-if="upgradeDuration>0">本次安装用时{{ formatUpgradeDuration }}</div>
<div class="mt-[20px]">
<el-button @click="handleBack()" v-if="installType=='cloud'" class="!w-[90px]">返回</el-button>
<el-button @click="installShowDialog=false" type="primary" class="!w-[90px]">完成</el-button>
</div>
</template>
</el-result>
</div>
<div class="mt-[50px]" v-show="errorDialog">
<el-result icon="error" :title="t('安装失败')" :sub-title="errorMsg">
<template #icon>
<img src="@/app/assets/images/error_icon.png" alt="">
</template>
<template #extra>
<el-button @click="handleBack()" v-if="installType=='cloud'" class="!w-[90px]">错误信息</el-button>
<el-button @click="installShowDialog=false" type="primary" class="!w-[90px]">完成</el-button>
</template>
</el-result>
</div>
</el-dialog>
@ -329,24 +385,24 @@
<p class="pt-[20px] pl-[20px]">{{ t("dirPermission") }}</p>
<div class="px-[20px] pt-[10px] text-[14px]">
<el-row class="py-[10px] items table-head-bg pl-[15px] mb-[10px]">
<el-col :span="12">
<el-col :span="18">
<span>{{ t("path") }}</span>
</el-col>
<el-col :span="6">
<el-col :span="3">
<span>{{ t("demand") }}</span>
</el-col>
<el-col :span="6">
<el-col :span="3">
<span>{{ t("status") }}</span>
</el-col>
</el-row>
<el-row class="pb-[10px] items pl-[15px]" v-for="(item, index) in uninstallCheckResult.dir.is_readable" :key="index">
<el-col :span="12">
<el-col :span="18">
<span>{{ item.dir }}</span>
</el-col>
<el-col :span="6">
<el-col :span="3">
<span>{{ t("readable") }}</span>
</el-col>
<el-col :span="6">
<el-col :span="3">
<span v-if="item.status">
<el-icon color="green">
<Select />
@ -360,13 +416,13 @@
</el-col>
</el-row>
<el-row class="pb-[10px] items pl-[15px]" v-for="(item, index) in uninstallCheckResult.dir.is_write" :key="index">
<el-col :span="12">
<el-col :span="18">
<span>{{ item.dir }}</span>
</el-col>
<el-col :span="6">
<el-col :span="3">
<span>{{ t("write") }}</span>
</el-col>
<el-col :span="6">
<el-col :span="3" >
<span v-if="item.status">
<el-icon color="green">
<Select />
@ -404,7 +460,7 @@
</template>
<script lang="ts" setup>
import { ref, reactive, watch, h } from 'vue'
import { ref, reactive, watch, h ,computed } from 'vue'
import { t } from '@/lang'
import {
getAddonLocal,
@ -415,7 +471,8 @@ import {
getAddonInstalltask,
getAddonCloudInstallLog,
preUninstallCheck,
cancelInstall
cancelInstall,
getAddonInit
} from '@/app/api/addon'
import { deleteAddonDevelop } from '@/app/api/tools'
import { downloadVersion, getAuthInfo, setAuthInfo } from '@/app/api/module'
@ -446,8 +503,20 @@ const frameworkVersion = ref('')
const upgradeLogRef = ref<any>(null)
getVersions().then((res) => {
frameworkVersion.value = res.data.version.version
})
const treeProps = reactive({
checkStrictly: false
})
const typeList = ref({})
const getAddonInitFn = () => {
getAddonInit().then((res) => {
typeList.value = res.data.type_list
})
}
getAddonInitFn()
const currDownData = ref()
const downEventHintFn = () => {
downEvent(currDownData.value, true)
@ -491,6 +560,7 @@ getAuthInfo().then((res) => {
* 本地下载的插件列表
*/
const search_name = ref('')
const search_type = ref('')
//
const info = ref({
installed: [],
@ -498,19 +568,80 @@ const info = ref({
all: [],
recentlyUpdated: []
})
const query = () => {
if (search_name.value == '' || search_name.value == null) {
info.value.installed = localList.value.installed
info.value.uninstalled = localList.value.uninstalled
info.value.all = localList.value.all
info.value.recentlyUpdated = localList.value.recentlyUpdated
return false
}
info.value.installed = localList.value.installed.filter((el: any) => el.title.indexOf(search_name.value) != -1)
info.value.uninstalled = localList.value.uninstalled.filter((el: any) => el.title.indexOf(search_name.value) != -1)
info.value.all = localList.value.all.filter((el: any) => el.title.indexOf(search_name.value) != -1)
info.value.recentlyUpdated = localList.value.recentlyUpdated.filter((el: any) => el.title.indexOf(search_name.value) != -1)
const buildInfo = (list: any[]) => {
const map = new Map()
const result: any[] = []
// map children
list.forEach(item => {
map.set(item.key, { ...item, children: [] })
})
//
list.forEach(item => {
if (item.support_app && map.has(item.support_app)) {
const parent = map.get(item.support_app)
parent.children.push(map.get(item.key)) //
}
})
//
map.forEach((item: any) => {
if (!item.support_app || !map.has(item.support_app)) {
result.push(item)
}
})
return result
}
// const query = () => {
// if (search_name.value == '' || search_name.value == null) {
// info.value.installed = buildInfo(localList.value.installed)
// info.value.uninstalled = buildInfo(localList.value.uninstalled)
// info.value.all = buildInfo(localList.value.all)
// info.value.recentlyUpdated = buildInfo(localList.value.recentlyUpdated)
// return false
// }
// const filteredInstalled = localList.value.installed.filter((el: any) => el.title.indexOf(search_name.value) != -1)
// const filteredUninstalled = localList.value.uninstalled.filter((el: any) => el.title.indexOf(search_name.value) != -1)
// const filteredAll = localList.value.all.filter((el: any) => el.title.indexOf(search_name.value) != -1)
// const filteredRecentlyUpdated = localList.value.recentlyUpdated.filter((el: any) => el.title.indexOf(search_name.value) != -1)
// //
// info.value.installed = buildInfo(filteredInstalled)
// info.value.uninstalled = buildInfo(filteredUninstalled)
// info.value.all = buildInfo(filteredAll)
// info.value.recentlyUpdated = buildInfo(filteredRecentlyUpdated)
// }
const query = () => {
const name = search_name.value
const type = search_type.value
//
if ((!name || name === '') && (type === '' || type == null)) {
info.value.installed = buildInfo(localList.value.installed)
info.value.uninstalled = buildInfo(localList.value.uninstalled)
info.value.all = buildInfo(localList.value.all)
info.value.recentlyUpdated = buildInfo(localList.value.recentlyUpdated)
return
}
//
const filterList = (list: any[]) => {
return list.filter((el: any) => {
const matchName = !name || el.title.includes(name)
const matchType = !type || el.type === type
return matchName && matchType
})
}
info.value.installed = buildInfo(filterList(localList.value.installed))
info.value.uninstalled = buildInfo(filterList(localList.value.uninstalled))
info.value.all = buildInfo(filterList(localList.value.all))
info.value.recentlyUpdated = buildInfo(filterList(localList.value.recentlyUpdated))
}
const localList = ref({
installed: [],
uninstalled: [],
@ -578,7 +709,7 @@ const currAddon = ref('')
const installShowDialog = ref(false)
//
const installStep = ref(1)
const installStep = ref(0)
//
const installCheckResult = ref({})
@ -615,7 +746,10 @@ const installAddonFn = (key: string) => {
currAddon.value = key
preInstallCheck(key).then((res) => {
installStep.value = 1
installStep.value = 0
isBack.value = false
errorDialog.value = false
installType.value = ''
installShowDialog.value = true
installAfterTips.value = []
installCheckResult.value = res.data
@ -626,10 +760,20 @@ const installAddonFn = (key: string) => {
/**
* 获取正在进行的安装任务
*/
const upgradeStartTime = ref<number | null>(null)
const upgradeDuration = ref(0) //
let upgradeTimer: ReturnType<typeof setInterval> | null = null
let notificationEl = null
const getInstallTask = (first: boolean = true) => {
getAddonInstalltask().then((res) => {
if (res.data) {
upgradeStartTime.value = Date.now()
upgradeDuration.value = 0
if (upgradeTimer) clearInterval(upgradeTimer)
upgradeTimer = setInterval(() => {
upgradeDuration.value++
}, 1000)
if (first) {
installLog = []
currAddon.value = res.data.addon
@ -647,7 +791,14 @@ const getInstallTask = (first: boolean = true) => {
}
}
if (res.data.error) {
ElMessage({ message: '插件安装失败', type: 'error', duration: 5000 })
terminalRef.value.pushMessage({ content: res.data.error, class: 'error' })
errorMsg.value = res.data.error
errorDialog.value = true
if (upgradeTimer) {
clearInterval(upgradeTimer)
upgradeTimer = null
}
// ElMessage({ message: '', type: 'error', duration: 5000 })
return
}
if (res.data.mode == 'cloud') {
@ -656,9 +807,14 @@ const getInstallTask = (first: boolean = true) => {
setTimeout(() => {
getInstallTask(false)
}, 2000)
} else {
if (!first) {
installStep.value = 3
installStep.value = 2
if (upgradeTimer) {
clearInterval(upgradeTimer)
upgradeTimer = null
}
localListFn()
userStore.clearRouters()
notificationEl.close()
@ -671,21 +827,52 @@ const getInstallTask = (first: boolean = true) => {
getInstallTask()
const isBack = ref(false)
const handleBack = () => {
isBack.value = true
installStep.value = 1
errorDialog.value = false
}
const formatUpgradeDuration = computed(() => {
const s = upgradeDuration.value
const h = Math.floor(s / 3600)
const m = Math.floor((s % 3600) / 60)
const sec = s % 60
return [
h > 0 ? `${h}小时` : '',
m > 0 ? `${m}分钟` : '',
`${sec}`
].filter(Boolean).join('')
})
const checkInstallTask = () => {
installShowDialog.value = true
installStep.value = 2
installStep.value = 1
}
const localInstalling = ref(false)
/**
* 安装插件
*/
const installType = ref('')
const handleInstall = () => {
if (!installCheckResult.value.is_pass || localInstalling.value) return
installType.value = 'local'
localInstalling.value = true
upgradeStartTime.value = Date.now()
upgradeDuration.value = 0
if (upgradeTimer) clearInterval(upgradeTimer)
upgradeTimer = setInterval(() => {
upgradeDuration.value++
}, 1000)
installAddon({ addon: currAddon.value }).then((res) => {
installStep.value = 3
installStep.value = 2
if (upgradeTimer) {
clearInterval(upgradeTimer)
upgradeTimer = null
}
localListFn()
localInstalling.value = false
if (res.data.length) installAfterTips.value = res.data
@ -707,10 +894,11 @@ const handleCloudInstall = () => {
if (!installCheckResult.value.is_pass || cloudInstalling.value) return
cloudInstalling.value = true
installType.value = 'cloud'
cloudInstallAddon({ addon: currAddon.value })
.then((res) => {
installStep.value = 2
installStep.value = 1
terminalRef.value.execute('clear')
terminalRef.value.execute('开始安装插件')
getInstallTask()
@ -734,7 +922,8 @@ const authElMessageBox = () => {
}
})
}
const errorDialog = ref(false)
const errorMsg = ref('')
let installLog: string[] = []
const getCloudInstallLog = () => {
getAddonCloudInstallLog(currAddon.value).then((res) => {
@ -835,16 +1024,23 @@ const market = () => {
* @param done
*/
const installShowDialogClose = (done: () => {}) => {
if (installStep.value == 2) {
if (installStep.value == 1 && !isBack.value && !errorDialog.value) {
ElMessageBox.confirm(t('installShowDialogCloseTips'), t('warning'), {
confirmButtonText: t('confirm'),
cancelButtonText: t('cancel'),
type: 'warning'
}).then(() => {
cancelInstall(currAddon.value)
if (upgradeTimer) {
clearInterval(upgradeTimer)
upgradeTimer = null
}
isBack.value = false
installType.value = ''
errorDialog.value = false
done()
})
} else if (installStep.value == 3) {
} else if (installStep.value == 2) {
activeNameTabFn('installed')
location.reload()
} else {
@ -866,7 +1062,6 @@ const getAddonDetailFn = (data: any) => {
const upgradeKey = ref<string>('')
const updateInformationFn = (data: any) => {
// updateInformationDialog.value = true
upgradeKey.value = data.key
upgradeLogRef.value?.open()
}
@ -929,6 +1124,10 @@ const goRouter = () => {
window.open('https://www.niucloud.com/app')
}
const cloudBuildCheckDirFn = () => {
window.open('https://doc.niucloud.com/v6.html?keywords=/chang-jian-wen-ti-chu-li/er-shi-wu-3001-sheng-7ea7-yun-bian-yi-mu-lu-du-xie-quan-xian-zhuang-tai-bu-tong-guo-ru-he-chu-li')
}
const deleteAddonFn = (key: string) => {
ElMessageBox.confirm(t('deleteAddonTips'), t('warning'), {
confirmButtonText: t('confirm'),
@ -942,7 +1141,7 @@ const deleteAddonFn = (key: string) => {
}
const versionJudge = (row: any) => {
if (!row.support_version) return true
if (!row.support_version) return false
const supportVersionApp = row.support_version.split('.')
const frameworkVersionArr = frameworkVersion.value.split('.')
if (parseFloat(`${ supportVersionApp[0] }.${ supportVersionApp[1] }`) < parseFloat(`${ frameworkVersionArr[0] }.${ frameworkVersionArr[1] }`)) return true
@ -959,8 +1158,13 @@ const batchUpgrade = () => {
ElMessage({ message: '请先勾选要升级的插件', type: 'error', duration: 5000 })
return
}
upgradeAddonFn(batchUpgradeApp.toString())
}
const visibleRowKeys = computed(() => {
return new Set((info.value[activeName.value] || []).map(row => row.key));
});
</script>
<style lang="scss" scoped>
@ -996,6 +1200,249 @@ html.dark .table-head-bg {
display: none !important;
}
}
:deep(.hide-expand .el-table__expand-icon>.el-icon){
visibility: hidden;
pointer-events: none;
}
:deep(.el-input__wrapper){
box-shadow: none !important;
border-radius: 4px !important;
border: 1px solid #D1D5DB !important;
height: 32px !important;
}
:deep(.el-select__wrapper){
box-shadow: none !important;
border-radius: 4px !important;
border: 1px solid #D1D5DB !important;
height: 32px !important;
}
:deep(.el-button){
border-radius: 4px !important;
}
/* 设置 el-select 的 placeholder 颜色 */
:deep(.search-form .el-select__placeholder.is-transparent) {
color: #C4C7DA;
font-size: 12px;
}
/* 设置 el-select 选中后的颜色 */
:deep(.search-form .el-select__placeholder) {
color: #4F516D;
font-size: 12px;
}
/* 设置 el-input 的 placeholder 颜色 */
:deep(.search-form .el-input__inner::placeholder) {
color: #C4C7DA;
font-size: 12px;
}
/* 设置 el-input 输入内容后的颜色 */
:deep(.search-form .el-input__inner) {
color: #4F516D;
font-size: 12px;
}
/* 设置 el-date-picker 的 placeholder 颜色 */
:deep(.search-form .el-date-editor .el-range-input::placeholder) {
color: #C4C7DA;
font-size: 12px;
}
/* 设置 el-date-picker 的输入内容颜色 */
:deep(.search-form .el-date-editor .el-range-input) {
color: #4F516D;
font-size: 12px;
}
:deep(.el-table tr td:first-child) {
border-bottom: none;
// background-color: inherit !important;
height: 100px;
}
:deep(.el-table__body tr:hover td:first-child) {
// border-bottom: 1px solid var(--el-table-border-color);
}
:deep(.el-table__body tr) {
position: relative;
}
:deep(.el-table__body td:first-child::before) {
opacity: 0;
content: '';
position: absolute;
top: -1px;
left: 0;
right: 0;
height: 1px;
background-color: var(--el-table-border-color);
// transition: opacity 0.2s;
z-index: 1;
}
:deep(.el-table__body td:first-child) {
position: relative;
}
:deep(.el-table__body tr:hover td:first-child::before) {
opacity: 1;
}
/* 创建伪元素当作 hover 边框线,默认隐藏 */
:deep(.el-table__body td:first-child::after) {
content: '';
position: absolute;
left: 0;
right: 0;
bottom: 0;
height: 1px;
background-color: var(--el-table-border-color);
opacity: 0;
// transition: opacity 0.2s ease;
pointer-events: none;
z-index: 1;
}
/* 悬浮时显示这条伪边框线 */
:deep(.el-table__body tr:hover td:first-child::after) {
opacity: 1;
}
:deep(.el-table__fixed-body-wrapper .el-table__row .el-table__cell) {
overflow: visible;
}
:deep(.el-table .el-table__expand-icon){
position: relative;
top: 12.5px;
left: -13px;
z-index: 99;
margin: 3px;
overflow: hidden;
}
:deep(.el-table__fixed-body-wrapper .el-table__cell:first-child) {
background-color: inherit !important; /* 从行继承背景色 */
}
:deep(.el-table tr td:nth-child(1)::before){
overflow: hidden !important;
}
:deep(.tree-child-cell) {
position: relative;
height: 100%;
}
:deep(.el-table .cell){
overflow: visible !important;
}
:deep(.tree-child-cell.is-tree-child::before) {
content: '';
position: absolute;
left: -5px;
top: -99px;
bottom: 0;
width: 1px;
height: 100px;
background-color: #F5F5F5;
}
:deep(.tree-child-cell.is-tree-child::after) {
content: '';
position: absolute;
top: 0;
left: -5px;
width: 8px;
height: 1px;
background-color: #F5F5F5;
}
:deep(.hidden-selection-column .cell) {
display: none;
}
:deep(.el-dialog__title){
font-size: 20px;
font-weight: bold;
}
:deep(.el-result__title p){
font-size: 25px;
color: #1D1F3A;
font-weight: 500;
}
:deep(.el-result__subtitle p){
font-size: 15px;
color: #4F516D;
font-weight: 500;
word-break: break-all;
text-overflow: ellipsis;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
::v-deep .number-of-steps {
.el-step__line {
margin: 0 25px;
background: #dddddd;
}
.el-step__head {
margin-top: 10px;
}
.is-success {
color: var(--el-color-primary);
border-color: var(--el-color-primary);
.el-step__icon {
background: var(--el-color-primary);
color: #fff;
// box-shadow: 0 0 0 4px var(--el-color-primary-light-9);
i {
color: #fff;
}
}
.el-step__line {
margin: 0 25px;
background: var(--el-color-primary);
}
}
.is-finish {
color: var(--el-color-primary);
border-color: var(--el-color-primary);
.el-step__icon {
background: var(--el-color-primary)!important;
color: #fff !important;
// box-shadow: 0 0 0 4px var(--el-color-primary-light-9);
i {
color: #fff;
}
}
.el-step__line {
margin: 0 25px;
background: var(--el-color-primary);
}
}
.is-process {
color: var(--el-color-primary);
font-weight: inherit;
// font-size: 18px;
.el-step__icon {
padding: 10px;
border: 1px solid var(--el-color-primary);
background: var(--el-color-primary)!important;
color: #fff !important;
// box-shadow: 0 0 0 4px var(--el-color-primary-light-9);
}
}
.is-wait {
color: #333;
}
}
</style>
<style>

View File

@ -53,7 +53,7 @@
</div>
</el-card>
<el-dialog v-model="showDialog" :title="t('addVerifier')" width="500px" :destroy-on-close="true">
<el-dialog v-model="showDialog" :title="formData.id ? t('editVerifier') : t('addVerifier')" width="500px" :destroy-on-close="true">
<el-form :model="formData" label-width="90px" ref="formRef" :rules="formRules" class="page-form" v-loading="addLoading">
<el-form-item :label="t('member')" prop="member_id">
<el-select v-model="formData.member_id" filterable remote reserve-keyword clearable @focus="handleSelectFocus" :disabled="isEditMode" :placeholder="t('searchPlaceholder')" :remote-method="searchMember" :loading="searchLoading" class="input-width">

View File

@ -225,6 +225,7 @@
<script lang="ts" setup>
import { ref, reactive, toRaw, watch, inject } from 'vue'
import { t } from '@/lang'
import { ArrowLeft } from "@element-plus/icons-vue"
import { img } from '@/utils/common'
import { useRoute, useRouter } from 'vue-router'
import { cloneDeep } from 'lodash-es'

View File

@ -118,6 +118,7 @@
<script lang="ts" setup>
import { ref, reactive, computed } from 'vue'
import { t } from '@/lang'
import { ArrowLeft } from "@element-plus/icons-vue"
import { FormInstance, ElMessage } from 'element-plus'
import { useRoute, useRouter } from 'vue-router'
import { deepClone } from '@/utils/common';

View File

@ -118,6 +118,7 @@
<script lang="ts" setup>
import { ref, reactive, computed } from 'vue'
import { t } from '@/lang'
import { ArrowLeft } from "@element-plus/icons-vue"
import { FormInstance, ElMessage } from 'element-plus'
import { useRoute, useRouter } from 'vue-router'
import { getPrinterType, addPrinterTemplate, editPrinterTemplate, getPrinterTemplateInfo } from '@/app/api/printer'

View File

@ -0,0 +1,118 @@
<template>
<el-dialog v-model="showDialog" :title="t('aliSms')" width="580px" :destroy-on-close="true">
<el-form :model="formData" label-width="140px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
<el-form-item :label="t('isUse')">
<el-radio-group v-model="formData.is_use">
<el-radio :label="1">{{ t('startUsing') }}</el-radio>
<el-radio :label="0">{{ t('statusDeactivate') }}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item :label="t('aliSign')" prop="sign">
<el-input v-model.trim="formData.sign" :placeholder="t('aliSignPlaceholder')" class="input-width" show-word-limit clearable />
</el-form-item>
<el-form-item :label="t('aliAppKey')" prop="app_key">
<el-input v-model.trim="formData.app_key" :placeholder="t('aliAppKeyPlaceholder')" class="input-width" clearable />
</el-form-item>
<el-form-item :label="t('aliSecretKey')" prop="secret_key">
<el-input v-model.trim="formData.secret_key" :placeholder="t('aliSecretKeyPlaceholder')" class="input-width" clearable />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button>
<el-button type="primary" :loading="loading" @click="confirm(formRef)">{{t('confirm')}}</el-button>
</span>
</template>
</el-dialog>
</template>
<script lang="ts" setup>
import { ref, reactive, computed } from 'vue'
import { t } from '@/lang'
import type { FormInstance } from 'element-plus'
import { getSmsInfo, editSms } from '@/app/api/notice'
const showDialog = ref(false)
const loading = ref(true)
/**
* 表单数据
*/
const initialFormData = {
sms_type: '',
sign: '',
app_key: '',
secret_key: '',
is_use: ''
}
const formData: Record<string, any> = reactive({ ...initialFormData })
const formRef = ref<FormInstance>()
//
const formRules = computed(() => {
return {
sign: [
{ required: true, message: t('aliSignPlaceholder'), trigger: 'blur' }
],
app_key: [
{ required: true, message: t('aliAppKeyPlaceholder'), trigger: 'blur' }
],
secret_key: [
{ required: true, message: t('aliSecretKeyPlaceholder'), trigger: 'blur' }
]
}
})
const emit = defineEmits(['complete'])
/**
* 确认
* @param formEl
*/
const confirm = async (formEl: FormInstance | undefined) => {
if (loading.value || !formEl) return
await formEl.validate(async (valid) => {
if (valid) {
loading.value = true
const data = formData
editSms(data).then(res => {
loading.value = false
showDialog.value = false
emit('complete')
}).catch(() => {
loading.value = false
// showDialog.value = false
})
}
})
}
const setFormData = async (row: any = null) => {
loading.value = true
Object.assign(formData, initialFormData)
if (row) {
const data = await (await getSmsInfo(row.sms_type)).data
Object.keys(formData).forEach((key: string) => {
if (data[key] != undefined) formData[key] = data[key]
if (data.params[key] != undefined) formData[key] = data.params[key].value
})
}
loading.value = false
}
defineExpose({
showDialog,
setFormData
})
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,522 @@
<template>
<el-card class="box-card !border-none" shadow="never" v-loading="loading">
<div v-if="type=='login'" >
<div class="bg-[var(--el-color-primary-light-9)] p-2 text-[14px] rounded-[6px]">
<span class="">还未注册牛云短信?</span>
<span @click="toRegister" class="cursor-pointer text-primary">去注册</span>
</div>
<el-form :model="formData" label-width="150px" ref="formRef" :rules="formRules" class="page-form mt-[20px]">
<el-form-item label="用户名" prop="username">
<el-input placeholder="请输入用户名" class="input-width" v-model="formData.username" clearable maxlength="50" autocomplete="off" />
</el-form-item>
<el-form-item label="密码" prop="password">
<el-input placeholder="请输入密码" type="password" show-password show-word-limit maxlength="16" class="input-width" autocomplete="new-password" v-model="formData.password" clearable />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="login">登录</el-button>
<el-button @click="editPass()">忘记密码</el-button>
<el-button @click="back()" v-if="props.isLogin">返回</el-button>
</el-form-item>
</el-form>
</div>
<div v-if="type=='register'" >
<div class="bg-[var(--el-color-primary-light-9)] p-2 text-[14px] rounded-[6px]">
<span class="">已有账号</span>
<span @click="type='login'" class="cursor-pointer text-primary">去登录</span>
</div>
<el-form :model="registerFormData" label-width="150px" ref="registerFormRef" :rules="registerFormRules" class="page-form mt-[20px]">
<h3 class="panel-title !text-[14px]">{{ t('基本信息') }}</h3>
<el-form-item label="用户名" prop="username">
<el-input placeholder="请输入用户名" class="input-width" autocomplete="off" maxlength="50" show-word-limit v-model="registerFormData.username" clearable />
</el-form-item>
<div class="mb-[10px] text-[12px] ml-[150px] text-[#999] leading-[20px]">子账户用户名仅支持6~50位英文+数字组合</div>
<el-form-item label="公司名称" prop="company">
<el-input placeholder="请输入公司名称" class="input-width" maxlength="50" show-word-limit v-model="registerFormData.company" clearable />
</el-form-item>
<el-form-item label="手机号" prop="mobile">
<el-input placeholder="请输入手机号" class="input-width" maxlength="11" show-word-limit v-model="registerFormData.mobile" clearable />
</el-form-item>
<el-form-item label="验证码" prop="captcha_code">
<div class="flex items-center">
<el-input placeholder="请输入验证码" class="input-width" maxlength="4" show-word-limit v-model="registerFormData.captcha_code" clearable />
<img :src="registerFormData.captcha_img" alt="验证码" class="w-[100px] h-[32px] cursor-pointer ml-[10px]" @click="getSmsCaptchaFn" />
</div>
</el-form-item>
<el-form-item label="动态码" prop="code">
<div class="flex items-center">
<el-input placeholder="请输入动态码" class="input-width" maxlength="4" show-word-limit v-model="registerFormData.code" clearable />
<el-button class="ml-[10px]" @click="getSmsSendFn" :disabled="countdown > 0" :loading="sending">
{{ countdown > 0 ? `${countdown}秒后重新获取` : '获取动态码' }}
</el-button>
</div>
</el-form-item>
<el-form-item label="初始密码" prop="password">
<el-input placeholder="请输入初始密码" type="password" autocomplete="new-password" maxlength="16" show-password class="input-width" v-model="registerFormData.password" clearable />
</el-form-item>
<div class="mb-[10px] text-[12px] ml-[150px] text-[#999] leading-[20px]">密码由数字大小写字母组成密码长度8-16</div>
<el-form-item :label="t('默认签名')" prop="signature">
<el-input v-model="registerFormData.signature" placeholder="请输入默认签名" class="input-width" maxlength="20" show-word-limit clearable />
</el-form-item>
<div class="ml-[150px] text-[12px] text-[#999] leading-[20px]">必须由包裹例如test</div>
<div class="my-[5px] ml-[150px] text-[12px] text-[#999] leading-[20px]">字数要求在2-20个字符不能使用空格和特殊符号 - + = * & % # @ ~;</div>
<el-form-item label="备注" prop="remark">
<el-input placeholder="请输入备注" class="input-width" type="textarea" maxlength="50" show-word-limit v-model="registerFormData.remark" clearable />
</el-form-item>
<h3 class="panel-title !text-[14px">{{ t('实名信息') }}</h3>
<el-form-item :label="t('短信示例内容')" prop="contentExample">
<el-input v-model="registerFormData.contentExample" placeholder="请输入短信示例内容" clearable maxlength="50" show-word-limit class="input-width" />
</el-form-item>
<el-form-item :label="t('企业名称')" prop="companyName">
<el-input v-model="registerFormData.companyName" placeholder="请输入企业名称" clearable maxlength="20" show-word-limit class="input-width" />
</el-form-item>
<el-form-item :label="t('社会统一信用代码')" prop="creditCode">
<el-input v-model="registerFormData.creditCode" placeholder="请输入社会统一信用代码" clearable maxlength="20" show-word-limit class="input-width" />
</el-form-item>
<el-form-item :label="t('法人姓名')" prop="legalPerson">
<el-input v-model="registerFormData.legalPerson" placeholder="请输入法人姓名" clearable maxlength="20" show-word-limit class="input-width" />
</el-form-item>
<el-form-item :label="t('经办人姓名')" prop="principalName">
<el-input v-model="registerFormData.principalName" placeholder="请输入经办人姓名" clearable maxlength="20" show-word-limit class="input-width" />
</el-form-item>
<el-form-item :label="t('经办人手机号')" prop="principalMobile">
<el-input v-model="registerFormData.principalMobile" placeholder="请输入经办人手机号" clearable maxlength="20" show-word-limit class="input-width" />
</el-form-item>
<el-form-item :label="t('经办人身份证')" prop="principalIdCard">
<el-input v-model="registerFormData.principalIdCard" placeholder="请输入经办人身份证" clearable maxlength="18" show-word-limit class="input-width" />
</el-form-item>
<el-form-item :label="t('签名来源')">
<el-radio-group v-model="registerFormData.signSource" >
<el-radio v-for="item in signCofig.signsourceList" :key="item.type" :label="item.type" >{{item.name}}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item :label="t('签名类型')">
<el-radio-group v-model="registerFormData.signType">
<el-radio v-for="item in signCofig.signTypeList" :key="item.type" :label="item.type" >{{item.name}}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item :label="t('上传图片')" prop="imgUrl">
<upload-image v-model="registerFormData.imgUrl" :limit="1" />
</el-form-item>
<div class="ml-[150px] text-[12px] text-[#999] leading-[20px]">当签名来源为商标APP小程序事业单位简称或企业名称简称时需必填此字段</div>
<div class="my-[5px] ml-[150px] text-[12px] text-[#999] leading-[20px]">当签名来源为事业单位全称或企业名称全称时选填此字段</div>
</el-form>
<div class="fixed-footer-wrap">
<div class="fixed-footer">
<el-button type="primary" @click="register">注册</el-button>
</div>
</div>
</div>
<div v-if="type=='password'">
<div class="bg-[var(--el-color-primary-light-9)] p-2 text-[14px] rounded-[6px]">
<span class="text-primary">忘记密码快去修改</span>
</div>
<el-form :model="changeFormData" label-width="150px" ref="changeFormRef" :rules="changeFormRules" class="page-form mt-[20px]">
<el-form-item label="手机号" prop="mobile">
<el-input placeholder="请输入手机号" class="input-width" maxlength="11" show-word-limit v-model="changeFormData.mobile" clearable />
</el-form-item>
<el-form-item label="验证码" prop="captcha_code">
<div class="flex items-center">
<el-input placeholder="请输入验证码" class="input-width" maxlength="4" show-word-limit v-model="changeFormData.captcha_code" clearable />
<img :src="changeFormData.captcha_img" alt="验证码" class="w-[100px] h-[32px] cursor-pointer ml-[10px]" @click="getSmsCaptchaFn" />
</div>
</el-form-item>
<el-form-item label="动态码" prop="code">
<div class="flex items-center">
<el-input placeholder="请输入动态码" class="input-width" maxlength="4" show-word-limit v-model="changeFormData.code" clearable />
<el-button class="ml-[10px]" @click="getSmsSendFn" :disabled="countdown > 0" :loading="sending">
{{ countdown > 0 ? `${countdown}秒后重新获取` : '获取动态码' }}
</el-button>
</div>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="reset()">重置密码</el-button>
<el-button @click="type='login'">返回</el-button>
</el-form-item>
</el-form>
</div>
</el-card>
</template>
<script lang="ts" setup>
import { ref ,computed,reactive} from 'vue'
import { loginAccount,getSmsCaptcha,getSmsSend,resetPassword,registerAccount ,getSmsSignConfig} from '@/app/api/notice'
import { t } from "@/lang";
const props = defineProps({
info:{
type: Object,
default: () => ({})
},
isLogin:{
type: Boolean,
default: false
}
})
const loading = ref(false)
const formRef = ref()
const type = ref('login')
const emit = defineEmits(['complete'])
const formData = ref({
username: '',
password: ''
})
const isBack = computed(() => {
return !!props.info && Object.keys(props.info).length > 0;
})
const formRules = computed(() => {
return {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' }
]
}
})
const login = async () => {
await formRef.value?.validate(async (valid) => {
if (valid) {
loginAccount(formData.value).then((res) => {
emit('complete')
})
}
})
}
const back = () => {
emit('complete')
}
//
const signCofig = reactive({
signTypeList: [],
signsourceList:[]
})
const getSmsSignConfigFn = ()=> {
getSmsSignConfig().then(res => {
signCofig.signTypeList = res.data.sign_type_list
signCofig.signsourceList = res.data.sign_source_list
registerFormData.value.signSource = res.data.sign_source_list[0].type
registerFormData.value.signType = res.data.sign_type_list[0].type
})
}
getSmsSignConfigFn()
const registerFormData = ref({
code: '',
key: '',
remark: '',
username: '',
password: '',
company: '',
mobile: '',
captcha_key : '',
captcha_code: '',
captcha_img: '',
imgUrl: '',
contentExample: '',
signType: '',
signSource: '',
principalIdCard: '',
principalName: '',
principalMobile: '',
legalPerson: '',
creditCode: '',
companyName: '',
signature: ''
})
const captchaType = ref('login')
const toRegister = async () => {
captchaType.value = 'register'
loading.value = true
const success = await getSmsCaptchaFn()
if (success) {
registerFormData.value.username = ''
registerFormData.value.password = ''
type.value = 'register'
loading.value = false
}else {
loading.value = false
}
}
const registerFormRef = ref()
const registerFormRules = computed(() => {
return {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{
pattern: /^[A-Za-z0-9]{6,50}$/,
message: '用户名格式不正确',
trigger: 'blur'
}
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{
pattern: /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])[A-Za-z\d]{8,16}$/,
message: '密码格式不正确',
trigger: 'blur'
}
],
mobile: [
{ required: true, message: '请输入手机号', trigger: 'blur' },
],
captcha_code: [
{ required: true, message: '请输入验证码', trigger: 'blur' },
],
code: [
{ required: true, message: '请输入动态码', trigger: 'blur' },
],
company:[
{ required: true, message: '请输入公司名称', trigger: 'blur' },
],
signature: [
{ required: true, message: '请输入短信签名', trigger: 'blur' },
{
validator: (rule, value, callback) => {
const singleBracketValid = /^【[^【】]*】$/.test(value);
if (!singleBracketValid) {
return callback(new Error('短信签名必须被【】包裹'));
}
const content = value.slice(1, -1);
const lengthValid = content.length >= 2 && content.length <= 20;
if (!lengthValid) {
return callback(new Error('短信签名内容需在 2-20 个字符之间'));
}
const invalidChars = /[\s\-+=*&%#@~;]/;
if (invalidChars.test(content)) {
return callback(new Error('短信签名不能包含空格或特殊字符 - + = * & % # @ ~ ;'));
}
callback();
},
trigger: 'blur'
}
],
principalMobile:[
{ required: true, message: '请输入经办人手机号', trigger: 'blur' },
{ validator: phoneVerify, trigger: 'blur' }
],
companyName: [
{ required: true, message: '请输入企业名称', trigger: 'blur' },
],
contentExample: [
{ required: true, message: '请输入短信示例内容', trigger: 'blur' },
],
creditCode: [
{ required: true, message: '请输入社会统一信用代码', trigger: 'blur' },
],
legalPerson: [
{ required: true, message: '请输入法人姓名', trigger: 'blur' },
],
principalName: [
{ required: true, message: '请输入经办人姓名', trigger: 'blur' },
],
principalIdCard: [
{ required: true, message: '请输入经办人身份证', trigger: 'blur' },
{ validator: idCardVerify, trigger: 'blur' }
],
imgUrl: [
{
validator: (rule, value, callback) => {
const needImage = [3, 4, 5].includes(registerFormData.value.signSource) || registerFormData.value.signType == 1;
if (needImage) {
if (!value || value.length === 0) {
callback(new Error('请上传图片'));
} else {
callback();
}
} else {
callback(); //
}
},
trigger: 'blur'
}
]
};
});
const idCardVerify = (rule: any, value: any, callback: any) => {
if (value && !/^[1-9]\d{5}(19|20)\d{2}((0\d)|(1[0-2]))(([0-2]\d)|3[0-1])\d{3}([0-9Xx])$/.test(value)) {
callback(new Error(t('请输入正确的身份证号码')))
} else {
callback()
}
}
const phoneVerify = (rule: any, value: any, callback: any) => {
if (value && !/^1[3-9]\d{9}$/.test(value)) {
callback(new Error(t('请输入正确的手机号码')))
} else {
callback()
}
}
const register = async () => {
await registerFormRef.value?.validate(async (valid) => {
if (valid) {
const { captcha_key, captcha_code, captcha_img, ...params } = registerFormData.value;
registerAccount(params).then((res) => {
type.value='login'
}).catch((err) => {
getSmsCaptchaFn()
})
}
});
}
//
const changeFormRef = ref()
const changeFormData = ref({
mobile: '',
captcha_key : '',
captcha_code: '',
captcha_img: '',
code: '',
key: ''
})
const getSmsCaptchaFn = async () => {
try {
const res = await getSmsCaptcha()
if (captchaType.value === 'register') {
registerFormData.value.captcha_key = res.data.captcha_key
registerFormData.value.captcha_img = res.data.img
} else if (captchaType.value === 'password') {
changeFormData.value.captcha_key = res.data.captcha_key
changeFormData.value.captcha_img = res.data.img
}
return true //
} catch (error) {
console.error('获取验证码失败', error)
return false //
}
}
const sending = ref(false); //
const countdown = ref(0); //
const getSmsSendFn = () => {
if (countdown.value > 0 || sending.value) return; //
if (type.value === 'register') {
registerFormRef.value.validateField(['mobile', 'captcha_code'], (valid) => {
if (!valid) return;
sending.value = true; //
const params = {
mobile: registerFormData.value.mobile,
captcha_key: registerFormData.value.captcha_key,
captcha_code: registerFormData.value.captcha_code
}
getSmsSend(params).then((res) => {
startCountdown(60); // 60
registerFormData.value.key = res.data.key;
}).catch((err) => {
getSmsCaptchaFn()
sending.value = false;
}).finally(() => {
sending.value = false; //
});
});
} else if (type.value === 'password') {
changeFormRef.value.validateField(['mobile', 'captcha_code'], (valid) => {
if (!valid) return;
sending.value = true; //
const params = {
mobile: changeFormData.value.mobile,
captcha_key: changeFormData.value.captcha_key,
captcha_code: changeFormData.value.captcha_code
}
getSmsSend(params).then((res) => {
startCountdown(60); // 60
changeFormData.value.key = res.data.key;
}).catch((err) => {
getSmsCaptchaFn()
sending.value = false;
}).finally(() => {
sending.value = false; //
});
});
}
}
//
const startCountdown = (seconds) => {
countdown.value = seconds;
const timer = setInterval(() => {
countdown.value--;
if (countdown.value <= 0) {
clearInterval(timer);
sending.value = false; //
}
}, 1000);
};
const changeFormRules = computed(() => {
return {
mobile: [
{ required: true, message: '请输入手机号', trigger: 'blur' },
{
pattern: /^1[3-9]\d{9}$/,
message: '请输入正确的手机号',
trigger: ['blur', 'change']
}
],
captcha_code: [
{ required: true, message: '请输入验证码', trigger: 'blur' },
],
code: [
{ required: true, message: '请输入动态码', trigger: 'blur' },
]
};
});
const editPass = async () => {
loading.value = true
captchaType.value = 'password'
const success = await getSmsCaptchaFn()
if (success) {
loading.value = false
type.value = 'password'
}else{
loading.value = false
}
}
const reset = async () => {
await changeFormRef.value?.validate(async (valid) => {
if (valid) {
let params = {
key: changeFormData.value.key,
code: changeFormData.value.code,
mobile: changeFormData.value.mobile
}
resetPassword(props.info.username,{...params}).then((res) => {
let newPassword = res.data.password
ElMessageBox.confirm(`新密码为:${newPassword}`, '请保存好新密码', {
confirmButtonText: '确定',
showCancelButton: false,
}).then(() => {
type.value='login'
emit('complete')
}).catch(() => {
type.value='login'
emit('complete')
})
})
}
});
}
</script>
<style lang="scss" scoped>
</style>

View File

@ -0,0 +1,142 @@
<template>
<el-card class="box-card !border-none p-[10px]" shadow="never" v-loading="loadingPackage">
<div class="panel-title">选择套餐</div>
<div class="flex flex-wrap mb-[30px]">
<div v-for="(item,index) in smsPackages" :key="index" :span="4">
<div class="package-card mr-[10px] mb-[10px]" :class="{ active: selectedPackage?.id === item.id }" @click="selectPackage(item)">
<div class="text-[14px] mb-1 using-hidden">{{ item.package_name }}</div>
<div class="text-[24px] text-primary ">{{ item.sms_num }}</div>
<div class="flex mt-2 text-[14px] justify-center items-center ">
<div>{{ item.price }}</div>
<div class="line-through ml-2">{{ item.original_price }}</div>
</div>
</div>
</div>
</div>
<div class="panel-title">选择支付方式</div>
<el-radio-group v-model="payMethod" class="mb-4">
<el-radio label="alipay">支付宝</el-radio>
<!-- 可扩展其他方式 -->
</el-radio-group>
<div class="mb-4 text-[14px] ml-[10px] mt-[10px]">
应付<span class="text-[24px] font-semibold text-primary"><span class="text-[14px] font-400"></span>{{ totalAmount }}</span>
</div>
<div class="ml-[50px]">
<el-button type="primary" :disabled="!selectedPackage || loading" :loading="loading" @click="submitPayment">支付</el-button>
<el-button @click="goBack">返回</el-button>
</div>
</el-card>
</template>
<script lang="ts" setup>
import { ref ,watch} from 'vue'
import { getSmsPackagesList, smsOrderCreate, getOrderPayInfo, getOrderPayStatus, calculateOrderPay } from '@/app/api/notice'
const props = defineProps({
username: {
type: String,
default: ''
},
isRecharge:{
type: Boolean,
default: false
}
})
const emit = defineEmits(['back', 'complete'])
const smsPackages = ref<any[]>([])
const selectedPackage = ref<any | null>(null)
const payMethod = ref('alipay')
const loadingPackage = ref(false)
const totalAmount = ref(0)
const getSmsPackagesListFn = () => {
loadingPackage.value = true
getSmsPackagesList().then(res => {
smsPackages.value = res.data.data
loadingPackage.value = false
if (smsPackages.value.length > 0 && props.username) {
selectPackage(smsPackages.value[0])
}
}).catch(() => {
loadingPackage.value = false
})
}
const selectPackage = (pkg: any) => {
selectedPackage.value = pkg
calculateAmount()
}
//
const calculateAmount = () => {
if (!selectedPackage.value) return
calculateOrderPay(props.username, {
package_id: selectedPackage.value.id
}
).then(res => {
totalAmount.value = res.data.pay_money
})
}
const loading = ref(false)
const submitPayment = async () => {
if (!selectedPackage.value || loading.value) return
loading.value = true
try {
const res = await smsOrderCreate(props.username, {
package_id: selectedPackage.value.id
})
if (res.data.order_status === 'payment') {
emit('complete')
} else {
const orderNo = res.data.out_trade_no
const payInfo = await getOrderPayInfo(props.username, { out_trade_no: orderNo })
window.open(payInfo.data.pay_info.url, '_blank')
await ElMessageBox.confirm('请确认支付是否完成', '支付提示', {
confirmButtonText: '已完成支付',
cancelButtonText: '返回',
type: 'warning',
})
emit('complete')
}
} catch (err) {
ElMessage.error('支付失败,请重试')
} finally {
loading.value = false
}
}
const goBack = () => {
emit('back')
}
const showRecharge = ref(false);
watch(() => props.isRecharge, (newVal) => {
showRecharge.value = newVal;
if (newVal) {
getSmsPackagesListFn();
}
});
</script>
<style lang="scss" scoped>
.package-card {
cursor: pointer;
text-align: center;
border: 1px solid #e4e7ed;
transition: all 0.2s;
padding: 10px;
width: 170px;
border-radius: 5px;
&.active {
border-color: var(--el-color-primary);
box-shadow: 0 0 6px rgba(64, 158, 255, 0.3);
}
}
</style>

View File

@ -0,0 +1,131 @@
<template>
<div>
<el-table :data="tableData.data" size="large" v-loading="tableData.loading" ref="goodBankListTableRef">
<template #empty>
<span>{{ !tableData.loading ? t("emptyData") : "" }}</span>
</template>
<el-table-column prop="order_no" :label="t('订单编号')" min-width="130" />
<el-table-column prop="package_name" :label="t('短信套餐')" min-width="130" />
<el-table-column prop="sms_num" :label="t('短信条数')" min-width="130" />
<el-table-column prop="order_money" :label="t('订单总价')" min-width="130" />
<el-table-column prop="pay_money" :label="t('实付金额')" min-width="130" />
<el-table-column prop="order_status_name" :label="t('订单状态')" min-width="130" />
<el-table-column prop="sms" :label="t('创建时间')" min-width="130" >
<template #default="{ row }">
<div>{{ row.create_time }}</div>
</template>
</el-table-column>
<el-table-column :label="t('operation')" fixed="right" align="right" min-width="120">
<template #default="{ row }">
<el-button type="primary" link @click="detailEvent(row)">{{ t("详情") }}</el-button>
</template>
</el-table-column>
</el-table>
<div class="mt-[16px] flex justify-end">
<el-pagination v-model:current-page="tableData.page" v-model:page-size="tableData.limit"
layout="total, sizes, prev, pager, next, jumper" :total="tableData.total" @size-change="loadRankList()" @current-change="loadRankList" />
</div>
<el-dialog v-model="visibleDetail" :title="t('模版详情')" width="600px" destroy-on-close >
<el-form label-width="100px" ref="formRef" class="page-form" v-loading="loading">
<el-form-item :label="t('订单编号')" prop="template_id">
<div>{{ detail.order_no }}</div>
</el-form-item>
<el-form-item :label="t('用户名称')" prop="template_id">
<div>{{ detail.username }}</div>
</el-form-item>
<el-form-item :label="t('套餐名称')" prop="template_id">
<div>{{ detail.package_name }}</div>
</el-form-item>
<el-form-item :label="t('订单状态')" prop="title">
<div >{{ detail.order_status_name }}</div>
</el-form-item>
<el-form-item :label="t('短信条数')" prop="title">
<div >{{ detail.sms_num }}</div>
</el-form-item>
<el-form-item :label="t('订单金额')" prop="title">
<div >{{ detail.order_money }}</div>
</el-form-item>
<el-form-item :label="t('付款金额')" prop="title">
<div >{{ detail.pay_money }}</div>
</el-form-item>
<el-form-item :label="t('创建时间')" prop="title">
<div >{{ detail.create_time }}</div>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visibleDetail = false">{{ t("cancel") }}</el-button>
<el-button type="primary" @click="visibleDetail = false">{{ t("confirm") }}</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { ref , reactive,onMounted} from 'vue'
import { getSmsOrdersList ,getOrderInfo} from '@/app/api/notice'
import { t } from "@/lang";
const props = defineProps({
username: {
type: String,
default: ''
}
})
//
const tableData = reactive({
page: 1,
limit: 10,
total: 0,
loading: false,
data: [],
searchParam: {
name: "",
order: '',
sort: ''
},
});
//
const loadRankList = () => {
tableData.loading = true;
let params = {
page: tableData.page,
limit: tableData.limit,
...tableData.searchParam
}
getSmsOrdersList(props.username, params).then((res) => {
tableData.loading = false;
tableData.data = res.data.data;
tableData.total = res.data.total;
}).catch(() => {
tableData.loading = false;
});
}
//
const detail = ref({})
const visibleDetail = ref(false)
const loading = ref(false)
const detailEvent = (row:any) => {
loading.value = true
visibleDetail.value = true
getOrderInfo(props.username,{out_trade_no:row.out_trade_no}).then(res=>{
detail.value = res.data
loading.value = false
})
visibleDetail.value = true
}
onMounted(() => {
if (props.username) {
loadRankList()
}
})
</script>
<style lang="scss" scoped>
</style>

View File

@ -0,0 +1,106 @@
<template>
<div>
<el-form :inline="true" :model="tableData.searchParam" ref="searchFormRef">
<el-form-item :label="t('短信标题')" prop="content">
<el-input v-model.trim="tableData.searchParam.content" :placeholder="t('请输入短信标题')" />
</el-form-item>
<el-form-item :label="t('接收人手机号')" prop="mobile">
<el-input v-model.trim="tableData.searchParam.mobile" :placeholder="t('请输入接收人手机号')" />
</el-form-item>
<el-form-item :label="t('状态')" prop="smsStatus">
<el-select v-model="tableData.searchParam.smsStatus" :placeholder="t('请选择状态')">
<el-option :label="t('全部')" :value="''"></el-option>
<el-option :label="t('发送成功')" :value="1"></el-option>
<el-option :label="t('发送失败')" :value="2"></el-option>
<el-option :label="t('待返回')" :value="3"></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="loadRankList()">{{ t("search") }}</el-button>
<el-button @click="resetForm(searchFormRef)">{{ t("reset") }}</el-button>
</el-form-item>
</el-form>
<el-table :data="tableData.data" size="large" v-loading="tableData.loading" ref="goodBankListTableRef">
<template #empty>
<span>{{ !tableData.loading ? t("emptyData") : "" }}</span>
</template>
<el-table-column prop="content" :label="t('标题')" min-width="250" show-overflow-tooltip />
<el-table-column prop="mobile" :label="t('接收人')" min-width="130" />
<el-table-column prop="smsStatusName" :label="t('发送状态')" min-width="130" />
<el-table-column prop="reportTime" :label="t('创建时间')" min-width="130" >
<template #default="{ row }">
<div>{{ timeStampTurnTime(row.reportTime) }}</div>
</template>
</el-table-column>
<el-table-column prop="sendTime" :label="t('发送时间')" min-width="130" >
<template #default="{ row }">
<div>{{timeStampTurnTime(row.sendTime) }}</div>
</template>
</el-table-column>
</el-table>
<div class="mt-[16px] flex justify-end">
<el-pagination v-model:current-page="tableData.page" v-model:page-size="tableData.limit"
layout="total, sizes, prev, pager, next, jumper" :total="tableData.total" @size-change="loadRankList()" @current-change="loadRankList" />
</div>
</div>
</template>
<script lang="ts" setup>
import { ref ,reactive,onMounted} from 'vue'
import { getSmsSendList } from '@/app/api/notice'
import { timeStampTurnTime } from '@/utils/common'
import { t } from "@/lang";
const props = defineProps({
username: {
type: String,
default: ''
}
})
//
const tableData = reactive({
page: 1,
limit: 10,
total: 0,
loading: false,
data: [],
searchParam: {
mobile: '',
content: '',
smsStatus: ''
}
})
//
const loadRankList = () => {
tableData.loading = true;
let params = {
page: tableData.page,
limit: tableData.limit,
...tableData.searchParam
}
getSmsSendList(props.username, params).then((res) => {
tableData.loading = false;
tableData.data = res.data.data;
tableData.total = res.data.total;
}).catch(() => {
tableData.loading = false;
});
}
onMounted(() => {
if (props.username) {
loadRankList();
}
})
const searchFormRef = ref<FormInstance>();
const resetForm = (formEl: FormInstance | undefined) => {
if (!formEl) return
formEl.resetFields()
loadRankList()
}
</script>
<style lang="scss" scoped>
</style>

View File

@ -0,0 +1,421 @@
<template>
<div>
<el-dialog v-model="visible" :title="t('选择签名')" width="1200px" destroy-on-close :close-on-click-modal="false">
<el-alert type="warning" :closable="false" class="!mb-[10px]">
<template #default>
<p class="">签名数据的变更新增 / 删除需经过五分钟的生效周期在此期间系统将完成数据同步与更新</p>
</template>
</el-alert>
<div class="flex justify-between items-center mb-[16px]">
<el-button type="primary" @click="addEvent">{{ t('添加短信签名') }}</el-button>
</div>
<div class="mb-[10px] flex items-center">
<el-checkbox v-model="toggleCheckbox" size="large" class="px-[14px]" @change="toggleChange" :indeterminate="isIndeterminate" />
<el-button @click="batchDeleteEvent" size="small">{{t("批量删除")}}</el-button>
</div>
<el-table :data="tableData.data" size="large" v-loading="tableData.loading" ref="smsSignListTableRef" @selection-change="handleSelectionChange">
<template #empty>
<span>{{ !tableData.loading ? t("emptyData") : "" }}</span>
</template>
<el-table-column type="selection" :selectable="checkSelectable" width="55" />
<el-table-column prop="sign" :label="t('签名名称')" min-width="200" />
<el-table-column prop="is_default" :label="t('使用状态')" min-width="120">
<template #default="{ row }">
<div>{{ row.is_default? t('使用中') : t('未使用') }}</div>
</template>
</el-table-column>
<el-table-column prop="auditResultName" :label="t('审核状态')" min-width="200">
<template #default="{ row }">
<div>
<div :class="[row.auditResult == 2 ? 'text-green-600' : '']">{{ row.auditResultName }}</div>
<div class="text-red-600" v-if="row.auditResult != 2">{{ row.auditMsg }}</div>
</div>
</template>
</el-table-column>
<el-table-column prop="realNameDx" :label="t('实名状态')" min-width="200">
<template #header>
<div style="display: inline-flex; align-items: center">
<span class="mr-[5px]">{{ t('实名状态') }}</span>
<el-tooltip class="box-item" effect="light" placement="top">
<template #content>
状态标识说明<br />未实名状态显示为灰色<br />实名通过状态显示为绿色<br />实名失败状态显示为红色<br />
短信接收条件仅当手机号在对应运营商处实名通过后才可接收短信
</template>
<el-icon color="#666">
<QuestionFilled />
</el-icon>
</el-tooltip>
</div>
</template>
<template #default="{ row }">
<div class="flex gap-[5px]">
<el-tag :type="row.realNameLt == 0 ? 'info' : row.realNameLt == 1 ? 'success' : 'danger'">{{ t("联通") }}</el-tag>
<el-tag :type="row.realNameYd == 0 ? 'info' : row.realNameYd == 1 ? 'success' : 'danger'">{{ t("移动") }}</el-tag>
<el-tag :type="row.realNameDx == 0 ? 'info' : row.realNameDx == 1 ? 'success' : 'danger'">{{ t("电信") }}</el-tag>
</div>
</template>
</el-table-column>
<el-table-column prop="create_time" :label="t('createTime')" min-width="120">
<template #default="{ row }">
<div>{{ row.createTime }}</div>
</template>
</el-table-column>
<el-table-column :label="t('操作')" fixed="right" align="right" min-width="120">
<template #default="{ row }">
<el-button type="primary" link @click="selectTemplate(row)" v-if="!row.is_default && row.auditResult == 2">
{{ t("使用") }}
</el-button>
<el-button type="primary" link @click="deleteTemplate(row)" v-if="!row.is_default">
{{ t("删除") }}
</el-button>
</template>
</el-table-column>
</el-table>
<div class="mt-[16px] flex justify-end">
<el-pagination v-model:current-page="tableData.page" v-model:page-size="tableData.limit"
layout="total, sizes, prev, pager, next, jumper" :total="tableData.total" @size-change="loadSignList()" @current-change="loadSignList" />
</div>
<template #footer>
<el-button @click="visible = false">{{ t("cancel") }}</el-button>
</template>
</el-dialog>
<el-dialog v-model="visibleAdd" :title="t('添加签名')" width="800px" destroy-on-close :close-on-click-modal="false">
<el-form label-width="150px" :model="formData" ref="formRef" :rules="formRules" class="page-form ml-[20px]">
<el-form-item :label="t('短信签名')" prop="signature">
<el-input v-model="formData.signature" placeholder="请输入短信签名" class="input-width" maxlength="20" show-word-limit clearable />
</el-form-item>
<div class="ml-[150px] text-[12px] text-[#999] leading-[20px]">必须由包裹例如test</div>
<div class="my-[5px] ml-[150px] text-[12px] text-[#999] leading-[20px]">字数要求在2-20个字符不能使用空格和特殊符号 - + = * & % # @ ~;</div>
<el-form-item :label="t('短信示例内容')" prop="contentExample">
<el-input v-model="formData.contentExample" placeholder="请输入短信示例内容" clearable maxlength="50" show-word-limit class="input-width" />
</el-form-item>
<el-form-item :label="t('企业名称')" prop="companyName">
<el-input v-model="formData.companyName" placeholder="请输入企业名称" clearable maxlength="20" show-word-limit class="input-width" />
</el-form-item>
<el-form-item :label="t('社会统一信用代码')" prop="creditCode">
<el-input v-model="formData.creditCode" placeholder="请输入社会统一信用代码" clearable maxlength="20" show-word-limit class="input-width" />
</el-form-item>
<el-form-item :label="t('法人姓名')" prop="legalPerson">
<el-input v-model="formData.legalPerson" placeholder="请输入法人姓名" clearable maxlength="20" show-word-limit class="input-width" />
</el-form-item>
<el-form-item :label="t('经办人姓名')" prop="principalName">
<el-input v-model="formData.principalName" placeholder="请输入经办人姓名" clearable maxlength="20" show-word-limit class="input-width" />
</el-form-item>
<el-form-item :label="t('经办人手机号')" prop="principalMobile">
<el-input v-model="formData.principalMobile" placeholder="请输入经办人手机号" clearable maxlength="20" show-word-limit class="input-width" />
</el-form-item>
<el-form-item :label="t('经办人身份证')" prop="principalIdCard">
<el-input v-model="formData.principalIdCard" placeholder="请输入经办人身份证" clearable maxlength="18" show-word-limit class="input-width" />
</el-form-item>
<el-form-item :label="t('签名来源')">
<el-radio-group v-model="formData.signSource" >
<el-radio v-for="item in signCofig.signsourceList" :key="item.type" :label="item.type" >{{item.name}}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item :label="t('签名类型')">
<el-radio-group v-model="formData.signType">
<el-radio v-for="item in signCofig.signTypeList" :key="item.type" :label="item.type" >{{item.name}}</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item :label="t('上传图片')" prop="imgUrl">
<upload-image v-model="formData.imgUrl" :limit="1" />
</el-form-item>
<div class="ml-[150px] text-[12px] text-[#999] leading-[20px]">当签名来源为商标APP小程序事业单位简称或企业名称简称时需必填此字段</div>
<div class="my-[5px] ml-[150px] text-[12px] text-[#999] leading-[20px]">当签名来源为事业单位全称或企业名称全称时选填此字段</div>
<el-form-item :label="t('是否默认')">
<el-radio-group v-model="formData.defaultSign" >
<el-radio :label="1"></el-radio>
<el-radio :label="0"></el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="visibleAdd = false">{{ t("cancel") }}</el-button>
<el-button type="primary" @click="onSave()">{{ t("confirm") }}</el-button>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, reactive } from 'vue'
import { getSignList,addSign,getSmsSignConfig,deleteSign } from '@/app/api/notice'
import { t } from "@/lang";
const visible = ref(false)
const visibleAdd = ref(false)
const emit = defineEmits(['select'])
const props = defineProps({
username: {
type: String,
default: ''
}
})
const initialFormData = {
defaultSign: 0,
imgUrl: '',
contentExample: '',
signType: '',
signSource: '',
principalIdCard: '',
principalName: '',
principalMobile: '',
legalPerson: '',
creditCode: '',
companyName: '',
signature: ''
}
const formData = reactive({ ...initialFormData })
const signCofig = reactive({
signTypeList: [],
signsourceList:[]
})
const getSmsSignConfigFn = ()=> {
getSmsSignConfig().then(res => {
signCofig.signTypeList = res.data.sign_type_list
signCofig.signsourceList = res.data.sign_source_list
formData.signSource = res.data.sign_source_list[0].type
formData.signType = res.data.sign_type_list[0].type
})
}
getSmsSignConfigFn()
const formRef = ref()
const formRules = computed(() => {
return {
signature: [
{ required: true, message: '请输入短信签名', trigger: 'blur' },
{
validator: (rule, value, callback) => {
const singleBracketValid = /^【[^【】]*】$/.test(value);
if (!singleBracketValid) {
return callback(new Error('短信签名必须被【】包裹'));
}
const content = value.slice(1, -1);
const lengthValid = content.length >= 2 && content.length <= 20;
if (!lengthValid) {
return callback(new Error('短信签名内容需在 2-20 个字符之间'));
}
const invalidChars = /[\s\-+=*&%#@~;]/;
if (invalidChars.test(content)) {
return callback(new Error('短信签名不能包含空格或特殊字符 - + = * & % # @ ~ ;'));
}
callback();
},
trigger: 'blur'
}
],
principalMobile:[
{ required: true, message: '请输入经办人手机号', trigger: 'blur' },
{ validator: phoneVerify, trigger: 'blur' }
],
companyName: [
{ required: true, message: '请输入企业名称', trigger: 'blur' },
],
contentExample: [
{ required: true, message: '请输入短信示例内容', trigger: 'blur' },
],
creditCode: [
{ required: true, message: '请输入社会统一信用代码', trigger: 'blur' },
],
legalPerson: [
{ required: true, message: '请输入法人姓名', trigger: 'blur' },
],
principalName: [
{ required: true, message: '请输入经办人姓名', trigger: 'blur' },
],
principalIdCard: [
{ required: true, message: '请输入经办人身份证', trigger: 'blur' },
{ validator: idCardVerify, trigger: 'blur' }
],
imgUrl: [
{
validator: (rule, value, callback) => {
const needImage = [3, 4, 5].includes(formData.signSource) || formData.signType === 1;
if (needImage) {
if (!value || value.length === 0) {
callback(new Error('请上传图片'));
} else {
callback();
}
} else {
callback(); //
}
},
trigger: 'blur'
}
]
};
})
const idCardVerify = (rule: any, value: any, callback: any) => {
if (value && !/^[1-9]\d{5}(19|20)\d{2}((0\d)|(1[0-2]))(([0-2]\d)|3[0-1])\d{3}([0-9Xx])$/.test(value)) {
callback(new Error(t('请输入正确的身份证号码')))
} else {
callback()
}
}
const phoneVerify = (rule: any, value: any, callback: any) => {
if (value && !/^1[3-9]\d{9}$/.test(value)) {
callback(new Error(t('请输入正确的手机号码')))
} else {
callback()
}
}
const onSave = async () => {
await formRef.value?.validate(async (valid) => {
if (valid) {
addSign(props.username,formData).then((res) => {
setTimeout(() => {
visibleAdd.value = false
loadSignList()
}, 500);
})
}
});
}
//
const tableData = reactive({
page: 1,
limit: 10,
total: 0,
loading: false,
data: [],
searchParam: {},
});
const open = () => {
visible.value = true
loadSignList()
}
//
const loadSignList = () => {
tableData.loading = true;
let params = {
page: tableData.page,
limit: tableData.limit,
...tableData.searchParam,
}
getSignList(props.username, params).then((res) => {
tableData.loading = false;
tableData.data = res.data.data;
tableData.total = res.data.total
}).catch(() => {
tableData.loading = false;
});
}
const addEvent = () => {
Object.assign(formData, initialFormData)
formData.signSource = signCofig.signsourceList[0].type
formData.signType = signCofig.signTypeList[0].type
visibleAdd.value = true
}
const deleteTemplate = (row:any) => {
ElMessageBox.confirm(t('确定删除该签名吗?'), t('提示'), {
confirmButtonText: t('确定'),
cancelButtonText: t('取消'),
type: 'warning',
}).then(() => {
deleteSign(props.username, { signatures: [row.sign] }).then((res) => {
// loadSignList()
tableData.loading = true;
setTimeout(() => {
loadSignList()
},1000)
})
})
}
//
const toggleCheckbox = ref()
//
const isIndeterminate = ref(false)
//
const toggleChange = (value: any) => {
isIndeterminate.value = false
smsSignListTableRef.value.toggleAllSelection()
}
const smsSignListTableRef = ref()
//
const multipleSelection: any = ref([])
//
const handleSelectionChange = (val: []) => {
multipleSelection.value = val
toggleCheckbox.value = false
if (multipleSelection.value.length > 0 && multipleSelection.value.length < tableData.data.length) {
isIndeterminate.value = true
} else {
isIndeterminate.value = false
}
if (multipleSelection.value.length == tableData.data.length && tableData.data.length && multipleSelection.value.length) {
toggleCheckbox.value = true
}
}
const checkSelectable = (row: any, index: number) => {
return !row.is_default; // 使
}
//
const batchDeleteEvent = () => {
if (multipleSelection.value.length == 0) {
ElMessage({
type: "warning",
message: `${ t("请选择要删除的签名") }`,
})
return
}
ElMessageBox.confirm(t("确定删除选中的签名吗?"), t("warning"), {
confirmButtonText: t("confirm"),
cancelButtonText: t("cancel"),
type: "warning"
}).then(() => {
const signatures: any = []
multipleSelection.value.forEach((item: any) => {
signatures.push(item.sign)
})
deleteSign(props.username,{
signatures: signatures
}).then(() => {
tableData.loading = true;
setTimeout(() => {
loadSignList()
},1000)
// loadSignList()
}).catch(() => {
})
})
}
const selectTemplate = (row:any) => {
visible.value = false
emit('select', row)
}
defineExpose({ open })
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,363 @@
<template>
<div>
<div class="flex justify-between items-start my-[10px]">
<el-form :inline="true" :model="tableData.searchParam" ref="searchFormRef">
<el-form-item :label="t('模版ID')" prop="template_id">
<el-input v-model.trim="tableData.searchParam.template_id" :placeholder="t('请输入模版ID')" />
</el-form-item>
<el-form-item :label="t('模版名称')" prop="name">
<el-input v-model.trim="tableData.searchParam.name" :placeholder="t('请输入模版名称')" />
</el-form-item>
<el-form-item :label="t('状态')" prop="status">
<el-select v-model="tableData.searchParam.status" :placeholder="t('请选择状态')">
<el-option :label="t('全部')" :value="''"></el-option>
<el-option v-for="(statusText, statusValue) in template_status_list" :key="statusValue" :label="statusText" :value="statusValue"></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="loadSmsTemplateList()">{{ t("search") }}</el-button>
<el-button @click="resetForm(searchFormRef)">{{ t("reset") }}</el-button>
</el-form-item>
</el-form>
<el-button type="primary" @click="syncEvent()">{{ t("同步模版状态") }}</el-button>
</div>
<el-table :data="pagedData" size="large" v-loading="tableData.loading" ref="goodBankListTableRef">
<template #empty>
<span>{{ !tableData.loading ? t("emptyData") : "" }}</span>
</template>
<el-table-column prop="template_id" :label="t('模版ID')" min-width="100" />
<el-table-column prop="name" :label="t('模版名称')" min-width="130" />
<el-table-column prop="template_type_name" :label="t('模版类型')" min-width="130" >
<template #default="{ row }">
<div>{{ row.template_type_name ? row.template_type_name : '--'}}</div>
</template>
</el-table-column>
<el-table-column prop="sms" :label="t('模版内容')" min-width="200" >
<template #default="{ row }">
<div>{{ row.sms?.content }}</div>
</template>
</el-table-column>
<el-table-column prop="audit_info" :label="t('审核状态')" min-width="130" >
<template #default="{ row }">
<div>{{ row.audit_info?.audit_status_name }} <span v-if="row.audit_info?.error_status_name" class="text-red-600">{{row.audit_info?.error_status_name}}</span> </div>
</template>
</el-table-column>
<el-table-column :label="t('operation')" fixed="right" align="right" min-width="120">
<template #default="{ row }">
<el-button type="primary" v-if="row.audit_info.audit_status!=2" link @click="reportEvent(row)">{{ row.audit_info.audit_status!=1 && row.audit_info.audit_status!=2? t("报备") : t("修改") }}</el-button>
<el-button type="primary" v-if="row.audit_info.audit_status==2" link @click="clearEvent(row)">{{ t('清除报备信息') }}</el-button>
<el-button type="primary" link @click="editEvent(row)">{{ t("详情") }}</el-button>
</template>
</el-table-column>
</el-table>
<div class="mt-[16px] flex justify-end">
<el-pagination v-model:current-page="tableData.page" v-model:page-size="tableData.limit"
layout="total, sizes, prev, pager, next, jumper" :total="tableData.total"/>
</div>
<el-dialog v-model="visibleDetail" :title="t('模版详情')" width="600px" destroy-on-close >
<el-form label-width="100px" ref="formRef" class="page-form">
<el-form-item :label="t('短信类型')" prop="template_id">
<div>{{ detail.sms_type }}</div>
</el-form-item>
<el-form-item :label="t('模版名称')" prop="template_id">
<div>{{ detail.name }}</div>
</el-form-item>
<el-form-item :label="t('模版类型')" prop="title">
<div >{{ detail.title }}</div>
</el-form-item>
<el-form-item :label="t('短信内容')" prop="title" v-if="detail.sms">
<div >{{ detail.sms?.content }}</div>
</el-form-item>
<el-form-item :label="t('审核状态')" prop="title">
<div >{{ detail.audit_info.audit_status_name }}</div>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<!-- <el-button @click="visibleDetail = false">{{ t("cancel") }}</el-button> -->
<el-button type="primary" @click="visibleDetail = false">{{ t("confirm") }}</el-button>
</span>
</template>
</el-dialog>
<el-dialog v-model="visibleReport" :title="t('模版报备')" width="820px" destroy-on-close >
<el-form label-width="100px" ref="formRef" class="page-form" v-loading="reportLoading">
<el-form-item :label="t('模版名称')" prop="template_id">
<div class="input-width">{{ detail.name }}</div>
</el-form-item>
<el-form-item :label="t('模版类型')" prop="title">
<el-radio-group v-model="reportData.template_type">
<el-radio v-for="[key, value] in Object.entries(template_type_list)" :key="key" :label="Number(key)">{{ value }}</el-radio>
</el-radio-group>
</el-form-item>
<div class="ml-[100px] mb-[10px] mt-[-10px] text-[12px] text-[#999] leading-[20px]">
<div>验证码仅支持验证码类型变量</div>
<div>行业通知不支持验证码类型变量</div>
<div>营销推广不支持变量</div>
</div>
<el-form-item :label="t('变量类型')" prop="params_json" v-if="detail.variable && Object.keys(detail.variable).length > 0">
<div v-for="(label, key) in detail.variable" :key="key" class="mb-2 flex items-center">
<div class="flex flex-1 items-center">
<div class="w-32 mr-1 ">{{ label }}</div>
<el-select v-model="reportData.params_json[key]" placeholder="请选择类型" class="flex-1" filterable clearable :disabled="isMarketingWithVariable">
<el-option v-for="item in filteredParamTypes" :key="item.type" :label="item.name + '' + item.desc + ''" :value="item.type"/>
</el-select>
</div>
</div>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visibleReport = false">{{ t("cancel") }}</el-button>
<el-button type="primary" @click="reportTemplateFn()" :disabled="isMarketingWithVariable">{{ t("confirm") }}</el-button>
</span>
</template>
</el-dialog>
<el-dialog v-model="visibleAsync" :title="t('同步模版状态')" width="800px" destroy-on-close >
<el-alert type="warning" :closable="false" class="!mb-[10px]">
<template #default>
以下模版名称重复请先调整模版名称后重新同步模版
</template>
</el-alert>
<el-form label-width="100px" ref="formRef" class="page-form">
<div v-if="Object.keys(repeatList).length" class="h-[500px] overflow-y-auto">
<el-table :data="repeatListArray" border style="width: 100%;">
<el-table-column label="模版名称" prop="name" />
<el-table-column label="插件名称">
<template #default="{ row }">
<el-tag v-for="item in row.platforms" :key="item" class="mr-1 mb-1">{{ item }}</el-tag>
</template>
</el-table-column>
</el-table>
</div>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visibleAsync = false">{{ t("cancel") }}</el-button>
<el-button type="primary" @click="visibleAsync = false">{{ t("confirm") }}</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { ref ,computed,reactive,onMounted,watch} from 'vue'
import { getTemplateList,getTemplateReportConfig,reportTemplate,templateSync ,getreportTemplateInfo,clearTemplate} from '@/app/api/notice'
import { t } from "@/lang";
const props = defineProps({
username: {
type: String,
default: ''
},
signature:{
type: String,
default: ''
}
})
//
const tableData = reactive({
page: 1,
limit: 10,
total: 0,
loading: false,
data: [], // computed
allData: [], //
searchParam: {
template_id: "",
name: '',
status: ''
}
});
const filterData = () => {
const { template_id, name, status } = tableData.searchParam;
return tableData.allData.filter(item => {
const matchId = !template_id || String(item.template_id || '').includes(template_id);
const matchName = !name || String(item.name || '').includes(name);
const matchStatus = !status || item.audit_info.audit_status == status;
return matchId && matchName && matchStatus;
});
};
watch(() => [tableData.limit, tableData.page], () => {
pagedDataChange()
})
//
const loadSmsTemplateList = () => {
tableData.loading = true;
getTemplateList({ sms_type: 'niuyun', username: props.username }).then((res) => {
tableData.allData = res.data;
tableData.page = 1; //
pagedDataChange()
}).catch(() => {
tableData.loading = false;
});
}
const searchFormRef = ref(null)
const resetForm = (formRef) => {
if (!formRef) return;
tableData.searchParam = {
template_id: "",
name: '',
status: ''
}
loadSmsTemplateList()
};
const pagedData = ref([])
const pagedDataChange = () => {
const filtered = filterData(); // 使
tableData.total = filtered.length;
const start = (tableData.page - 1) * tableData.limit;
const end = start + tableData.limit;
pagedData.value = filtered.slice(start, end);
tableData.loading = false;
}
onMounted(() => {
if (props.username) {
loadSmsTemplateList()
}
})
const visibleAsync = ref(false)
const repeatList = ref({})
const syncEvent = () => {
templateSync('niuyun', props.username).then((res) => {
repeatList.value = res.data.repeat_list;
if (repeatList.value && Object.keys(repeatList.value).length > 0) {
visibleAsync.value = true
} else {
loadSmsTemplateList()
}
})
}
const repeatListArray = computed(() => {
return Object.entries(repeatList.value).map(([name, platforms]) => ({
name,
platforms
}))
})
//
const detail = ref(null)
const visibleDetail = ref(false)
const editEvent = (row:any) => {
visibleDetail.value = true
detail.value = row
}
//
const clearEvent = (row:any) => {
ElMessageBox.confirm(t('确定要清除报备信息吗'), t('提示'), {
confirmButtonText: t('确定'),
cancelButtonText: t('取消'),
type: 'warning',
}).then(() => {
clearTemplate(props.username, row.template_id).then(() => {
loadSmsTemplateList()
})
}).catch(() => {
})
}
//
const template_params_type_list = ref({})
const template_type_list = ref({})
const template_status_list = ref({})
const visibleReport = ref(false)
const reportData = ref({
template_type: 1,
template_key: '',
params_json:{}
})
const getTemplateReportConfigFn = () => {
getTemplateReportConfig().then((res) => {
template_params_type_list.value = res.data.template_params_type_list
template_type_list.value = res.data.template_type_list
template_status_list.value = res.data.template_status_list
})
}
getTemplateReportConfigFn()
const filteredParamTypes = computed(() => {
if (reportData.value.template_type === 1) {
return template_params_type_list.value.filter(item => item.type === 'valid_code')
} else {
return template_params_type_list.value
}
})
const isMarketingWithVariable = computed(() => {
return reportData.value.template_type === 3 && detail.value.variable && Object.keys(detail.value.variable).length > 0
})
watch(isMarketingWithVariable, (val) => {
if (val) {
ElMessage.error('营销推广类型不支持变量');
}
});
const reportLoading = ref(false)
const reportEvent = (row:any) => {
reportLoading.value = true
let signature = props.signature
if(!signature){
ElMessage.error('请先配置签名')
return
}else{
if(row.template_id){
visibleReport.value = true;
detail.value = row;
getreportTemplateInfo('niuyun', props.username, { template_key: row.key }).then((res) => {
const paramJson = res.data?.param_json ?? {};
reportData.value.template_key = res.data.template_key;
reportData.value.template_type = Number(res.data.template_type);
reportData.value.params_json = {};
if (detail.value.variable) {
for (const key in detail.value.variable) {
reportData.value.params_json[key] = paramJson[key] ?? '';
}
}
reportLoading.value = false;
});
}else{
visibleReport.value = true
reportLoading.value = false
detail.value = row
reportData.value.template_type = 1
reportData.value.template_key = detail.value.key
reportData.value.params_json = {}
}
}
}
const reportTemplateFn = () => {
if (!detail.value.sms) {
ElMessage.error('请先配置模版内容')
return
}
//
const missingParams = Object.entries(detail.value.variable).some(
([key]) => !reportData.value.params_json[key]
);
if (missingParams) {
ElMessage.error('请为每个变量选择类型');
return;
}
if (detail.value.template_id) {
reportData.value.template_id = Number(detail.value.template_id)
}
reportTemplate(detail.value.sms_type, props.username, reportData.value).then((res) => {
visibleReport.value = false
loadSmsTemplateList()
})
}
</script>
<style lang="scss" scoped>
</style>

View File

@ -11,7 +11,7 @@
<el-form-item :label="t('companyName')" prop="company_name">
<el-input v-model.trim="formData.company_name" :placeholder="t('companyNamePlaceholder')" class="input-width" clearable maxlength="30"/>
</el-form-item>
<el-form-item :label="t('copyrightLink')" >
<el-form-item :label="t('copyrightLink')" prop="copyright_link">
<el-input v-model.trim="formData.copyright_link" :placeholder="t('copyrightLinkPlaceholder')" class="input-width" clearable />
</el-form-item>
<el-form-item :label="t('copyrightDesc')" >
@ -81,7 +81,25 @@ setFormData()
const formRef = ref<FormInstance>()
//
const formRules = reactive<FormRules>({})
const formRules = reactive<FormRules>({
copyright_link: [
{
validator(rule, value, callback) {
//
if (!value) return callback();
// http/https
const reg = /^.*?(http|https).*?$/i;
if (!reg.test(value)) {
callback(new Error('链接必须包含 http 或 https'));
} else {
callback();
}
},
trigger: 'blur'
}
]
})
/**
* 保存

View File

@ -1,7 +1,7 @@
<template>
<div class="main-container">
<el-form class="page-form" :model="formData" :rules="formRules" label-width="150px" ref="ruleFormRef" v-loading="loading">
<el-form class="page-form" :model="formData" :rules="formRules" label-width="150px" ref="ruleFormRef" v-loading="loading" @submit.prevent>
<el-card class="box-card !border-none" shadow="never">
<h3 class="panel-title !text-sm">{{ t('commonSetting') }}</h3>

View File

@ -10,7 +10,7 @@
<el-card class="box-card !border-none my-[10px] table-search-wrap" shadow="never">
<el-form :inline="true" :model="recordsTableData.searchParam" ref="searchFormRef">
<el-form-item :label="t('searchReceiver')" prop="receiver">
<el-input v-model.trim="recordsTableData.searchParam.receiver" :placeholder="t('receiverPlaceholder')" />
<el-input class="!w-[200px]" v-model.trim="recordsTableData.searchParam.receiver" :placeholder="t('receiverPlaceholder')" />
</el-form-item>
<el-form-item :label="t('noticeKey')" prop="key">
@ -75,7 +75,7 @@
<script lang="ts" setup>
import { reactive, ref } from 'vue'
import { t } from '@/lang'
import { getNoticeList } from '@/app/api/notice'
import { getNoticeLog, getNoticeList } from '@/app/api/notice'
import RecordsInfo from '@/app/views/setting/components/notice-records-info.vue'
import { FormInstance } from 'element-plus'
import { useRoute } from 'vue-router'
@ -144,7 +144,7 @@ const loadNoticeLogList = (page: number = 1) => {
recordsTableData.loading = true
recordsTableData.page = page
getNoticeList({
getNoticeLog({
page: recordsTableData.page,
limit: recordsTableData.limit,
...recordsTableData.searchParam
@ -157,7 +157,6 @@ const loadNoticeLogList = (page: number = 1) => {
})
}
loadNoticeLogList()
const resetForm = (formEl: FormInstance | undefined) => {
if (!formEl) return
formEl.resetFields()

View File

@ -39,9 +39,10 @@
<script lang="ts" setup>
import { defineAsyncComponent, reactive, ref } from 'vue'
import { t } from '@/lang'
import { getSmsList } from '@/app/api/notice'
import { useRoute } from 'vue-router'
import { getSmsList ,getAccountIsLogin} from '@/app/api/notice'
import { useRoute, useRouter } from 'vue-router'
const router = useRouter()
const route = useRoute()
const pageName = route.meta.title
const smsTypeRefs = ref([])
@ -73,10 +74,19 @@ const setSmsTypeRefs = (el, index) => {
}
loadSmsList()
const isLogin = ref(false)
const editEvent = (data: any, index: number) => {
smsTypeRefs.value[index].setFormData(data)
smsTypeRefs.value[index].showDialog = true
if (data.sms_type == 'niuyun') {
getAccountIsLogin().then((res: any) => {
router.push('/setting/niusms/setting')
})
} else {
smsTypeRefs.value[index].setFormData(data)
smsTypeRefs.value[index].showDialog = true
}
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,346 @@
<template>
<!--短信设置-->
<div class="main-container" v-loading = "loading">
<div v-if="!loading">
<div v-if="isLogin && !isRecharge">
<el-card class="box-card !border-none mb-[15px]" shadow="never">
<el-page-header :content="pageName" :icon="ArrowLeft" @back="back()" />
</el-card>
<el-form label-width="100px" ref="formRef" class="page-form">
<el-card class="box-card !border-none relative" shadow="never">
<el-alert type="warning" :closable="false" class="!mb-[30px]">
<template #default>
<p class="mb-[5px]">牛云短信操作指引</p>
<p class="mb-[5px]">* 开启准备若要开启牛云短信功能需登录对应账号并配置可用的短信签名</p>
<p class="mb-[5px]">* 审核说明短信签名设置与模板消息开启均需经过审核审核时间为周一至周日的 9:30 - 22:00法定节假日审核时间顺延工作日内审核预计耗时 2 小时非工作日预计耗时 4 小时</p>
<p class="mb-[5px]">* 签名报备要求签名可使用公司全称或简称简称需为公司全称的一部分不能增减或跳字且签名需具备唯一性若使用非公司简称作为签名需提供 app ICP 备案截图或商标证明 且签名必须与资质名称完全一致</p>
<p class="mb-[5px]">* 模版报备要点报备模板时请确认模板中变量对应的类型若模板某变量内容超出长度限制系统将自动截取以确保短信正常发出</p>
<p class="mb-[5px]">* 短信发送条件短信成功发送需满足两个条件一是签名审核通过且在运营商处实名认证成功二是模板审核通过</p>
<p class="mb-[5px]">* 其他事项短信数量不足时请及时进行充值如有任何疑问可联系客服客服电话400 - 886 - 7993服务时间为 9:00 - 18:00 </p>
</template>
</el-alert>
<h3 class="panel-title">{{ t('短信信息') }}</h3>
<el-row class="row-bg px-[30px] mb-[20px]">
<el-col :span="8">
<el-form-item :label="t('用户名')">
<div class="input-width">
<span>{{ formData.username }}</span>
<el-button type="primary" link @click="isLogin = false" class="ml-[10px]">{{ t('切换账户') }}</el-button>
</div>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item :label="t('公司名称')">
<div class="input-width">{{ formData.company }}</div>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item :label="t('账户状态')">
<div class="input-width">{{ formData.status_name }}</div>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item :label="t('手机号')">
<div class="input-width">
<span>{{ formData.mobiles ? formData.mobiles : '暂无'}}</span>
<el-button type="primary" link @click="changeMobile()" class="ml-[10px]">{{ t('更换手机号') }}</el-button>
</div>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item :label="t('签名')">
<div class="input-width">
<span>{{ formData.signature ? formData.signature : '暂无'}}</span>
<el-button type="primary" link @click="openDialog" class="ml-[10px]">{{ t('更换签名') }}</el-button>
</div>
</el-form-item>
</el-col>
</el-row>
<h3 class="panel-title">{{ t('短信权限') }}</h3>
<el-row class="row-bg px-[30px] mb-[20px]">
<el-col :span="24">
<el-form-item :label="t('是否开启')">
<el-switch v-model="is_enable" :active-value="1" :inactive-value="0" :before-change="beforeChangeIsEnable" />
</el-form-item>
<div class="mb-[10px] text-[12px] ml-[30px] text-[#999] leading-[20px]">是否开启牛云短信模版</div>
</el-col>
</el-row>
<h3 class="panel-title">{{ t('短信条数') }}</h3>
<el-row class="row-bg px-[30px] mb-[20px]">
<el-col :span="24">
<el-form-item :label="t('短信')">
<div class="flex justify-between items-center">
<span class="text-primary text-[20px] mx-[5px]">{{formData.sms_count}}</span>
<el-button @click="isRecharge =true" class="ml-[30px]">{{ t('短信充值') }}</el-button>
</div>
</el-form-item>
</el-col>
</el-row>
<el-tabs v-model="activeName" class="demo-tabs">
<el-tab-pane label="短信模版" name="template">
<smsTemplate :username="username" :signature="formData.signature"></smsTemplate>
</el-tab-pane>
<el-tab-pane label="充值记录" name="recharge">
<smsRechargeRecord :username="username"></smsRechargeRecord>
</el-tab-pane>
<el-tab-pane label="发送记录" name="send">
<smsSend :username="username"></smsSend>
</el-tab-pane>
</el-tabs>
</el-card>
</el-form>
</div>
<smsNiuLogin v-show="!isLogin" :info="formData" :isLogin="isLoginStatus" @complete="getAccountIsLoginFn"></smsNiuLogin>
<smsRecharge v-show="isRecharge" @back="isRecharge = false" @complete="backRecharge" :username="username" :isRecharge="isRecharge"></smsRecharge>
<smsSignature ref="signatureDialogRef" :username="username" @select="handleSelectTemplate" ></smsSignature>
<el-dialog v-model="visibleMobile" :title="t('更换手机号')" width="600px" destroy-on-close >
<el-form label-width="120px" :model="changeFormData" ref="changeFormRef" :rules="changeFormRules" class="page-form ml-[20px]">
<el-form-item label="手机号" prop="mobile" v-if="formData.mobiles">
<el-input placeholder="请输入手机号" disabled class="input-width" maxlength="11" show-word-limit v-model="formData.mobiles" clearable />
</el-form-item>
<el-form-item label="新手机号" prop="new_mobile" v-if="formData.mobiles">
<el-input placeholder="请输入新手机号" class="input-width" maxlength="11" show-word-limit v-model="changeFormData.new_mobile" clearable />
</el-form-item>
<el-form-item label="手机号" prop="new_mobile" v-if="!formData.mobiles">
<el-input placeholder="请输入手机号" class="input-width" maxlength="11" show-word-limit v-model="changeFormData.new_mobile" clearable />
</el-form-item>
<el-form-item label="验证码" prop="captcha_code" v-if="formData.mobiles">
<div class="flex items-center">
<el-input placeholder="请输入验证码" class="input-width" maxlength="4" show-word-limit v-model="changeFormData.captcha_code" clearable />
<img :src="changeFormData.captcha_img" alt="验证码" class="w-[100px] h-[32px] cursor-pointer ml-[10px]" @click="getSmsCaptchaFn" />
</div>
</el-form-item>
<el-form-item label="动态码" prop="code" v-if="formData.mobiles">
<div class="flex items-center">
<el-input placeholder="请输入动态码" class="input-width" maxlength="4" show-word-limit v-model="changeFormData.code" clearable />
<el-button class="ml-[10px]" @click="getSmsSendFn" :disabled="countdown > 0" :loading="sending">
{{ countdown > 0 ? `${countdown}秒后重新获取` : '获取动态码' }}
</el-button>
</div>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="visibleMobile = false">{{ t("cancel") }}</el-button>
<el-button type="primary" @click="onSave()">{{ t("confirm") }}</el-button>
</template>
</el-dialog>
</div>
<div v-else>
<el-card class="box-card !border-none mb-[15px] min-h-[100vh]" shadow="never"></el-card>
</div>
</div>
</template>
<script lang="ts" setup>
import { reactive, ref ,computed, onMounted} from 'vue'
import { t } from '@/lang'
import { ArrowLeft } from "@element-plus/icons-vue"
import { getAccountIsLogin,getAccountInfo,editAccount ,editSms,getSmsCaptcha,getSmsSend,enableNiusms} from '@/app/api/notice'
import { useRoute, useRouter} from 'vue-router'
import smsNiuLogin from '@/app/views/setting/components/sms_niu_login.vue'
import smsTemplate from '@/app/views/setting/components/sms_template.vue'
import smsRechargeRecord from '@/app/views/setting/components/sms_recharge_record.vue'
import smsRecharge from '@/app/views/setting/components/sms_recharge.vue'
import smsSend from '@/app/views/setting/components/sms_send.vue'
import smsSignature from '@/app/views/setting/components/sms_signature.vue'
const router = useRouter()
const route = useRoute()
const pageName = route.meta.title
const isLogin = ref(true)
const formData = reactive({
mobiles: '',
sms_count: '',
username: '',
company: '',
signature: '',
status_name: ''
})
const isInit = ref(false) //
const activeName = ref('template')
const loading = ref(true)
const isRecharge = ref(false)
const isLoginStatus = ref(true)
const is_enable = ref(0)
const username = ref('')
onMounted(() => {
getAccountIsLoginFn()
})
const backRecharge = () => {
isRecharge.value = false
getAccountIsLoginFn()
}
const getAccountIsLoginFn =()=> {
loading.value = true
getAccountIsLogin().then(res => {
isLoginStatus.value = res.data.is_login
isLogin.value = res.data.is_login
username.value = res.data.username
is_enable.value = res.data.is_enable
if (res.data.is_login) {
getAccountInfo(username.value).then(res => {
Object.assign(formData, res.data)
loading.value = false
isInit.value = true
})
} else {
loading.value = false
isInit.value = true
}
}).catch(err => {
loading.value = false
isInit.value = true
})
}
const signatureDialogRef = ref(null)
const openDialog = () => {
signatureDialogRef.value?.open()
}
const beforeChangeIsEnable = (val) => {
if (!isInit.value) return false
let enable = is_enable.value == 1 ? 0 : 1
return new Promise((resolve, reject) => {
enableNiusms({ is_enable: enable }).then(() => {
resolve(true)
}).catch(() => {
reject(false)
})
})
}
const handleSelectTemplate = (val) => {
loading.value = true
editAccount(username.value,{signature: val.sign}).then(res=>{
getAccountIsLoginFn()
})
}
//
const changeFormRef = ref()
const changeFormData = ref({
new_mobile: '',
captcha_key: '',
captcha_code: '',
captcha_img: '',
code: '',
key: ''
})
const changeMobile = async () => {
changeFormData.value.new_mobile = ''
changeFormData.value.captcha_key = ''
changeFormData.value.captcha_code = ''
changeFormData.value.captcha_img = ''
changeFormData.value.code = ''
changeFormData.value.key = ''
try {
await getSmsCaptchaFn()
visibleMobile.value = true
} catch (e) {
}
}
const getSmsCaptchaFn = () => {
return getSmsCaptcha().then(res => {
changeFormData.value.captcha_key = res.data.captcha_key
changeFormData.value.captcha_img = res.data.img
}).catch(err => {
})
}
const visibleMobile = ref(false)
const changeFormRules = computed(() => {
return {
new_mobile: [
{ required: true, message: '请输入手机号', trigger: 'blur' },
{
pattern: /^1[3-9]\d{9}$/,
message: '请输入正确的手机号',
trigger: ['blur', 'change']
}
],
captcha_code: [
{ required: true, message: '请输入验证码', trigger: 'blur' },
],
code: [
{ required: true, message: '请输入动态码', trigger: 'blur' },
]
};
});
const sending = ref(false); //
const countdown = ref(0); //
const getSmsSendFn = () => {
if (countdown.value > 0 || sending.value) return; //
changeFormRef.value.validateField(['captcha_code'], (valid) => {
if (!valid) return;
sending.value = true; //
const params = {
mobile: formData.mobiles,
captcha_key: changeFormData.value.captcha_key,
captcha_code: changeFormData.value.captcha_code
};
getSmsSend(params).then((res) => {
changeFormData.value.key = res.data.key;
startCountdown(60); // 60
}).catch((err) => {
getSmsCaptchaFn()
sending.value = false;
}).finally(() => {
sending.value = false; //
});
});
};
//
const startCountdown = (seconds) => {
countdown.value = seconds;
const timer = setInterval(() => {
countdown.value--;
if (countdown.value <= 0) {
clearInterval(timer);
}
}, 1000);
}
const onSave = async () => {
await changeFormRef.value?.validate(async (valid) => {
if (valid) {
let params = {}
if (formData.mobiles) {
params = {
mobile: formData.mobiles,
new_mobile: changeFormData.value.new_mobile,
key: changeFormData.value.key,
code: changeFormData.value.code
}
} else {
params = {
new_mobile: changeFormData.value.new_mobile,
}
}
editAccount(username.value, params).then(res => {
visibleMobile.value = false
getAccountIsLoginFn()
})
}
});
}
const back = () => {
router.push('/setting/sms/setting')
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,78 @@
<template>
<div class="main-container">
<el-card class="box-card !border-none mb-[15px]" shadow="never">
<el-page-header :content="pageName" :icon="ArrowLeft" @back="back()" />
</el-card>
<el-card class="box-card !border-none mb-[15px]" shadow="never">
<el-result v-if="status === 'payment'" icon="success" title="支付成功">
<template #extra>
<el-button type="primary" @click="back()">{{ t('back') }}</el-button>
</template>
</el-result>
<el-result v-else-if="status === 'close'" icon="error" title="订单已关闭">
<template #extra>
<el-button type="primary" @click="back()">{{ t('back') }}</el-button>
</template>
</el-result>
<el-result v-else icon="info" title="支付状态查询中" sub-title="请稍候正在确认支付状态..." />
</el-card>
</div>
</template>
<script lang="ts" setup>
import { onMounted, onBeforeUnmount, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { t } from '@/lang'
import { ArrowLeft } from "@element-plus/icons-vue"
import { getOrderPayStatus } from '@/app/api/notice'
const router = useRouter()
const route = useRoute()
const pageName = route.meta.title
const username = route.query.username || ''
const back = () => {
router.push('/setting/niusms/setting')
}
// waitpaymentclose
const status = ref('wait')
let timer: ReturnType<typeof setInterval> | null = null
onMounted(() => {
const orderNo = route.query.out_trade_no as string
if (!orderNo) return
//
checkStatus(orderNo)
//
timer = setInterval(() => {
checkStatus(orderNo)
}, 2000)
})
onBeforeUnmount(() => {
if (timer) clearInterval(timer)
})
const checkStatus = async (orderNo: string) => {
try {
const res = await getOrderPayStatus(username, {
out_trade_no: orderNo
})
status.value = res.data.order_status
if (res.data.order_status === 'payment' || res.data.order_status === 'close') {
if (timer) clearInterval(timer)
}
} catch (err) {
console.error('获取支付状态失败', err)
}
}
</script>
<style lang="scss" scoped></style>

View File

@ -6,7 +6,7 @@
<span class="text-page-title">{{ pageName }}</span>
</div>
<el-tabs v-model="type">
<el-tabs v-model="type" tab-position="top">
<el-tab-pane :label="t(tab)" v-for="(tab, index) in attachmentType" :name="tab" :key="index">
<attachment scene="attachment" :type="tab" />
</el-tab-pane>
@ -44,7 +44,7 @@ const type = ref(attachmentType[0])
.el-tabs {
display: flex;
flex-direction: column;
flex-direction: column;
height: calc(100% - 40px);
}

View File

@ -23,9 +23,7 @@
</el-form>
</el-card>
<div class="mb-[10px] flex items-center">
<el-button @click="batchDelete" size="small">{{ t('batchDelete') }}</el-button>
</div>
<el-table :data="tableData.data" size="large" v-loading="tableData.loading" ref="tableRef" @selection-change="handleSelectionChange">
@ -35,7 +33,19 @@
<el-table-column type="selection" width="55" />
<el-table-column prop="id" :label="t('id')" width="120" />
<el-table-column prop="content" :label="t('content')" width="120" />
<el-table-column prop="content" :label="t('操作')" width="120" align="left">
<template #default="{ row }">
<span v-if="row.content=='手动备份'" class="multi-hidden">
<el-tag type="primary">{{ row.content }}</el-tag>
</span>
<span v-else-if="row.content=='自动备份'" class="multi-hidden">
<el-tag type="success">{{ row.content }}</el-tag>
</span>
<span v-else class="multi-hidden">
<el-tag type="warning">{{ row.content }}</el-tag>
</span>
</template>
</el-table-column>
<el-table-column prop="version" :label="t('currentVersion')" width="120" />
<el-table-column prop="backup_dir" :label="t('backupDir')" width="220" />
<el-table-column prop="complete_time" :label="t('completeTime')" width="220" />
@ -53,7 +63,9 @@
</template>
</el-table-column>
</el-table>
<div class="mt-[10px] flex items-center">
<el-button @click="batchDelete" size="small">{{ t('batchDelete') }}</el-button>
</div>
<div class="mt-[16px] flex justify-end">
<el-pagination v-model:current-page="tableData.page"
v-model:page-size="tableData.limit" layout="total, sizes, prev, pager, next, jumper"
@ -64,7 +76,7 @@
<el-dialog v-model="showDialog" :title="iSBackupRecovery == 1 ? t('manualBackupTitle') : t('restoreTitle')" width="850px" :close-on-click-modal="false" :close-on-press-escape="false" :show-close="true" :before-close="dialogClose">
<el-steps :active="numberOfSteps" align-center class="number-of-steps" finish-status="success" process-status="process">
<el-steps :active="numberOfSteps" align-center class="number-of-steps" finish-status="success" process-status="process" v-if="active !='error' && active !='complete'">
<template v-if="iSBackupRecovery == 1">
<!-- 手动备份 -->
<el-step :title="t('testDirectoryPermissions')" />
@ -149,14 +161,37 @@
</div>
<!-- 执行任务 -->
<div class="h-[370px] mt-[30px]" v-if="active == 'execute'">
<div class="h-[370px] mt-[30px]" v-show="active == 'execute'">
<terminal ref="terminalRef" context="" :init-log="null" :show-header="false" :show-log-time="true" @exec-cmd="onExecCmd"/>
</div>
<!-- 完成 -->
<div class="mt-[50px]" v-if="active == 'complete'">
<el-result icon="success" :title="iSBackupRecovery == 1 ? t('backupCompleteTips') : t('restoreCompleteTips')"></el-result>
<div class="mt-[50px]" v-show="active == 'complete'">
<el-result icon="success" :title="iSBackupRecovery == 1 ? t('backupCompleteTips') : t('restoreCompleteTips')" :sub-title="iSBackupRecovery == 1 ?`备份耗时${formattedDuration}成功备份完成` : `恢复耗时${formattedDuration}成功恢复完成`">
<template #icon>
<img src="@/app/assets/images/success_icon.png" alt="">
</template>
<template #extra>
<el-button @click="handleReturn" class="!w-[90px]">返回</el-button>
<el-button @click="showDialog=false" type="primary" class="!w-[90px]">完成</el-button>
</template>
</el-result>
</div>
<!-- 失败 -->
<div class="mt-[50px]" v-show="active == 'error'">
<el-result icon="success" :title="iSBackupRecovery == 1 ? t('备份失败') : t('恢复失败')" :sub-title="backupErrorMessage" >
<template #icon>
<img src="@/app/assets/images/error_icon.png" alt="">
</template>
<template #extra>
<el-button @click="handleReturn" class="!w-[90px]">错误信息</el-button>
<el-button @click="showDialog=false" type="primary" class="!w-[90px]">完成</el-button>
</template>
</el-result>
</div>
</div>
<template #footer>
@ -188,7 +223,7 @@
</template>
<script lang="ts" setup>
import { ref, reactive, nextTick, watch, h } from 'vue'
import { ref, reactive, nextTick, watch, h,computed } from 'vue'
import { t } from '@/lang'
import { ElMessage, ElMessageBox, FormInstance } from 'element-plus'
import { useRoute } from 'vue-router'
@ -229,7 +264,6 @@ const currentId: any = ref(0)
let backupContents = []
let restoreContents = []
const resetForm = (formEl: FormInstance | undefined) => {
if (!formEl) return
@ -276,6 +310,7 @@ const manualBackupEvent = () => {
// if (repeat.value) return
// repeat.value = true
backupContents = []
numberOfSteps.value = 0
iSBackupRecovery.value = 1
showDialog.value = true
uploading.value = true
@ -284,7 +319,17 @@ const manualBackupEvent = () => {
checkPermissionFn()
})
}
//
const buildStartTime = ref<number | null>(null)
const buildDuration = ref<number>(0)
let buildTimer: number | null = null
const formattedDuration = computed(() => {
const seconds = buildDuration.value
const mins = Math.floor(seconds / 60)
const secs = seconds % 60
return mins > 0 ? `${mins}${secs}` : `${secs}`
})
const backupErrorMessage = ref('')
//
const manualBackupFn = (task: any = '') => {
@ -300,6 +345,21 @@ const manualBackupFn = (task: any = '') => {
if (task == '') {
terminalRef.value.execute('clear')
terminalRef.value.execute('开始执行')
const storedTime = localStorage.getItem('manual_back_start_time')
if (storedTime) {
buildStartTime.value = Number(storedTime)
} else {
const now = Date.now()
buildStartTime.value = now
localStorage.setItem('manual_back_start_time', String(now))
}
buildDuration.value = Math.floor((Date.now() - buildStartTime.value) / 1000)
buildTimer && clearInterval(buildTimer)
buildTimer = setInterval(() => {
if (buildStartTime.value) {
buildDuration.value = Math.floor((Date.now() - buildStartTime.value) / 1000)
}
}, 1000)
}
if (data.content && !backupContents.includes(data.content)) {
backupContents.push(data.content)
@ -310,6 +370,8 @@ const manualBackupFn = (task: any = '') => {
setTimeout(() => {
numberOfSteps.value = 3
active.value = 'complete'
buildTimer && clearInterval(buildTimer) //
localStorage.removeItem('manual_back_start_time')
loadList()
repeat.value = false
}, 1500)
@ -319,6 +381,18 @@ const manualBackupFn = (task: any = '') => {
loadList()
repeat.value = false
}, 2000)
backupErrorMessage.value = data.content
active.value = 'error'
//
if (buildTimer) {
clearInterval(buildTimer)
buildTimer = null
}
// duration
if (buildStartTime.value) {
buildDuration.value = Math.floor((Date.now() - buildStartTime.value) / 1000)
}
localStorage.removeItem('manual_back_start_time')
} else {
// 2
setTimeout(() => {
@ -420,6 +494,7 @@ const restoreEvent = (data: any) => {
).then(() => {
// if (repeat.value) return
// repeat.value = true
numberOfSteps.value = 0
restoreContents = []
iSBackupRecovery.value = 2
currentId.value = data.id
@ -467,6 +542,21 @@ const restoreUpgradeBackupFn = (id: any, task: any = '') => {
uploading.value = false
terminalRef.value.execute('clear')
terminalRef.value.execute('开始执行')
const storedTime = localStorage.getItem('manual_back_start_time')
if (storedTime) {
buildStartTime.value = Number(storedTime)
} else {
const now = Date.now()
buildStartTime.value = now
localStorage.setItem('manual_back_start_time', String(now))
}
buildDuration.value = Math.floor((Date.now() - buildStartTime.value) / 1000)
buildTimer && clearInterval(buildTimer)
buildTimer = setInterval(() => {
if (buildStartTime.value) {
buildDuration.value = Math.floor((Date.now() - buildStartTime.value) / 1000)
}
}, 1000)
}
if (data.content && !restoreContents.includes(data.content)) {
restoreContents.push(data.content)
@ -477,6 +567,8 @@ const restoreUpgradeBackupFn = (id: any, task: any = '') => {
setTimeout(() => {
numberOfSteps.value = 3
active.value = 'complete'
buildTimer && clearInterval(buildTimer) //
localStorage.removeItem('manual_back_start_time')
loadList()
repeat.value = false
}, 1500)
@ -486,6 +578,18 @@ const restoreUpgradeBackupFn = (id: any, task: any = '') => {
loadList()
repeat.value = false
}, 2000)
backupErrorMessage.value = data.content
active.value = 'error'
//
if (buildTimer) {
clearInterval(buildTimer)
buildTimer = null
}
// duration
if (buildStartTime.value) {
buildDuration.value = Math.floor((Date.now() - buildStartTime.value) / 1000)
}
localStorage.removeItem('manual_back_start_time')
} else {
// 2
setTimeout(() => {
@ -576,9 +680,14 @@ const restoreTaskList = () => {
}
})
}
const isBack = ref(false)
const handleReturn = () => {
active.value = 'execute'
isBack.value = true
}
const dialogClose = (done: () => {}) => {
if (active.value == 'execute') {
if (active.value == 'execute' && !isBack.value) {
ElMessageBox.confirm(
t('showDialogCloseTips'),
t('warning'),
@ -591,6 +700,11 @@ const dialogClose = (done: () => {}) => {
terminalRef.value.execute('clear')
interrupt.value = true //
done()
localStorage.removeItem('manual_back_start_time')
buildTimer && clearInterval(buildTimer)
buildTimer = null
buildStartTime.value = null
buildDuration.value = 0
}).catch(() => {
})
} else {
@ -754,8 +868,27 @@ const batchDelete = () => {
.el-step__icon {
background: var(--el-color-primary);
color: #fff;
// box-shadow: 0 0 0 4px var(--el-color-primary-light-9);
box-shadow: 0 0 0 4px var(--el-color-primary-light-9);
i {
color: #fff;
}
}
.el-step__line {
margin: 0 25px;
background: var(--el-color-primary);
}
}
.is-finish {
color: var(--el-color-primary);
border-color: var(--el-color-primary);
.el-step__icon {
background: var(--el-color-primary)!important;
color: #fff !important;
// box-shadow: 0 0 0 4px var(--el-color-primary-light-9);
i {
color: #fff;
@ -776,7 +909,9 @@ const batchDelete = () => {
.el-step__icon {
padding: 10px;
border: 1px solid var(--el-color-primary);
box-shadow: 0 0 0 4px var(--el-color-primary-light-9);
background: var(--el-color-primary)!important;
color: #fff !important;
// box-shadow: 0 0 0 4px var(--el-color-primary-light-9);
}
}
@ -784,6 +919,26 @@ const batchDelete = () => {
color: #333;
}
}
:deep(.el-result__icon) {
color: unset !important; //
}
:deep(.el-result__title p){
font-size: 25px;
color: #1D1F3A;
font-weight: 500;
}
:deep(.el-result__subtitle p){
font-size: 15px;
color: #4F516D;
font-weight: 500;
word-break: break-all;
text-overflow: ellipsis;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 5;
-webkit-box-orient: vertical;
}
/* 多行超出隐藏 */
.multi-hidden {
word-break: break-all;

View File

@ -2,159 +2,153 @@
<div v-loading="loading" class="main-container w-full">
<div class="p-5 bg-[#fff] overflow-hidden">
<div class="bg-[#fff] w-[100%] rounded-[8px] overflow-hidden">
<div class=" relative pb-[13px]" style="border-bottom: 2px solid #f0f2f6">
<div class="w-[66px] bg-primary h-[2px] absolute bottom-[0px]"></div>
<span class=" text-primary text-[18px] ml-[4px]">云编译</span>
</div>
<div class="flex mt-[20px] ml-[20px]">
<el-button class="w-[98px] !h-[36px]" type="primary" @click="handleCloudBuild" :loading="cloudBuildRef?.loading">云编译</el-button>
<div class="btn-time w-[181px] h-[36px] rounded-[4px] text-[#606266] text-[14px] ml-[10px]">
<span>云编译执行时间大约</span>
<span class="text-[16px] text-[#D43030] mx-[3px]">3</span>
<span>分钟</span>
<div class="bg-[#fff] w-[100%] overflow-hidden">
<div class="flex items-center justify-between mb-[20px]">
<div class="text-[#1D1F3A] text-[16px] font-bold ml-[4px]">云编译</div>
<div class="flex ml-[20px]">
<div class="btn-time w-[181px] h-[36px] rounded-[4px] text-[#9699B6] text-[12px] ml-[10px]">
<span>云编译执行时间大约</span>
<span class="text-[14px] text-[#DA203E] mx-[3px]">5</span>
<span>分钟</span>
</div>
<el-button class="w-[98px] !h-[36px]" type="primary" @click="handleCloudBuild" :loading="cloudBuildRef?.loading">云编译</el-button>
</div>
</div>
<div class="mt-[21px] flex mb-[21px] items-center">
<span class="flex ml-[20px] text-[16px] items-center">
<i class="w-[3px] h-[12px] bg-primary mr-[6px] block"></i>
<div class="panel-title bg-[#F4F5F7] border-[#E6E6E6] border-solid border-b-[1px] h-[40px] flex items-center p-[10px]">
<span class="text-[16px] font-500 text-[#1D1F3A]">云编译</span>
<span class="text-[12px] text-[#9699B6] ml-[10px]">云编译不需要本地安装node环境即可进行针对使用者方便快捷</span>
</div>
<div class="mt-[20px] flex mb-[14px] items-center">
<span class="flex ml-[20px] font-500 text-[16px] items-center text-[#1D1F3A]">
<!-- <i class="w-[3px] h-[12px] bg-primary mr-[6px] block"></i> -->
温馨提示
</span>
<span class="text-[14px] text-[#606266] ml-[7px]"> 以下情况可以进行云编译</span>
<span class="text-[12px] text-[#9699B6] ml-[10px]"> 以下情况可以进行云编译</span>
</div>
<div class="text-[14px] text-[#606266] ml-[13px] mb-[18px]">云编译不需要本地安装node环境即可进行针对使用者方便快捷</div>
<div class="ml-[40px] text-[14px] text-[#606266] mb-[18px]">1系统或插件每次安装或升级完成后需要云编译</div>
<div class="ml-[40px] text-[14px] text-[#606266] mb-[18px]">2开发者编写完前端代码之后可以使用云编译进行源码编译</div>
<div class="ml-[40px] text-[14px] text-[#606266] mb-[18px]">3由于云编译不是针对某个插件进行编译而是系统整体编译因此如果同时需要安装多个插件时往往需要安装到最后一个插件才整体进行云编译</div>
<div class="mt-[21px] flex mb-[21px] text-[16px] items-center">
<!-- <div class="text-[14px] text-[#606266] ml-[13px] mb-[18px]">云编译不需要本地安装node环境即可进行针对使用者方便快捷</div> -->
<div class="ml-[40px] text-[14px] text-[#4F516D] mb-[18px]">1系统或插件每次安装或升级完成后需要云编译</div>
<div class="ml-[40px] text-[14px] text-[#4F516D] mb-[18px]">2开发者编写完前端代码之后可以使用云编译进行源码编译</div>
<div class="ml-[40px] text-[14px] text-[#4F516D] mb-[18px]">3由于云编译不是针对某个插件进行编译而是系统整体编译因此如果同时需要安装多个插件时往往需要安装到最后一个插件才整体进行云编译</div>
<div class="mt-[21px] flex mb-[21px] text-[16px] text-[#1D1F3A] font-500 items-center">
<span class="flex ml-[20px] items-center">
<i class="w-[3px] h-[12px] bg-primary mr-[6px] block"></i>
<!-- <i class="w-[3px] h-[12px] bg-primary mr-[6px] block"></i> -->
云编译流程
</span>
</div>
<div class="ml-[40px]">
<el-timeline>
<el-timeline-item color="#4268EF">
<template #dot>
<el-timeline-item :hollow="true">
<!-- <template #dot>
<div class="w-[15px] h-[15px] bg-primary rounded-[50%] text-[9px] text-[#fff] flex items-center justify-center">1</div>
</template>
<div class="text-[16px] text-[#303133]">编译admin代码</div>
<div class="py-[12px] px-[10px] bg-[#F7F8FA] mt-[10px] text-[#606266] text-[14px] w-[1085px]">
</template> -->
<div class="text-[16px] text-[#1D1F3A]">编译admin代码</div>
<div class="p-[10px] bg-[#F9F9FB] mt-[10px] text-[#4F516D] text-[14px] w-[1085px] border-[#F1F1F8] border-solid border-[1px] h-[40px] flex items-center rounded-[4px]">
<span>云编译会将admin端的vue代码编译为对应的html文件同时将生成的代码下载到系统 niucloud 下的</span>
<span class="text-[#FF9D31] mx-[3px]">public/admin</span>
<span class="text-[#F09000] mx-[3px] font-bold">public/admin</span>
<span>目录中后台的访问路径将变为</span>
<span class="text-primary ml-[3px]">https:///admin</span>
<span class="text-primary ml-[3px] font-500">https:///admin</span>
</div>
</el-timeline-item>
<el-timeline-item color="#4268EF">
<template #dot>
<div class="w-[15px] h-[15px] bg-primary rounded-[50%] text-[9px] text-[#fff] flex items-center justify-center">2</div>
</template>
<div class="text-[16px] text-[#303133]">编译uniapp代码</div>
<div class="py-[12px] px-[10px] bg-[#F7F8FA] mt-[10px] text-[#606266] text-[14px] w-[1085px]">
<el-timeline-item :hollow="true">
<div class="text-[16px] text-[#1D1F3A]">编译uniapp代码</div>
<div class="p-[10px] bg-[#F9F9FB] mt-[10px] text-[#4F516D] text-[14px] w-[1085px] border-[#F1F1F8] border-solid border-[1px] h-[40px] flex items-center rounded-[4px]">
<span>云编泽会将uniapp端的vue代码编译为对应的html文件同时将生成的代码下载到系统 niucloud下的</span>
<span class="text-[#FF9D31] mx-[3px]">public/wap</span>
<span class="text-[#F09000] mx-[3px] font-bold">public/wap</span>
<span>目录中这样手机端网页的访问路径将变为</span>
<span class="text-primary ml-[3px]"> https:///wap</span>
<span class="text-primary ml-[3px] font-500"> https:///wap</span>
</div>
</el-timeline-item>
<el-timeline-item color="#4268EF">
<template #dot>
<div class="w-[15px] h-[15px] bg-primary rounded-[50%] text-[9px] text-[#fff] flex items-center justify-center">3</div>
</template>
<div class="text-[16px] text-[#303133]">编译web代码</div>
<div class="py-[12px] px-[10px] bg-[#F7F8FA] mt-[10px] text-[#606266] text-[14px] w-[1085px]">
<el-timeline-item :hollow="true">
<div class="text-[16px] text-[#1D1F3A]">编译web代码</div>
<div class="p-[10px] bg-[#F9F9FB] mt-[10px] text-[#4F516D] text-[14px] w-[1085px] border-[#F1F1F8] border-solid border-[1px] h-[40px] flex items-center rounded-[4px]">
<span>云编泽会将web端的vue代码编译为对应的html文件同时将生成的代码下载到系统 niucloud下的</span>
<span class="text-[#FF9D31] mx-[3px]">public/web</span>
<span class="text-[#F09000] mx-[3px] font-bold">public/web</span>
<span>目录中这样电脑端网页的访问路径将变为</span>
<span class="text-primary ml-[3px]"> https:///web</span>
<span class="text-primary ml-[3px] font-500"> https:///web</span>
</div>
</el-timeline-item>
</el-timeline>
</div>
</div>
</div>
<div class="p-5 bg-[#fff] mt-[26px]">
<div class="bg-[#fff] w-[100%] rounded-[8px] overflow-hidden">
<div class="relative pb-[13px]" style="border-bottom: 2px solid #f0f2f6">
<div class="w-[85px] bg-primary h-[2px] absolute bottom-[0px]"></div>
<span class="text-primary text-[18px] ml-[4px]">本地编译</span>
<div class="mt-[10px]">
<div class="panel-title bg-[#F4F5F7] border-[#E6E6E6] border-solid border-b-[1px] h-[40px] flex items-center p-[10px]">
<span class="text-[16px] font-500 text-[#1D1F3A]">本地编译</span>
</div>
<div class="mt-[21px] flex mb-[21px] text-[16px] items-center">
<div class="mt-[20px] flex mb-[14px] text-[16px] items-center text-[#1D1F3A]">
<span class="flex ml-[20px] items-center">
<i class="w-[3px] h-[12px] bg-primary mr-[6px] block"></i>
<!-- <i class="w-[3px] h-[12px] bg-primary mr-[6px] block"></i> -->
温馨提示
</span>
</div>
<div class="ml-[40px] text-[14px] text-[#606266] mb-[18px]">
<div class="ml-[40px] text-[14px] text-[#4F516D] mb-[18px]">
<span>1如果本地安装了Node环境可以进行本地编译要求</span>
<span class="text-[#D43030] ml-[3px]">Node版本>18</span>
<span class="text-[#DA203E] ml-[3px] font-500">Node版本>=18</span>
</div>
<div class="ml-[40px] text-[14px] text-[#606266] mb-[18px]">2默认本地编译流程与云编译相同执行本地编译命令后会将编译后的代码移动到系统niucloud下的public下的对应端口目录下</div>
<div class="ml-[40px] text-[14px] text-[#606266] mb-[18px]">3由于云编译配置的访问路径时固定的针对客户有独立部署adminwapweb等个性化端口名称配置需求需要进行本地编译</div>
<div class="mt-[34px] flex mb-[21px] text-[16px] items-center">
<div class="ml-[40px] text-[14px] text-[#4F516D] mb-[18px]">2默认本地编译流程与云编译相同执行本地编译命令后会将编译后的代码移动到系统niucloud下的public下的对应端口目录下</div>
<div class="ml-[40px] text-[14px] text-[#4F516D] mb-[18px]">3由于云编译配置的访问路径是固定的针对客户有独立部署adminwapweb等个性化端口名称配置需求需要进行本地编译</div>
<div class="mt-[20px] flex mb-[14px] text-[16px] items-center text-[#1D1F3A]">
<span class="flex ml-[20px] items-center">
<i class="w-[3px] h-[12px] bg-primary mr-[6px] block"></i>
<!-- <i class="w-[3px] h-[12px] bg-primary mr-[6px] block"></i> -->
本地编译命令参考
</span>
</div>
<div>
<div class="ml-[40px] text-[14px] text-[#606266]">
<span class=" text-[#303133]">安装依赖</span>
进入admin端与uniapp端以及web端目录都可执行
<div class="ml-[40px] text-[#374151] text-[14px] italic">
<span class="text-[16px] italic">#安装依赖</span>
<span class="italic">进入admin端与uniapp端以及web端目录都可执行</span>
</div>
<div class="ml-[40px] w-[900px] h-[42px] bg-[#282C34] rounded-[4px] mt-[10px] flex items-center">
<span class="text-[16px] text-[#FF9D31] ml-[10px]">npm install</span>
<span class="w-[58px] h-[20px] bg-[rgba(204,204,204,0.3)] ml-[auto] text-[#fff] text-[10px] flex cursor-pointer rounded-[4px] mr-[17px] items-center justify-center" @click="copyEvent('npm install')">复制命令</span>
<div class="ml-[40px] w-[1085px] h-[40px] bg-[#F9F9FB] rounded-[4px] mt-[10px] flex items-center justify-between border-[#F1F1F8] border-solid border-[1px] px-[10px]">
<span class="text-[14px] text-[#374151]">npm install</span>
<span class="iconfont iconfuzhiV6xx1 !text-[#252B3A] cursor-pointer" @click="copyEvent('npm install')"></span>
</div>
</div>
<div class="mt-[21px]">
<div class="ml-[40px] text-[14px] text-[#606266]">
<span class="text-[#303133]">后台admin端口打包</span>
<div class="ml-[40px] text-[14px] text-[#374151] italic">
<span class="text-[16px] italic">#后台admin端口打包</span>
<span>进入admin目录下执行执行后编译代码默认移动到系统的niucloud下的</span>
<span class="text-[#FF9D31] mx-[3px]">public/admin</span>
<span class="text-[#F09000] mx-[3px]">public/admin</span>
<span>目录下</span>
</div>
<div class="ml-[40px] w-[900px] h-[42px] bg-[#282C34] rounded-[4px] mt-[10px] flex items-center">
<span class="text-[16px] text-[#FF9D31] ml-[10px]">npm run build</span>
<span class="w-[58px] h-[20px] bg-[rgba(204,204,204,0.3)] ml-[auto] text-[#fff] text-[10px] flex cursor-pointer rounded-[4px] mr-[17px] items-center justify-center" @click="copyEvent('npm run build')">复制命令</span>
<div class="ml-[40px] w-[1085px] h-[40px] bg-[#F9F9FB] rounded-[4px] mt-[10px] flex items-center justify-between border-[#F1F1F8] border-solid border-[1px] px-[10px]">
<span class="text-[14px] text-[#374151]">npm run build</span>
<span class="iconfont iconfuzhiV6xx1 !text-[#252B3A] cursor-pointer" @click="copyEvent('npm run build')"></span>
</div>
</div>
<div class="mt-[21px]">
<div class="ml-[40px] text-[14px] text-[#606266]">
<span class="text-[#303133]">使用uniapp打包H5</span>
<div class="ml-[40px] text-[14px] text-[#374151] italic">
<span class="text-[16px] italic">#使用uniapp打包H5</span>
<span>进入uniapp目录下执行执行后编译代码默认移动到系统niucloud下的</span>
<span class="text-[#FF9D31] mx-[3px]">public/wap</span>
<span class="text-[#F09000] mx-[3px]">public/wap</span>
<span>目录下</span>
</div>
<div class="ml-[40px] w-[900px] h-[42px] bg-[#282C34] rounded-[4px] mt-[10px] flex items-center">
<span class="text-[16px] text-[#FF9D31] ml-[10px]">npm run build:h5</span>
<span class="w-[58px] h-[20px] bg-[rgba(204,204,204,0.3)] ml-[auto] text-[#fff] text-[10px] flex cursor-pointer rounded-[4px] mr-[17px] items-center justify-center" @click="copyEvent('npm run build:h5')">复制命令</span>
<div class="ml-[40px] w-[1085px] h-[40px] bg-[#F9F9FB] rounded-[4px] mt-[10px] flex items-center justify-between border-[#F1F1F8] border-solid border-[1px] px-[10px]">
<span class="text-[14px] text-[#374151]">npm run build:h5</span>
<span class="iconfont iconfuzhiV6xx1 !text-[#252B3A] cursor-pointer" @click="copyEvent('npm run build:h5')"></span>
</div>
</div>
<div class="mt-[21px]">
<div class="ml-[40px] text-[14px] text-[#606266]">
<span class=" text-[#303133]">使用uniapp打包微信小程序</span>
<div class="ml-[40px] text-[14px] text-[#374151] italic">
<span class="text-[16px] italic">#使用uniapp打包微信小程序</span>
<span>进入uniapp目录下执行执行后编译代码默认移动到系统niucloud下的</span>
<span class="text-[#FF9D31] mx-[3px]">uni-app/dist/build/mp-weixin</span>
<span class="text-[#F09000] mx-[3px]">uni-app/dist/build/mp-weixin</span>
<span>目录</span>
</div>
<div class="ml-[40px] w-[900px] h-[42px] bg-[#282C34] rounded-[4px] mt-[10px] flex items-center">
<span class="text-[16px] text-[#FF9D31] ml-[10px]">npm run build:mp-weixin</span>
<span class="w-[58px] h-[20px] bg-[rgba(204,204,204,0.3)] ml-[auto] text-[#fff] text-[10px] flex cursor-pointer rounded-[4px] mr-[17px] items-center justify-center" @click="copyEvent('npm run build:mp-weixin')">复制命令</span>
<div class="ml-[40px] w-[1085px] h-[40px] bg-[#F9F9FB] rounded-[4px] mt-[10px] flex items-center justify-between border-[#F1F1F8] border-solid border-[1px] px-[10px]">
<span class="text-[14px] text-[#374151]">npm run build:mp-weixin</span>
<span class="iconfont iconfuzhiV6xx1 !text-[#252B3A] cursor-pointer" @click="copyEvent('npm run build:mp-weixin')"></span>
</div>
</div>
<div class="mt-[21px]">
<div class="ml-[40px] text-[14px] text-[#606266]">
<span class="text-[#303133]">web端打包</span>
<div class="ml-[40px] text-[14px] text-[#374151] italic">
<span class="text-[16px] italic">#前台web(pc)端打包:</span>
<span>进入web目录下执行执行后编译代码默认移动到系统niucloud下的</span>
<span class="text-[#FF9D31] mx-[3px]">public/web</span>
<span class="text-[#F09000] mx-[3px]">public/web</span>
<span>目录下</span>
</div>
<div class="ml-[40px] w-[900px] h-[42px] bg-[#282C34] rounded-[4px] mt-[10px] flex items-center">
<span class="text-[16px] text-[#FF9D31] ml-[10px]">npm run generate</span>
<span class="w-[58px] h-[20px] bg-[rgba(204,204,204,0.3)] ml-[auto] text-[#fff] text-[10px] flex cursor-pointer rounded-[4px] mr-[17px] items-center justify-center" @click="copyEvent('npm run build')">复制命令</span>
<div class="ml-[40px] w-[1085px] h-[40px] bg-[#F9F9FB] rounded-[4px] mt-[10px] flex items-center justify-between border-[#F1F1F8] border-solid border-[1px] px-[10px]">
<span class="text-[14px] text-[#374151]">npm run generate</span>
<span class="iconfont iconfuzhiV6xx1 !text-[#252B3A] cursor-pointer" @click="copyEvent('npm run build')"></span>
</div>
</div>
</div>
@ -215,4 +209,18 @@ watch(copied, () => {
line-height: 36px;
text-align: center;
}
:deep(.el-button){
border-radius: 4px !important;
}
:deep(.el-timeline-item__node--normal){
width: 16px !important;
height: 16px !important;
}
:deep(.el-timeline-item__tail){
left: 6px !important;
}
:deep(.el-timeline-item__node.is-hollow){
background: #9699B6 !important;
border-width: 3px !important;
}
</style>

View File

@ -26,13 +26,13 @@
<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-select v-model="cronTableData.searchParam.key" placeholder="全部" filterable remote clearable :remote-method="loadCronList">
<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-select v-model="cronTableData.searchParam.status" placeholder="全部" filterable remote clearable :remote-method="loadCronList">
<el-option label="全部" value="all" />
<el-option label="启用" value="1" />
<el-option label="关闭" value="0" />
@ -51,7 +51,7 @@
<template #empty>
<span>{{ !cronTableData.loading ? t('emptyData') : '' }}</span>
</template>
<el-table-column prop="key" :label="t('key')" min-width="150" />
<el-table-column prop="name" :label="t('title')" min-width="150" />
<el-table-column :label="t('crondType')" min-width="150">
@ -248,22 +248,26 @@ const formRules = computed(() => {
})
const validatePass = (rule: any, value: any, callback: any) => {
if (formData.time.type == 'min' && formData.time.min != '') {
return callback()
const time = formData.time;
const isPositiveInt = (v: any) => /^[1-9]\d*$/.test(v);
const error = () => callback(new Error(t('cronTimeTips')));
switch (time.type) {
case 'min':
return isPositiveInt(time.min) ? callback() : error();
case 'hour':
return isPositiveInt(time.hour) && isPositiveInt(time.min) ? callback() : error();
case 'day':
return isPositiveInt(time.day) && isPositiveInt(time.hour) && isPositiveInt(time.min) ? callback() : error();
case 'week':
return time.week !== '' && isPositiveInt(time.hour) && isPositiveInt(time.min) ? callback() : error();
case 'month':
return isPositiveInt(time.day) && isPositiveInt(time.hour) && isPositiveInt(time.min) ? callback() : error();
default:
return error();
}
if (formData.time.type == 'week' && formData.time.week != '' && formData.time.hour != '' && formData.time.min != '') {
return callback()
}
if (formData.time.type == 'month' && formData.time.day != '' && formData.time.hour != '' && formData.time.min != '') {
return callback()
}
if (formData.time.type == 'day' && formData.time.day != '' && formData.time.hour != '' && formData.time.min != '') {
return callback()
}
if (formData.time.type == 'hour' && formData.time.hour != '' && formData.time.min != '') {
return callback()
}
return callback(new Error(t('cronTimeTips')))
}
const save_type = ref(false)
const addEvent = async (formEl: FormInstance | undefined) => {

View File

@ -117,6 +117,7 @@
<script lang="ts" setup>
import { reactive, ref, computed } from 'vue'
import { t } from '@/lang'
import { ArrowLeft } from "@element-plus/icons-vue"
import { getCronLogList, getCronTemplate, deleteCronLog, clearCronLog } from '@/app/api/sys'
import { ElMessageBox, ElMessage, FormInstance } from 'element-plus'
import { useRouter, useRoute } from 'vue-router'

View File

@ -19,10 +19,6 @@
</el-form>
</el-card>
<div class="mb-[10px] flex items-center">
<el-button @click="batchDelete" size="small">{{ t('batchDelete') }}</el-button>
</div>
<el-table :data="tableData.data" size="large" v-loading="tableData.loading" ref="tableRef" @selection-change="handleSelectionChange">
<template #empty>
@ -42,7 +38,7 @@
</template>
</el-table-column>
<el-table-column prop="create_time" :label="t('completeTime')" width="220px" />
<el-table-column prop="status_name" :label="t('status')" width="120px" />
<el-table-column prop="status_name" :label="t('status')" width="120px"/>
<el-table-column :label="t('operation')" align="right" width="160px">
<template #default="{ row }">
<el-button type="primary" link v-if="row.status == 'fail'" @click="handleFailReason(row)">{{ t('failReason') }}</el-button>
@ -50,7 +46,9 @@
</template>
</el-table-column>
</el-table>
<div class="mt-[10px] flex items-center">
<el-button @click="batchDelete" size="small">{{ t('batchDelete') }}</el-button>
</div>
<div class="mt-[16px] flex justify-end">
<el-pagination v-model:current-page="tableData.page"
v-model:page-size="tableData.limit" layout="total, sizes, prev, pager, next, jumper"

View File

@ -92,7 +92,7 @@
</template>
<div v-else class="flex flex-wrap">
<div v-for="(item, index) in childList" :key="index"
class="border border-br rounded-[3px] mr-[10px] mb-[10px] px-4 h-[32px] leading-[32px] cursor-pointer hover:bg-primary-light-9 px-[10px] hover:text-primary"
class="border border-br rounded-[3px] mr-[10px] mb-[10px] h-[32px] leading-[32px] cursor-pointer hover:bg-primary-light-9 px-[10px] hover:text-primary"
:class="{ 'border-primary text-primary': (parentLinkName != 'DIY_PAGE' && item.name == selectLink.name) || (parentLinkName == 'DIY_PAGE' && item.url == selectLink.url) }"
@click="changeChildLink(item)">{{ item.title }}
</div>
@ -194,12 +194,25 @@ const show = () => {
const getLinkFn = (callback: any = null) => {
getLink({}).then((res: any) => {
link.value = res.data
if(link.value && link.value.length == 0) return;
if (prop.ignore && prop.ignore.length) {
for (let key in link.value) {
for (let i = 0; i < prop.ignore.length; i++) {
if (key == prop.ignore[i]) {
delete link.value[key];
break;
delete link.value[key]
break
}
}
if (link.value[key] && link.value[key].child_list) {
for (let i = 0; i < link.value[key].child_list.length; i++) {
for (let j = 0; j < prop.ignore.length; j++) {
if (link.value[key].child_list[i].name == prop.ignore[j]) {
console.log(link.value[key].child_list[i])
link.value[key].child_list.splice(i, 1)
break
}
}
}
}
}

View File

@ -47,13 +47,12 @@ let editorEl = null
const serverHeaders = {}
serverHeaders[import.meta.env.VITE_REQUEST_HEADER_TOKEN_KEY] = getToken()
const baseUrl = import.meta.env.VITE_APP_BASE_URL.substr(-1) == '/' ? import.meta.env.VITE_APP_BASE_URL : `${import.meta.env.VITE_APP_BASE_URL}/`
const editorConfig = ref({
debug: false,
UEDITOR_HOME_URL: import.meta.env.MODE == 'development' ? '/public/ueditor/' : '/admin/ueditor/',
UEDITOR_CORS_URL: import.meta.env.MODE == 'development' ? location.origin + '/ueditor/' : location.origin + '/admin/ueditor/',
serverUrl: `${baseUrl}sys/ueditor`,
serverUrl: `${import.meta.env.VITE_APP_BASE_URL}sys/ueditor`,
serverHeaders,
//
autoHeightEnabled: false,

View File

@ -48,7 +48,7 @@
<el-col :span="10">
<div class="flex">
<el-upload v-bind="upload" ref="uploadRef" v-if="prop.type != 'icon'">
<el-button type="primary">{{ t('upload.upload' + type) }} {{ isOpen }}</el-button>
<el-button type="primary">{{ t('upload.upload' + type) }}</el-button>
</el-upload>
<div v-if="scene == 'attachment' && prop.type != 'icon'">
<el-button v-if="operate === false" class="ml-[10px]" type="primary" @click="operate = true">{{ t('edit') }}</el-button>
@ -192,9 +192,7 @@ import {
editAttachmentCategory as updateCategory,
deleteAttachmentCategory as deleteCategory,
deleteAttachment,
moveAttachment,
getIconCategoryList,
getIconList
moveAttachment
} from '@/app/api/sys'
import { debounce, img, getToken } from '@/utils/common'
import { ElMessage, UploadFile, UploadFiles, ElMessageBox, MessageParams } from 'element-plus'
@ -202,7 +200,6 @@ import storage from '@/utils/storage'
const attachmentCategoryName = ref('')
const operate = ref(false)
const repeat = ref(false)
const prop = defineProps({
@ -260,6 +257,72 @@ const attachmentParam = reactive({
cate_id: 0
})
const iconJsonFile = import.meta.glob('../../styles/icon/**/*.json', { eager: true })
const iconList = {
0: []
}
const getIconCategoryList = (search) => {
return new Promise((resolve, reject) => {
try {
const result = {
code: 1,
data: []
}
Object.keys(iconJsonFile).reverse().forEach(key => {
let item = iconJsonFile[key].default
if (search.name && item.name.indexOf(search.name) != -1) {
result.data.push(item)
} else if (!search.name) {
result.data.push(item)
}
if (!iconList[item.id]) {
item.glyphs = item.glyphs.map(iconItem => {
iconItem.url = `${ item.font_family } ${ item.css_prefix_text }${ iconItem.font_class }`
iconItem.real_name = iconItem.name
iconItem.att_id = iconItem.icon_id
return iconItem
})
iconList[0].push(...item.glyphs)
iconList[item.id] = item.glyphs
}
})
resolve(result)
} catch (e) {
console.error(e)
reject(e)
}
})
}
const getIconList = (search) => {
return new Promise((resolve, reject) => {
try {
const result = {
code: 1,
data: {
current_page: search.page,
per_page: 1,
total: 0,
data: []
}
}
let icons = JSON.parse(JSON.stringify(iconList[search.cate_id]))
if (search.real_name) icons = icons.filter(item => item.real_name.indexOf(search.real_name) != -1)
result.data.total = icons.length
result.data.data = icons.splice((search.page - 1) * search.limit, search.limit)
result.data.per_page = Math.ceil(icons.length / search.limit)
resolve(result)
} catch (e) {
console.error(e)
reject(e)
}
})
}
/**
* 查询分组
*/

View File

@ -167,12 +167,15 @@
"rollback": "回滚",
"showDialogCloseTips": "升级任务尚未完成,关闭将取消升级,是否要继续关闭?",
"upgradeCompleteTips": "升级完成后还必须要重新编译admin wap web端以免影响到程序正常运行。",
"upgradeTips": "应用和插件升级时,系统会自动备份当前程序及数据库。升级功能不会造成您当前程序的损坏或者数据的丢失,请放心使用,但是升级过程可能会因为兼容性等各种原因出现意外的升级错误,当出现错误时,请参考链接<a href='https://www.kancloud.cn/niushop/niushop_v6/3228611' target='_blank' class='text-primary'> https://www.kancloud.cn/niushop/niushop_v6/3228611 </a>手动回退上一版本!",
"upgradeTips": "应用和插件升级时,系统会自动备份当前程序及数据库。升级功能不会造成您当前程序的损坏或者数据的丢失,请放心使用,但是升级过程可能会因为兼容性等各种原因出现意外的升级错误,当出现错误时,系统将会自动回退上一版本!若回退失败,请参考链接<a href='https://www.kancloud.cn/niushop/niushop_v6/3228611' target='_blank' class='text-primary'> https://www.kancloud.cn/niushop/niushop_v6/3228611 </a>手动回退上一版本!",
"knownToKnow": "我已知晓,不需要再次提示",
"cloudBuildErrorTips": "一键云编译队列任务过多,请等待几分钟后重试!",
"isNeedBackup": "是否需要备份",
"isNeedBackupTips": "检测到已存在备份,此次升级是否需要备份,不需要备份在升级出现异常时将会恢复最近的一次备份。",
"isNeedBackupBtn": "查看备份记录"
"isNeedBackupBtn": "查看备份记录",
"option": "选项",
"isNeedCloudbuild": "是否需要云编译",
"cloudbuildTips": "此次升级的同时是否需要进行云编译"
},
"cloudbuild": {
"title": "云编译",

View File

@ -8,28 +8,37 @@
<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="p-[10px]">
<div class="userinfo flex h-full items-center pb-[10px] border-b-[1px] border-solid border-[#e5e5e5]">
<el-avatar v-if="userStore.userInfo.head_img" :size="45" :icon="UserFilled" :src="img(userStore.userInfo.head_img)"/>
<img v-else src="@/app/assets/images/member_head.png" class="w-[45px] rounded-full" />
<div>
<div class="user-name pl-[8px] text-[14px]">{{ userStore.userInfo.username }}</div>
<div class="pl-[8px] text-[13px] text-[#9699B6]">个人中心</div>
</div>
</div>
<el-dropdown-menu>
<el-dropdown-item @click="getUserInfoFn">
<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 @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>
</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>
</div>
</template>
</el-dropdown>
<el-dialog v-model="changePasswordDialog" width="450px" title="修改密码">
@ -100,16 +109,17 @@ const saveInfo = reactive({
});
//
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" },
]
});
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) => {

View File

@ -109,7 +109,7 @@ const oneMenuActive = ref(route.matched[1].name)
watch(route, () => {
if (route.meta.attr != '') {
oneMenuActive.value = route.matched[2].name
if (route.matched[2]) oneMenuActive.value = route.matched[2].name
twoMenuData.value = route.matched[1].children ?? []
} else {
//

File diff suppressed because it is too large Load Diff