mirror of
https://gitee.com/niucloud-team/niucloud.git
synced 2026-04-06 13:00:04 +00:00
同步admin
This commit is contained in:
parent
6f1167fe29
commit
b984a4c81c
@ -11,7 +11,7 @@ export function getNoticeList(params: any) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息发送记录
|
||||
* 消发送记录
|
||||
* @param params
|
||||
* @returns
|
||||
*/
|
||||
@ -19,6 +19,58 @@ export function getNoticeLog(params: any) {
|
||||
return request.get(`notice/log`, { params })
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取消息发送记录
|
||||
* @param params
|
||||
* @returns
|
||||
*/
|
||||
export function getBindInfo() {
|
||||
return request.get(`notice/bind/info`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消绑定
|
||||
* @param params
|
||||
* @returns
|
||||
*/
|
||||
export function cancelBind(params: any) {
|
||||
return request.post(`notice/bind/cancel`, params)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取微信配置
|
||||
* @returns
|
||||
*/
|
||||
export function getWechatAuthUrl() {
|
||||
return request.get('notice/bind/wechat')
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取微信配置
|
||||
* @returns
|
||||
*/
|
||||
export function getWeappAuthUrl() {
|
||||
return request.get('notice/bind/weapp')
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取微信配置
|
||||
* @returns
|
||||
*/
|
||||
export function sendSms(params: Record<string, any>) {
|
||||
return request.post('notice/bind/sms/send',params, { showSuccessMessage: true })
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取微信配置
|
||||
* @returns
|
||||
*/
|
||||
export function bindSms(params: Record<string, any>) {
|
||||
return request.post('notice/bind/sms/bind',params, { showSuccessMessage: true })
|
||||
}
|
||||
|
||||
/**
|
||||
* 消息详情
|
||||
* @param key
|
||||
@ -60,7 +112,7 @@ export function getSmsList() {
|
||||
* @returns
|
||||
*/
|
||||
export function getSmsInfo(sms_type: string) {
|
||||
return request.get(`notice/notice/sms/${ sms_type }`,)
|
||||
return request.get(`notice/notice/sms/${ sms_type }`)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -91,7 +143,7 @@ export function getAccountIsLogin() {
|
||||
* @param params
|
||||
*/
|
||||
export function loginAccount(params: Record<string, any>) {
|
||||
return request.post(`notice/niusms/account/login`,params,{ showSuccessMessage: true })
|
||||
return request.post(`notice/niusms/account/login`, params, { showSuccessMessage: true })
|
||||
}
|
||||
|
||||
/**
|
||||
@ -99,7 +151,7 @@ export function loginAccount(params: Record<string, any>) {
|
||||
* @param params
|
||||
*/
|
||||
export function registerAccount(params: Record<string, any>) {
|
||||
return request.post(`notice/niusms/account/register`,params,{ showSuccessMessage: true })
|
||||
return request.post(`notice/niusms/account/register`, params, { showSuccessMessage: true })
|
||||
}
|
||||
|
||||
/**
|
||||
@ -115,7 +167,7 @@ export function getAccountInfo(username: string) {
|
||||
* @param params
|
||||
*/
|
||||
export function getTemplateList(params: Record<string, any>) {
|
||||
return request.get(`notice/niusms/template/list/${params.sms_type}/${params.username}`,{})
|
||||
return request.get(`notice/niusms/template/list/${params.sms_type}/${params.username}`, {})
|
||||
}
|
||||
|
||||
/**
|
||||
@ -124,7 +176,7 @@ export function getTemplateList(params: Record<string, any>) {
|
||||
* @param params
|
||||
*/
|
||||
export function getSignList(username: string, params: Record<string, any>) {
|
||||
return request.get(`notice/niusms/sign/list/${username}`,{params})
|
||||
return request.get(`notice/niusms/sign/list/${username}`, { params })
|
||||
}
|
||||
|
||||
/**
|
||||
@ -150,8 +202,8 @@ export function deleteSign(username: string, params: Record<string, any>) {
|
||||
* @param username
|
||||
* @param params
|
||||
*/
|
||||
export function editAccount(username: string,params: Record<string, any>) {
|
||||
return request.post(`notice/niusms/account/edit/${username}`, params, { showSuccessMessage: true });
|
||||
export function editAccount(username: string, params: Record<string, any>) {
|
||||
return request.post(`notice/niusms/account/edit/${username}`, params, { showSuccessMessage: true })
|
||||
}
|
||||
|
||||
/**
|
||||
@ -160,7 +212,7 @@ export function editAccount(username: string,params: Record<string, any>) {
|
||||
* @param params
|
||||
*/
|
||||
export function getSmsSendList(username: string, params: Record<string, any>) {
|
||||
return request.get(`notice/niusms/account/send_list/${username}`,{params})
|
||||
return request.get(`notice/niusms/account/send_list/${username}`, { params })
|
||||
}
|
||||
|
||||
/**
|
||||
@ -169,7 +221,7 @@ export function getSmsSendList(username: string, params: Record<string, any>) {
|
||||
* @param params
|
||||
*/
|
||||
export function getSmsOrdersList(username: string, params: Record<string, any>) {
|
||||
return request.get(`notice/niusms/order/list/${username}`,{params})
|
||||
return request.get(`notice/niusms/order/list/${username}`, { params })
|
||||
}
|
||||
|
||||
/**
|
||||
@ -197,7 +249,7 @@ export function getsiteCaptcha() {
|
||||
* @param params
|
||||
*/
|
||||
export function getSmsSend(params: Record<string, any>) {
|
||||
return request.post(`notice/niusms/send`,params,{ showSuccessMessage: true })
|
||||
return request.post(`notice/niusms/send`, params, { showSuccessMessage: true })
|
||||
}
|
||||
|
||||
/**
|
||||
@ -223,7 +275,7 @@ export function getTemplateReportConfig() {
|
||||
* @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 })
|
||||
return request.post(`notice/niusms/template/report/${sms_type}/${username}`, params, { showSuccessMessage: true })
|
||||
}
|
||||
|
||||
/**
|
||||
@ -234,8 +286,8 @@ export function reportTemplate(sms_type: string, username: string, params: Recor
|
||||
* @param username
|
||||
* @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})
|
||||
export function getreportTemplateInfo(sms_type: string, username: string, params: Record<string, any>) {
|
||||
return request.get(`notice/niusms/template/info/${sms_type}/${username}`, { params })
|
||||
}
|
||||
|
||||
|
||||
@ -254,7 +306,7 @@ export function smsOrderCreate(username: string, params: Record<string, any>) {
|
||||
* @param params
|
||||
*/
|
||||
export function getOrderPayInfo(username: string, params: Record<string, any>) {
|
||||
return request.get(`notice/niusms/order/pay/${username}`, {params})
|
||||
return request.get(`notice/niusms/order/pay/${username}`, { params })
|
||||
}
|
||||
|
||||
/**
|
||||
@ -263,7 +315,7 @@ export function getOrderPayInfo(username: string, params: Record<string, any>) {
|
||||
* @param params
|
||||
*/
|
||||
export function getOrderInfo(username: string, params: Record<string, any>) {
|
||||
return request.get(`notice/niusms/order/info/${username}`, {params})
|
||||
return request.get(`notice/niusms/order/info/${username}`, { params })
|
||||
}
|
||||
|
||||
/**
|
||||
@ -272,7 +324,7 @@ export function getOrderInfo(username: string, params: Record<string, any>) {
|
||||
* @param params
|
||||
*/
|
||||
export function getOrderPayStatus(username: string, params: Record<string, any>) {
|
||||
return request.get(`notice/niusms/order/status/${username}`, {params})
|
||||
return request.get(`notice/niusms/order/status/${username}`, { params })
|
||||
}
|
||||
|
||||
/**
|
||||
@ -289,7 +341,7 @@ export function calculateOrderPay(username: string, params: Record<string, any>)
|
||||
* @param params
|
||||
*/
|
||||
export function enableNiusms(params: Record<string, any>) {
|
||||
return request.put(`notice/niusms/enable`,params,{ showSuccessMessage: true })
|
||||
return request.put(`notice/niusms/enable`, params, { showSuccessMessage: true })
|
||||
}
|
||||
|
||||
/**
|
||||
@ -307,8 +359,8 @@ export function templateSync(sms_type: string, username: string) {
|
||||
* @param username
|
||||
* @param params
|
||||
*/
|
||||
export function resetPassword(username: string,params: Record<string, any>) {
|
||||
return request.post(`notice/niusms/account/reset/password/${username}`,params,{ showSuccessMessage: true})
|
||||
export function resetPassword(username: string, params: Record<string, any>) {
|
||||
return request.post(`notice/niusms/account/reset/password/${username}`, params, { showSuccessMessage: true })
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -159,10 +159,10 @@ export function getPrinterBrand(params: Record<string, any>) {
|
||||
* @returns
|
||||
*/
|
||||
export function refreshPrinterToken(printer_id: number) {
|
||||
return request.put(`sys/printer/refreshtoken/${ printer_id }`,{},{
|
||||
return request.put(`sys/printer/refreshtoken/${ printer_id }`, {}, {
|
||||
showErrorMessage: true,
|
||||
showSuccessMessage: true
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@ -171,7 +171,7 @@ export function refreshPrinterToken(printer_id: number) {
|
||||
* @returns
|
||||
*/
|
||||
export function testPrint(printer_id: number) {
|
||||
return request.put(`sys/printer/testprint/${ printer_id }`, {},{ showErrorMessage: true, showSuccessMessage: true });
|
||||
return request.put(`sys/printer/testprint/${ printer_id }`, {}, { showErrorMessage: true, showSuccessMessage: true })
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -141,7 +141,7 @@ export function deleteSiteGroup(group_id: number) {
|
||||
* @param params
|
||||
* @returns
|
||||
*/
|
||||
export function getSiteGroupAll(params: Record<string, any> = {}) {
|
||||
export function getSiteGroupAll() {
|
||||
return request.get(`site/group/all`)
|
||||
}
|
||||
|
||||
|
||||
@ -591,21 +591,21 @@ export function setPatConfig(params: Record<string, any>) {
|
||||
/**
|
||||
* 刷新菜单
|
||||
*/
|
||||
export function menuRefresh(params: Record<string, any>) {
|
||||
export function menuRefresh() {
|
||||
return request.post(`sys/menu/refresh`, {})
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理数据字段缓存
|
||||
*/
|
||||
export function clearSchemaCache(params: Record<string, any>) {
|
||||
export function clearSchemaCache() {
|
||||
return request.post(`sys/schema/clear`, {}, { showSuccessMessage: true })
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理缓存
|
||||
*/
|
||||
export function clearCache(params: Record<string, any>) {
|
||||
export function clearCache() {
|
||||
return request.post(`sys/cache/clear`, {}, { showSuccessMessage: true })
|
||||
}
|
||||
|
||||
@ -794,3 +794,11 @@ export function getWxoplatform() {
|
||||
export function getQrcode(params: Record<string, any>) {
|
||||
return request.get(`sys/qrcode`, { params, showErrorMessage: false })
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验消息队列
|
||||
* @returns
|
||||
*/
|
||||
export function checkJobStatus() {
|
||||
return request.get(`sys/job`)
|
||||
}
|
||||
|
||||
@ -36,7 +36,7 @@ export function getVerifyDetailInfo(verifyCode: string) {
|
||||
* @returns
|
||||
*/
|
||||
export function verify(verifyCode: string, params: Record<string, any>) {
|
||||
return request.post(`verify/verify/${ verifyCode }`,params,{ showSuccessMessage: true})
|
||||
return request.post(`verify/verify/${ verifyCode }`, params, { showSuccessMessage: true })
|
||||
}
|
||||
/***************************************************** 核销员 ****************************************************/
|
||||
|
||||
@ -97,5 +97,5 @@ export function getVerifyInfo(id: number) {
|
||||
* @returns
|
||||
*/
|
||||
export function editVerifier(params: Record<string, any>) {
|
||||
return request.post(`verify/verifier/${ params.id }`, params,{ showSuccessMessage: true })
|
||||
return request.post(`verify/verifier/${ params.id }`, params, { showSuccessMessage: true })
|
||||
}
|
||||
|
||||
@ -8,6 +8,14 @@ export function getWeappConfig() {
|
||||
return request.get('weapp/config')
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取微信小程序配置
|
||||
* @returns
|
||||
*/
|
||||
export function getWeappAuthUrl() {
|
||||
return request.get('weapp/auth_url')
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑微信小程序配置
|
||||
* @param params
|
||||
|
||||
@ -377,7 +377,7 @@ const dialogCancel = () => {
|
||||
}
|
||||
|
||||
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')
|
||||
window.open('https://doc.press.niucloud.com/php/v6-shop/use/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.html')
|
||||
}
|
||||
|
||||
watch(() => showDialog.value, () => {
|
||||
|
||||
@ -33,6 +33,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, defineProps, nextTick } from "vue"
|
||||
import { t } from "@/lang"
|
||||
import { ElMessage } from "element-plus"
|
||||
import { getAppVersionList, getFrameworkVersionList } from "@/app/api/module"
|
||||
|
||||
const props = defineProps({
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -17,7 +17,7 @@
|
||||
"mobileOrUsernameNoEmpty": "普通注册方式至少需启用一种",
|
||||
"loginPageSet": "界面设置",
|
||||
"bgUrl": "背景图",
|
||||
"bgUrlPlaceholder": "建议图片尺寸:750*669像素;图片格式:jpg、png、jpeg",
|
||||
"bgUrlPlaceholder": "前台快捷登录/注册页的背景图,建议图片尺寸:750*669像素;图片格式:jpg、png、jpeg",
|
||||
"desc": "描述",
|
||||
"descPlaceholder": "请输入描述"
|
||||
}
|
||||
|
||||
@ -30,8 +30,8 @@
|
||||
"customerServiceCode": "客服二维码",
|
||||
"contactsTel": "联系电话",
|
||||
"contactsTelPlaceholder": "请输入联系电话",
|
||||
"logoPlaceholder": "建议图片尺寸:210*30像素;图片格式:jpg、png、jpeg。",
|
||||
"iconPlaceholder": "建议图片尺寸:100*100像素;图片格式:jpg、png、jpeg。",
|
||||
"logoPlaceholder": "管理系统左上角的长方形Logo,建议图片尺寸:210*30像素;图片格式:jpg、png、jpeg。",
|
||||
"iconPlaceholder": "管理系统左上角的正方形Logo,建议图片尺寸:100*100像素;图片格式:jpg、png、jpeg。",
|
||||
"siteLoginLogo": "站点登录Logo",
|
||||
"siteLoginLogoTips": "站点端登录Logo,建议图片尺寸:132*40像素;图片格式:jpg、png、jpeg。",
|
||||
"siteLoginBgImg": "站点登录背景图",
|
||||
|
||||
@ -479,7 +479,7 @@ const handleFailReason = (data: any) => {
|
||||
}
|
||||
|
||||
const helpInfo = () => {
|
||||
window.open('https://doc.niucloud.com/saasUse.html?keywords=/configFAQ/minWaChatUpload')
|
||||
window.open('https://doc.press.niucloud.com/php/saas-framework/use/configFAQ/minWaChatUpload.html')
|
||||
}
|
||||
|
||||
const knownToKnow = () => {
|
||||
|
||||
@ -33,6 +33,12 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="t('消息类型')" min-width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<span class="text-[#fff] rounded-[3px] px-[8px] py-[2px]" :class="{'bg-[#008000]': row.receiver_type == 1, 'bg-[#0000ff]': row.receiver_type == 0}">{{ row.receiver_type == 1 ? t('buyerNews') : t('sellerMessage') }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="t('response')" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<div v-for="(item, index) in row.weapp.content" :key="'a' + index" class="text-left">{{ item.join(":") }}</div>
|
||||
|
||||
@ -15,50 +15,53 @@
|
||||
<el-tab-pane :label="t('reply')" name="/channel/wechat/reply" />
|
||||
</el-tabs>
|
||||
|
||||
<el-table :data="cronTableData.data" :span-method="templateSpan" size="large" v-loading="cronTableData.loading">
|
||||
<template #empty>
|
||||
<span>{{ !cronTableData.loading ? t('emptyData') : '' }}</span>
|
||||
</template>
|
||||
|
||||
<el-table-column prop="addon_name" :label="t('addon')" min-width="120" />
|
||||
<el-table-column prop="name" :show-overflow-tooltip="true" :label="t('name')" min-width="150" >
|
||||
<template #default="{ row }">
|
||||
<div class="flex items-center">
|
||||
<span class="mr-[5px]">{{row.name }}</span>
|
||||
<el-tooltip :content="row.wechat.tips" v-if="row.wechat.tips" placement="top">
|
||||
<icon name="element WarningFilled" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<el-alert :title="t('微信平台最多支持25个消息模板获取时请注意微信平台剩余模板数量是否充足')" type="info" show-icon />
|
||||
<div class="mt-[20px]">
|
||||
<el-table :data="cronTableData.data" :span-method="templateSpan" size="large" v-loading="cronTableData.loading">
|
||||
<template #empty>
|
||||
<span>{{ !cronTableData.loading ? t('emptyData') : '' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="t('messageType')" min-width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.message_type == 1 ? t('buyerNews') : t('sellerMessage') }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="addon_name" :label="t('addon')" min-width="120" />
|
||||
<el-table-column prop="name" :show-overflow-tooltip="true" :label="t('name')" min-width="150" >
|
||||
<template #default="{ row }">
|
||||
<div class="flex items-center">
|
||||
<span class="mr-[5px]">{{row.name }}</span>
|
||||
<el-tooltip :content="row.wechat.tips" v-if="row.wechat.tips" placement="top">
|
||||
<icon name="element WarningFilled" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="t('isStart')" min-width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ row.is_wechat == 1 ? t('startUsing') : t('statusDeactivate') }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('messageType')" min-width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<span class="text-[#fff] rounded-[3px] px-[8px] py-[2px]" :class="{'bg-[#008000]': row.receiver_type == 1, 'bg-[#0000ff]': row.receiver_type == 0}">{{ row.receiver_type == 1 ? t('buyerNews') : t('sellerMessage') }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="t('response')" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<div v-for="(item, index) in row.wechat.content" :key="'a' + index" class="text-left">{{ item.join(":") }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('isStart')" min-width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ row.is_wechat == 1 ? t('startUsing') : t('statusDeactivate') }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="wechat_template_id" :label="t('serialNumber')" min-width="140" />
|
||||
<el-table-column :label="t('response')" min-width="180">
|
||||
<template #default="{ row }">
|
||||
<div v-for="(item, index) in row.wechat.content" :key="'a' + index" class="text-left">{{ item.join(":") }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="t('operation')" fixed="right" align="right" width="200">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="infoSwitch(row)">{{ row.is_wechat == 1 ? t('close') : t('open') }}</el-button>
|
||||
<el-button type="primary" link @click="batchAcquisitionFn(row)">{{ t('regain') }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<el-table-column prop="wechat_template_id" :label="t('serialNumber')" min-width="140" />
|
||||
|
||||
<el-table-column :label="t('operation')" fixed="right" align="right" width="200">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="infoSwitch(row)">{{ row.is_wechat == 1 ? t('close') : t('open') }}</el-button>
|
||||
<el-button type="primary" link @click="batchAcquisitionFn(row)">{{ t('regain') }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -47,7 +47,7 @@ const formRules = reactive<FormRules>({
|
||||
callback('请输入积分数量')
|
||||
} else if (isNaN(value) || !regExp.number.test(value)) {
|
||||
callback('积分数量格式错误')
|
||||
} else if (value <=0) {
|
||||
} else if (value <= 0) {
|
||||
callback('积分数量不能小于等于0')
|
||||
} else{
|
||||
callback();
|
||||
|
||||
@ -14,7 +14,9 @@
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('adjustBalance')" prop="adjust">
|
||||
<el-input-number v-model="formData.adjust" clearable :min="0" :max="999999" :placeholder="t('adjustBalancePlaceholder')" @focus="formData.adjust = ''" class="!w-[200px]"/>
|
||||
<div>
|
||||
<el-input-number v-model="formData.adjust" clearable :min="0" :max="999999" :placeholder="t('adjustBalancePlaceholder')" @focus="formData.adjust = ''" class="!w-[200px]"/>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('memo')" prop="memo">
|
||||
@ -70,7 +72,7 @@ const formRules = computed(() => {
|
||||
callback(new Error(t('adjustBalancePlaceholder')))
|
||||
}
|
||||
|
||||
if (formData.adjust_type == -1 && (parseFloat(formData.balance) - adjust < 0)) {
|
||||
if (formData.adjust_type == -1 && (parseFloat(formData.balance) - adjust) < 0) {
|
||||
callback(new Error(t('adjustBalanceMaxAccountMessage')))
|
||||
}
|
||||
|
||||
|
||||
@ -14,7 +14,9 @@
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('adjustPoint')" prop="adjust">
|
||||
<el-input-number v-model="formData.adjust" clearable :min="0" :max="999999" :placeholder="t('adjustPlaceholder')" @focus="formData.adjust = ''" class="!w-[200px]"/>
|
||||
<div>
|
||||
<el-input-number v-model="formData.adjust" clearable :min="0" :max="999999" :placeholder="t('adjustPlaceholder')" @focus="formData.adjust = ''" class="!w-[200px]"/>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('memo')" prop="memo">
|
||||
@ -72,7 +74,7 @@ const formRules = computed(() => {
|
||||
callback(new Error(t('adjustPointPlaceholder')))
|
||||
}
|
||||
|
||||
if (formData.adjust_type == -1 && (parseFloat(formData.point) - adjust < 0)) {
|
||||
if (formData.adjust_type == -1 && (parseFloat(formData.point) - adjust) < 0) {
|
||||
callback(new Error(t('adjustPointMaxAccountMessage')))
|
||||
}
|
||||
|
||||
|
||||
@ -1,38 +1,68 @@
|
||||
<template>
|
||||
<el-dialog v-model="showDialog" :title="t('noticeSetting')" width="550px" :destroy-on-close="true">
|
||||
<el-form :model="formData" label-width="110px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
|
||||
<el-form-item :label="t('status')">
|
||||
<el-radio-group v-model="formData.is_sms">
|
||||
<el-radio :label="1">{{ t('startUsing') }}</el-radio>
|
||||
<el-radio :label="0">{{ t('statusDeactivate') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-dialog
|
||||
v-model="showDialog"
|
||||
:title="t('noticeSetting')"
|
||||
width="550px"
|
||||
:destroy-on-close="true"
|
||||
>
|
||||
<el-alert
|
||||
v-if="!formData.bind_sms && formData.is_need_bind_merchant"
|
||||
title="未绑定接收者手机号"
|
||||
type="error"
|
||||
:closable="false"
|
||||
/>
|
||||
<el-form
|
||||
:model="formData"
|
||||
label-width="110px"
|
||||
ref="formRef"
|
||||
:rules="formRules"
|
||||
class="page-form"
|
||||
v-loading="loading"
|
||||
>
|
||||
<el-form-item :label="t('status')">
|
||||
<el-radio-group v-model="formData.is_sms">
|
||||
<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('name')">
|
||||
<div class="input-width"> {{ formData.name }} </div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('name')">
|
||||
<div class="input-width">{{ formData.name }}</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('title')">
|
||||
<div class="input-width"> {{ formData.title }} </div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('title')">
|
||||
<div class="input-width">{{ formData.title }}</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('smsId')" prop="sms_id">
|
||||
<el-input v-model.trim="formData.sms_id" :placeholder="t('smsIdPlaceholder')" class="input-width" show-word-limit clearable />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('smsId')" prop="sms_id">
|
||||
<el-input
|
||||
v-model.trim="formData.sms_id"
|
||||
:placeholder="t('smsIdPlaceholder')"
|
||||
class="input-width"
|
||||
show-word-limit
|
||||
clearable
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('smsContent')">
|
||||
<div class="input-width"> {{ formData.content }} </div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('smsContent')">
|
||||
<div class="input-width">{{ formData.content }}</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
</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 #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>
|
||||
@ -48,14 +78,16 @@ const loading = ref(true)
|
||||
* 表单数据
|
||||
*/
|
||||
const initialFormData = {
|
||||
is_sms: 0,
|
||||
key: '',
|
||||
name: '',
|
||||
sms_default_content: '',
|
||||
title: '',
|
||||
type: '',
|
||||
sms_id: '',
|
||||
content: ''
|
||||
is_sms: 0,
|
||||
key: '',
|
||||
name: '',
|
||||
sms_default_content: '',
|
||||
title: '',
|
||||
type: '',
|
||||
sms_id: '',
|
||||
content: '',
|
||||
bind_sms: '',
|
||||
is_need_bind_merchant: '',
|
||||
}
|
||||
const formData: Record<string, any> = reactive({ ...initialFormData })
|
||||
|
||||
@ -63,11 +95,11 @@ const formRef = ref<FormInstance>()
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = computed(() => {
|
||||
return {
|
||||
sms_id: [
|
||||
{ required: true, message: t('smsIdPlaceholder'), trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
return {
|
||||
sms_id: [
|
||||
{ required: true, message: t('smsIdPlaceholder'), trigger: 'blur' },
|
||||
],
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['complete'])
|
||||
@ -77,44 +109,47 @@ const emit = defineEmits(['complete'])
|
||||
* @param formEl
|
||||
*/
|
||||
const confirm = async (formEl: FormInstance | undefined) => {
|
||||
if (loading.value || !formEl) return
|
||||
if (loading.value || !formEl) return
|
||||
|
||||
await formEl.validate(async (valid) => {
|
||||
if (valid) {
|
||||
loading.value = true
|
||||
await formEl.validate(async (valid) => {
|
||||
if (valid) {
|
||||
loading.value = true
|
||||
|
||||
const data = formData
|
||||
data.status = data.is_sms
|
||||
const data = formData
|
||||
data.status = data.is_sms
|
||||
|
||||
editNotice(data).then(res => {
|
||||
loading.value = false
|
||||
showDialog.value = false
|
||||
emit('complete')
|
||||
}).catch(() => {
|
||||
loading.value = false
|
||||
// showDialog.value = false
|
||||
})
|
||||
}
|
||||
})
|
||||
editNotice(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)
|
||||
loading.value = true
|
||||
Object.assign(formData, initialFormData)
|
||||
|
||||
if (row) {
|
||||
Object.keys(formData).forEach((key: string) => {
|
||||
if (row[key] != undefined) formData[key] = row[key]
|
||||
if (row.sms && row.sms[key] != undefined) formData[key] = row.sms[key]
|
||||
})
|
||||
}
|
||||
if (row) {
|
||||
Object.keys(formData).forEach((key: string) => {
|
||||
if (row[key] != undefined) formData[key] = row[key]
|
||||
if (row.sms && row.sms[key] != undefined)
|
||||
formData[key] = row.sms[key]
|
||||
})
|
||||
}
|
||||
|
||||
loading.value = false
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
showDialog,
|
||||
setFormData
|
||||
showDialog,
|
||||
setFormData,
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@ -1,36 +1,62 @@
|
||||
<template>
|
||||
<el-dialog v-model="showDialog" :title="t('noticeSetting')" width="550px" :destroy-on-close="true">
|
||||
<el-form :model="formData" label-width="110px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
|
||||
<el-form-item :label="t('status')">
|
||||
<el-radio-group v-model="formData.is_weapp">
|
||||
<el-radio :label="1">{{ t('startUsing') }}</el-radio>
|
||||
<el-radio :label="0">{{ t('statusDeactivate') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-dialog
|
||||
v-model="showDialog"
|
||||
:title="t('noticeSetting')"
|
||||
width="550px"
|
||||
:destroy-on-close="true"
|
||||
>
|
||||
<el-alert
|
||||
v-if="!formData.bind_weapp && formData.is_need_bind_merchant"
|
||||
title="未绑定接收者小程序"
|
||||
type="error"
|
||||
:closable="false"
|
||||
/>
|
||||
<el-form
|
||||
:model="formData"
|
||||
label-width="110px"
|
||||
ref="formRef"
|
||||
:rules="formRules"
|
||||
class="page-form"
|
||||
v-loading="loading"
|
||||
>
|
||||
<el-form-item :label="t('status')">
|
||||
<el-radio-group v-model="formData.is_weapp">
|
||||
<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('name')">
|
||||
<div class="input-width"> {{ formData.name }} </div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('name')">
|
||||
<div class="input-width">{{ formData.name }}</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('weappTempKey')">
|
||||
<div class="input-width"> {{ formData.tid }} </div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('weappTempKey')">
|
||||
<div class="input-width">{{ formData.tid }}</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('content')">
|
||||
<div class="input-width">
|
||||
<div v-for="(item, index) in formData.content" :key="index">{{ item[0] }}:{{ item[1] }} </div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('content')">
|
||||
<div class="input-width">
|
||||
<div v-for="(item, index) in formData.content" :key="index">
|
||||
{{ item[0] }}:{{ item[1] }}
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
</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 #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>
|
||||
@ -46,15 +72,17 @@ const loading = ref(true)
|
||||
* 表单数据
|
||||
*/
|
||||
const initialFormData = {
|
||||
is_weapp: 0,
|
||||
key: '',
|
||||
name: '',
|
||||
title: '',
|
||||
type: '',
|
||||
content: [],
|
||||
first: '',
|
||||
remark: '',
|
||||
tid: ''
|
||||
is_weapp: 0,
|
||||
key: '',
|
||||
name: '',
|
||||
title: '',
|
||||
type: '',
|
||||
content: [],
|
||||
first: '',
|
||||
remark: '',
|
||||
tid: '',
|
||||
bind_weapp: '',
|
||||
is_need_bind_merchant: '',
|
||||
}
|
||||
const formData: Record<string, any> = reactive({ ...initialFormData })
|
||||
|
||||
@ -62,9 +90,7 @@ const formRef = ref<FormInstance>()
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = computed(() => {
|
||||
return {
|
||||
|
||||
}
|
||||
return {}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['complete'])
|
||||
@ -74,43 +100,45 @@ const emit = defineEmits(['complete'])
|
||||
* @param formEl
|
||||
*/
|
||||
const confirm = async (formEl: FormInstance | undefined) => {
|
||||
if (loading.value || !formEl) return
|
||||
if (loading.value || !formEl) return
|
||||
|
||||
await formEl.validate(async (valid) => {
|
||||
if (valid) {
|
||||
loading.value = true
|
||||
await formEl.validate(async (valid) => {
|
||||
if (valid) {
|
||||
loading.value = true
|
||||
|
||||
const data = formData
|
||||
data.status = data.is_weapp
|
||||
const data = formData
|
||||
data.status = data.is_weapp
|
||||
|
||||
editNoticeStatus(data).then(res => {
|
||||
loading.value = false
|
||||
showDialog.value = false
|
||||
emit('complete')
|
||||
}).catch(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
})
|
||||
editNoticeStatus(data)
|
||||
.then((res) => {
|
||||
loading.value = false
|
||||
showDialog.value = false
|
||||
emit('complete')
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const setFormData = async (row: any = null) => {
|
||||
loading.value = true
|
||||
Object.assign(formData, initialFormData)
|
||||
loading.value = true
|
||||
Object.assign(formData, initialFormData)
|
||||
|
||||
if (row) {
|
||||
Object.keys(formData).forEach((key: string) => {
|
||||
if (row[key] != undefined) formData[key] = row[key]
|
||||
if (row.weapp && row.weapp[key] != undefined) formData[key] = row.weapp[key]
|
||||
})
|
||||
}
|
||||
|
||||
loading.value = false
|
||||
if (row) {
|
||||
Object.keys(formData).forEach((key: string) => {
|
||||
if (row[key] != undefined) formData[key] = row[key]
|
||||
if (row.weapp && row.weapp[key] != undefined)
|
||||
formData[key] = row.weapp[key]
|
||||
})
|
||||
}
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
showDialog,
|
||||
setFormData
|
||||
showDialog,
|
||||
setFormData,
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@ -1,48 +1,80 @@
|
||||
<template>
|
||||
<el-dialog v-model="showDialog" :title="t('noticeSetting')" width="550px" :destroy-on-close="true">
|
||||
<el-form :model="formData" label-width="110px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
|
||||
<el-form-item :label="t('status')">
|
||||
<el-radio-group v-model="formData.is_wechat">
|
||||
<el-radio :label="1">{{ t('startUsing') }}</el-radio>
|
||||
<el-radio :label="0">{{ t('statusDeactivate') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-dialog
|
||||
v-model="showDialog"
|
||||
:title="t('noticeSetting')"
|
||||
width="550px"
|
||||
:destroy-on-close="true"
|
||||
>
|
||||
<el-alert
|
||||
v-if="!formData.bind_wechat && formData.is_need_bind_merchant"
|
||||
title="未绑定接收者微信"
|
||||
type="error"
|
||||
:closable="false"
|
||||
/>
|
||||
<el-form
|
||||
:model="formData"
|
||||
label-width="110px"
|
||||
ref="formRef"
|
||||
:rules="formRules"
|
||||
class="page-form"
|
||||
v-loading="loading"
|
||||
>
|
||||
<el-form-item :label="t('status')">
|
||||
<el-radio-group v-model="formData.is_wechat">
|
||||
<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('name')">
|
||||
<div class="input-width">{{ formData.name }} </div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('name')">
|
||||
<div class="input-width">{{ formData.name }}</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('tempKey')">
|
||||
<div class="input-width">{{ formData.temp_key }} </div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('tempKey')">
|
||||
<div class="input-width">{{ formData.temp_key }}</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('keywordNameList')">
|
||||
<div class="input-width">{{ formData.keyword_name_list ? formData.keyword_name_list.join(',') : '' }} </div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('keywordNameList')">
|
||||
<div class="input-width">
|
||||
{{
|
||||
formData.keyword_name_list
|
||||
? formData.keyword_name_list.join(',')
|
||||
: ''
|
||||
}}
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<!-- <el-form-item :label="t('first')" prop="first">-->
|
||||
<!-- <el-input v-model.trim="formData.wechat_first" :placeholder="t('firstPlaceholder')" class="input-width" show-word-limit clearable />-->
|
||||
<!-- </el-form-item>-->
|
||||
<!-- <el-form-item :label="t('first')" prop="first">-->
|
||||
<!-- <el-input v-model.trim="formData.wechat_first" :placeholder="t('firstPlaceholder')" class="input-width" show-word-limit clearable />-->
|
||||
<!-- </el-form-item>-->
|
||||
|
||||
<el-form-item :label="t('content')">
|
||||
<div class="input-width">
|
||||
<div v-for="(item, index) in formData.content" :key="index">{{ item[0] }}:{{ item[1] }} </div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('content')">
|
||||
<div class="input-width">
|
||||
<div v-for="(item, index) in formData.content" :key="index">
|
||||
{{ item[0] }}:{{ item[1] }}
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<!-- <el-form-item :label="t('remark')" prop="remark">-->
|
||||
<!-- <el-input v-model.trim="formData.wechat_remark" :placeholder="t('remarkPlaceholder')" class="input-width" show-word-limit clearable />-->
|
||||
<!-- </el-form-item>-->
|
||||
<!-- <el-form-item :label="t('remark')" prop="remark">-->
|
||||
<!-- <el-input v-model.trim="formData.wechat_remark" :placeholder="t('remarkPlaceholder')" class="input-width" show-word-limit clearable />-->
|
||||
<!-- </el-form-item>-->
|
||||
</el-form>
|
||||
|
||||
</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 #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>
|
||||
@ -58,18 +90,20 @@ const loading = ref(true)
|
||||
* 表单数据
|
||||
*/
|
||||
const initialFormData = {
|
||||
is_wechat: 0,
|
||||
key: '',
|
||||
name: '',
|
||||
title: '',
|
||||
type: '',
|
||||
content: [],
|
||||
// first: '',
|
||||
// remark: '',
|
||||
temp_key: '',
|
||||
keyword_name_list: ''
|
||||
// wechat_first: '',
|
||||
// wechat_remark: ''
|
||||
is_wechat: 0,
|
||||
key: '',
|
||||
name: '',
|
||||
title: '',
|
||||
type: '',
|
||||
content: [],
|
||||
// first: '',
|
||||
// remark: '',
|
||||
temp_key: '',
|
||||
keyword_name_list: '',
|
||||
// wechat_first: '',
|
||||
// wechat_remark: ''
|
||||
bind_wechat: '',
|
||||
is_need_bind_merchant: '',
|
||||
}
|
||||
|
||||
const formData: Record<string, any> = reactive({ ...initialFormData })
|
||||
@ -78,9 +112,7 @@ const formRef = ref<FormInstance>()
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = computed(() => {
|
||||
return {
|
||||
|
||||
}
|
||||
return {}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['complete'])
|
||||
@ -90,42 +122,45 @@ const emit = defineEmits(['complete'])
|
||||
* @param formEl
|
||||
*/
|
||||
const confirm = async (formEl: FormInstance | undefined) => {
|
||||
if (loading.value || !formEl) return
|
||||
if (loading.value || !formEl) return
|
||||
|
||||
await formEl.validate(async (valid) => {
|
||||
if (valid) {
|
||||
loading.value = true
|
||||
await formEl.validate(async (valid) => {
|
||||
if (valid) {
|
||||
loading.value = true
|
||||
|
||||
const data = formData
|
||||
data.status = data.is_wechat
|
||||
editNotice(data).then(res => {
|
||||
loading.value = false
|
||||
showDialog.value = false
|
||||
emit('complete')
|
||||
}).catch(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
})
|
||||
const data = formData
|
||||
data.status = data.is_wechat
|
||||
editNotice(data)
|
||||
.then((res) => {
|
||||
loading.value = false
|
||||
showDialog.value = false
|
||||
emit('complete')
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const setFormData = async (row: any = null) => {
|
||||
loading.value = true
|
||||
Object.assign(formData, initialFormData)
|
||||
if (row) {
|
||||
Object.keys(formData).forEach((key: string) => {
|
||||
if (row[key] != undefined) formData[key] = row[key]
|
||||
if (row.wechat && row.wechat[key] != undefined) formData[key] = row.wechat[key]
|
||||
})
|
||||
// if (!row.wechat_first) formData['wechat_first'] = row['first']
|
||||
// if (!row.wechat_remark) formData['wechat_remark'] = row['remark']
|
||||
}
|
||||
loading.value = false
|
||||
loading.value = true
|
||||
Object.assign(formData, initialFormData)
|
||||
if (row) {
|
||||
Object.keys(formData).forEach((key: string) => {
|
||||
if (row[key] != undefined) formData[key] = row[key]
|
||||
if (row.wechat && row.wechat[key] != undefined)
|
||||
formData[key] = row.wechat[key]
|
||||
})
|
||||
// if (!row.wechat_first) formData['wechat_first'] = row['first']
|
||||
// if (!row.wechat_remark) formData['wechat_remark'] = row['remark']
|
||||
}
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
showDialog,
|
||||
setFormData
|
||||
showDialog,
|
||||
setFormData,
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@ -1,142 +1,247 @@
|
||||
<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>签名数据的变更(新增 / 删除)需经过五分钟的生效周期,在此期间系统将完成数据同步与更新</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>
|
||||
<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>签名数据的变更(新增 / 删除)需经过五分钟的生效周期,在此期间系统将完成数据同步与更新</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>
|
||||
<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 signConfig.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 signConfig.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 #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">
|
||||
<div class="max-h-[600px] overflow-auto">
|
||||
<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 signConfig.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 signConfig.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('营业执照')" prop="bizLicenseUrl">
|
||||
<upload-image v-model="formData.bizLicenseUrl" :limit="1" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('企查查唯一性截图')" prop="qccUrl">
|
||||
<upload-image v-model="formData.qccUrl" :limit="1" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('中国商标网截图')" prop="tmnetUrl">
|
||||
<upload-image v-model="formData.tmnetUrl" :limit="1" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('移动ICP截图')" prop="mobileIcpUrl">
|
||||
<upload-image v-model="formData.mobileIcpUrl" :limit="1" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('应用商店 /小程序页面开发者截图')" prop="telecomAppstoreUrl">
|
||||
<upload-image v-model="formData.telecomAppstoreUrl" :limit="1" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('身份证正面(人像面)')" prop="idcardFrontUrl">
|
||||
<upload-image v-model="formData.idcardFrontUrl" :limit="1" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('身份证反面URL')" prop="idcardBackUrl">
|
||||
<upload-image v-model="formData.idcardBackUrl" :limit="1" />
|
||||
</el-form-item>
|
||||
<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>
|
||||
</div>
|
||||
<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>
|
||||
@ -148,196 +253,277 @@ const visible = ref(false)
|
||||
const visibleAdd = ref(false)
|
||||
const emit = defineEmits(['select'])
|
||||
const props = defineProps({
|
||||
username: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
username: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
const initialFormData = {
|
||||
defaultSign: 0,
|
||||
imgUrl: '',
|
||||
contentExample: '',
|
||||
signType: '',
|
||||
signSource: '',
|
||||
principalIdCard: '',
|
||||
principalName: '',
|
||||
principalMobile: '',
|
||||
legalPerson: '',
|
||||
creditCode: '',
|
||||
companyName: '',
|
||||
signature: ''
|
||||
defaultSign: 0,
|
||||
imgUrl: '',
|
||||
bizLicenseUrl: '',
|
||||
qccUrl: '',
|
||||
tmnetUrl: '',
|
||||
mobileIcpUrl: '',
|
||||
telecomAppstoreUrl: '',
|
||||
idcardFrontUrl: '',
|
||||
idcardBackUrl: '',
|
||||
contentExample: '',
|
||||
signType: '',
|
||||
signSource: '',
|
||||
principalIdCard: '',
|
||||
principalName: '',
|
||||
principalMobile: '',
|
||||
legalPerson: '',
|
||||
creditCode: '',
|
||||
companyName: '',
|
||||
signature: '',
|
||||
}
|
||||
const formData = reactive({ ...initialFormData })
|
||||
|
||||
const signConfig = reactive({
|
||||
signTypeList: [],
|
||||
signSourceList: []
|
||||
signTypeList: [],
|
||||
signSourceList: [],
|
||||
})
|
||||
const getSmsSignConfigFn = () => {
|
||||
getSmsSignConfig().then(res => {
|
||||
signConfig.signTypeList = res.data.sign_type_list
|
||||
signConfig.signSourceList = res.data.sign_source_list
|
||||
formData.signSource = res.data.sign_source_list[0].type
|
||||
formData.signType = res.data.sign_type_list[0].type
|
||||
})
|
||||
getSmsSignConfig().then((res) => {
|
||||
signConfig.signTypeList = res.data.sign_type_list
|
||||
signConfig.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('短信签名必须被【】包裹'))
|
||||
}
|
||||
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 content = value.slice(1, -1)
|
||||
|
||||
const lengthValid = content.length >= 2 && content.length <= 20
|
||||
if (!lengthValid) {
|
||||
return callback(new Error('短信签名内容需在 2-20 个字符之间'))
|
||||
}
|
||||
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('短信签名不能包含空格或特殊字符 - + = * & % # @ ~ ;'))
|
||||
}
|
||||
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'
|
||||
}
|
||||
]
|
||||
|
||||
}
|
||||
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',
|
||||
},
|
||||
],
|
||||
bizLicenseUrl: [{ required: true, message: '请输入营业执照', trigger: 'blur' }],
|
||||
qccUrl: [
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
const needImage = [1, 2].includes(formData.signSource) || formData.signType === 1
|
||||
if (needImage) {
|
||||
if (!value || value.length === 0) {
|
||||
callback(new Error('请上传企查查唯一性截图'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
} else {
|
||||
callback() // 不需要校验
|
||||
}
|
||||
},
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
tmnetUrl: [
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
const needImage = [3].includes(formData.signSource)
|
||||
if (needImage) {
|
||||
if (!value || value.length === 0) {
|
||||
callback(new Error('请上传中国商标网截图'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
} else {
|
||||
callback() // 不需要校验
|
||||
}
|
||||
},
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
mobileIcpUrl: [
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
const needImage = [4, 5].includes(formData.signSource)
|
||||
if (needImage) {
|
||||
if (!value || value.length === 0) {
|
||||
callback(new Error('请上传移动ICP截图'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
} else {
|
||||
callback() // 不需要校验
|
||||
}
|
||||
},
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
telecomAppstoreUrl: [
|
||||
{
|
||||
validator: (rule, value, callback) => {
|
||||
const needImage = [4, 5].includes(formData.signSource)
|
||||
if (needImage) {
|
||||
if (!value || value.length === 0) {
|
||||
callback(new Error('请上传应用商店/小程序页面开发者截图'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
} else {
|
||||
callback() // 不需要校验
|
||||
}
|
||||
},
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
idcardFrontUrl: [
|
||||
{
|
||||
required: true,
|
||||
message: '请输入身份证正面(人像面)',
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
idcardBackUrl: [{ required: true, message: '请输入身份证反面', 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()
|
||||
}
|
||||
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()
|
||||
}
|
||||
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)
|
||||
})
|
||||
}
|
||||
})
|
||||
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: {}
|
||||
page: 1,
|
||||
limit: 10,
|
||||
total: 0,
|
||||
loading: false,
|
||||
data: [],
|
||||
searchParam: {},
|
||||
})
|
||||
|
||||
const open = () => {
|
||||
visible.value = true
|
||||
loadSignList()
|
||||
visible.value = true
|
||||
loadSignList()
|
||||
}
|
||||
// 获取列表
|
||||
const loadSignList = () => {
|
||||
tableData.loading = true
|
||||
const 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
|
||||
})
|
||||
tableData.loading = true
|
||||
const 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 = signConfig.signSourceList[0].type
|
||||
formData.signType = signConfig.signTypeList[0].type
|
||||
visibleAdd.value = true
|
||||
Object.assign(formData, initialFormData)
|
||||
formData.signSource = signConfig.signSourceList[0].type
|
||||
formData.signType = signConfig.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 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)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 批量复选框
|
||||
@ -348,8 +534,8 @@ const isIndeterminate = ref(false)
|
||||
|
||||
// 监听批量复选框事件
|
||||
const toggleChange = (value: any) => {
|
||||
isIndeterminate.value = false
|
||||
smsSignListTableRef.value.toggleAllSelection()
|
||||
isIndeterminate.value = false
|
||||
smsSignListTableRef.value.toggleAllSelection()
|
||||
}
|
||||
|
||||
const smsSignListTableRef = ref()
|
||||
@ -359,58 +545,59 @@ const multipleSelection: any = ref([])
|
||||
|
||||
// 监听表格单行选中
|
||||
const handleSelectionChange = (val: []) => {
|
||||
multipleSelection.value = 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
|
||||
}
|
||||
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
|
||||
}
|
||||
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 // 只有不是“使用中”的行可选
|
||||
return !row.is_default // 只有不是“使用中”的行可选
|
||||
}
|
||||
|
||||
// 批量删除
|
||||
const batchDeleteEvent = () => {
|
||||
if (multipleSelection.value.length == 0) {
|
||||
ElMessage({
|
||||
type: 'warning',
|
||||
message: `${t('请选择要删除的签名')}`
|
||||
})
|
||||
return
|
||||
}
|
||||
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)
|
||||
})
|
||||
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
|
||||
}).then(() => {
|
||||
tableData.loading = true
|
||||
setTimeout(() => {
|
||||
loadSignList()
|
||||
}, 1000)
|
||||
}).catch(() => {
|
||||
})
|
||||
})
|
||||
deleteSign(props.username, {
|
||||
signatures,
|
||||
})
|
||||
.then(() => {
|
||||
tableData.loading = true
|
||||
setTimeout(() => {
|
||||
loadSignList()
|
||||
}, 1000)
|
||||
})
|
||||
.catch(() => {})
|
||||
})
|
||||
}
|
||||
|
||||
const selectTemplate = (row:any) => {
|
||||
visible.value = false
|
||||
emit('select', row)
|
||||
const selectTemplate = (row: any) => {
|
||||
visible.value = false
|
||||
emit('select', row)
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
|
||||
@ -1,80 +1,80 @@
|
||||
<template>
|
||||
<div class="main-container">
|
||||
<el-card class="box-card !border-none" shadow="never">
|
||||
<div class="main-container">
|
||||
<el-card class="box-card !border-none" shadow="never">
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-page-title">{{ pageName }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-page-title">{{ pageName }}</span>
|
||||
</div>
|
||||
|
||||
<div class="mt-[20px]">
|
||||
<el-table :data="addonList" size="large" v-loading="loading">
|
||||
<template #empty>
|
||||
<span>{{ !loading ? t('emptyData') : '' }}</span>
|
||||
</template>
|
||||
<div class="mt-[20px]">
|
||||
<el-table :data="addonList" size="large" v-loading="loading">
|
||||
<template #empty>
|
||||
<span>{{ !loading ? t('emptyData') : '' }}</span>
|
||||
</template>
|
||||
|
||||
<el-table-column prop="title" :label="t('app')" min-width="120" >
|
||||
<template #default="{ row }">
|
||||
<div class="flex items-center">
|
||||
<el-image class="w-[40px] h-[40px] rounded-md overflow-hidden" :src="row.icon" fit="contain">
|
||||
<template #error>
|
||||
<div class="flex items-center w-full h-full">
|
||||
<img class="w-full h-full" src="@/app/assets/images/icon-addon-one.png" alt="">
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
<div class="flex-1 ml-2 truncate">{{ row.title }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="title" :label="t('app')" min-width="120" >
|
||||
<template #default="{ row }">
|
||||
<div class="flex items-center">
|
||||
<el-image class="w-[40px] h-[40px] rounded-md overflow-hidden" :src="row.icon" fit="contain">
|
||||
<template #error>
|
||||
<div class="flex items-center w-full h-full">
|
||||
<img class="w-full h-full" src="@/app/assets/images/icon-addon-one.png" alt="">
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
<div class="flex-1 ml-2 truncate">{{ row.title }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="t('operation')" align="right" fixed="right" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="openSettingLayer(row.key)">{{ t('setting') }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="showDialog" :title="t('selectLayout')" width="800" :destroy-on-close="true">
|
||||
<div class="h-[300px]">
|
||||
<el-scrollbar >
|
||||
<div class="flex justify-between items-center mb-[20px]">
|
||||
<h3 class="!text-sm !text-[#444]">{{ t('layout') }}</h3>
|
||||
<div class="flex items-center cursor-pointer" @click="toDiyLayout">
|
||||
<span class="iconfont iconwenhao text-[#999] !text-[14px]"></span>
|
||||
<div class="ml-[2px] text-[12px] text-[#999]">如何开发自定义布局</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-items-stretch">
|
||||
<div class="w-[180px] h-[130px] mr-[10px] mb-[10px] border hover:border-primary cursor-pointer"
|
||||
:class="{'border-primary': ((!layoutConfig[currAddon] && item.layout == 'default') || (layoutConfig[currAddon] == item.layout)) }"
|
||||
@click="layoutConfig[currAddon] = item.layout"
|
||||
v-for="item in systemLayout">
|
||||
<img :src="img(item.cover)" class="w-full h-full" />
|
||||
</div>
|
||||
<div class="w-[180px] h-[130px] mr-[20px] border hover:border-primary cursor-pointer"
|
||||
:class="{'border-primary': ((!layoutConfig[currAddon] && addonLayout.layout == 'default') || (layoutConfig[currAddon] == addonLayout.layout)) }"
|
||||
@click="layoutConfig[currAddon] = addonLayout.layout"
|
||||
v-if="addonLayout">
|
||||
<img :src="img(addonLayout.cover)" class="w-full h-full" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- <h3 class="panel-title !text-sm">{{ t('themeColor') }}</h3>
|
||||
<div>
|
||||
<el-color-picker v-model="themeColor[currAddon]" size="large" />
|
||||
<div class="form-tip text-[#999] mt-2">设置的色调会在前端站点列表体现[home/index],用于区分不同的应用</div>
|
||||
</div> -->
|
||||
</el-scrollbar>
|
||||
<el-table-column :label="t('operation')" align="right" fixed="right" width="100">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="openSettingLayer(row.key)">{{ t('setting') }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
<template #footer>
|
||||
|
||||
<el-dialog v-model="showDialog" :title="t('selectLayout')" width="800" :destroy-on-close="true">
|
||||
<div class="h-[330px]">
|
||||
<el-scrollbar >
|
||||
<div class="flex justify-between items-center mb-[20px]">
|
||||
<h3 class="!text-sm !text-[#444]">{{ t('layout') }}</h3>
|
||||
<div class="flex items-center cursor-pointer" @click="toDiyLayout">
|
||||
<span class="iconfont iconwenhao text-[#999] !text-[14px]"></span>
|
||||
<div class="ml-[2px] text-[12px] text-[#999]">如何开发自定义布局</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-wrap justify-items-stretch">
|
||||
<div class="w-[180px] h-[130px] mr-[10px] mb-[10px] border hover:border-primary cursor-pointer"
|
||||
:class="{'border-primary': ((!layoutConfig[currAddon] && item.layout == 'default') || (layoutConfig[currAddon] == item.layout)) }"
|
||||
@click="layoutConfig[currAddon] = item.layout"
|
||||
v-for="item in systemLayout">
|
||||
<img :src="img(item.cover)" class="w-full h-full" />
|
||||
</div>
|
||||
<div class="w-[180px] h-[130px] mr-[20px] border hover:border-primary cursor-pointer"
|
||||
:class="{'border-primary': ((!layoutConfig[currAddon] && addonLayout.layout == 'default') || (layoutConfig[currAddon] == addonLayout.layout)) }"
|
||||
@click="layoutConfig[currAddon] = addonLayout.layout"
|
||||
v-if="addonLayout">
|
||||
<img :src="img(addonLayout.cover)" class="w-full h-full" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- <h3 class="panel-title !text-sm">{{ t('themeColor') }}</h3>
|
||||
<div>
|
||||
<el-color-picker v-model="themeColor[currAddon]" size="large" />
|
||||
<div class="form-tip text-[#999] mt-2">设置的色调会在前端站点列表体现[home/index],用于区分不同的应用</div>
|
||||
</div> -->
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button>
|
||||
<el-button type="primary" :loading="loading" @click="confirm()">{{ t('confirm') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@ -93,67 +93,67 @@ const systemLayout = ref([])
|
||||
const addonLayout = ref(null)
|
||||
const themeColor = ref({})
|
||||
const addonList = ref([
|
||||
{
|
||||
title: t('manyApp'),
|
||||
key: 'system',
|
||||
icon: ''
|
||||
}
|
||||
{
|
||||
title: t('manyApp'),
|
||||
key: 'system',
|
||||
icon: ''
|
||||
}
|
||||
])
|
||||
|
||||
const layouts = import.meta.globEager('@/layout/**/layout.json')
|
||||
for (const key in layouts) {
|
||||
const layout = layouts[key]
|
||||
systemLayout.value.push(layout.default)
|
||||
const layout = layouts[key]
|
||||
systemLayout.value.push(layout.default)
|
||||
}
|
||||
const addonLayouts = import.meta.globEager('@/addon/**/layout/layout.json')
|
||||
|
||||
getInstalledAddonList().then(({ data }) => {
|
||||
Object.keys(data).forEach((key) => {
|
||||
const item = data[key]
|
||||
item.type == 'app' && addonList.value.push(item)
|
||||
})
|
||||
loading.value = false
|
||||
Object.keys(data).forEach((key) => {
|
||||
const item = data[key]
|
||||
item.type == 'app' && addonList.value.push(item)
|
||||
})
|
||||
loading.value = false
|
||||
})
|
||||
|
||||
const getLayoutConfig = () => {
|
||||
getLayout().then(({ data }) => {
|
||||
layoutConfig.value = data
|
||||
})
|
||||
getThemecolor().then(({ data }) => {
|
||||
themeColor.value = data
|
||||
})
|
||||
getLayout().then(({ data }) => {
|
||||
layoutConfig.value = data
|
||||
})
|
||||
getThemecolor().then(({ data }) => {
|
||||
themeColor.value = data
|
||||
})
|
||||
}
|
||||
getLayoutConfig()
|
||||
|
||||
const currAddon = ref('')
|
||||
const showDialog = ref(false)
|
||||
const openSettingLayer = async (key: string) => {
|
||||
if (key != 'system') {
|
||||
addonLayout.value = null
|
||||
Object.keys(addonLayouts).forEach((path) => {
|
||||
path.indexOf(`/addon/${key}/`) != -1 && (addonLayout.value = addonLayouts[path].default)
|
||||
})
|
||||
}
|
||||
currAddon.value = key
|
||||
showDialog.value = true
|
||||
if (key != 'system') {
|
||||
addonLayout.value = null
|
||||
Object.keys(addonLayouts).forEach((path) => {
|
||||
path.indexOf(`/addon/${key}/`) != -1 && (addonLayout.value = addonLayouts[path].default)
|
||||
})
|
||||
}
|
||||
currAddon.value = key
|
||||
showDialog.value = true
|
||||
}
|
||||
|
||||
const confirm = () => {
|
||||
setThemecolor({
|
||||
key: currAddon.value,
|
||||
value: themeColor.value[currAddon.value] ? themeColor.value[currAddon.value] : ''
|
||||
})
|
||||
setLayout({
|
||||
key: currAddon.value,
|
||||
value: layoutConfig.value[currAddon.value] ? layoutConfig.value[currAddon.value] : 'default'
|
||||
})
|
||||
showDialog.value = false
|
||||
setThemecolor({
|
||||
key: currAddon.value,
|
||||
value: themeColor.value[currAddon.value] ? themeColor.value[currAddon.value] : ''
|
||||
})
|
||||
setLayout({
|
||||
key: currAddon.value,
|
||||
value: layoutConfig.value[currAddon.value] ? layoutConfig.value[currAddon.value] : 'default'
|
||||
})
|
||||
showDialog.value = false
|
||||
}
|
||||
|
||||
// 跳转自定义布局
|
||||
const toDiyLayout = () => {
|
||||
let url = 'https://doc.niucloud.com/saas.html?keywords=/pluginDev/diyLayoutDev';
|
||||
window.open(url)
|
||||
let url = 'https://doc.press.niucloud.com/php/saas-framework/dev/pluginDev/diyLayoutDev.html';
|
||||
window.open(url)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@ -1,176 +1,719 @@
|
||||
<template>
|
||||
<!--消息模板-->
|
||||
<div class="main-container" v-loading="noticeTableData.loading">
|
||||
<el-card class="box-card !border-none" shadow="never">
|
||||
<h3 class="panel-title !text-sm">{{ t('buyerNotice') }}</h3>
|
||||
<!--消息模板-->
|
||||
<div class="main-container" v-loading="noticeTableData.loading">
|
||||
<el-card class="box-card !border-none" shadow="never">
|
||||
<h3 class="panel-title !text-sm">{{ t('buyerNotice') }}</h3>
|
||||
|
||||
<div class="flex flex-row flex-wrap">
|
||||
<el-table :data="noticeTableData.buyer" size="large" :span-method="buyerSpan">
|
||||
<el-table-column prop="addon_name" :label="t('addon')" min-width="120" />
|
||||
<el-table-column prop="name" :label="t('noticeType')" min-width="120" />
|
||||
<el-table-column :label="t('operation')" align="right" fixed="right" min-width="300">
|
||||
<template #default="{ row }">
|
||||
<div class="flex">
|
||||
<div class="text-sm mr-1 flex items-center cursor-pointer" v-if="row.support_type.indexOf('sms') != -1" @click="setNotice(row, 'sms')">
|
||||
<el-icon class="text-[15px] mr-[3px]" :class="row.is_sms ? 'open' : ''">
|
||||
<SuccessFilled />
|
||||
</el-icon>
|
||||
<span class="ml-0.5">{{ t('sms') }}</span>
|
||||
</div>
|
||||
<div class="text-sm flex items-center cursor-pointer ml-[20px]" v-if="row.support_type.indexOf('wechat') != -1" @click="setNotice(row, 'wechat')">
|
||||
<el-icon class="text-[15px] mr-[3px]" :class="row.is_wechat ? 'open' : ''">
|
||||
<SuccessFilled />
|
||||
</el-icon>
|
||||
<span class="ml-0.5">{{ t('wechat') }}</span>
|
||||
</div>
|
||||
<div class="text-sm flex items-center cursor-pointer ml-[20px]" v-if="row.support_type.indexOf('weapp') != -1" @click="setNotice(row, 'weapp')">
|
||||
<el-icon class="text-[15px] mr-[3px]" :class="row.is_weapp ? 'open' : ''">
|
||||
<SuccessFilled />
|
||||
</el-icon>
|
||||
<span class="ml-0.5">{{ t('weapp') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-card>
|
||||
<div class="flex flex-row flex-wrap">
|
||||
<el-table
|
||||
:data="noticeTableData.buyer"
|
||||
size="large"
|
||||
:span-method="buyerSpan"
|
||||
>
|
||||
<el-table-column
|
||||
prop="addon_name"
|
||||
:label="t('addon')"
|
||||
min-width="120"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="name"
|
||||
:label="t('noticeType')"
|
||||
min-width="120"
|
||||
/>
|
||||
<el-table-column
|
||||
:label="t('operation')"
|
||||
align="right"
|
||||
fixed="right"
|
||||
min-width="300"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div class="flex">
|
||||
<div
|
||||
class="text-sm mr-1 flex items-center cursor-pointer"
|
||||
v-if="row.support_type.indexOf('sms') != -1"
|
||||
@click="setNotice(row, 'sms')"
|
||||
>
|
||||
<el-icon
|
||||
class="text-[15px] mr-[3px]"
|
||||
:class="row.is_sms ? 'open' : ''"
|
||||
>
|
||||
<SuccessFilled />
|
||||
</el-icon>
|
||||
<span class="ml-0.5">{{ t('sms') }}</span>
|
||||
</div>
|
||||
<div
|
||||
class="text-sm flex items-center cursor-pointer ml-[20px]"
|
||||
v-if="
|
||||
row.support_type.indexOf('wechat') != -1
|
||||
"
|
||||
@click="setNotice(row, 'wechat')"
|
||||
>
|
||||
<el-icon
|
||||
class="text-[15px] mr-[3px]"
|
||||
:class="row.is_wechat ? 'open' : ''"
|
||||
>
|
||||
<SuccessFilled />
|
||||
</el-icon>
|
||||
<span class="ml-0.5">{{
|
||||
t('wechat')
|
||||
}}</span>
|
||||
</div>
|
||||
<div
|
||||
class="text-sm flex items-center cursor-pointer ml-[20px]"
|
||||
v-if="
|
||||
row.support_type.indexOf('weapp') != -1
|
||||
"
|
||||
@click="setNotice(row, 'weapp')"
|
||||
>
|
||||
<el-icon
|
||||
class="text-[15px] mr-[3px]"
|
||||
:class="row.is_weapp ? 'open' : ''"
|
||||
>
|
||||
<SuccessFilled />
|
||||
</el-icon>
|
||||
<span class="ml-0.5">{{ t('weapp') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card class="box-card mt-[15px] !border-none" shadow="never">
|
||||
<h3 class="panel-title !text-sm">{{ t('sellerNotice') }}</h3>
|
||||
<el-card class="box-card mt-[15px] !border-none" shadow="never">
|
||||
<div class="flex items-center mb-[20px] gap-[10px]">
|
||||
<h3 class="panel-title !text-sm !mb-[0]">
|
||||
{{ t('sellerNotice') }}
|
||||
</h3>
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="openBindAccountDialog"
|
||||
class="!text-[12px]"
|
||||
>绑定接收信息账号</el-button
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row flex-wrap">
|
||||
<el-table :data="noticeTableData.seller" size="large" :span-method="buyerSpan">
|
||||
<el-table-column prop="addon_name" :label="t('addon')" min-width="120" />
|
||||
<el-table-column prop="name" :label="t('noticeType')" min-width="120" />
|
||||
<el-table-column :label="t('operation')" align="right" fixed="right" min-width="300">
|
||||
<template #default="{ row }">
|
||||
<div class="flex">
|
||||
<div class="text-sm mr-1 flex items-center cursor-pointer" v-if="row.support_type.indexOf('sms') != -1" @click="setNotice(row, 'sms')">
|
||||
<el-icon class="text-[15px] mr-[3px]" :class="row.is_sms ? 'open' : ''">
|
||||
<SuccessFilled />
|
||||
</el-icon>
|
||||
<span class="ml-0.5">{{ t('sms') }}</span>
|
||||
</div>
|
||||
<div class="text-sm flex items-center cursor-pointer ml-[20px]" v-if="row.support_type.indexOf('wechat') != -1" @click="setNotice(row, 'wechat')">
|
||||
<el-icon class="text-[15px] mr-[3px]" :class="row.is_wechat ? 'open' : ''">
|
||||
<SuccessFilled />
|
||||
</el-icon>
|
||||
<span class="ml-0.5">{{ t('wechat') }}</span>
|
||||
</div>
|
||||
<div class="text-sm flex items-center cursor-pointer ml-[20px]" v-if="row.support_type.indexOf('weapp') != -1" @click="setNotice(row, 'weapp')">
|
||||
<el-icon class="text-[15px] mr-[3px]" :class="row.is_weapp ? 'open' : ''">
|
||||
<SuccessFilled />
|
||||
</el-icon>
|
||||
<span class="ml-0.5">{{ t('weapp') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-card>
|
||||
<div class="flex flex-row flex-wrap">
|
||||
<el-table
|
||||
:data="noticeTableData.seller"
|
||||
size="large"
|
||||
:span-method="buyerSpan"
|
||||
>
|
||||
<el-table-column
|
||||
prop="addon_name"
|
||||
:label="t('addon')"
|
||||
min-width="120"
|
||||
/>
|
||||
<el-table-column
|
||||
prop="name"
|
||||
:label="t('noticeType')"
|
||||
min-width="120"
|
||||
/>
|
||||
<el-table-column
|
||||
:label="t('operation')"
|
||||
align="right"
|
||||
fixed="right"
|
||||
min-width="300"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div class="flex">
|
||||
<div
|
||||
class="text-sm mr-1 flex items-center cursor-pointer"
|
||||
v-if="row.support_type.indexOf('sms') != -1"
|
||||
@click="setNotice(row, 'sms')"
|
||||
>
|
||||
<el-icon
|
||||
class="text-[15px] mr-[3px]"
|
||||
:class="row.is_sms ? 'open' : ''"
|
||||
>
|
||||
<SuccessFilled />
|
||||
</el-icon>
|
||||
<span class="ml-0.5">{{ t('sms') }}</span>
|
||||
</div>
|
||||
<div
|
||||
class="text-sm flex items-center cursor-pointer ml-[20px]"
|
||||
v-if="
|
||||
row.support_type.indexOf('wechat') != -1
|
||||
"
|
||||
@click="setNotice(row, 'wechat')"
|
||||
>
|
||||
<el-icon
|
||||
class="text-[15px] mr-[3px]"
|
||||
:class="row.is_wechat ? 'open' : ''"
|
||||
>
|
||||
<SuccessFilled />
|
||||
</el-icon>
|
||||
<span class="ml-0.5">{{
|
||||
t('wechat')
|
||||
}}</span>
|
||||
</div>
|
||||
<div
|
||||
class="text-sm flex items-center cursor-pointer ml-[20px]"
|
||||
v-if="
|
||||
row.support_type.indexOf('weapp') != -1
|
||||
"
|
||||
@click="setNotice(row, 'weapp')"
|
||||
>
|
||||
<el-icon
|
||||
class="text-[15px] mr-[3px]"
|
||||
:class="row.is_weapp ? 'open' : ''"
|
||||
>
|
||||
<SuccessFilled />
|
||||
</el-icon>
|
||||
<span class="ml-0.5">{{ t('weapp') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<sms ref="smsDialog" @complete="loadNoticeList()" />
|
||||
<wechat ref="wechatDialog" @complete="loadNoticeList()" />
|
||||
<weapp ref="weappDialog" @complete="loadNoticeList()" />
|
||||
<sms ref="smsDialog" @complete="loadNoticeList()" />
|
||||
<wechat ref="wechatDialog" @complete="loadNoticeList()" />
|
||||
<weapp ref="weappDialog" @complete="loadNoticeList()" />
|
||||
<el-dialog
|
||||
v-model="bindDialogVisible"
|
||||
@close="bindDialogClose"
|
||||
width="300"
|
||||
>
|
||||
<view class="flex flex-col justify-center items-center">
|
||||
<span class="mb-[15px]">{{ isBind.title }}</span>
|
||||
<el-image
|
||||
v-if="isBind.key == 'weapp'"
|
||||
class="w-[120px] h-[120px]"
|
||||
:src="isBind.weapp_qrcode"
|
||||
:fit="contain"
|
||||
/>
|
||||
<el-image
|
||||
v-else
|
||||
class="w-[120px] h-[120px]"
|
||||
:src="isBind.wechat_qrcode"
|
||||
:fit="contain"
|
||||
/>
|
||||
</view>
|
||||
</el-dialog>
|
||||
|
||||
</div>
|
||||
<!-- 绑定接收信息账号弹窗 -->
|
||||
<el-dialog
|
||||
v-model="bindAccountDialogVisible"
|
||||
title="绑定接收信息账号"
|
||||
width="500px"
|
||||
@close="bindAccountDialogClose"
|
||||
>
|
||||
<div class="account-bind-options">
|
||||
<div class="option-item">
|
||||
<div class="flex flex-col">
|
||||
<span class="title" v-if="!isBind.bind_sms_info"
|
||||
>手机号</span
|
||||
>
|
||||
<span class="info" v-else
|
||||
>手机号:{{ isBind.bind_sms_info }}</span
|
||||
>
|
||||
</div>
|
||||
<template v-if="!Number(isBind.bind_sms)">
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="bindSmsFn"
|
||||
class="!text-[12px]"
|
||||
>绑定</el-button
|
||||
>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-button
|
||||
type="danger"
|
||||
@click="cancelBindFn('sms')"
|
||||
class="!text-[12px]"
|
||||
>解绑</el-button
|
||||
>
|
||||
</template>
|
||||
</div>
|
||||
<div class="option-item">
|
||||
<div class="flex flex-col">
|
||||
<span class="title" v-if="!isBind.bind_wechat_info"
|
||||
>微信账号</span
|
||||
>
|
||||
<span class="info" v-else
|
||||
>微信openId:{{ isBind.bind_wechat_info }}</span
|
||||
>
|
||||
</div>
|
||||
<template v-if="!Number(isBind.bind_wechat)">
|
||||
<el-button
|
||||
type="primary"
|
||||
@click="bindWeChatFn"
|
||||
class="!text-[12px]"
|
||||
>绑定</el-button
|
||||
>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-button
|
||||
type="danger"
|
||||
@click="cancelBindFn('wechat')"
|
||||
class="!text-[12px]"
|
||||
>解绑</el-button
|
||||
>
|
||||
</template>
|
||||
</div>
|
||||
<!-- <div class="option-item">-->
|
||||
<!-- <div class="flex flex-col">-->
|
||||
<!-- <span class="title" v-if="!isBind.bind_weapp_info"-->
|
||||
<!-- >小程序账号</span-->
|
||||
<!-- >-->
|
||||
<!-- <span class="info" v-else-->
|
||||
<!-- >小程序openId:{{ isBind.bind_weapp_info }}</span-->
|
||||
<!-- >-->
|
||||
<!-- </div>-->
|
||||
<!-- <template v-if="!Number(isBind.bind_weapp)">-->
|
||||
<!-- <el-button-->
|
||||
<!-- type="primary"-->
|
||||
<!-- @click="bindWeappFn"-->
|
||||
<!-- class="!text-[12px]"-->
|
||||
<!-- >绑定</el-button-->
|
||||
<!-- >-->
|
||||
<!-- </template>-->
|
||||
<!-- <template v-else>-->
|
||||
<!-- <el-button-->
|
||||
<!-- type="danger"-->
|
||||
<!-- @click="cancelBindFn('weapp')"-->
|
||||
<!-- class="!text-[12px]"-->
|
||||
<!-- >解绑</el-button-->
|
||||
<!-- >-->
|
||||
<!-- </template>-->
|
||||
<!-- </div>-->
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 新增:短信绑定弹窗 -->
|
||||
<el-dialog
|
||||
v-model="smsBindDialogVisible"
|
||||
title="绑定接收信息手机号"
|
||||
width="400px"
|
||||
@close="resetSmsBindForm"
|
||||
>
|
||||
<el-form
|
||||
:model="smsBindForm"
|
||||
:rules="smsBindRules"
|
||||
ref="smsBindFormRef"
|
||||
label-width="80px"
|
||||
>
|
||||
<el-form-item label="手机号" prop="mobile">
|
||||
<el-input
|
||||
v-model="smsBindForm.mobile"
|
||||
placeholder="请输入手机号"
|
||||
maxlength="11"
|
||||
@input="handleMobileInput"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="验证码" prop="code">
|
||||
<el-input
|
||||
v-model="smsBindForm.code"
|
||||
placeholder="请输入验证码"
|
||||
maxlength="6"
|
||||
style="width: 50%"
|
||||
/>
|
||||
<el-button
|
||||
class="ml-2"
|
||||
type="primary"
|
||||
:disabled="
|
||||
!canSendCode ||
|
||||
sendingCode ||
|
||||
smsBindForm.mobile.length !== 11
|
||||
"
|
||||
@click="sendSmsCode"
|
||||
>
|
||||
{{ codeBtnText }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="smsBindDialogVisible = false"
|
||||
>取消</el-button
|
||||
>
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="bindingSms"
|
||||
@click="submitSmsBind"
|
||||
>
|
||||
确认绑定
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import { getNoticeList } from '@/app/api/notice'
|
||||
import {
|
||||
getNoticeList,
|
||||
cancelBind,
|
||||
getBindInfo,
|
||||
getWechatAuthUrl,
|
||||
getWeappAuthUrl,
|
||||
sendSms,
|
||||
bindSms,
|
||||
} from '@/app/api/notice'
|
||||
import Sms from '@/app/views/setting/components/notice-sms.vue'
|
||||
import Wechat from '@/app/views/setting/components/notice-wechat.vue'
|
||||
import Weapp from '@/app/views/setting/components/notice-weapp.vue'
|
||||
import QRCode from 'qrcode'
|
||||
import { chain } from 'lodash-es'
|
||||
import { img } from '@/utils/common'
|
||||
import { SuccessFilled } from '@element-plus/icons-vue'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
const smsDialog : Record<string, any> | null = ref(null)
|
||||
const wechatDialog : Record<string, any> | null = ref(null)
|
||||
const weappDialog : Record<string, any> | null = ref(null)
|
||||
const smsDialog: Record<string, any> | null = ref(null)
|
||||
const wechatDialog: Record<string, any> | null = ref(null)
|
||||
const weappDialog: Record<string, any> | null = ref(null)
|
||||
|
||||
const noticeTableData = reactive({
|
||||
loading: true,
|
||||
buyer: [],
|
||||
seller: []
|
||||
})
|
||||
// 新增:绑定接收信息账号弹窗显示状态
|
||||
const bindAccountDialogVisible = ref(false)
|
||||
|
||||
// 打开绑定接收信息账号弹窗
|
||||
const openBindAccountDialog = () => {
|
||||
bindAccountDialogVisible.value = true
|
||||
}
|
||||
|
||||
// 关闭绑定接收信息账号弹窗
|
||||
const bindAccountDialogClose = () => {
|
||||
bindAccountDialogVisible.value = false
|
||||
}
|
||||
|
||||
const noticeTableData = reactive({ loading: true, buyer: [], seller: [] })
|
||||
|
||||
/**
|
||||
* 获取配置信息
|
||||
*/
|
||||
const loadNoticeList = () => {
|
||||
noticeTableData.loading = true
|
||||
|
||||
getNoticeList({}).then(res => {
|
||||
noticeTableData.loading = true
|
||||
getNoticeList({})
|
||||
.then((res) => {
|
||||
noticeTableData.buyer = []
|
||||
noticeTableData.seller = []
|
||||
res.data.forEach(item => {
|
||||
if (item.notice.length) {
|
||||
const buyer = []; const seller = []
|
||||
Object.keys(item.notice).forEach((key, index) => {
|
||||
const notice = item.notice[key]
|
||||
notice.addon_name = item.title
|
||||
notice.receiver_type == 1 ? buyer.push(notice) : seller.push(notice)
|
||||
})
|
||||
if (buyer.length) {
|
||||
buyer[0].rowspan = buyer.length
|
||||
noticeTableData.buyer = noticeTableData.buyer.concat(buyer)
|
||||
}
|
||||
if (seller.length) {
|
||||
seller[0].rowspan = seller.length
|
||||
noticeTableData.seller = noticeTableData.seller.concat(seller)
|
||||
}
|
||||
res.data.forEach((item) => {
|
||||
if (item.notice.length) {
|
||||
const buyer = []
|
||||
const seller = []
|
||||
Object.keys(item.notice).forEach((key, index) => {
|
||||
const notice = item.notice[key]
|
||||
notice.addon_name = item.title
|
||||
notice.receiver_type == 1
|
||||
? buyer.push(notice)
|
||||
: seller.push(notice)
|
||||
})
|
||||
if (buyer.length) {
|
||||
buyer[0].rowspan = buyer.length
|
||||
noticeTableData.buyer =
|
||||
noticeTableData.buyer.concat(buyer)
|
||||
}
|
||||
if (seller.length) {
|
||||
seller[0].rowspan = seller.length
|
||||
noticeTableData.seller =
|
||||
noticeTableData.seller.concat(seller)
|
||||
}
|
||||
}
|
||||
})
|
||||
noticeTableData.loading = false
|
||||
}).catch((e) => {
|
||||
})
|
||||
.catch((e) => {
|
||||
noticeTableData.loading = false
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const buyerSpan = (row : any) => {
|
||||
if (row.columnIndex === 0) {
|
||||
if (row.row.rowspan) {
|
||||
return {
|
||||
rowspan: row.row.rowspan,
|
||||
colspan: 1
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
rowspan: 0,
|
||||
colspan: 0
|
||||
}
|
||||
}
|
||||
const isBind = ref({
|
||||
bind_sms: 0, // 新增:短信绑定状态
|
||||
bind_wechat: 0,
|
||||
bind_weapp: 0,
|
||||
bind_sms_info: '',
|
||||
bind_wechat_info: '',
|
||||
bind_weapp_info: '',
|
||||
wechat_qrcode: '',
|
||||
weapp_qrcode: '',
|
||||
title: '',
|
||||
key: 'wechat',
|
||||
})
|
||||
// 查看是否绑定
|
||||
const getBindInfoFn = (isFirstQuery = false) => {
|
||||
getBindInfo().then((res) => {
|
||||
console.log(res.data)
|
||||
if (Object.keys(res.data).length > 0) {
|
||||
isBind.value.bind_sms = res.data.mobile ? 1 : 0 // 新增:更新短信绑定状态
|
||||
isBind.value.bind_wechat = res.data.wechat_openid ? 1 : 0 // 新增:更新微信绑定状态
|
||||
isBind.value.bind_weapp = res.data.weapp_openid ? 1 : 0 // 新增:更新小程序绑定状态
|
||||
|
||||
isBind.value.bind_sms_info = res.data.mobile
|
||||
isBind.value.bind_wechat_info = res.data.wechat_openid
|
||||
isBind.value.bind_weapp_info = res.data.weapp_openid
|
||||
if (!isFirstQuery && isBind.value[`bind_${isBind.value.key}`]) {
|
||||
bindDialogClose()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
getBindInfoFn(true)
|
||||
// --------------------- 绑定微信 ---------------------
|
||||
let isBindRepeat = false
|
||||
const bindDialogVisible = ref(false)
|
||||
const requestTimer = ref()
|
||||
const bindWeChatFn = () => {
|
||||
if (isBindRepeat) return
|
||||
isBindRepeat = true
|
||||
getWechatAuthUrl()
|
||||
.then((res) => {
|
||||
isBindRepeat = false
|
||||
if (res?.data?.url) generateQRCode(res.data.url, 'wechat_qrcode')
|
||||
bindDialogVisible.value = true
|
||||
isBind.value.title = '微信扫一扫,绑定接收信息微信'
|
||||
isBind.value.key = 'wechat'
|
||||
requestTimer.value = setInterval(getBindInfoFn, 1000)
|
||||
})
|
||||
.catch((e) => {
|
||||
isBindRepeat = false
|
||||
})
|
||||
}
|
||||
|
||||
const bindWeappFn = () => {
|
||||
if (isBindRepeat) return
|
||||
isBindRepeat = true
|
||||
getWeappAuthUrl()
|
||||
.then((res) => {
|
||||
isBindRepeat = false
|
||||
if (res?.data?.url) isBind.value.weapp_qrcode = img(res.data.url)
|
||||
bindDialogVisible.value = true
|
||||
isBind.value.title = '微信扫一扫,绑定接收信息小程序'
|
||||
isBind.value.key = 'weapp'
|
||||
requestTimer.value = setInterval(getBindInfoFn, 1000)
|
||||
})
|
||||
.catch((e) => {
|
||||
isBindRepeat = false
|
||||
})
|
||||
}
|
||||
|
||||
const generateQRCode = async (url, type) => {
|
||||
try {
|
||||
// 生成二维码
|
||||
isBind.value[type] = await QRCode.toDataURL(url, {
|
||||
errorCorrectionLevel: 'L',
|
||||
margin: 0,
|
||||
width: 120,
|
||||
})
|
||||
} catch (e) {
|
||||
isBind.value[type] = ''
|
||||
console.error('生成二维码失败', e)
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------- 取消绑定 ---------------------
|
||||
const isUnbindRepeat = ref(false)
|
||||
const cancelBindFn = (key: any) => {
|
||||
ElMessageBox.confirm('确定取消绑定吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
}).then(() => {
|
||||
if (isUnbindRepeat.value) return
|
||||
isUnbindRepeat.value = true
|
||||
|
||||
cancelBind({ unbind_type: [key] })
|
||||
.then((res) => {
|
||||
ElMessage({ message: '取消绑定成功', type: 'success' })
|
||||
getBindInfoFn()
|
||||
isUnbindRepeat.value = false
|
||||
})
|
||||
.catch((e) => {
|
||||
isUnbindRepeat.value = false
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const bindDialogClose = () => {
|
||||
clearInterval(requestTimer.value)
|
||||
bindDialogVisible.value = false
|
||||
}
|
||||
// --------------------- 新增:短信绑定相关逻辑 ---------------------
|
||||
// 短信绑定弹窗显示状态
|
||||
const smsBindDialogVisible = ref(false)
|
||||
// 表单ref
|
||||
const smsBindFormRef = ref<FormInstance>()
|
||||
// 绑定中状态
|
||||
const bindingSms = ref(false)
|
||||
// 是否可以发送验证码
|
||||
const canSendCode = ref(true)
|
||||
// 发送验证码中
|
||||
const sendingCode = ref(false)
|
||||
// 验证码按钮文字
|
||||
const codeBtnText = ref('发送验证码')
|
||||
// 倒计时秒数
|
||||
const countdown = ref(60)
|
||||
|
||||
// 短信绑定表单
|
||||
const smsBindForm = reactive({
|
||||
mobile: '', // 手机号
|
||||
code: '', // 验证码
|
||||
mobile_key: '', // 发送验证码返回的key
|
||||
})
|
||||
|
||||
// 短信绑定表单验证规则
|
||||
const smsBindRules = reactive<FormRules>({
|
||||
mobile: [
|
||||
{ required: true, message: '请输入手机号', trigger: 'blur' },
|
||||
{
|
||||
pattern: /^1[3-9]\d{9}$/,
|
||||
message: '请输入正确的手机号',
|
||||
trigger: 'blur',
|
||||
},
|
||||
],
|
||||
code: [
|
||||
{ required: true, message: '请输入验证码', trigger: 'blur' },
|
||||
{ pattern: /^\d{4}$/, message: '请输入6位数字验证码', trigger: 'blur' },
|
||||
],
|
||||
})
|
||||
|
||||
// 手机号输入处理(只保留数字)
|
||||
const handleMobileInput = (val: string) => {
|
||||
smsBindForm.mobile = val.replace(/\D/g, '')
|
||||
}
|
||||
|
||||
// 发送验证码
|
||||
const sendSmsCode = async () => {
|
||||
try {
|
||||
sendingCode.value = true
|
||||
// 发送验证码请求
|
||||
const res = await sendSms({ mobile: smsBindForm.mobile })
|
||||
if (res.data) {
|
||||
// 保存返回的mobile_key
|
||||
smsBindForm.mobile_key = res.data.key || ''
|
||||
|
||||
// 开始倒计时
|
||||
canSendCode.value = false
|
||||
codeBtnText.value = `${countdown.value}秒后重新发送`
|
||||
const timer = setInterval(() => {
|
||||
countdown.value--
|
||||
codeBtnText.value = `${countdown.value}秒后重新发送`
|
||||
if (countdown.value <= 0) {
|
||||
clearInterval(timer)
|
||||
canSendCode.value = true
|
||||
codeBtnText.value = '发送验证码'
|
||||
countdown.value = 60
|
||||
}
|
||||
}, 1000)
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
} finally {
|
||||
sendingCode.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 提交短信绑定
|
||||
const submitSmsBind = async () => {
|
||||
if (!smsBindFormRef.value) return
|
||||
|
||||
// 表单验证
|
||||
try {
|
||||
await smsBindFormRef.value.validate()
|
||||
bindingSms.value = true
|
||||
|
||||
// 调用绑定接口
|
||||
const res = await bindSms({
|
||||
mobile: smsBindForm.mobile,
|
||||
mobile_key: smsBindForm.mobile_key,
|
||||
mobile_code: smsBindForm.code,
|
||||
})
|
||||
|
||||
if (res) {
|
||||
smsBindDialogVisible.value = false
|
||||
getBindInfoFn() // 更新绑定状态
|
||||
}
|
||||
} catch (error) {
|
||||
} finally {
|
||||
bindingSms.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 重置短信绑定表单
|
||||
const resetSmsBindForm = () => {
|
||||
smsBindForm.mobile = ''
|
||||
smsBindForm.code = ''
|
||||
smsBindForm.mobile_key = ''
|
||||
canSendCode.value = true
|
||||
codeBtnText.value = '发送验证码'
|
||||
countdown.value = 60
|
||||
if (smsBindFormRef.value) {
|
||||
smsBindFormRef.value.clearValidate()
|
||||
}
|
||||
}
|
||||
|
||||
// 实现bindSmsFn方法
|
||||
const bindSmsFn = () => {
|
||||
// 打开短信绑定弹窗
|
||||
smsBindDialogVisible.value = true
|
||||
// 重置表单
|
||||
resetSmsBindForm()
|
||||
}
|
||||
// --------------------- 短信绑定逻辑结束 ---------------------
|
||||
|
||||
const buyerSpan = (row: any) => {
|
||||
if (row.columnIndex === 0) {
|
||||
if (row.row.rowspan) {
|
||||
return { rowspan: row.row.rowspan, colspan: 1 }
|
||||
} else {
|
||||
return { rowspan: 0, colspan: 0 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
loadNoticeList()
|
||||
|
||||
const setNotice = (data : any, type : string) => {
|
||||
data.type = type
|
||||
data.status = data['is_' + type]
|
||||
if (type === 'sms') {
|
||||
smsDialog.value.setFormData(data)
|
||||
smsDialog.value.showDialog = true
|
||||
} else if (type === 'wechat') {
|
||||
wechatDialog.value.setFormData(data)
|
||||
wechatDialog.value.showDialog = true
|
||||
} else if (type === 'weapp') {
|
||||
weappDialog.value.setFormData(data)
|
||||
weappDialog.value.showDialog = true
|
||||
}
|
||||
const setNotice = (data: any, type: string) => {
|
||||
data.type = type
|
||||
data.status = data['is_' + type]
|
||||
if (type === 'sms') {
|
||||
data.bind_sms = isBind.value.bind_sms
|
||||
smsDialog.value.setFormData(data)
|
||||
smsDialog.value.showDialog = true
|
||||
} else if (type === 'wechat') {
|
||||
data.bind_wechat = isBind.value.bind_wechat
|
||||
wechatDialog.value.setFormData(data)
|
||||
wechatDialog.value.showDialog = true
|
||||
} else if (type === 'weapp') {
|
||||
data.bind_weapp = isBind.value.bind_weapp
|
||||
weappDialog.value.setFormData(data)
|
||||
weappDialog.value.showDialog = true
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.open {
|
||||
color: var(--el-color-primary);
|
||||
.open {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.notice-type {
|
||||
> div:nth-last-child(1):first-child {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.account-bind-options {
|
||||
.option-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 0;
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.notice-type {
|
||||
>div:nth-last-child(1):first-child {
|
||||
width: 100%;
|
||||
}
|
||||
.title {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
</style>
|
||||
.info {
|
||||
color: #333;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@ -1,79 +1,79 @@
|
||||
<template>
|
||||
<div class="main-container">
|
||||
<el-form class="page-form loading-box" :model="formData" label-width="150px" ref="formRef" :rules="formRules" v-loading="loading">
|
||||
<el-card class="box-card !border-none" shadow="never">
|
||||
<h3 class="text-[16px] text-[#1D1F3A] font-bold mb-4">{{ pageName }}</h3>
|
||||
<h3 class="panel-title !text-[14px] bg-[#F4F5F7] p-3 border-[#E6E6E6] border-solid border-b-[1px]">{{ t('websiteInfo') }}</h3>
|
||||
<div class="main-container">
|
||||
<el-form class="page-form loading-box" :model="formData" label-width="150px" ref="formRef" :rules="formRules" v-loading="loading">
|
||||
<el-card class="box-card !border-none" shadow="never">
|
||||
<h3 class="text-[16px] text-[#1D1F3A] font-bold mb-4">{{ pageName }}</h3>
|
||||
<h3 class="panel-title !text-[14px] bg-[#F4F5F7] p-3 border-[#E6E6E6] border-solid border-b-[1px]">{{ t('websiteInfo') }}</h3>
|
||||
|
||||
<el-form-item :label="t('siteName')" prop="site_name">
|
||||
<el-input v-model.trim="formData.site_name" :placeholder="t('siteNamePlaceholder')" class="input-width" clearable maxlength="20" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('logo')">
|
||||
<div>
|
||||
<upload-image v-model="formData.logo" />
|
||||
<p class="text-[12px] text-[#a9a9a9]">{{ t('logoPlaceholder') }}</p>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('icon')">
|
||||
<div>
|
||||
<upload-image v-model="formData.icon" />
|
||||
<p class="text-[12px] text-[#a9a9a9]">{{ t('iconPlaceholder') }}</p>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('keywords')">
|
||||
<el-input v-model.trim="formData.keywords" :placeholder="t('keywordsPlaceholder')" class="input-width" clearable maxlength="20" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('desc')">
|
||||
<el-input v-model.trim="formData.desc" type="textarea" :rows="4" clearable :placeholder="t('descPlaceholder')" class="input-width" maxlength="100" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('siteName')" prop="site_name">
|
||||
<el-input v-model.trim="formData.site_name" :placeholder="t('siteNamePlaceholder')" class="input-width" clearable maxlength="20" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('logo')" prop="logo">
|
||||
<div>
|
||||
<upload-image v-model="formData.logo" />
|
||||
<p class="text-[12px] text-[#a9a9a9]">{{ t('logoPlaceholder') }}</p>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('icon')" prop="icon">
|
||||
<div>
|
||||
<upload-image v-model="formData.icon" />
|
||||
<p class="text-[12px] text-[#a9a9a9]">{{ t('iconPlaceholder') }}</p>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('keywords')">
|
||||
<el-input v-model.trim="formData.keywords" :placeholder="t('keywordsPlaceholder')" class="input-width" clearable maxlength="20" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('desc')">
|
||||
<el-input v-model.trim="formData.desc" type="textarea" :rows="4" clearable :placeholder="t('descPlaceholder')" class="input-width" maxlength="100" show-word-limit />
|
||||
</el-form-item>
|
||||
|
||||
<div class="mt-[20px]" v-show="appType == 'site'">
|
||||
<h3 class="panel-title !text-[14px] bg-[#F4F5F7] p-3 border-[#E6E6E6] border-solid border-b-[1px]">{{ t('frontEndInfo') }}</h3>
|
||||
<el-form-item :label="t('frontEndName')">
|
||||
<el-input v-model.trim="formData.front_end_name" :placeholder="t('frontEndNamePlaceholder')" class="input-width" clearable maxlength="20" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('phone')">
|
||||
<el-input v-model.trim="formData.phone" :placeholder="t('phonePlaceholder')" class="input-width" clearable maxlength="20" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('logo')">
|
||||
<upload-image v-model="formData.front_end_logo" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('icon')">
|
||||
<upload-image v-model="formData.front_end_icon" />
|
||||
</el-form-item>
|
||||
<div class="mt-[20px]" v-show="appType == 'site'">
|
||||
<h3 class="panel-title !text-[14px] bg-[#F4F5F7] p-3 border-[#E6E6E6] border-solid border-b-[1px]">{{ t('frontEndInfo') }}</h3>
|
||||
<el-form-item :label="t('frontEndName')">
|
||||
<el-input v-model.trim="formData.front_end_name" :placeholder="t('frontEndNamePlaceholder')" class="input-width" clearable maxlength="20" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('phone')">
|
||||
<el-input v-model.trim="formData.phone" :placeholder="t('phonePlaceholder')" class="input-width" clearable maxlength="20" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('logo')">
|
||||
<upload-image v-model="formData.front_end_logo" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('icon')">
|
||||
<upload-image v-model="formData.front_end_icon" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('metaTitle')">
|
||||
<el-input v-model.trim="formData.meta_title" :placeholder="t('MetaPlaceholder')" class="input-width" clearable maxlength="40" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('metaDescription')">
|
||||
<el-input v-model.trim="formData.meta_desc" :placeholder="t('metaDescriptionPlaceholder')" class="input-width" clearable maxlength="200" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('metaKeywords')">
|
||||
<el-input v-model.trim="formData.meta_keyword" :placeholder="t('metaKeywordsPlaceholder')" class="input-width" clearable maxlength="200" show-word-limit />
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="mt-[20px]" v-if="appType == 'admin'">
|
||||
<h3 class="panel-title !text-[14px] bg-[#F4F5F7] p-3 border-[#E6E6E6] border-solid border-b-[1px]">{{ t('serviceInformation') }}</h3>
|
||||
<el-form-item :label="t('contactsTel')">
|
||||
<el-input v-model.trim="formData.tel" :placeholder="t('contactsTelPlaceholder')" class="input-width" clearable maxlength="20" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('wechatCode')">
|
||||
<upload-image v-model="formData.wechat_code" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('customerServiceCode')">
|
||||
<upload-image v-model="formData.enterprise_wechat" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-form>
|
||||
|
||||
<div class="fixed-footer-wrap">
|
||||
<div class="fixed-footer">
|
||||
<el-button type="primary" :loading="loading" @click="save(formRef)">{{ t('save') }}</el-button>
|
||||
</div>
|
||||
<el-form-item :label="t('metaTitle')">
|
||||
<el-input v-model.trim="formData.meta_title" :placeholder="t('MetaPlaceholder')" class="input-width" clearable maxlength="40" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('metaDescription')">
|
||||
<el-input v-model.trim="formData.meta_desc" :placeholder="t('metaDescriptionPlaceholder')" class="input-width" clearable maxlength="200" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('metaKeywords')">
|
||||
<el-input v-model.trim="formData.meta_keyword" :placeholder="t('metaKeywordsPlaceholder')" class="input-width" clearable maxlength="200" show-word-limit />
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="mt-[20px]" v-if="appType == 'admin'">
|
||||
<h3 class="panel-title !text-[14px] bg-[#F4F5F7] p-3 border-[#E6E6E6] border-solid border-b-[1px]">{{ t('serviceInformation') }}</h3>
|
||||
<el-form-item :label="t('contactsTel')">
|
||||
<el-input v-model.trim="formData.tel" :placeholder="t('contactsTelPlaceholder')" class="input-width" clearable maxlength="20" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('wechatCode')">
|
||||
<upload-image v-model="formData.wechat_code" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('customerServiceCode')">
|
||||
<upload-image v-model="formData.enterprise_wechat" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-form>
|
||||
|
||||
<div class="fixed-footer-wrap">
|
||||
<div class="fixed-footer">
|
||||
<el-button type="primary" :loading="loading" @click="save(formRef)">{{ t('save') }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@ -91,46 +91,46 @@ const pageName = route.meta.title
|
||||
const loading = ref(true)
|
||||
const appType = ref(getAppType())
|
||||
const formData: any = reactive<Record<string, string>>({
|
||||
site_name: '',
|
||||
logo: '',
|
||||
desc: '',
|
||||
latitude: '',
|
||||
keywords: '',
|
||||
longitude: '',
|
||||
province_id: '',
|
||||
city_id: '',
|
||||
district_id: '',
|
||||
address: '',
|
||||
full_address: '',
|
||||
business_hours: '',
|
||||
phone: '',
|
||||
front_end_name: '',
|
||||
front_end_logo: '',
|
||||
front_end_icon: '',
|
||||
icon: '',
|
||||
wechat_code: '',
|
||||
enterprise_wechat: '',
|
||||
tel: '',
|
||||
site_login_logo: '',
|
||||
site_login_bg_img: '',
|
||||
meta_title:"",
|
||||
meta_desc:"",
|
||||
meta_keyword:"",
|
||||
site_name: '',
|
||||
logo: '',
|
||||
desc: '',
|
||||
latitude: '',
|
||||
keywords: '',
|
||||
longitude: '',
|
||||
province_id: '',
|
||||
city_id: '',
|
||||
district_id: '',
|
||||
address: '',
|
||||
full_address: '',
|
||||
business_hours: '',
|
||||
phone: '',
|
||||
front_end_name: '',
|
||||
front_end_logo: '',
|
||||
front_end_icon: '',
|
||||
icon: '',
|
||||
wechat_code: '',
|
||||
enterprise_wechat: '',
|
||||
tel: '',
|
||||
site_login_logo: '',
|
||||
site_login_bg_img: '',
|
||||
meta_title: '',
|
||||
meta_desc: '',
|
||||
meta_keyword: ''
|
||||
})
|
||||
|
||||
const setFormData = async () => {
|
||||
const data = await (await getWebsite()).data
|
||||
Object.keys(formData).forEach((key: string) => {
|
||||
if (data[key] != undefined) formData[key] = data[key]
|
||||
})
|
||||
const data = await (await getWebsite()).data
|
||||
Object.keys(formData).forEach((key: string) => {
|
||||
if (data[key] != undefined) formData[key] = data[key]
|
||||
})
|
||||
|
||||
const service_data: any = await (await getService()).data
|
||||
formData.wechat_code = service_data.wechat_code
|
||||
formData.enterprise_wechat = service_data.enterprise_wechat
|
||||
formData.tel = service_data.tel
|
||||
formData.site_login_logo = service_data.site_login_logo
|
||||
formData.site_login_bg_img = service_data.site_login_bg_img
|
||||
loading.value = false
|
||||
const service_data: any = await (await getService()).data
|
||||
formData.wechat_code = service_data.wechat_code
|
||||
formData.enterprise_wechat = service_data.enterprise_wechat
|
||||
formData.tel = service_data.tel
|
||||
formData.site_login_logo = service_data.site_login_logo
|
||||
formData.site_login_bg_img = service_data.site_login_bg_img
|
||||
loading.value = false
|
||||
}
|
||||
setFormData()
|
||||
|
||||
@ -138,36 +138,42 @@ const formRef = ref<FormInstance>()
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = reactive<FormRules>({
|
||||
site_name: [
|
||||
{ required: true, message: t('siteNamePlaceholder'), trigger: 'blur' }
|
||||
],
|
||||
front_end_name: [
|
||||
{ required: true, message: t('frontEndNamePlaceholder'), trigger: 'blur' }
|
||||
]
|
||||
site_name: [
|
||||
{ required: true, message: t('siteNamePlaceholder'), trigger: 'blur' }
|
||||
],
|
||||
logo: [
|
||||
{ required: true, message: t('请选择长方形Logo'), trigger: 'blur' }
|
||||
],
|
||||
icon: [
|
||||
{ required: true, message: t('请选择正方形Logo'), trigger: 'blur' }
|
||||
],
|
||||
front_end_name: [
|
||||
{ required: true, message: t('frontEndNamePlaceholder'), trigger: 'blur' }
|
||||
]
|
||||
})
|
||||
|
||||
/**
|
||||
* 保存
|
||||
*/
|
||||
const save = async (formEl: FormInstance | undefined) => {
|
||||
if (loading.value || !formEl) return
|
||||
if (loading.value || !formEl) return
|
||||
|
||||
await formEl.validate(async (valid) => {
|
||||
if (valid) {
|
||||
loading.value = true
|
||||
setWebsite(formData).then(() => {
|
||||
loading.value = false
|
||||
appType.value == 'admin' ? useSystemStore().getWebsiteInfo() : useUserStore().getSiteInfo()
|
||||
}).catch(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
})
|
||||
await formEl.validate(async (valid) => {
|
||||
if (valid) {
|
||||
loading.value = true
|
||||
setWebsite(formData).then(() => {
|
||||
loading.value = false
|
||||
appType.value == 'admin' ? useSystemStore().getWebsiteInfo() : useUserStore().getSiteInfo()
|
||||
}).catch(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.loading-box .el-loading-spinner){
|
||||
top: 33%;
|
||||
top: 33%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -1,18 +1,18 @@
|
||||
<template>
|
||||
<div class="main-container attachment-container">
|
||||
<el-card class="box-card !border-none full-container" shadow="never">
|
||||
<div class="main-container attachment-container">
|
||||
<el-card class="box-card !border-none full-container" shadow="never">
|
||||
|
||||
<div class="flex justify-between items-center mb-[20px]">
|
||||
<span class="text-page-title">{{ pageName }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center mb-[20px]">
|
||||
<span class="text-page-title">{{ pageName }}</span>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</el-tabs>
|
||||
</el-card>
|
||||
</div>
|
||||
<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>
|
||||
</el-tabs>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
@ -30,46 +30,46 @@ const type = ref(attachmentType[0])
|
||||
|
||||
<style lang="scss">
|
||||
.attachment-container {
|
||||
overflow: hidden;
|
||||
min-height: calc(100vh - 94px);
|
||||
background-color: var(--el-bg-color-overlay);
|
||||
overflow: hidden;
|
||||
min-height: calc(100vh - 94px);
|
||||
background-color: var(--el-bg-color-overlay);
|
||||
|
||||
.full-container {
|
||||
height: calc(100vh - 100px);
|
||||
.full-container {
|
||||
height: calc(100vh - 100px);
|
||||
}
|
||||
|
||||
.el-card__body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.el-tabs {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100% - 40px);
|
||||
}
|
||||
|
||||
.el-tabs__content {
|
||||
flex: 1;
|
||||
|
||||
.el-tab-pane {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.el-tabs__nav-wrap::after {
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
.main-wrap {
|
||||
border: none;
|
||||
|
||||
.group-wrap {
|
||||
padding: 0 15px 0 0;
|
||||
}
|
||||
|
||||
.el-card__body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.el-tabs {
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
height: calc(100% - 40px);
|
||||
}
|
||||
|
||||
.el-tabs__content {
|
||||
flex: 1;
|
||||
|
||||
.el-tab-pane {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.el-tabs__nav-wrap::after {
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
.main-wrap {
|
||||
border: none;
|
||||
|
||||
.group-wrap {
|
||||
padding: 0 15px 0 0;
|
||||
}
|
||||
|
||||
.attachment-list-wrap {
|
||||
padding: 0 0 0 15px;
|
||||
}
|
||||
.attachment-list-wrap {
|
||||
padding: 0 0 0 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -83,7 +83,7 @@
|
||||
温馨提示
|
||||
</span>
|
||||
<span class="text-[12px] text-[#9699B6] ml-[10px]">运行环境要求:需预先配置 Nodejs 环境</span>
|
||||
<span class="text-[14px] text-primary cursor-pointer ml-[10px] border-b-[1px] border-solid border-primary" @click="linkEvent('https://doc.niucloud.com/saas.html?keywords=/di-san-fang-yun-bian-yi-pei-zhi')">搭建教程</span>
|
||||
<span class="text-[14px] text-primary cursor-pointer ml-[10px] border-b-[1px] border-solid border-primary" @click="linkEvent('https://doc.press.niucloud.com/php/saas-framework/use/other/third-party-cloud-compilation.html')">搭建教程</span>
|
||||
</div>
|
||||
<div class="ml-[40px] text-[14px] text-[#4F516D] mb-[18px]">
|
||||
<span>1、下载第三方云编译服务器搭建程序包</span><span class="text-primary cursor-pointer " @click="linkEvent('https://gitee.com/niucloud-team/niucloud-compile-server')"> niucloud-compile-server</span>
|
||||
@ -139,7 +139,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<div class="ml-[40px] text-[#374151] text-[14px] italic">
|
||||
<span class="text-[16px] italic">#安装依赖:</span>
|
||||
<span class="text-[16px] italic font-500">#安装依赖:</span>
|
||||
<span class="italic">进入admin端与uniapp端以及web端目录都可执行</span>
|
||||
</div>
|
||||
<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]">
|
||||
@ -149,9 +149,9 @@
|
||||
</div>
|
||||
<div class="mt-[21px]">
|
||||
<div class="ml-[40px] text-[14px] text-[#374151] italic">
|
||||
<span class="text-[16px] italic">#后台admin端口打包:</span>
|
||||
<span class="text-[16px] italic font-500">#后台admin端口打包:</span>
|
||||
<span>进入admin目录下执行,执行后编译代码默认移动到系统的niucloud下的</span>
|
||||
<span class="text-[#F09000] mx-[3px]">public/admin</span>
|
||||
<span class="text-[#F09000] mx-[3px] font-bold">public/admin</span>
|
||||
<span>目录下</span>
|
||||
</div>
|
||||
<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]">
|
||||
@ -161,9 +161,9 @@
|
||||
</div>
|
||||
<div class="mt-[21px]">
|
||||
<div class="ml-[40px] text-[14px] text-[#374151] italic">
|
||||
<span class="text-[16px] italic">#使用uniapp打包H5:</span>
|
||||
<span class="text-[16px] italic font-500">#使用uniapp打包H5:</span>
|
||||
<span>进入uniapp目录下执行,执行后编译代码默认移动到系统niucloud下的</span>
|
||||
<span class="text-[#F09000] mx-[3px]">public/wap</span>
|
||||
<span class="text-[#F09000] mx-[3px] font-bold">public/wap</span>
|
||||
<span>目录下</span>
|
||||
</div>
|
||||
<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]">
|
||||
@ -173,9 +173,9 @@
|
||||
</div>
|
||||
<div class="mt-[21px]">
|
||||
<div class="ml-[40px] text-[14px] text-[#374151] italic">
|
||||
<span class="text-[16px] italic">#使用uniapp打包微信小程序:</span>
|
||||
<span class="text-[16px] italic font-500">#使用uniapp打包微信小程序:</span>
|
||||
<span>进入uniapp目录下执行,执行后编译代码默认移动到系统niucloud下的</span>
|
||||
<span class="text-[#F09000] mx-[3px]">uni-app/dist/build/mp-weixin</span>
|
||||
<span class="text-[#F09000] mx-[3px] font-bold">uni-app/dist/build/mp-weixin</span>
|
||||
<span>目录</span>
|
||||
</div>
|
||||
<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]">
|
||||
@ -185,9 +185,9 @@
|
||||
</div>
|
||||
<div class="mt-[21px]">
|
||||
<div class="ml-[40px] text-[14px] text-[#374151] italic">
|
||||
<span class="text-[16px] italic">#前台web(pc)端打包::</span>
|
||||
<span class="text-[16px] italic font-500">#前台web(pc)端打包::</span>
|
||||
<span>进入web目录下执行,执行后编译代码默认移动到系统niucloud下的</span>
|
||||
<span class="text-[#F09000] mx-[3px]">public/web</span>
|
||||
<span class="text-[#F09000] mx-[3px] font-bold">public/web</span>
|
||||
<span>目录下</span>
|
||||
</div>
|
||||
<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]">
|
||||
|
||||
@ -146,7 +146,7 @@ const handleFailReason = (data: any) => {
|
||||
failReasonDialogShow.value = true
|
||||
}
|
||||
const helpInfo = () => {
|
||||
window.open('https://doc.niucloud.com/saasUse.html?keywords=/configFAQ/minWaChatUpload')
|
||||
window.open('https://doc.press.niucloud.com/php/saas-framework/use/configFAQ/minWaChatUpload.html')
|
||||
}
|
||||
|
||||
const deleteVersion = (data: any) => {
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
<icon name="element Delete" color="#fff" size="18px" @click.stop="removeImage" />
|
||||
</div>
|
||||
</div>
|
||||
<upload-attachment :limit="limit" @confirm="confirmSelect" v-else>
|
||||
<upload-attachment :limit="uploadImgNum" @confirm="confirmSelect" v-else>
|
||||
<div class="w-full h-full flex items-center justify-center flex-col content-wrap">
|
||||
<icon name="element Plus" size="20px" color="var(--el-text-color-secondary)" />
|
||||
<div class="leading-none text-xs mt-[10px] text-secondary">{{ imageText || t('upload.root') }}</div>
|
||||
@ -34,7 +34,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="rounded cursor-pointer overflow-hidden relative border border-dashed border-color" :style="style" v-if="images.data.length < limit">
|
||||
<upload-attachment :limit="limit" @confirm="confirmSelect">
|
||||
<upload-attachment :limit="uploadImgNum" @confirm="confirmSelect">
|
||||
<div class="w-full h-full flex items-center justify-center flex-col content-wrap">
|
||||
<icon name="element Plus" size="20px" color="var(--el-text-color-secondary)" />
|
||||
<div class="leading-none text-xs mt-[10px] text-secondary">{{ imageText || t('upload.root') }}</div>
|
||||
@ -105,6 +105,11 @@ const setValue = () => {
|
||||
previewImageList = toRaw(images.data).map((url: string) => { return url.indexOf('data:image') != -1 ? url : img(url) })
|
||||
}
|
||||
|
||||
const uploadImgNum = computed(() => {
|
||||
const num = prop.limit - images.data.length
|
||||
return num
|
||||
})
|
||||
|
||||
watch(() => value.value, () => {
|
||||
if (value.value.indexOf('data:image') != -1) {
|
||||
images.data = [value.value]
|
||||
|
||||
@ -77,10 +77,12 @@ routers.forEach(item => {
|
||||
oneMenuData.value.push(item)
|
||||
})
|
||||
|
||||
const oneMenuActive = ref(oneMenuData.value[0].name)
|
||||
const oneMenuActive = ref(oneMenuData.value[0]?.name || '')
|
||||
watch(route, () => {
|
||||
twoMenuData.value = route.matched[2].children ?? []
|
||||
oneMenuActive.value = route.matched[1].name == ADMIN_ROUTE.children[0].name ? route.matched[2].name : route.matched[1].name
|
||||
twoMenuData.value = route.matched[2]?.children ?? []
|
||||
if (route.matched[1] && route.matched[2]) {
|
||||
oneMenuActive.value = route.matched[1].name == ADMIN_ROUTE.children[0]?.name ? route.matched[2].name : route.matched[1].name
|
||||
}
|
||||
defaultOpeneds.value = twoMenuData.value.map(item => item.name)
|
||||
}, { immediate: true })
|
||||
|
||||
|
||||
@ -53,7 +53,10 @@ const tabClick = (content: any) => {
|
||||
const removeTab = (content: any) => {
|
||||
if (route.path == content) {
|
||||
const tabs = Object.keys(tabbarStore.tabs)
|
||||
router.push({ path: tabs[tabs.indexOf(content) - 1] })
|
||||
const currentIndex = tabs.indexOf(content)
|
||||
if (currentIndex > 0) {
|
||||
router.push({ path: tabs[currentIndex - 1] })
|
||||
}
|
||||
}
|
||||
tabbarStore.removeTab(content)
|
||||
}
|
||||
|
||||
@ -94,17 +94,17 @@ routers.forEach(item => {
|
||||
}
|
||||
|
||||
// 排序, 功能正确,改了排序后需要把菜单排序的默认值重新调整一下【多应用一级菜单,单应用二级菜单】
|
||||
// oneMenuData.value.sort((a, b) => {
|
||||
// if (a.meta.sort && b.meta.sort) {
|
||||
// return b.meta.sort - a.meta.sort
|
||||
// } else if (a.meta.sort) {
|
||||
// return -1
|
||||
// } else if (b.meta.sort) {
|
||||
// return 1
|
||||
// } else {
|
||||
// return 0
|
||||
// }
|
||||
// })
|
||||
oneMenuData.value.sort((a, b) => {
|
||||
if (a.meta.sort && b.meta.sort) {
|
||||
return b.meta.sort - a.meta.sort
|
||||
} else if (a.meta.sort) {
|
||||
return -1
|
||||
} else if (b.meta.sort) {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 多应用时将应用插入菜单
|
||||
@ -119,17 +119,17 @@ if (siteInfo?.apps.length > 1) {
|
||||
oneMenuData.value.unshift(...routers)
|
||||
|
||||
// 排序, 功能正确,改了排序后需要把菜单排序的默认值重新调整一下【多应用一级菜单,单应用二级菜单】
|
||||
// oneMenuData.value.sort((a, b) => {
|
||||
// if (a.meta.sort && b.meta.sort) {
|
||||
// return b.meta.sort - a.meta.sort
|
||||
// } else if (a.meta.sort) {
|
||||
// return -1
|
||||
// } else if (b.meta.sort) {
|
||||
// return 1
|
||||
// } else {
|
||||
// return 0
|
||||
// }
|
||||
// })
|
||||
oneMenuData.value.sort((a, b) => {
|
||||
if (a.meta.sort && b.meta.sort) {
|
||||
return b.meta.sort - a.meta.sort
|
||||
} else if (a.meta.sort) {
|
||||
return -1
|
||||
} else if (b.meta.sort) {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const oneMenuActive = ref(route.matched[1].name)
|
||||
|
||||
@ -86,7 +86,7 @@
|
||||
<input type="hidden" v-model="comparisonToken">
|
||||
<input type="hidden" v-model="comparisonSiteId">
|
||||
|
||||
<el-dialog v-model="detectionLoginDialog" :title="t('layout.detectionLoginTip')" width="30%" :close-on-click-modal="false" :close-on-press-escape="false" :show-close="false">
|
||||
<el-dialog v-model="detectionLoginDialog" :title="t('layout.detectionLoginTip')" width="30%" :close-on-click-modal="false" :close-on-press-escape="false" :show-close="false" :append-to-body="true">
|
||||
<span>{{ t('layout.detectionLoginContent') }}</span>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
|
||||
@ -94,17 +94,17 @@ routers.forEach((item, index) => {
|
||||
}
|
||||
|
||||
// 排序, 功能正确,改了排序后需要把菜单排序的默认值重新调整一下【多应用一级菜单,单应用二级菜单】
|
||||
// menuData.value.sort((a, b) => {
|
||||
// if (a.meta.sort && b.meta.sort) {
|
||||
// return b.meta.sort - a.meta.sort
|
||||
// } else if (a.meta.sort) {
|
||||
// return -1
|
||||
// } else if (b.meta.sort) {
|
||||
// return 1
|
||||
// } else {
|
||||
// return 0
|
||||
// }
|
||||
// })
|
||||
menuData.value.sort((a, b) => {
|
||||
if (a.meta.sort && b.meta.sort) {
|
||||
return b.meta.sort - a.meta.sort
|
||||
} else if (a.meta.sort) {
|
||||
return -1
|
||||
} else if (b.meta.sort) {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 多应用时将应用插入菜单
|
||||
@ -119,17 +119,17 @@ if (siteInfo?.apps.length > 1) {
|
||||
menuData.value.unshift(...routers)
|
||||
|
||||
// 排序, 功能正确,改了排序后需要把菜单排序的默认值重新调整一下【多应用一级菜单,单应用二级菜单】
|
||||
// menuData.value.sort((a, b) => {
|
||||
// if (a.meta.sort && b.meta.sort) {
|
||||
// return b.meta.sort - a.meta.sort
|
||||
// } else if (a.meta.sort) {
|
||||
// return -1
|
||||
// } else if (b.meta.sort) {
|
||||
// return 1
|
||||
// } else {
|
||||
// return 0
|
||||
// }
|
||||
// })
|
||||
menuData.value.sort((a, b) => {
|
||||
if (a.meta.sort && b.meta.sort) {
|
||||
return b.meta.sort - a.meta.sort
|
||||
} else if (a.meta.sort) {
|
||||
return -1
|
||||
} else if (b.meta.sort) {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@ -86,7 +86,7 @@
|
||||
<input type="hidden" v-model="comparisonToken">
|
||||
<input type="hidden" v-model="comparisonSiteId">
|
||||
|
||||
<el-dialog v-model="detectionLoginDialog" :title="t('layout.detectionLoginTip')" width="30%" :close-on-click-modal="false" :close-on-press-escape="false" :show-close="false">
|
||||
<el-dialog v-model="detectionLoginDialog" :title="t('layout.detectionLoginTip')" width="30%" :close-on-click-modal="false" :close-on-press-escape="false" :show-close="false" :append-to-body="true">
|
||||
<span>{{ t('layout.detectionLoginContent') }}</span>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
|
||||
@ -184,10 +184,10 @@ const handleJump = (routeName: string) => {
|
||||
routeName = 'addon_list'
|
||||
}
|
||||
// 跳转时添加随机查询参数(用于触发页面感知)
|
||||
const query = route.name === routeName
|
||||
? { refresh: Date.now() } // 相同路由时添加随机参数
|
||||
: {};
|
||||
|
||||
const query = route.name === routeName
|
||||
? { refresh: Date.now() } // 相同路由时添加随机参数
|
||||
: {};
|
||||
|
||||
// 执行跳转
|
||||
router.push({ name: routeName, query });
|
||||
}
|
||||
|
||||
@ -30,8 +30,8 @@ import useSystemStore from '@/stores/modules/system'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
import menuItem from './menu-item.vue'
|
||||
import { img } from '@/utils/common'
|
||||
import { findFirstValidRoute ,formatRouters} from '@/router/routers'
|
||||
import { getShowApp,getShowSpecialMenu} from '@/app/api/site'
|
||||
import { findFirstValidRoute, formatRouters } from '@/router/routers'
|
||||
import { getShowApp, getShowSpecialMenu } from '@/app/api/site'
|
||||
|
||||
import storage from '@/utils/storage'
|
||||
|
||||
@ -52,9 +52,9 @@ const appList = ref<Record<string, any>[]>([])
|
||||
const getAppList = async () => {
|
||||
const res = await getShowApp()
|
||||
appList.value = res.data
|
||||
|
||||
storage.set({ key: 'defaultAppList', data: appList.value })
|
||||
}
|
||||
|
||||
const specialList = ref<Record<string, any>[]>([])
|
||||
const getShowSpecialMenuList = async () => {
|
||||
const res = await getShowSpecialMenu()
|
||||
@ -64,7 +64,6 @@ const getShowSpecialMenuList = async () => {
|
||||
storage.set({ key: 'specialAppList', data: specialList.value })
|
||||
}
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
getAppList()
|
||||
getShowSpecialMenuList()
|
||||
@ -97,17 +96,17 @@ routers.forEach(item => {
|
||||
|
||||
// console.log('menuData', menuData.value)
|
||||
// 排序, 功能正确,改了排序后需要把菜单排序的默认值重新调整一下【多应用一级菜单,单应用二级菜单】
|
||||
// menuData.value.sort((a, b) => {
|
||||
// if (a.meta.sort && b.meta.sort) {
|
||||
// return b.meta.sort - a.meta.sort
|
||||
// } else if (a.meta.sort) {
|
||||
// return -1
|
||||
// } else if (b.meta.sort) {
|
||||
// return 1
|
||||
// } else {
|
||||
// return 0
|
||||
// }
|
||||
// })
|
||||
menuData.value.sort((a, b) => {
|
||||
if (a.meta.sort && b.meta.sort) {
|
||||
return b.meta.sort - a.meta.sort
|
||||
} else if (a.meta.sort) {
|
||||
return -1
|
||||
} else if (b.meta.sort) {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 多应用时将应用插入菜单
|
||||
@ -122,17 +121,17 @@ if (siteInfo?.apps.length > 1) {
|
||||
menuData.value.unshift(...routers)
|
||||
|
||||
// 排序, 功能正确,改了排序后需要把菜单排序的默认值重新调整一下【多应用一级菜单,单应用二级菜单】
|
||||
// menuData.value.sort((a, b) => {
|
||||
// if (a.meta.sort && b.meta.sort) {
|
||||
// return b.meta.sort - a.meta.sort
|
||||
// } else if (a.meta.sort) {
|
||||
// return -1
|
||||
// } else if (b.meta.sort) {
|
||||
// return 1
|
||||
// } else {
|
||||
// return 0
|
||||
// }
|
||||
// })
|
||||
menuData.value.sort((a, b) => {
|
||||
if (a.meta.sort && b.meta.sort) {
|
||||
return b.meta.sort - a.meta.sort
|
||||
} else if (a.meta.sort) {
|
||||
return -1
|
||||
} else if (b.meta.sort) {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@ -87,7 +87,7 @@
|
||||
<input type="hidden" v-model="comparisonToken">
|
||||
<input type="hidden" v-model="comparisonSiteId">
|
||||
|
||||
<el-dialog v-model="detectionLoginDialog" :title="t('layout.detectionLoginTip')" width="30%" :close-on-click-modal="false" :close-on-press-escape="false" :show-close="false">
|
||||
<el-dialog v-model="detectionLoginDialog" :title="t('layout.detectionLoginTip')" width="30%" :close-on-click-modal="false" :close-on-press-escape="false" :show-close="false" :append-to-body="true">
|
||||
<span>{{ t('layout.detectionLoginContent') }}</span>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
|
||||
433
admin/src/layout/lightside/components/aside/index.vue
Normal file
433
admin/src/layout/lightside/components/aside/index.vue
Normal file
@ -0,0 +1,433 @@
|
||||
<template>
|
||||
<div :class="['layout-aside ease-in duration-200 flex box-border', { 'bright': !dark}]">
|
||||
<div class="flex flex-col border-0 border-r-[1px] border-solid border-[var(--el-color-info-light-8)] box-border overflow-hidden">
|
||||
<div :class="['w-[150px] one-menu hide-scrollbar', { 'expanded': systemStore.menuIsCollapse }]" >
|
||||
<div class="flex flex-col items-center">
|
||||
<template v-for="(item, index) in oneMenuData">
|
||||
<div v-if="item.meta.show" :title="systemStore.menuIsCollapse ? item.meta.title : item.meta.short_title" class="menu-item my-[2px] p-2 flex w-full box-border cursor-pointer relative" :class="{'is-active':oneMenuActive===item.original_name,'hover-left': systemStore.menuIsCollapse, 'vertical': !systemStore.menuIsCollapse , 'horizontal': systemStore.menuIsCollapse }" :style="{ height: (systemStore.menuIsCollapse ) ? '40px' : '55px' }" @click="router.push({ name: item.name })">
|
||||
<div class="w-[20px] h-[20px] flex items-center justify-center menu-icon" :class="{'is-active':oneMenuActive===item.original_name}">
|
||||
<template v-if="item.meta.icon">
|
||||
<el-image class="w-[20px] h-[20px] overflow-hidden" :src="item.meta.icon" fit="fill" v-if="isUrl(item.meta.icon)"/>
|
||||
<icon :name="item.meta.icon" size="20px" color="#1D1F3A" v-else />
|
||||
</template>
|
||||
<icon v-else :name="'iconfont iconshezhi1'" color="#1D1F3A" />
|
||||
</div>
|
||||
<div v-if="systemStore.menuIsCollapse" class="text-left text-[14px] mt-[3px] w-[75px] using-hidden ml-[10px]">{{ item.meta.title || item.meta.short_title }}</div>
|
||||
<div v-else class="text-center text-[12px] using-hidden mt-1">{{ item.meta.short_title || item.meta.title }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="flex flex-col two-menu w-[185px] " v-if="twoMenuData.length">
|
||||
<el-scrollbar class="flex-1" >
|
||||
<el-menu :default-active="route.name" :router="true" class="aside-menu">
|
||||
<menu-item v-for="(route, index) in twoMenuData" :routes="route" :key="index" :isNewVersion="isNewVersion" />
|
||||
</el-menu>
|
||||
<div class="h-[48px]"></div>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { watch, ref, computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
import { findFirstValidRoute, formatRouters } from "@/router/routers"
|
||||
import { isUrl } from '@/utils/common'
|
||||
import menuItem from './menu-item.vue'
|
||||
import { getVersions } from "@/app/api/auth"
|
||||
import { getFrameworkVersionList } from "@/app/api/module"
|
||||
import storage from '@/utils/storage'
|
||||
|
||||
const route = useRoute()
|
||||
const userStore = useUserStore()
|
||||
const routers = userStore.routers
|
||||
const systemStore = useSystemStore()
|
||||
const siteInfo = userStore.siteInfo
|
||||
const router = useRouter()
|
||||
const addonRouters: Record<string, any> = {}
|
||||
const addonIndexRoute = userStore.addonIndexRoute
|
||||
const dark = computed(() => {
|
||||
return systemStore.dark
|
||||
})
|
||||
|
||||
const twoMenuData = ref<Record<string, any>[]>([])
|
||||
const oneMenuData = ref<Record<string, any>[]>([])
|
||||
routers.forEach(item => {
|
||||
item.original_name = item.name
|
||||
if (item.meta.addon == '') {
|
||||
if (item.meta.attr == '') {
|
||||
if (item.children && item.children.length) {
|
||||
item.name = findFirstValidRoute(item.children)
|
||||
}
|
||||
oneMenuData.value.push(item)
|
||||
}
|
||||
} else if (item.meta.addon != '' && siteInfo?.apps.length <= 1 && siteInfo?.apps[0].key == item.meta.addon && item.meta.show) {
|
||||
if (item.children) {
|
||||
item.children.forEach((citem: Record<string, any>) => {
|
||||
citem.original_name = citem.name
|
||||
if (citem.children && citem.children.length) {
|
||||
citem.name = findFirstValidRoute(citem.children)
|
||||
}
|
||||
})
|
||||
oneMenuData.value.unshift(...item.children)
|
||||
} else {
|
||||
oneMenuData.value.unshift(item)
|
||||
}
|
||||
} else {
|
||||
addonRouters[item.meta.addon] = item
|
||||
}
|
||||
|
||||
// 排序, 功能正确,改了排序后需要把菜单排序的默认值重新调整一下【多应用一级菜单,单应用二级菜单】
|
||||
oneMenuData.value.sort((a, b) => {
|
||||
if (a.meta.sort && b.meta.sort) {
|
||||
return b.meta.sort - a.meta.sort
|
||||
} else if (a.meta.sort) {
|
||||
return -1
|
||||
} else if (b.meta.sort) {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 多应用时将应用插入菜单
|
||||
if (siteInfo?.apps.length > 1) {
|
||||
const routers:Record<string, any>[] = []
|
||||
siteInfo?.apps.forEach((item: Record<string, any>) => {
|
||||
if (addonRouters[item.key]) {
|
||||
addonRouters[item.key].name = addonIndexRoute[item.key]
|
||||
routers.push(addonRouters[item.key])
|
||||
}
|
||||
})
|
||||
oneMenuData.value.unshift(...routers)
|
||||
|
||||
// 排序, 功能正确,改了排序后需要把菜单排序的默认值重新调整一下【多应用一级菜单,单应用二级菜单】
|
||||
oneMenuData.value.sort((a, b) => {
|
||||
if (a.meta.sort && b.meta.sort) {
|
||||
return b.meta.sort - a.meta.sort
|
||||
} else if (a.meta.sort) {
|
||||
return -1
|
||||
} else if (b.meta.sort) {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const oneMenuActive = ref(route.matched[1].name)
|
||||
// 从 addonKeys 中提取所有需要匹配的 key
|
||||
const getAddonAllKeys = (addonData) => {
|
||||
if (!addonData || typeof addonData !== 'object') return [];
|
||||
const allKeys = [];
|
||||
Object.values(addonData).forEach(category => {
|
||||
if (Array.isArray(category.list)) {
|
||||
category.list.forEach(item => {
|
||||
if (item.key) allKeys.push(item.key);
|
||||
});
|
||||
}
|
||||
});
|
||||
return allKeys;
|
||||
};
|
||||
|
||||
// 处理 specialMenusKeys 子菜单 show 的方法
|
||||
const handleSpecialMenus = () => {
|
||||
const specialMenusKeys = storage.get('specialAppList')
|
||||
if (Array.isArray(specialMenusKeys) && specialMenusKeys.length) {
|
||||
const processedSpecialMenus = JSON.parse(JSON.stringify(specialMenusKeys));
|
||||
const activeAppKey = storage.get('activeAppKey');
|
||||
|
||||
// 收集所有特殊菜单的name
|
||||
processedSpecialMenus.forEach(menu => {
|
||||
if (menu.children && Array.isArray(menu.children)) {
|
||||
const traverseChildren = (children) => {
|
||||
children.forEach(child => {
|
||||
if (child && child.is_show !== undefined) {
|
||||
child.is_show = (child.menu_key === activeAppKey) ? 1 : 0;
|
||||
}
|
||||
});
|
||||
};
|
||||
traverseChildren(menu.children);
|
||||
}
|
||||
});
|
||||
// 过滤掉 children 为空的特殊菜单
|
||||
const filteredSpecialMenus = processedSpecialMenus.filter(menu => {
|
||||
return menu.children && menu.children.length > 0;
|
||||
});
|
||||
return formatRouters(filteredSpecialMenus);
|
||||
}
|
||||
return [];
|
||||
};
|
||||
watch(route, () => {
|
||||
if (route.meta.attr != '') {
|
||||
oneMenuActive.value = route.matched[1].name
|
||||
twoMenuData.value = route.matched[1].children ?? []
|
||||
} else {
|
||||
// 多应用
|
||||
if (siteInfo?.apps.length > 1) {
|
||||
twoMenuData.value = route.matched[2].children
|
||||
oneMenuActive.value = route.matched[2].name
|
||||
} else {
|
||||
// 单应用
|
||||
const oneMenu = route.matched[2]
|
||||
if (oneMenu.meta.addon == '') {
|
||||
oneMenuActive.value = route.matched[2].name
|
||||
twoMenuData.value = route.matched[2].children ?? []
|
||||
} else {
|
||||
if (oneMenu.meta.addon == siteInfo?.apps[0].key) {
|
||||
oneMenuActive.value = route.matched[3].name
|
||||
twoMenuData.value = route.matched[3].children ?? []
|
||||
} else {
|
||||
oneMenuActive.value = route.matched[2].name
|
||||
twoMenuData.value = route.matched[2].children ?? []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// const addonKeys = storage.get('defaultAppList')
|
||||
// const addonAllKeys = getAddonAllKeys(addonKeys)
|
||||
// twoMenuData.value = twoMenuData.value.filter((child) =>{
|
||||
// return !child.name || !addonAllKeys.includes(child.name);
|
||||
// })
|
||||
// if(oneMenuActive.value == 'addon'){
|
||||
// // 处理特殊菜单并插入到 twoMenuData 中(与 addon_list 同级)
|
||||
// const processedSpecialMenus = handleSpecialMenus();
|
||||
// if (processedSpecialMenus.length) {
|
||||
// // 先找到 addon_list 在 twoMenuData 中的索引
|
||||
// const addonListIndex = twoMenuData.value.findIndex(
|
||||
// (item) => item.name === 'addon_list'
|
||||
// );
|
||||
// if (addonListIndex !== -1) {
|
||||
// // 将特殊菜单插入到 addon_list 后面(同级)
|
||||
// twoMenuData.value.splice(
|
||||
// addonListIndex + 1,
|
||||
// 0,
|
||||
// ...processedSpecialMenus
|
||||
// );
|
||||
// } else {
|
||||
// // 如果没有 addon_list,直接将特殊菜单添加到 twoMenuData 中
|
||||
// twoMenuData.value.push(...processedSpecialMenus);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}, { immediate: true })
|
||||
|
||||
const frameworkVersionList = ref([])
|
||||
const isNewVersion = 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 getFrameworkVersionListFn = () => {
|
||||
getFrameworkVersionList().then(({ data }) => {
|
||||
frameworkVersionList.value = data
|
||||
}).catch(() => {
|
||||
})
|
||||
}
|
||||
getFrameworkVersionListFn()
|
||||
|
||||
const newVersion: any = computed(() => {
|
||||
return frameworkVersionList.value.length ? frameworkVersionList.value[0] : null
|
||||
})
|
||||
const version = ref('')
|
||||
const getVersionsInfo = () => {
|
||||
getVersions().then((res) => {
|
||||
version.value = res.data.version.version
|
||||
})
|
||||
}
|
||||
getVersionsInfo()
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.one-menu{
|
||||
padding: 20px 10px 10px;
|
||||
width: 78px;
|
||||
overflow-y: auto;
|
||||
// transition: width 0.1s ease-out;
|
||||
&.expanded {
|
||||
width: 185px;
|
||||
padding: 18px 15px 15px;
|
||||
}
|
||||
.menu-item{
|
||||
border-radius: 2px;
|
||||
justify-content: center;
|
||||
&.vertical {
|
||||
width: 55px;
|
||||
height: 55px;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&.horizontal {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
.menu-icon {
|
||||
// background-color: transparent; /* 默认无背景色 */
|
||||
color: #1D1F3A;
|
||||
}
|
||||
|
||||
// .menu-icon.is-active {
|
||||
// background-color: var(--el-color-primary); /* 选中时背景色 */
|
||||
// color: white; /* 选中时图标颜色变白 */
|
||||
// border-radius: 4px; /* 可选:使图标背景为圆形 */
|
||||
// }
|
||||
|
||||
&:hover{
|
||||
background-color: #EAEBF0 !important;
|
||||
border-radius: 6px;
|
||||
// background-color: var(--el-color-primary-light-9) !important;
|
||||
// color:var(--el-color-primary);
|
||||
}
|
||||
&.is-active{
|
||||
background-color: #EAEBF0 !important;
|
||||
border-radius: 6px;
|
||||
// background-color: var(--el-color-primary-light-9) !important;
|
||||
// border: none;
|
||||
// color:var(--el-color-primary);
|
||||
}
|
||||
span{
|
||||
font-size: 14px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
.menu-item.hover-left {
|
||||
justify-content: flex-start;
|
||||
padding-left: 5px;
|
||||
}
|
||||
&.expanded .menu-item .text-center {
|
||||
opacity: 1;
|
||||
}
|
||||
.el-menu{
|
||||
border: 0;
|
||||
}
|
||||
.el-scrollbar{
|
||||
height: calc(100vh - 65px);
|
||||
}
|
||||
}
|
||||
.two-menu{
|
||||
.aside-menu:not(.el-menu--collapse) {
|
||||
width: 185px;
|
||||
border: 0;
|
||||
padding-top: 15px;
|
||||
.el-menu-item{
|
||||
height: 40px;
|
||||
margin: 4px 15px;
|
||||
padding: 0 8px !important;
|
||||
border-radius: 2px;
|
||||
span{
|
||||
margin-left: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
&.is-active{
|
||||
background-color: #EAEBF0 !important;
|
||||
border-radius: 6px;
|
||||
color: inherit;
|
||||
// background-color: var(--el-color-primary-light-9) !important;
|
||||
}
|
||||
&:hover{
|
||||
background-color: #EAEBF0 !important;
|
||||
border-radius: 6px;
|
||||
// background-color: var(--el-color-primary-light-9) !important;
|
||||
// color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
.el-sub-menu{
|
||||
width: 185px;
|
||||
margin: 4px 0;
|
||||
// margin-bottom: 8px;
|
||||
.el-sub-menu__title{
|
||||
margin: 0 15px;
|
||||
height: 40px;
|
||||
padding-left: 8px;
|
||||
border-radius: 2px;
|
||||
span{
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
&:hover{
|
||||
background-color:#EAEBF0 !important;
|
||||
border-radius: 6px;
|
||||
// background-color: var(--el-color-primary-light-9) !important;
|
||||
// color: var(--el-color-primary);
|
||||
}
|
||||
.el-icon.el-sub-menu__icon-arrow{
|
||||
right: 5px;
|
||||
}
|
||||
}
|
||||
.el-menu-item{
|
||||
padding-left: 25px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.logo-wrap {
|
||||
padding: 0;
|
||||
display: flex;
|
||||
white-space: nowrap;
|
||||
align-items: center;
|
||||
|
||||
.logo {
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.logo-title {
|
||||
flex: 1;
|
||||
width: 0;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
font-size: var(--el-font-size-base);
|
||||
}
|
||||
}
|
||||
// :deep(.el-scrollbar__bar){
|
||||
// display: none !important;
|
||||
// }
|
||||
// .layout-aside .el-scrollbar__wrap--hidden-default, .layout-aside .el-scrollbar{
|
||||
// overflow: inherit !important;
|
||||
// }
|
||||
// 隐藏滚动条
|
||||
.hide-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
/* Chrome/Safari/Edge */
|
||||
}
|
||||
|
||||
.hide-scrollbar {
|
||||
-ms-overflow-style: none;
|
||||
/* IE/Edge */
|
||||
scrollbar-width: none;
|
||||
/* Firefox */
|
||||
}
|
||||
// .layout-aside .menu-item.is-active{
|
||||
// position: relative;
|
||||
// &:after{
|
||||
// content: "";
|
||||
// position: absolute;
|
||||
// top: 0;
|
||||
// bottom: 0;
|
||||
// width: 1px;
|
||||
// background: var(--el-color-primary);
|
||||
// right: -1px;
|
||||
// }
|
||||
// }
|
||||
</style>
|
||||
80
admin/src/layout/lightside/components/aside/menu-item.vue
Normal file
80
admin/src/layout/lightside/components/aside/menu-item.vue
Normal file
@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<template v-if="meta.show">
|
||||
<el-sub-menu v-if="routes.children" :index="String(routes.name)">
|
||||
<template #title>
|
||||
<div v-if="meta.icon && props.level != 2" class="w-[13px] h-[13px] mr-[10rpx] relative flex justify-center items-center">
|
||||
<icon v-if="meta.icon && props.level != 2" :name="meta.icon" color="#1D1F3A" class="absolute !w-auto" />
|
||||
</div>
|
||||
<span class="using-hidden" :class="['ml-[10px]', {'text-[15px]': routes.meta.class == 1}, {'text-[14px]': routes.meta.class != 1}]">{{ meta.title }}</span>
|
||||
</template>
|
||||
<menu-item v-for="(route, index) in routes.children" :routes="route" :level="props.level + 1" :key="index" :isNewVersion="props.isNewVersion" />
|
||||
</el-sub-menu>
|
||||
<el-menu-item v-else :index="String(routes.name)" :route="routes.path">
|
||||
<template #title>
|
||||
<div v-if="meta.icon && props.level != 2" class="w-[13px] h-[13px] mr-[10rpx] relative flex justify-center items-center">
|
||||
<icon v-if="meta.icon && props.level != 2" color="#1D1F3A" :name="meta.icon" class="absolute !w-auto" />
|
||||
</div>
|
||||
<span class="using-hidden" :class="[{'text-[15px]': routes.meta.class == 1}, {'text-[14px]': routes.meta.class != 1}, {'ml-[10px]': routes.meta.class == 2, 'ml-[15px]': routes.meta.class == 3}]">{{ meta.title }}
|
||||
<div v-if="meta.view=='app/upgrade'&& props.isNewVersion" class="w-[7px] h-[7px] bg-[#DA203E] absolute flex items-center justify-center rounded-full top-[10px] right-[65px]"></div>
|
||||
</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { t } from '@/lang'
|
||||
import { ref, computed } from 'vue'
|
||||
import menuItem from './menu-item.vue'
|
||||
import { CollectionTag } from '@element-plus/icons-vue'
|
||||
|
||||
const props = defineProps({
|
||||
routes: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
level: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
isNewVersion: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
const meta = computed(() => props.routes.meta)
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.el-sub-menu{
|
||||
.el-icon{
|
||||
width: auto;
|
||||
}
|
||||
li{
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
.el-alert .el-alert__description{
|
||||
margin-top: 0;
|
||||
}
|
||||
.index-item {
|
||||
border: 1px solid;
|
||||
border-color: var(--el-color-primary);
|
||||
&:hover {
|
||||
color: #fff;
|
||||
background-color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
<style scoped>
|
||||
.using-hidden {
|
||||
word-break: break-all;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 1;
|
||||
-webkit-box-orient: vertical;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
141
admin/src/layout/lightside/components/aside/side.vue
Normal file
141
admin/src/layout/lightside/components/aside/side.vue
Normal file
@ -0,0 +1,141 @@
|
||||
<template>
|
||||
<el-container class="w-100" :class="[{ 'sidebar-dark-mode': systemStore.sidebar == 'twoType' }, { 'sidebar-brightness-mode': systemStore.sidebar == 'oneType' }]">
|
||||
<el-main class="menu-wrap">
|
||||
<el-scrollbar>
|
||||
<el-menu :default-active="menuActive" :router="true" class="aside-menu h-full" :unique-opened="true" :collapse="systemStore.menuIsCollapse">
|
||||
<menu-item v-for="(route, index) in userStore.routers[0].children" :routes="route" :key="index" />
|
||||
</el-menu>
|
||||
<div class="h-[48px]"></div>
|
||||
</el-scrollbar>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
import menuItem from './menu-item.vue'
|
||||
|
||||
const logo = ref('@/app/assets/images/login_logo.png')
|
||||
const systemStore = useSystemStore()
|
||||
const userStore = useUserStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const siteInfo = userStore.siteInfo
|
||||
|
||||
const menuActive = computed(() => String(route.name))
|
||||
|
||||
userStore.routers = userStore.routers.filter((item, index) => {
|
||||
if (item.name == 'setting_manage') {
|
||||
// item.meta.class = 1
|
||||
if (item.children) {
|
||||
item.children.forEach((subItem, subIndex) => {
|
||||
subItem.meta.class = 1
|
||||
if (subItem.children) {
|
||||
subItem.children.forEach((threeItem, threeIndex) => {
|
||||
threeItem.meta.class = 2
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
return item.children
|
||||
}
|
||||
})
|
||||
|
||||
// userStore.routers.forEach((item, index) => {
|
||||
// item.meta.class = 1
|
||||
// if (item.children) {
|
||||
// item.children.forEach((subItem, subIndex) => {
|
||||
// subItem.meta.class = 2
|
||||
// if (subItem.children) {
|
||||
// subItem.children.forEach((threeItem, threeIndex) => {
|
||||
// threeItem.meta.class = 3
|
||||
// })
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
// })
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.logo-wrap {
|
||||
padding: 0;
|
||||
display: flex;
|
||||
white-space: nowrap;
|
||||
align-items: center;
|
||||
|
||||
.logo {
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.logo-title {
|
||||
flex: 1;
|
||||
width: 0;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
font-size: var(--el-font-size-base);
|
||||
}
|
||||
}
|
||||
|
||||
.menu-wrap {
|
||||
flex: 1 !important;
|
||||
padding: 0 !important;
|
||||
|
||||
.el-menu {
|
||||
border-right: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-dark-mode {
|
||||
background-color: #191a23;
|
||||
|
||||
&>.logo-wrap {
|
||||
.logo>i {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
border-bottom: 2px solid #101117;
|
||||
}
|
||||
|
||||
.el-menu {
|
||||
background-color: #191a23;
|
||||
|
||||
.el-sub-menu {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.el-sub-menu__title,
|
||||
.el-menu-item {
|
||||
background: transparent !important;
|
||||
color: #B7B7ba;
|
||||
|
||||
&:hover {
|
||||
background-color: transparent !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
}
|
||||
|
||||
.el-menu-item.is-active {
|
||||
color: #fff !important;
|
||||
background-color: var(--el-color-primary) !important;
|
||||
}
|
||||
|
||||
li::after {
|
||||
content: "";
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-brightness-mode {
|
||||
&>.logo-wrap {
|
||||
.logo>i {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
289
admin/src/layout/lightside/components/header/index.vue
Normal file
289
admin/src/layout/lightside/components/header/index.vue
Normal file
@ -0,0 +1,289 @@
|
||||
<template>
|
||||
<el-container class="h-[64px] w-full layout-admin flex items-center justify-between pr-[15px] border-b-[1px] border-solid border-[var(--el-color-info-light-8)]" >
|
||||
<!-- :class="['h-full px-[10px]',{'layout-header border-b border-color': !dark}]" -->
|
||||
<div class="flex items-center">
|
||||
<!-- <div class="navbar-item flex items-center h-full cursor-pointer" @click="toggleMenuCollapse">
|
||||
<icon name="element Expand" v-if="systemStore.menuIsCollapse" />
|
||||
<icon name="element Fold" v-else />
|
||||
</div> -->
|
||||
<div class="flex justify-center items-center flex-shrink-0" :class="{'w-[185px]': systemStore.menuIsCollapse,'w-[78px]': !systemStore.menuIsCollapse}">
|
||||
<div class="w-full h-[40px] overflow-hidden">
|
||||
<el-image style="width: 100%; height: 100%" :src="img(logoUrl)" fit="contain" v-if="!systemStore.menuIsCollapse">
|
||||
<template #error>
|
||||
<div class="flex justify-center items-center w-full h-full"><img class="max-w-[70px]" src="@/app/assets/images/logo.default.png" alt="" object-fit="contain"></div>
|
||||
</template>
|
||||
</el-image>
|
||||
<el-image style="width: 100%; height: 100%" :src="img(longLogoUrl)" fit="contain" v-else>
|
||||
<template #error>
|
||||
<div class="flex justify-center items-center w-full h-full"><img class="max-w-[180px]" src="@/app/assets/images/logo.default.png" alt="" object-fit="contain"></div>
|
||||
</template>
|
||||
</el-image>
|
||||
</div>
|
||||
</div>
|
||||
<div class="left-panel flex items-center text-[14px] leading-[1]">
|
||||
<div class="navbar-item flex items-center h-full cursor-pointer" @click="toggleMenuCollapse">
|
||||
<icon name="element Fold" v-if="systemStore.menuIsCollapse" />
|
||||
<icon name="element Expand" v-else />
|
||||
</div>
|
||||
<!-- 刷新当前页 -->
|
||||
<div class="navbar-item flex items-center h-full cursor-pointer" @click="refreshRouter">
|
||||
<icon name="element Refresh" />
|
||||
</div>
|
||||
<!-- 面包屑导航 -->
|
||||
<div class="flex items-center h-full pl-[10px] hidden-xs-only">
|
||||
<el-breadcrumb separator="/">
|
||||
<el-breadcrumb-item v-for="(route, index) in breadcrumb" :key="index" :to="route.path" class="inter">{{route.meta.title }}</el-breadcrumb-item>
|
||||
</el-breadcrumb>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div>
|
||||
<el-input placeholder="搜索站点或应用" v-model.trim="keywords" />
|
||||
</div> -->
|
||||
|
||||
<div>
|
||||
<div class="right-panel h-full flex items-center justify-end">
|
||||
<div class="flex items-center flex-shrink-0 hidden-xs-only">
|
||||
<el-dropdown trigger="hover" :hide-on-click="false" popper-class="site-info-wrap" class="mr-[8px]">
|
||||
<!-- 状态 -->
|
||||
<div class="mx-[8px] bg-[#f6f6f6] border-[1px] border-solid border-[#eee] rounded-[4px] px-[9px] py-[6px] flex items-center">
|
||||
<span class="mr-[6px] text-[12px] !text-[#333]">{{siteInfo.site_name}}</span>
|
||||
<span class="!text-[10px] text-[#f56c6c]" :class="{'!text-[#67c23a]': siteInfo.status == 1, '!text-[#f56c6c]': siteInfo.status == 3}">{{ siteInfo.status_name }}</span>
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item>
|
||||
<!-- 站点id -->
|
||||
<div class="text-[14px]">站点编号:{{siteInfo.site_id}}</div>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item>
|
||||
<!-- 到期时间 -->
|
||||
<div v-if="siteInfo.expire_time == 0" class="text-[14px]">到期时间:永久</div>
|
||||
<div v-else class="text-[14px]">到期时间:{{ siteInfo.expire_time }}</div>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
<div class="flex items-center flex-shrink-0 hidden-xs-only">
|
||||
<el-popover placement="bottom" :width="330" trigger="click" v-model:visible="isMenuSearch" >
|
||||
<template #reference>
|
||||
<i class="iconfont icona-sousuoV6xx-36 cursor-pointer px-[8px] !text-[14px]"></i>
|
||||
</template>
|
||||
<template #default>
|
||||
<div class="flex items-center">
|
||||
<el-select v-model="selectedRoute" filterable class="!w-[250px] mr-[20px] menu-select" :teleported="false" clearable @change="handleRouteSelect">
|
||||
<el-option v-for="item in flatRoutes" :key="item.name" :label="item.full_title" :value="item.name" >
|
||||
</el-option>
|
||||
</el-select>
|
||||
<el-button type="primary" link @click="isMenuSearch = false">{{t('取消')}}</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-popover>
|
||||
</div>
|
||||
<!-- 预览 只有站点时展示-->
|
||||
<i class="iconfont iconicon_huojian1 cursor-pointer px-[8px]" :title="t('visitWap')" @click="toPreview"></i>
|
||||
<i class="iconfont iconlingdang-xianxing cursor-pointer px-[8px]" :title="t('newInfo')" v-if="appType == 'site'"></i>
|
||||
<!-- 切换语言 -->
|
||||
<!-- <div class="navbar-item flex items-center h-full cursor-pointer">
|
||||
<switch-lang />
|
||||
</div> -->
|
||||
<!-- 切换全屏 -->
|
||||
<!-- <div class="navbar-item flex items-center h-full cursor-pointer" @click="toggleFullscreen">
|
||||
<icon name="iconfont icontuichuquanping" v-if="isFullscreen" />
|
||||
<icon name="iconfont iconquanping" v-else />
|
||||
</div> -->
|
||||
<!-- 布局设置 -->
|
||||
<div class="navbar-item flex items-center h-full cursor-pointer">
|
||||
<layout-setting />
|
||||
</div>
|
||||
<!-- 用户信息 -->
|
||||
<div class="navbar-item flex items-center h-full cursor-pointer">
|
||||
<user-info />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="hidden" v-model="comparisonToken" />
|
||||
<input type="hidden" v-model="comparisonSiteId" />
|
||||
|
||||
<el-dialog v-model="detectionLoginDialog" :title="t('layout.detectionLoginTip')" width="30%" :close-on-click-modal="false" :close-on-press-escape="false" :show-close="false" :append-to-body="true">
|
||||
<span>{{ t('layout.detectionLoginContent') }}</span>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="detectionLoginFn">{{ t('layout.detectionLoginOperation') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
import useAppStore from '@/stores/modules/app'
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { img } from '@/utils/common'
|
||||
import { t } from '@/lang'
|
||||
import storage from '@/utils/storage'
|
||||
import userInfo from './user-info.vue'
|
||||
import layoutSetting from './layout-setting.vue'
|
||||
|
||||
const systemStore = useSystemStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const appStore = useAppStore()
|
||||
const userStore = useUserStore()
|
||||
const routers = userStore.routers
|
||||
const siteInfo:any = computed(() => {
|
||||
return userStore.siteInfo
|
||||
})
|
||||
|
||||
const logoUrl = computed(() => {
|
||||
return userStore.siteInfo.icon ? userStore.siteInfo.icon : systemStore.website.icon
|
||||
})
|
||||
const longLogoUrl = computed(() => {
|
||||
return userStore.siteInfo.logo ? userStore.siteInfo.logo : systemStore.website.logo
|
||||
})
|
||||
// 检测登录 start
|
||||
const detectionLoginDialog = ref(false)
|
||||
const comparisonToken = ref('')
|
||||
const comparisonSiteId = ref('')
|
||||
if (storage.get('comparisonTokenStorage')) {
|
||||
comparisonToken.value = storage.get('comparisonTokenStorage')
|
||||
}
|
||||
if (storage.get('comparisonSiteIdStorage')) {
|
||||
comparisonSiteId.value = storage.get('comparisonSiteIdStorage')
|
||||
}
|
||||
// 监听标签页面切换
|
||||
document.addEventListener('visibilitychange', e => {
|
||||
if (document.visibilityState === 'visible' && (comparisonSiteId.value != storage.get('siteId') || comparisonToken.value != storage.get('token'))) {
|
||||
detectionLoginDialog.value = true
|
||||
}
|
||||
})
|
||||
|
||||
const getParentTitleChain = (meta: any) => {
|
||||
let titles = []
|
||||
let current = meta?.parent_route
|
||||
|
||||
while (current) {
|
||||
if (current.short_title) {
|
||||
titles.unshift(current.short_title)
|
||||
}
|
||||
current = current.parent_route
|
||||
}
|
||||
|
||||
return titles.join(' - ');
|
||||
};
|
||||
|
||||
// 2. 改造 flattenRoutes:增加 parentShow 参数,传递父级 show 状态
|
||||
const flattenRoutes = (routes: any, parent = null, parentShow = 1) => {
|
||||
let flat = [];
|
||||
|
||||
routes.forEach(route => {
|
||||
const { path, name, meta = {}, short_title, children } = route;
|
||||
// 关键:当前菜单的最终 show 状态 = 自身 show(默认1) && 父级 show(默认1)
|
||||
// 若父级 show 不是1,当前菜单直接隐藏,不加入列表
|
||||
const currentShow = meta.show === undefined ? 1 : meta.show;
|
||||
const finalShow = currentShow && parentShow; // 父级隐藏则子级必隐藏
|
||||
|
||||
// 叶子节点判断:type=1 + 最终 show=1(父级+自身都显示)
|
||||
const isLeaf = meta.type === 1 && finalShow === 1;
|
||||
|
||||
if (isLeaf) {
|
||||
const title = meta.title || short_title || '';
|
||||
const parentTitleChain = getParentTitleChain(meta);
|
||||
const fullTitle = parentTitleChain ? `${parentTitleChain} - ${title}` : title;
|
||||
const item = {
|
||||
path,
|
||||
name,
|
||||
title,
|
||||
parent_title: parentTitleChain,
|
||||
full_title: fullTitle
|
||||
};
|
||||
flat.push(item);
|
||||
}
|
||||
|
||||
// 递归处理子菜单:传递当前菜单的 finalShow 作为子级的 parentShow
|
||||
if (children && children.length > 0) {
|
||||
flat = flat.concat(flattenRoutes(children, route, finalShow));
|
||||
}
|
||||
});
|
||||
|
||||
return flat;
|
||||
}
|
||||
|
||||
const isMenuSearch = ref(false)
|
||||
const selectedRoute = ref('')
|
||||
const flatRoutes = flattenRoutes(routers);
|
||||
const handleRouteSelect = (name:any) => {
|
||||
if (name) {
|
||||
router.push({ name })
|
||||
isMenuSearch.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const detectionLoginFn = () => {
|
||||
detectionLoginDialog.value = false
|
||||
location.reload()
|
||||
}
|
||||
// 检测登录 end
|
||||
|
||||
// 刷新路由
|
||||
const refreshRouter = () => {
|
||||
if (!appStore.routeRefreshTag) return
|
||||
appStore.refreshRouterView()
|
||||
}
|
||||
|
||||
// 面包屑导航
|
||||
const breadcrumb = computed(() => {
|
||||
const matched = route.matched.filter(item => { return item.meta.title })
|
||||
if (matched[0] && matched[0].path == '/') matched.splice(0, 1)
|
||||
return matched
|
||||
})
|
||||
storage.set({ key: 'currHeadMenuName', data: "" })
|
||||
systemStore.toggleMenuCollapse(storage.get('menuiscollapse') || false)
|
||||
const toggleMenuCollapse = () => {
|
||||
systemStore.toggleMenuCollapse(!systemStore.menuIsCollapse)
|
||||
}
|
||||
|
||||
const appType = storage.get('app_type')
|
||||
// 跳转去预览
|
||||
const toPreview = () => {
|
||||
const url = router.resolve({
|
||||
path: '/preview/wap',
|
||||
query: {
|
||||
page:'/'
|
||||
}
|
||||
})
|
||||
window.open(url.href)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.layout-header{
|
||||
position: relative;
|
||||
z-index: 5;
|
||||
border-bottom: 1px solid #e8e9eb;
|
||||
}
|
||||
.navbar-item {
|
||||
padding: 0 8px;
|
||||
}
|
||||
.index-item {
|
||||
border: 1px solid;
|
||||
border-color: var(--el-color-primary);
|
||||
&:hover {
|
||||
color: #fff;
|
||||
background-color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
// :deep(.el-input__wrapper) {
|
||||
// box-shadow: none !important;
|
||||
// border-radius: 4px !important;
|
||||
// background: #F7F7FA !important;
|
||||
// min-width: 638px;
|
||||
// height: 40px;
|
||||
// border-radius: 4px !important;
|
||||
// }
|
||||
</style>
|
||||
@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<div class="flex">
|
||||
<icon name="element Setting" @click="drawer = true" />
|
||||
|
||||
<el-drawer v-model="drawer" :title="t('layout.layoutSetting')" size="300px">
|
||||
<el-scrollbar>
|
||||
<!-- 黑暗模式 -->
|
||||
<div class="setting-item flex items-center justify-between mb-[10px]">
|
||||
<div class="title text-base text-tx-secondary">{{ t('layout.darkMode') }}</div>
|
||||
<div>
|
||||
<el-switch v-model="dark" :active-value="true" :inactive-value="false" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 主题颜色 -->
|
||||
<div class="setting-item flex items-center justify-between mb-[10px]">
|
||||
<div class="title text-base text-tx-secondary">{{ t('layout.themeColor') }}</div>
|
||||
<div>
|
||||
<el-color-picker v-model="theme" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 标签栏 -->
|
||||
<div class="setting-item flex items-center justify-between mb-[10px]">
|
||||
<div class="title text-base text-tx-secondary">{{ t('layout.tab') }}</div>
|
||||
<div>
|
||||
<el-switch v-model="tab" :active-value="true" :inactive-value="false" />
|
||||
</div>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
import { useDark, useToggle } from '@vueuse/core'
|
||||
import { setThemeColor } from '@/utils/common'
|
||||
import { t } from '@/lang'
|
||||
import storage from "@/utils/storage";
|
||||
|
||||
const drawer = ref(false)
|
||||
const systemStore = useSystemStore()
|
||||
|
||||
const isDark = useDark()
|
||||
const toggleDark = useToggle(isDark)
|
||||
|
||||
const dark = computed({
|
||||
get () {
|
||||
return systemStore.dark
|
||||
},
|
||||
set (val) {
|
||||
systemStore.setTheme('dark', val)
|
||||
toggleDark(val)
|
||||
setThemeColor(systemStore.theme, systemStore.dark ? 'dark' : 'light')
|
||||
}
|
||||
})
|
||||
|
||||
const tab = computed({
|
||||
get () {
|
||||
return systemStore.tab
|
||||
},
|
||||
set (val) {
|
||||
systemStore.$patch((state) => {
|
||||
state.tab = val
|
||||
storage.set({ key: 'tab', data: val })
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const theme = computed({
|
||||
get () {
|
||||
return systemStore.theme
|
||||
},
|
||||
set (val) {
|
||||
systemStore.setTheme('theme', val)
|
||||
setThemeColor(systemStore.theme, systemStore.dark ? 'dark' : 'light')
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-drawer__header) {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.layout-style {
|
||||
&>div:nth-child(2n+2) {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
32
admin/src/layout/lightside/components/header/switch-lang.vue
Normal file
32
admin/src/layout/lightside/components/header/switch-lang.vue
Normal file
@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<el-dropdown @command="switchLang" :tabindex="1">
|
||||
<icon name="iconfont iconfanyi" />
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="zh-cn" :disabled="systemStore.lang == 'zh-cn'">简体中文</el-dropdown-item>
|
||||
<el-dropdown-item command="en" :disabled="systemStore.lang == 'en'">English</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
import { language } from '@/lang'
|
||||
import { useRoute } from 'vue-router'
|
||||
import storage from '@/utils/storage'
|
||||
|
||||
const route = useRoute()
|
||||
const systemStore = useSystemStore()
|
||||
|
||||
const switchLang = (command: string) => {
|
||||
systemStore.$patch((state) => {
|
||||
state.lang = command
|
||||
storage.set({ key: 'lang', data: command })
|
||||
})
|
||||
language.loadLocaleMessages(route.path, systemStore.lang)
|
||||
location.reload()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
160
admin/src/layout/lightside/components/header/user-info.vue
Normal file
160
admin/src/layout/lightside/components/header/user-info.vue
Normal file
@ -0,0 +1,160 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-dropdown @command="clickEvent" :tabindex="1">
|
||||
<div class="userinfo flex h-full items-center">
|
||||
<el-avatar v-if="userStore.userInfo.head_img" :size="25" :icon="UserFilled" :src="img(userStore.userInfo.head_img)"/>
|
||||
<img v-else src="@/app/assets/images/member_head.png" class="w-[25px] rounded-full" />
|
||||
<div class="user-name pl-[8px]">{{ userStore.userInfo.username }}</div>
|
||||
<icon name="element ArrowDown" class="ml-[5px]" />
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<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="toLink('/home/index')" v-if="isAllowChange">
|
||||
<div class="flex items-center leading-[1] py-[5px]">
|
||||
<span class="iconfont iconqiehuan ml-[4px] !text-[14px] mr-[10px]"></span>
|
||||
<span class="text-[14px]">切换站点</span>
|
||||
</div>
|
||||
</el-dropdown-item>
|
||||
<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>
|
||||
</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="修改密码">
|
||||
<div>
|
||||
<el-form :model="saveInfo" label-width="90px" ref="formRef" :rules="formRules" class="page-form">
|
||||
<el-form-item :label="t('originalPassword')" prop="original_password">
|
||||
<el-input v-model="saveInfo.original_password" type="password" :placeholder="t('originalPasswordPlaceholder')" clearable class="input-width" maxlength="40" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('newPassword')" prop="password">
|
||||
<el-input v-model="saveInfo.password" type="password" :placeholder="t('passwordPlaceholder')" clearable class="input-width" maxlength="40" />
|
||||
<div class="form-tip">{{t('passwordTip')}}</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('passwordCopy')" prop="password_copy">
|
||||
<el-input v-model="saveInfo.password_copy" type="password" :placeholder="t('passwordPlaceholder')" clearable class="input-width" maxlength="40" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="changePasswordDialog = false">{{t('cancel')}}</el-button>
|
||||
<el-button type="primary" @click="submitForm(formRef)">{{t('save')}}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<user-info-edit ref="userInfoEditRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { UserFilled } from '@element-plus/icons-vue'
|
||||
import { reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import type { FormInstance, FormRules, ElNotification } from 'element-plus'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
import { setUserInfo } from '@/app/api/personal'
|
||||
import { img } from '@/utils/common'
|
||||
import { t } from '@/lang'
|
||||
import userInfoEdit from '@/app/components/user-info-edit/index.vue'
|
||||
|
||||
const isAllowChange = localStorage.getItem('isAllowChange') === 'true';
|
||||
const userStore = useUserStore()
|
||||
const router = useRouter()
|
||||
const clickEvent = (command: string) => {
|
||||
switch (command) {
|
||||
case 'logout':
|
||||
userStore.logout()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const logout = () => {
|
||||
userStore.logout()
|
||||
}
|
||||
const toLink = (link) => {
|
||||
router.push(link)
|
||||
}
|
||||
const userInfoEditRef = ref(null)
|
||||
const getUserInfoFn = () => {
|
||||
userInfoEditRef.value?.open()
|
||||
}
|
||||
// 修改密码 --- start
|
||||
const changePasswordDialog = ref(false)
|
||||
const formRef = ref<FormInstance>()
|
||||
// 提交信息
|
||||
const saveInfo = reactive({
|
||||
original_password: '',
|
||||
password: '',
|
||||
password_copy: ''
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = reactive<FormRules>({
|
||||
original_password: [
|
||||
{ required: true, message: t('originalPasswordPlaceholder'), trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: t('passwordPlaceholder'), trigger: 'blur' }
|
||||
],
|
||||
password_copy: [
|
||||
{ required: true, message: t('passwordPlaceholder'), trigger: 'blur' }
|
||||
]
|
||||
})
|
||||
|
||||
const submitForm = (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
formEl.validate((valid) => {
|
||||
if (valid) {
|
||||
let msg = ''
|
||||
if (saveInfo.password && !saveInfo.original_password) msg = t('originalPasswordHint')
|
||||
if (saveInfo.password && saveInfo.original_password && !saveInfo.password_copy) msg = t('newPasswordHint')
|
||||
if (saveInfo.password && saveInfo.original_password && saveInfo.password_copy && saveInfo.password != saveInfo.password_copy) msg = t('doubleCipherHint')
|
||||
if (msg) {
|
||||
ElNotification({
|
||||
type: 'error',
|
||||
message: msg
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
setUserInfo(saveInfo).then((res: any) => {
|
||||
changePasswordDialog.value = false
|
||||
})
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.el-popper .el-dropdown-menu{
|
||||
width: 150px;
|
||||
}
|
||||
</style>
|
||||
136
admin/src/layout/lightside/components/tabs.vue
Normal file
136
admin/src/layout/lightside/components/tabs.vue
Normal file
@ -0,0 +1,136 @@
|
||||
<template>
|
||||
<div class="tab-wrap w-full px-[16px]">
|
||||
<el-tabs :closable="tabbarStore.tabLength > 1" :model-value="route.path" @tab-click="tabClick" @tab-remove="removeTab">
|
||||
<el-tab-pane v-for="(tab, key, index) in tabbarStore.tabs" :name="tab.path" :key="index">
|
||||
<template #label>
|
||||
<el-dropdown trigger="contextmenu" placement="bottom-start">
|
||||
<span :class="{ 'text-primary': route.path == tab.path }" class="tab-name">{{ tab.title }}</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item icon="Back" :disabled="index == 0" @click="closeLeft(tab.path)">{{t('tabs.closeLeft') }}</el-dropdown-item>
|
||||
<el-dropdown-item icon="Right" :disabled="index == (tabbarStore.tabLength - 1)" @click="closeRight(tab.path)">{{t('tabs.closeRight') }}</el-dropdown-item>
|
||||
<el-dropdown-item icon="Close" :disabled="tabbarStore.tabLength == 1" @click="closeOther(tab.path)">{{t('tabs.closeOther') }}</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { watch, onMounted } from 'vue'
|
||||
import useTabbarStore from '@/stores/modules/tabbar'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { t } from '@/lang'
|
||||
|
||||
const tabbarStore = useTabbarStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
onMounted(() => {
|
||||
tabbarStore.addTab(route)
|
||||
})
|
||||
|
||||
watch(route, (nval: any) => {
|
||||
tabbarStore.addTab(nval)
|
||||
})
|
||||
|
||||
/**
|
||||
* 添加tab
|
||||
* @param content
|
||||
*/
|
||||
const tabClick = (content: any) => {
|
||||
const tabRoute = tabbarStore.tabs[content.props.name]
|
||||
router.push({ path: tabRoute.path, query: tabRoute.query })
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除tab
|
||||
* @param content
|
||||
*/
|
||||
const removeTab = (content: any) => {
|
||||
if (route.path == content) {
|
||||
const tabs = Object.keys(tabbarStore.tabs)
|
||||
router.push({ path: tabs[tabs.indexOf(content) - 1] })
|
||||
}
|
||||
tabbarStore.removeTab(content)
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭左侧
|
||||
* @param path
|
||||
*/
|
||||
const closeLeft = (path: string) => {
|
||||
const tabs = Object.keys(tabbarStore.tabs)
|
||||
for (let i = tabs.indexOf(path) - 1; i >= 0; i--) {
|
||||
delete tabbarStore.tabs[tabs[i]]
|
||||
}
|
||||
router.push({ path })
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭右侧
|
||||
* @param path
|
||||
*/
|
||||
const closeRight = (path: string) => {
|
||||
const tabs = Object.keys(tabbarStore.tabs)
|
||||
for (let i = tabs.indexOf(path) + 1; i < tabs.length; i++) {
|
||||
delete tabbarStore.tabs[tabs[i]]
|
||||
}
|
||||
router.push({ path })
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭其他
|
||||
* @param path
|
||||
*/
|
||||
const closeOther = (path: string) => {
|
||||
const tabs = Object.keys(tabbarStore.tabs)
|
||||
tabs.forEach((key: string) => { key != path && delete tabbarStore.tabs[key] })
|
||||
router.push({ path })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-tabs) {
|
||||
.el-tabs--border-card {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.el-tabs__header {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.el-tabs__nav-wrap {
|
||||
margin-bottom: 0;
|
||||
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.el-tabs__content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.el-tabs__item {
|
||||
display: inline-flex !important;
|
||||
padding: 0 20px !important;
|
||||
align-items: center;
|
||||
|
||||
.tab-name:focus {
|
||||
outline: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.el-tabs__active-bar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.el-tabs__item.is-active {
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
49
admin/src/layout/lightside/index.vue
Normal file
49
admin/src/layout/lightside/index.vue
Normal file
@ -0,0 +1,49 @@
|
||||
<template>
|
||||
<div class="flex w-full h-screen">
|
||||
<el-container>
|
||||
<!-- 顶部 -->
|
||||
<el-header>
|
||||
<layout-header></layout-header>
|
||||
</el-header>
|
||||
<!-- 顶部 end -->
|
||||
<el-container :style="{height:'calc(100vh - 64px)'}">
|
||||
<!-- 左侧边栏 -->
|
||||
<layout-aside></layout-aside>
|
||||
<!-- 左侧边栏 end -->
|
||||
<!-- 主体 -->
|
||||
<el-main class="h-full p-0 bg-page">
|
||||
<el-scrollbar>
|
||||
<div class="p-[15px]">
|
||||
<router-view v-slot="{ Component, route }" v-if="appStore.routeRefreshTag">
|
||||
<keep-alive :include="tabbarStore.tabNames" :max="15">
|
||||
<component :is="Component" :key="route.fullPath" />
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</el-main>
|
||||
<!-- 主体 end -->
|
||||
</el-container>
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import layoutHeader from './components/header/index.vue'
|
||||
import layoutAside from './components/aside/index.vue'
|
||||
import useAppStore from '@/stores/modules/app'
|
||||
import useTabbarStore from '@/stores/modules/tabbar'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const tabbarStore = useTabbarStore()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.bg-page {
|
||||
background-color: #F7F7FA;
|
||||
}
|
||||
:deep(.inter .el-breadcrumb__inner){
|
||||
font-weight: inherit !important;
|
||||
color: var(--el-text-color-regular) !important;
|
||||
}
|
||||
</style>
|
||||
4
admin/src/layout/lightside/layout.json
Normal file
4
admin/src/layout/lightside/layout.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"layout": "lightside",
|
||||
"cover": "/static/resource/images/system/layout_lightside.png"
|
||||
}
|
||||
@ -107,17 +107,17 @@ routers.forEach(item => {
|
||||
addonRouters[item.meta.addon] = item
|
||||
}
|
||||
// 排序, 功能正确,改了排序后需要把菜单排序的默认值重新调整一下【多应用一级菜单,单应用二级菜单】
|
||||
// oneMenuData.value.sort((a, b) => {
|
||||
// if (a.meta.sort && b.meta.sort) {
|
||||
// return b.meta.sort - a.meta.sort
|
||||
// } else if (a.meta.sort) {
|
||||
// return -1
|
||||
// } else if (b.meta.sort) {
|
||||
// return 1
|
||||
// } else {
|
||||
// return 0
|
||||
// }
|
||||
// })
|
||||
oneMenuData.value.sort((a, b) => {
|
||||
if (a.meta.sort && b.meta.sort) {
|
||||
return b.meta.sort - a.meta.sort
|
||||
} else if (a.meta.sort) {
|
||||
return -1
|
||||
} else if (b.meta.sort) {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
})
|
||||
})
|
||||
// 多应用时将应用插入菜单
|
||||
if (siteInfo?.apps.length > 1) {
|
||||
@ -130,17 +130,17 @@ if (siteInfo?.apps.length > 1) {
|
||||
})
|
||||
oneMenuData.value.unshift(...routers)
|
||||
// 排序, 功能正确,改了排序后需要把菜单排序的默认值重新调整一下【多应用一级菜单,单应用二级菜单】
|
||||
// oneMenuData.value.sort((a, b) => {
|
||||
// if (a.meta.sort && b.meta.sort) {
|
||||
// return b.meta.sort - a.meta.sort
|
||||
// } else if (a.meta.sort) {
|
||||
// return -1
|
||||
// } else if (b.meta.sort) {
|
||||
// return 1
|
||||
// } else {
|
||||
// return 0
|
||||
// }
|
||||
// })
|
||||
oneMenuData.value.sort((a, b) => {
|
||||
if (a.meta.sort && b.meta.sort) {
|
||||
return b.meta.sort - a.meta.sort
|
||||
} else if (a.meta.sort) {
|
||||
return -1
|
||||
} else if (b.meta.sort) {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const appList = ref(null)
|
||||
|
||||
@ -88,7 +88,7 @@
|
||||
<input type="hidden" v-model="comparisonToken">
|
||||
<input type="hidden" v-model="comparisonSiteId">
|
||||
|
||||
<el-dialog v-model="detectionLoginDialog" :title="t('layout.detectionLoginTip')" width="30%" :close-on-click-modal="false" :close-on-press-escape="false" :show-close="false">
|
||||
<el-dialog v-model="detectionLoginDialog" :title="t('layout.detectionLoginTip')" width="30%" :close-on-click-modal="false" :close-on-press-escape="false" :show-close="false" :append-to-body="true">
|
||||
<span>{{ t('layout.detectionLoginContent') }}</span>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
|
||||
34
admin/src/layout/routine/components/aside/index.vue
Normal file
34
admin/src/layout/routine/components/aside/index.vue
Normal file
@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<el-aside class="layout-aside w-auto">
|
||||
<side class="hidden-xs-only" />
|
||||
</el-aside>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import side from './side.vue'
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
|
||||
const systemStore = useSystemStore()
|
||||
|
||||
const route = useRoute()
|
||||
watch(route, () => {
|
||||
systemStore.$patch(state => {
|
||||
state.menuDrawer = false
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.layout-aside {
|
||||
background-color: var(--side-dark-color, var(--el-bg-color));
|
||||
border-right: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
.aside-drawer {
|
||||
.el-drawer__body {
|
||||
padding: 0px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
86
admin/src/layout/routine/components/aside/menu-item.vue
Normal file
86
admin/src/layout/routine/components/aside/menu-item.vue
Normal file
@ -0,0 +1,86 @@
|
||||
<template>
|
||||
<template v-if="meta.show">
|
||||
<el-sub-menu v-if="hasVisibleChild" :index="String(routes.name)">
|
||||
<template #title>
|
||||
<div class="w-[25px] h-full flex items-center justify-center">
|
||||
<template v-if="meta.icon">
|
||||
<el-image class="w-[16px] h-[16px] overflow-hidden" :src="meta.icon" fit="fill" v-if="isUrl(meta.icon)"/>
|
||||
<icon :name="meta.icon" v-else />
|
||||
</template>
|
||||
<icon v-else :name="'iconfont iconshezhi1'" />
|
||||
</div>
|
||||
<span class="text-[14px]">{{ meta.title }}</span>
|
||||
</template>
|
||||
<menu-item v-for="(route, index) in routes.children" :routes="route" :key="index" :level="props.level + 1" />
|
||||
</el-sub-menu>
|
||||
<el-menu-item :index="String(routes.name)" @click="handleJump(routes.name)" v-else>
|
||||
<template #title>
|
||||
<div class="w-[25px] h-full flex items-center justify-center" v-if="props.level == 1">
|
||||
<template v-if="meta.icon">
|
||||
<el-image class="w-[16px] h-[16px] overflow-hidden" :src="meta.icon" fit="fill" v-if="isUrl(meta.icon)"/>
|
||||
<icon :name="meta.icon" v-else />
|
||||
</template>
|
||||
<icon v-else :name="'iconfont iconshezhi1'" />
|
||||
</div>
|
||||
<span class="text-[14px]">{{ meta.title }}</span>
|
||||
</template>
|
||||
</el-menu-item>
|
||||
</template>
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ref, computed } from 'vue'
|
||||
import { img, isUrl } from '@/utils/common'
|
||||
import menuItem from './menu-item.vue'
|
||||
import storage from '@/utils/storage'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const props = defineProps({
|
||||
routes: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
level: {
|
||||
type: Number,
|
||||
default: 1
|
||||
}
|
||||
})
|
||||
|
||||
const meta = computed(() => props.routes.meta)
|
||||
|
||||
const hasVisibleChild = computed(() => {
|
||||
if (!props.routes.children || !Array.isArray(props.routes.children)) {
|
||||
return false
|
||||
}
|
||||
return props.routes.children.some(child => child.meta?.show === 1)
|
||||
})
|
||||
|
||||
// 统一处理跳转逻辑
|
||||
const handleJump = (routeName: string) => {
|
||||
// 检查目标路由是否在特殊菜单列表中
|
||||
const specialMenuNames = storage.get('specialMenuNames')
|
||||
const specialMenuNamesLevel1 = storage.get('specialMenuNamesLevel1')
|
||||
const isInSpecialMenus = specialMenuNames.includes(routeName)
|
||||
// 核心逻辑:如果不在特殊菜单中,就删除activeAppKey
|
||||
if (!isInSpecialMenus) {
|
||||
storage.remove('activeAppKey')
|
||||
}
|
||||
// 点击特殊菜单的一级,跳转应用列表
|
||||
if (specialMenuNamesLevel1.includes(routeName)) {
|
||||
routeName = 'addon_list'
|
||||
}
|
||||
// 跳转时添加随机查询参数(用于触发页面感知)// 相同路由时添加随机参数
|
||||
const query = route.name === routeName ? { refresh: Date.now() } : {}
|
||||
|
||||
// 执行跳转
|
||||
router.push({ name: routeName, query })
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
</style>
|
||||
343
admin/src/layout/routine/components/aside/side.vue
Normal file
343
admin/src/layout/routine/components/aside/side.vue
Normal file
@ -0,0 +1,343 @@
|
||||
<template>
|
||||
<el-container class="w-100 h-[100%]">
|
||||
<el-main class="p-0 flex">
|
||||
<el-scrollbar v-if="twoMenuData.length" class="two-menu w-[200px]">
|
||||
<el-menu class="aside-menu" :default-active="route.name" :default-openeds="menuOption" :router="true" :collapse="systemStore.menuIsCollapse">
|
||||
<menu-item v-for="(route, index) in twoMenuData" :routes="route" :key="index" />
|
||||
</el-menu>
|
||||
<div class="h-[48px]"></div>
|
||||
</el-scrollbar>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import menuItem from './menu-item.vue'
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import storage from '@/utils/storage'
|
||||
import { getShowApp, getShowSpecialMenu } from '@/app/api/site'
|
||||
import { formatRouters } from '@/router/routers'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const systemStore = useSystemStore()
|
||||
const userStore = useUserStore()
|
||||
|
||||
const siteInfo = userStore.siteInfo
|
||||
|
||||
const appList = ref(null)
|
||||
const twoMenuData = ref<Record<string, any>[]>([])
|
||||
const oneMenuActive = ref(route.matched[1].name)
|
||||
|
||||
const getAppList = async () => {
|
||||
const res = await getShowApp()
|
||||
appList.value = res.data
|
||||
storage.set({ key: 'defaultAppList', data: appList.value })
|
||||
}
|
||||
const specialList = ref<Record<string, any>[]>([])
|
||||
const getShowSpecialMenuList = async () => {
|
||||
const res = await getShowSpecialMenu()
|
||||
specialList.value = res.data.list
|
||||
storage.set({ key: 'specialAppList', data: specialList.value })
|
||||
}
|
||||
|
||||
const specialMenuNames = ref<string[]>([])
|
||||
const specialMenuNamesLevel1 = ref<string[]>([])
|
||||
|
||||
onMounted(() => {
|
||||
getAppList()
|
||||
getShowSpecialMenuList()
|
||||
const processedSpecialMenus = handleSpecialMenus()
|
||||
specialMenuNames.value = collectSpecialMenuNames(processedSpecialMenus)
|
||||
specialMenuNamesLevel1.value = collectSpecialMenuNamesLevel1(processedSpecialMenus)
|
||||
storage.set({ key: 'specialMenuNames', data: specialMenuNames.value })
|
||||
storage.set({ key: 'specialMenuNamesLevel1', data: specialMenuNamesLevel1.value })
|
||||
})
|
||||
|
||||
// 让二级菜单默认展开
|
||||
const menuOption = ref([])
|
||||
const secondMenuShowWayFn = () => {
|
||||
menuOption.value = []
|
||||
if (oneMenuActive.value !== 'active' && oneMenuActive.value !== 'addon' && twoMenuData.value && Object.values(twoMenuData.value).length) {
|
||||
const data = cloneDeep(twoMenuData.value)
|
||||
for (const key in data) {
|
||||
menuOption.value.push(data[key].name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 从 addonKeys 中提取所有需要匹配的 key
|
||||
const getAddonAllKeys = (addonData) => {
|
||||
if (!addonData || typeof addonData !== 'object') return []
|
||||
const allKeys = []
|
||||
Object.values(addonData).forEach(category => {
|
||||
if (Array.isArray(category.list)) {
|
||||
category.list.forEach(item => {
|
||||
if (item.key) allKeys.push(item.key)
|
||||
})
|
||||
}
|
||||
})
|
||||
return allKeys
|
||||
}
|
||||
|
||||
// 处理 specialMenusKeys 子菜单 show 的方法
|
||||
const handleSpecialMenus = () => {
|
||||
const specialMenusKeys = storage.get('specialAppList')
|
||||
if (Array.isArray(specialMenusKeys) && specialMenusKeys.length) {
|
||||
const processedSpecialMenus = JSON.parse(JSON.stringify(specialMenusKeys))
|
||||
const activeAppKey = storage.get('activeAppKey')
|
||||
// 收集所有特殊菜单的name
|
||||
processedSpecialMenus.forEach(menu => {
|
||||
if (menu.children && Array.isArray(menu.children)) {
|
||||
const traverseChildren = (children) => {
|
||||
children.forEach(child => {
|
||||
if (child && child.is_show !== undefined) {
|
||||
child.is_show = (child.menu_key === activeAppKey) ? 1 : 0
|
||||
}
|
||||
})
|
||||
}
|
||||
traverseChildren(menu.children)
|
||||
}
|
||||
})
|
||||
// 过滤掉 children 为空的特殊菜单
|
||||
const filteredSpecialMenus = processedSpecialMenus.filter(menu => {
|
||||
return menu.children && menu.children.length > 0
|
||||
})
|
||||
return formatRouters(filteredSpecialMenus)
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
watch(route, () => {
|
||||
if (route.meta.attr != '') {
|
||||
oneMenuActive.value = route.matched[1].name
|
||||
twoMenuData.value = route.matched[1].children ?? []
|
||||
} else {
|
||||
// 多应用
|
||||
if (siteInfo?.apps.length > 1) {
|
||||
twoMenuData.value = route.matched[2].children
|
||||
oneMenuActive.value = route.matched[2].name
|
||||
} else {
|
||||
// 单应用
|
||||
const oneMenu = route.matched[2]
|
||||
if (oneMenu.meta.addon == '') {
|
||||
oneMenuActive.value = route.matched[2].name
|
||||
twoMenuData.value = route.matched[2].children ?? []
|
||||
} else {
|
||||
if (oneMenu.meta.addon == siteInfo?.apps[0].key) {
|
||||
oneMenuActive.value = route.matched[3].name
|
||||
twoMenuData.value = route.matched[3].children ?? []
|
||||
} else {
|
||||
oneMenuActive.value = route.matched[2].name
|
||||
twoMenuData.value = route.matched[2].children ?? []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
secondMenuShowWayFn()
|
||||
const addonKeys = storage.get('defaultAppList')
|
||||
const addonAllKeys = getAddonAllKeys(addonKeys)
|
||||
twoMenuData.value = twoMenuData.value.filter((child) => {
|
||||
return !child.name || !addonAllKeys.includes(child.name)
|
||||
})
|
||||
if (oneMenuActive.value == 'addon') {
|
||||
// 处理特殊菜单并插入到 twoMenuData 中(与 addon_list 同级)
|
||||
const processedSpecialMenus = handleSpecialMenus()
|
||||
if (processedSpecialMenus.length) {
|
||||
// 先找到 addon_list 在 twoMenuData 中的索引
|
||||
const addonListIndex = twoMenuData.value.findIndex(
|
||||
(item) => item.name === 'addon_list'
|
||||
)
|
||||
if (addonListIndex !== -1) {
|
||||
// 将特殊菜单插入到 addon_list 后面(同级)
|
||||
twoMenuData.value.splice(
|
||||
addonListIndex + 1,
|
||||
0,
|
||||
...processedSpecialMenus
|
||||
)
|
||||
} else {
|
||||
// 如果没有 addon_list,直接将特殊菜单添加到 twoMenuData 中
|
||||
twoMenuData.value.push(...processedSpecialMenus)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
|
||||
// 提取所有特殊菜单的name
|
||||
const collectSpecialMenuNames = (menus: any[]) => {
|
||||
const names: string[] = []
|
||||
const traverse = (children: any[]) => {
|
||||
children.forEach(child => {
|
||||
if (child.name) {
|
||||
names.push(child.name)
|
||||
}
|
||||
// 递归处理子菜单
|
||||
if (child.children && Array.isArray(child.children)) {
|
||||
traverse(child.children)
|
||||
}
|
||||
})
|
||||
}
|
||||
menus.forEach(menu => {
|
||||
if (menu.children && Array.isArray(menu.children)) {
|
||||
traverse(menu.children)
|
||||
}
|
||||
})
|
||||
return names
|
||||
}
|
||||
|
||||
// 提取所有一级特殊菜单的name
|
||||
const collectSpecialMenuNamesLevel1 = (menus: any[]) =>{
|
||||
const names: string[] = []
|
||||
menus.forEach(menu => {
|
||||
if (menu.name) {
|
||||
names.push(menu.name)
|
||||
}
|
||||
})
|
||||
return names
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
:root,
|
||||
body {
|
||||
--layout-side-hover-bg: #f7f8fa;
|
||||
--layout-side-active-bg: var(--el-color-primary-light-9);
|
||||
--layout-side-active-text: var(--el-color-primary);
|
||||
}
|
||||
</style>
|
||||
<style lang="scss">
|
||||
.two-menu {
|
||||
|
||||
.aside-menu:not(.el-menu--collapse) {
|
||||
width: 200px;
|
||||
padding-top: 16px;
|
||||
border: 0;
|
||||
|
||||
.el-menu-item {
|
||||
height: 36px;
|
||||
margin: 0 8px 4px;
|
||||
padding: 0 !important;
|
||||
border-radius: 2px;
|
||||
|
||||
span {
|
||||
font-size: 14px;
|
||||
line-height: 36px;
|
||||
}
|
||||
i {
|
||||
line-height: 36px;
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
background-color: #fff !important;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
background-color: #fff !important;
|
||||
}
|
||||
}
|
||||
|
||||
.el-sub-menu {
|
||||
margin-bottom: 8px;
|
||||
|
||||
.el-sub-menu__title {
|
||||
height: 36px;
|
||||
margin: 0 8px 4px;
|
||||
padding-left: 0;
|
||||
border-radius: 2px;
|
||||
|
||||
span {
|
||||
font-size: 14px;
|
||||
display: flex;
|
||||
height: 36px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
background-color: #fff !important;
|
||||
}
|
||||
.el-icon.el-sub-menu__icon-arrow {
|
||||
right: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.el-menu-item {
|
||||
padding-left: 25px !important;
|
||||
span{
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
}
|
||||
.el-sub-menu{
|
||||
.el-sub-menu__title{
|
||||
margin: 0 8px 2px;
|
||||
height: 40px;
|
||||
padding-left: 18px;
|
||||
border-radius: 2px;
|
||||
span{
|
||||
height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
&:hover{
|
||||
background-color: transparent;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
.el-menu-item{
|
||||
padding-left: 40px !important;
|
||||
span{
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 二级菜单折叠之后,弹窗样式
|
||||
.el-menu--vertical.el-menu--popup-container{
|
||||
.el-menu--popup{
|
||||
padding: 8px;
|
||||
min-width: auto;
|
||||
}
|
||||
.el-menu-item, .el-sub-menu, .el-sub-menu{
|
||||
height: auto!important;
|
||||
padding: 10px;
|
||||
line-height: 1;
|
||||
color: #999;
|
||||
&.is-active{
|
||||
color: #333;
|
||||
background-color: transparent;
|
||||
}
|
||||
&:hover{
|
||||
background-color: var(--layout-side-hover-bg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.logo-wrap {
|
||||
padding: 0;
|
||||
display: flex;
|
||||
white-space: nowrap;
|
||||
align-items: center;
|
||||
|
||||
.logo {
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.logo-title {
|
||||
flex: 1;
|
||||
width: 0;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
font-size: var(--el-font-size-base);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
451
admin/src/layout/routine/components/header/index.vue
Normal file
451
admin/src/layout/routine/components/header/index.vue
Normal file
@ -0,0 +1,451 @@
|
||||
<template>
|
||||
<el-container :class="['h-full bg-[var(--layout-header-bg)]',{'layout-header border-b border-color': !dark}]" >
|
||||
<div class="flex items-center h-full px-[10px] w-full box-border">
|
||||
<div class="left-panel h-full flex items-center">
|
||||
<el-header class="logo-wrap flex items-center w-[190px]">
|
||||
<div class="logo flex items-center" v-if="!systemStore.menuIsCollapse">
|
||||
<el-image style="width: 30px; height: 30px" class="rounded-[6px]" :src="img(logoUrl ? logoUrl : '' )" fit="contain">
|
||||
<template #error>
|
||||
<div class="flex justify-center items-center w-full h-[30px] rounded-[6px]"><img class="max-w-[30px]" src="@/app/assets/images/icon-addon.png" alt="" object-fit="contain"></div>
|
||||
</template>
|
||||
</el-image>
|
||||
</div>
|
||||
<div class="logo flex items-center justify-center" v-else>
|
||||
<i class="text-3xl iconfont iconyunkongjian"></i>
|
||||
</div>
|
||||
<!-- 店铺名称 -->
|
||||
<div class="ml-[10px] text-[#333] max-w-[150px] using-hidden">{{siteInfo.site_name}}</div>
|
||||
</el-header>
|
||||
</div>
|
||||
<div class="flex items-center gap-[8px] h-[var(--layout-header-height)] text-[var(--layout-header-text-color)]">
|
||||
<template v-for="(item, index) in oneMenuData" :key="index">
|
||||
<div v-if="item.meta.show" class="flex items-center cursor-pointer" @click="handleJump(item.name)">
|
||||
<span class="px-[14px] truncate text-[14px] text-[#333] h-[60px] leading-[60px] rounded-[4px] hover:text-primary" :class="{ '!text-primary font-bold oneMenu': oneMenuActive === item.original_name }">{{item.meta.short_title || item.meta.title }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="ml-auto right-panel h-full flex items-center justify-end">
|
||||
<div class="flex items-center flex-shrink-0 hidden-xs-only">
|
||||
<el-dropdown trigger="hover" :hide-on-click="false" popper-class="site-info-wrap" class="mr-[8px]">
|
||||
<!-- 状态 -->
|
||||
<div class="mx-[8px] bg-[#f6f6f6] border-[1px] border-solid border-[#eee] rounded-[4px] px-[9px] py-[6px] flex items-center">
|
||||
<span class="mr-[6px] text-[12px] !text-[#333]">{{ siteInfo.site_name }}</span>
|
||||
<span class="!text-[10px] text-[#f56c6c]" :class="{'!text-[#67c23a]': siteInfo.status == 1, '!text-[#f56c6c]': siteInfo.status == 3}">{{ siteInfo.status_name }}</span>
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item>
|
||||
<!-- 站点id -->
|
||||
<div class="text-[14px]">站点编号:{{siteInfo.site_id}}</div>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item>
|
||||
<!-- 到期时间 -->
|
||||
<div v-if="siteInfo.expire_time == 0" class="text-[14px]">到期时间:永久</div>
|
||||
<div v-else class="text-[14px]">到期时间:{{ siteInfo.expire_time }}</div>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
<div class="flex items-center flex-shrink-0 hidden-xs-only">
|
||||
<el-popover placement="bottom" :width="330" trigger="click" v-model:visible="isMenuSearch" >
|
||||
<template #reference>
|
||||
<i class="iconfont icona-sousuoV6xx-36 cursor-pointer px-[8px] !text-[14px]"></i>
|
||||
</template>
|
||||
<template #default>
|
||||
<div class="flex items-center">
|
||||
<el-select v-model="selectedRoute" filterable class="!w-[250px] mr-[20px] menu-select" :teleported="false" clearable @change="handleRouteSelect">
|
||||
<el-option v-for="item in flatRoutes" :key="item.name" :label="item.full_title" :value="item.name" >
|
||||
</el-option>
|
||||
</el-select>
|
||||
<el-button type="primary" link @click="isMenuSearch = false">{{t('取消')}}</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-popover>
|
||||
</div>
|
||||
<!-- 预览 只有站点时展示-->
|
||||
<i class="navbar-item iconfont iconicon_huojian1 cursor-pointer text-[#333]" :title="t('visitWap')" @click="toPreview"></i>
|
||||
<i class="navbar-item iconfont iconlingdang-xianxing cursor-pointer text-[#333]" :title="t('newInfo')" v-if="appType == 'site'"></i>
|
||||
<!-- 布局设置 -->
|
||||
<div class="navbar-item flex items-center h-full cursor-pointer">
|
||||
<layout-setting />
|
||||
</div>
|
||||
<!-- 用户信息 -->
|
||||
<div class="navbar-item flex items-center h-full cursor-pointer">
|
||||
<user-info />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" v-model="comparisonToken">
|
||||
<input type="hidden" v-model="comparisonSiteId">
|
||||
|
||||
<el-dialog v-model="detectionLoginDialog" :title="t('layout.detectionLoginTip')" width="30%" :close-on-click-modal="false" :close-on-press-escape="false" :show-close="false" :append-to-body="true">
|
||||
<span>{{ t('layout.detectionLoginContent') }}</span>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="detectionLoginFn">{{ t('layout.detectionLoginOperation') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, onMounted, watch } from 'vue'
|
||||
import layoutSetting from './layout-setting.vue'
|
||||
import userInfo from './user-info.vue'
|
||||
import { useFullscreen } from '@vueuse/core'
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
import useAppStore from '@/stores/modules/app'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { findFirstValidRoute, formatRouters } from '@/router/routers'
|
||||
import { t } from '@/lang'
|
||||
import { img } from '@/utils/common'
|
||||
import storage from '@/utils/storage'
|
||||
|
||||
const appType = storage.get('app_type')
|
||||
const { toggle: toggleFullscreen } = useFullscreen()
|
||||
const systemStore = useSystemStore()
|
||||
const appStore = useAppStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const screenWidth = ref(window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth)
|
||||
|
||||
const userStore = useUserStore()
|
||||
const siteInfo:any = computed(() => {
|
||||
return userStore.siteInfo
|
||||
})
|
||||
const addonIndexRoute = userStore.addonIndexRoute
|
||||
|
||||
const dark = computed(() => {
|
||||
return systemStore.dark
|
||||
})
|
||||
const isMenuSearch = ref(false)
|
||||
const routers = userStore.routers
|
||||
|
||||
const logoUrl = computed(() => {
|
||||
return userStore.siteInfo.logo ? userStore.siteInfo.logo : systemStore.website.logo
|
||||
})
|
||||
|
||||
// 检测登录 start
|
||||
const detectionLoginDialog = ref(false)
|
||||
const comparisonToken = ref('')
|
||||
const comparisonSiteId = ref('')
|
||||
if (storage.get('comparisonTokenStorage')) {
|
||||
comparisonToken.value = storage.get('comparisonTokenStorage')
|
||||
// storage.remove(['comparisonTokenStorage']);
|
||||
}
|
||||
if (storage.get('comparisonSiteIdStorage')) {
|
||||
comparisonSiteId.value = storage.get('comparisonSiteIdStorage')
|
||||
// storage.remove(['comparisonSiteIdStorage']);
|
||||
}
|
||||
// 监听标签页面切换
|
||||
document.addEventListener('visibilitychange', e => {
|
||||
if (document.visibilityState === 'visible' && (comparisonSiteId.value != storage.get('siteId') || comparisonToken.value != storage.get('token'))) {
|
||||
detectionLoginDialog.value = true
|
||||
}
|
||||
})
|
||||
|
||||
const detectionLoginFn = () => {
|
||||
detectionLoginDialog.value = false
|
||||
location.href = `${location.origin}/site/`
|
||||
}
|
||||
// 检测登录 end
|
||||
|
||||
const specialMenuNames = ref<string[]>([])
|
||||
const specialMenuNamesLevel1 = ref<string[]>([])
|
||||
|
||||
onMounted(() => {
|
||||
const processedSpecialMenus = handleSpecialMenus()
|
||||
specialMenuNames.value = collectSpecialMenuNames(processedSpecialMenus)
|
||||
specialMenuNamesLevel1.value = collectSpecialMenuNamesLevel1(processedSpecialMenus)
|
||||
storage.set({ key: 'specialMenuNames', data: specialMenuNames.value })
|
||||
storage.set({ key: 'specialMenuNamesLevel1', data: specialMenuNamesLevel1.value })
|
||||
// 监听窗体宽度变化
|
||||
window.onresize = () => {
|
||||
return (() => {
|
||||
screenWidth.value = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth
|
||||
})()
|
||||
}
|
||||
})
|
||||
|
||||
/* 一级菜单 */
|
||||
const oneMenuData = ref<Record<string, any>[]>([])
|
||||
const addonRouters: Record<string, any> = {}
|
||||
routers.forEach(item => {
|
||||
item.original_name = item.name
|
||||
if (item.meta.addon == '') {
|
||||
if (item.meta.attr == '') {
|
||||
if (item.children && item.children.length) {
|
||||
item.name = findFirstValidRoute(item.children)
|
||||
}
|
||||
oneMenuData.value.push(item)
|
||||
}
|
||||
} else if (item.meta.addon != '' && siteInfo.value?.apps.length <= 1 && siteInfo.value?.apps[0].key == item.meta.addon && item.meta.show) {
|
||||
if (item.children) {
|
||||
item.children.forEach((citem: Record<string, any>) => {
|
||||
citem.original_name = citem.name
|
||||
if (citem.children && citem.children.length) {
|
||||
citem.name = findFirstValidRoute(citem.children)
|
||||
}
|
||||
})
|
||||
oneMenuData.value.unshift(...item.children)
|
||||
} else {
|
||||
oneMenuData.value.unshift(item)
|
||||
}
|
||||
} else {
|
||||
addonRouters[item.meta.addon] = item
|
||||
}
|
||||
|
||||
// 排序, 功能正确,改了排序后需要把菜单排序的默认值重新调整一下【多应用一级菜单,单应用二级菜单】
|
||||
oneMenuData.value.sort((a, b) => {
|
||||
if (a.meta.sort && b.meta.sort) {
|
||||
return b.meta.sort - a.meta.sort
|
||||
} else if (a.meta.sort) {
|
||||
return -1
|
||||
} else if (b.meta.sort) {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
})
|
||||
})
|
||||
// 多应用时将应用插入菜单
|
||||
if (siteInfo.value?.apps.length > 1) {
|
||||
const routers:Record<string, any>[] = []
|
||||
siteInfo.value?.apps.forEach((item: Record<string, any>) => {
|
||||
if (addonRouters[item.key]) {
|
||||
addonRouters[item.key].name = addonIndexRoute[item.key]
|
||||
routers.push(addonRouters[item.key])
|
||||
}
|
||||
})
|
||||
oneMenuData.value.unshift(...routers)
|
||||
|
||||
// 排序, 功能正确,改了排序后需要把菜单排序的默认值重新调整一下【多应用一级菜单,单应用二级菜单】
|
||||
oneMenuData.value.sort((a, b) => {
|
||||
if (a.meta.sort && b.meta.sort) {
|
||||
return b.meta.sort - a.meta.sort
|
||||
} else if (a.meta.sort) {
|
||||
return -1
|
||||
} else if (b.meta.sort) {
|
||||
return 1
|
||||
} else {
|
||||
return 0
|
||||
}
|
||||
})
|
||||
}
|
||||
const oneMenuActive = ref(oneMenuData.value[0]?.name || '')
|
||||
|
||||
watch(route, () => {
|
||||
if (route.meta.attr != '') {
|
||||
oneMenuActive.value = route.matched[1].name
|
||||
} else {
|
||||
// 多应用
|
||||
if (siteInfo.value?.apps.length > 1) {
|
||||
oneMenuActive.value = route.matched[2].name
|
||||
} else {
|
||||
// 单应用
|
||||
const oneMenu = route.matched[2]
|
||||
if (oneMenu.meta.addon == '') {
|
||||
oneMenuActive.value = route.matched[2].name
|
||||
} else {
|
||||
if (oneMenu.meta.addon == siteInfo.value?.apps[0].key) {
|
||||
oneMenuActive.value = route.matched[3].name
|
||||
} else {
|
||||
oneMenuActive.value = route.matched[2].name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
// 统一处理跳转逻辑
|
||||
const handleJump = (routeName: string) => {
|
||||
// 检查目标路由是否在特殊菜单列表中
|
||||
const isInSpecialMenus = specialMenuNames.value.includes(routeName)
|
||||
// 核心逻辑:如果不在特殊菜单中,就删除activeAppKey
|
||||
if (!isInSpecialMenus) {
|
||||
storage.remove('activeAppKey')
|
||||
} else {
|
||||
}
|
||||
|
||||
// 执行跳转
|
||||
router.push({ name: routeName })
|
||||
}
|
||||
|
||||
// 处理 specialMenusKeys 子菜单 show 的方法
|
||||
const handleSpecialMenus = () => {
|
||||
const specialMenusKeys = storage.get('specialAppList')
|
||||
if (Array.isArray(specialMenusKeys) && specialMenusKeys.length) {
|
||||
const processedSpecialMenus = JSON.parse(JSON.stringify(specialMenusKeys))
|
||||
const activeAppKey = storage.get('activeAppKey')
|
||||
|
||||
// 收集所有特殊菜单的name
|
||||
processedSpecialMenus.forEach(menu => {
|
||||
if (menu.children && Array.isArray(menu.children)) {
|
||||
const traverseChildren = (children) => {
|
||||
children.forEach(child => {
|
||||
if (child && child.is_show !== undefined) {
|
||||
child.is_show = (child.menu_key === activeAppKey) ? 1 : 0
|
||||
}
|
||||
})
|
||||
}
|
||||
traverseChildren(menu.children)
|
||||
}
|
||||
})
|
||||
// 过滤掉 children 为空的特殊菜单
|
||||
const filteredSpecialMenus = processedSpecialMenus.filter(menu => {
|
||||
return menu.children && menu.children.length > 0
|
||||
})
|
||||
return formatRouters(filteredSpecialMenus)
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
// 提取所有特殊菜单的name
|
||||
const collectSpecialMenuNames = (menus: any[]) => {
|
||||
const names: string[] = []
|
||||
const traverse = (children: any[]) => {
|
||||
children.forEach(child => {
|
||||
if (child.name) {
|
||||
names.push(child.name)
|
||||
}
|
||||
// 递归处理子菜单
|
||||
if (child.children && Array.isArray(child.children)) {
|
||||
traverse(child.children)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
menus.forEach(menu => {
|
||||
if (menu.children && Array.isArray(menu.children)) {
|
||||
traverse(menu.children)
|
||||
}
|
||||
})
|
||||
return names
|
||||
}
|
||||
|
||||
// 提取所有一级特殊菜单的name
|
||||
const collectSpecialMenuNamesLevel1 = (menus: any[]) =>{
|
||||
const names: string[] = []
|
||||
menus.forEach(menu => {
|
||||
if (menu.name) {
|
||||
names.push(menu.name)
|
||||
}
|
||||
})
|
||||
return names
|
||||
}
|
||||
|
||||
// 跳转去预览
|
||||
const toPreview = () => {
|
||||
const url = router.resolve({
|
||||
path: '/preview/wap',
|
||||
query: {
|
||||
page: `/addon/mall/pages/index?site_id=${siteInfo.value.site_id}`
|
||||
}
|
||||
})
|
||||
window.open(url.href)
|
||||
}
|
||||
|
||||
const getParentTitleChain = (meta:any) => {
|
||||
const titles = []
|
||||
let current = meta?.parent_route
|
||||
|
||||
while (current) {
|
||||
if (current.short_title) {
|
||||
titles.unshift(current.short_title)
|
||||
}
|
||||
current = current.parent_route
|
||||
}
|
||||
|
||||
return titles.join(' - ')
|
||||
}
|
||||
const flattenRoutes = (routes:any, parent = null)=> {
|
||||
let flat: any = []
|
||||
routes.forEach(route => {
|
||||
const { path, name, meta = {}, short_title, children } = route
|
||||
const isLeaf = meta.type == 1 && meta.show == 1
|
||||
if (isLeaf) {
|
||||
const title = meta.title || short_title || ''
|
||||
const parentTitleChain = getParentTitleChain(meta)
|
||||
const fullTitle = parentTitleChain ? `${parentTitleChain} - ${title}` : title
|
||||
const item = {
|
||||
path,
|
||||
name,
|
||||
title,
|
||||
parent_title: parentTitleChain,
|
||||
full_title: fullTitle
|
||||
}
|
||||
|
||||
flat.push(item)
|
||||
}
|
||||
if (children && children.length > 0) {
|
||||
flat = flat.concat(flattenRoutes(children, route))
|
||||
}
|
||||
})
|
||||
|
||||
return flat
|
||||
}
|
||||
const flatRoutes = flattenRoutes(routers)
|
||||
const selectedRoute = ref('')
|
||||
const handleRouteSelect = (name:any) => {
|
||||
if (name) {
|
||||
router.push({ name })
|
||||
isMenuSearch.value = false
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
<style>
|
||||
:root,
|
||||
body {
|
||||
--layout-header-bg: #fff;
|
||||
--layout-header-height: 44px;
|
||||
--layout-header-text-color: #333;
|
||||
--layout-header-text-hover: #fff;
|
||||
--layout-header-text-selected: rgba(0, 0, 0, .2);
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.layout-header{
|
||||
position: relative;
|
||||
z-index: 5;
|
||||
box-shadow: 0px 0px 4px 0px rgba(0,145,255,0.1);
|
||||
}
|
||||
.oneMenu{
|
||||
position: relative;
|
||||
&::after{
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 2px;
|
||||
height: 3px;
|
||||
background-color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
.navbar-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 32px;
|
||||
height: 32px;
|
||||
padding: 0 5px;
|
||||
border-radius: 4px;
|
||||
box-sizing: border-box;
|
||||
&:hover {
|
||||
background-color: var(--layout-header-text-hover);
|
||||
}
|
||||
}
|
||||
.index-item {
|
||||
border: 1px solid;
|
||||
border-color: var(--el-color-primary);
|
||||
&:hover {
|
||||
color: #fff;
|
||||
background-color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@ -0,0 +1,72 @@
|
||||
<template>
|
||||
<div class="flex">
|
||||
<icon name="element Setting" color="#333" @click="drawer = true" />
|
||||
|
||||
<el-drawer v-model="drawer" :title="t('layout.layoutSetting')" size="300px">
|
||||
<el-scrollbar>
|
||||
<!-- 黑暗模式 -->
|
||||
<div class="setting-item flex items-center justify-between mb-[10px]">
|
||||
<div class="title text-base text-tx-secondary">{{ t('layout.darkMode') }}</div>
|
||||
<div class="">
|
||||
<el-switch v-model="dark" :active-value="true" :inactive-value="false" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 主题颜色 -->
|
||||
<div class="setting-item flex items-center justify-between mb-[10px]">
|
||||
<div class="title text-base text-tx-secondary">{{ t('layout.themeColor') }}</div>
|
||||
<div class="">
|
||||
<el-color-picker v-model="theme" />
|
||||
</div>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</el-drawer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
import { useDark, useToggle } from '@vueuse/core'
|
||||
import { setThemeColor } from '@/utils/common'
|
||||
import { t } from '@/lang'
|
||||
|
||||
const drawer = ref(false)
|
||||
const systemStore = useSystemStore()
|
||||
|
||||
const isDark = useDark()
|
||||
const toggleDark = useToggle(isDark)
|
||||
|
||||
const dark = computed({
|
||||
get () {
|
||||
return systemStore.dark
|
||||
},
|
||||
set (val) {
|
||||
systemStore.setTheme('dark', val)
|
||||
toggleDark(val)
|
||||
setThemeColor(systemStore.theme, systemStore.dark ? 'dark' : 'light')
|
||||
}
|
||||
})
|
||||
|
||||
const theme = computed({
|
||||
get () {
|
||||
return systemStore.theme
|
||||
},
|
||||
set (val) {
|
||||
systemStore.setTheme('theme', val)
|
||||
setThemeColor(systemStore.theme, systemStore.dark ? 'dark' : 'light')
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-drawer__header) {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.layout-style {
|
||||
&>div:nth-child(2n+2) {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
32
admin/src/layout/routine/components/header/switch-lang.vue
Normal file
32
admin/src/layout/routine/components/header/switch-lang.vue
Normal file
@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<el-dropdown @command="switchLang" :tabindex="1">
|
||||
<icon name="iconfont-iconfanyi" />
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item command="zh-cn" :disabled="systemStore.lang == 'zh-cn'">简体中文</el-dropdown-item>
|
||||
<el-dropdown-item command="en" :disabled="systemStore.lang == 'en'">English</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import useSystemStore from '@/stores/modules/system'
|
||||
import { language } from '@/lang'
|
||||
import { useRoute } from 'vue-router'
|
||||
import storage from '@/utils/storage'
|
||||
|
||||
const route = useRoute()
|
||||
const systemStore = useSystemStore()
|
||||
|
||||
const switchLang = (command: string) => {
|
||||
systemStore.$patch((state) => {
|
||||
state.lang = command
|
||||
storage.set({ key: 'lang', data: command })
|
||||
})
|
||||
language.loadLocaleMessages(route.path, systemStore.lang)
|
||||
location.reload()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
240
admin/src/layout/routine/components/header/user-info.vue
Normal file
240
admin/src/layout/routine/components/header/user-info.vue
Normal file
@ -0,0 +1,240 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-dropdown @command="clickEvent" :tabindex="1">
|
||||
<div class="userinfo flex h-full items-center">
|
||||
<el-avatar :size="25" :icon="UserFilled" :src="info && info.head_img ? img(info.head_img) : ''"/>
|
||||
<div class="user-name pl-[8px] text-[#333]">{{ userStore.userInfo.username }}</div>
|
||||
<icon name="element ArrowDown" color="#333" class="ml-[5px]" />
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="changeSite">
|
||||
<div class="flex items-center leading-[1] py-[5px]">
|
||||
<span class="iconfont iconqiehuan ml-[4px] !text-[14px] mr-[10px]"></span>
|
||||
<span class="text-[14px]">切换店铺</span>
|
||||
</div>
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="getUserInfoFn">
|
||||
<!-- <router-link to="/user/center"> -->
|
||||
<div class="flex items-center leading-[1] py-[5px]">
|
||||
<span class="iconfont iconshezhi1 ml-[4px] !text-[14px] mr-[10px]"></span>
|
||||
<span class="text-[14px]">账号设置</span>
|
||||
</div>
|
||||
<!-- </router-link> -->
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @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 command="logout">
|
||||
<div class="flex items-center leading-[1] py-[2px]">
|
||||
<span class="iconfont icontuichudenglu !text-[21px] mr-[8px]"></span>
|
||||
<span class="text-[14px]">退出登录</span>
|
||||
</div>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
|
||||
<el-dialog v-model="changePasswordDialog" width="450px" title="修改密码">
|
||||
<div>
|
||||
<el-form :model="saveInfo" label-width="90px" ref="formRef" :rules="formRules" class="page-form">
|
||||
<el-form-item :label="t('originalPassword')" prop="original_password">
|
||||
<el-input v-model="saveInfo.original_password" type="password" :placeholder="t('originalPasswordPlaceholder')" clearable class="input-width" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('newPassword')" prop="password">
|
||||
<el-input v-model="saveInfo.password" type="password" :placeholder="t('passwordPlaceholder')" clearable class="input-width" />
|
||||
<div class="form-tip">{{t('passwordTip')}}</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('passwordCopy')" prop="password_copy">
|
||||
<el-input v-model="saveInfo.password_copy" type="password" :placeholder="t('passwordPlaceholder')" clearable class="input-width" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="changePasswordDialog = false">{{t('cancel')}}</el-button>
|
||||
<el-button type="primary" @click="submitForm(formRef)">{{t('save')}}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<el-dialog v-model="changeSiteDialog" width="1260px" title="切换店铺" append-to-body>
|
||||
<div class="min-h-[540px]">
|
||||
<div class="flex flex-wrap" v-loading="site.loading">
|
||||
<div v-for="(item, index) in site.tableData" :key="index" @click="selectSite(item)" :class="['home-item w-[285px] box-border mb-[20px] cursor-pointer',{'mr-[20px]': index ==0 || (index + 1) % 4 != 0,'border-[1px] border-solid border-[var(--el-color-primary)]': siteInfo.site_id == item.site_id}]">
|
||||
<div class="flex items-center px-[24px] pt-[22px] pb-[16px] bg-[#F0F2F4] home-item-head">
|
||||
<img v-if="item.front_end_logo" class="w-[48px] h-[48px] mr-[15px] rounded-[50%] overflow-hidden" :src="img(item.front_end_logo)" />
|
||||
<img v-else class="w-[48px] h-[48px] mr-[15px] rounded-[50%] overflow-hidden" src="@/app/assets/images/site_logo.png" />
|
||||
<div class="flex flex-col flex-1 justify-center">
|
||||
<div class="flex items-center flex-wrap">
|
||||
<span class="text-[16px] text-[#000] max-w-[145px] font-bold truncate mr-[10px]">{{item.site_name}}</span>
|
||||
<div class="flex items-center justify-center min-w-[42px] h-[18px] bg-[#FF5500] rounded-tl-md rounded-br-md items-tab" v-if="item.app_name">
|
||||
<span class="text-[12px] text-[#fff]">{{item.app_name}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-[12px] mt-[3px] text-[#555]" v-if="item.status !== 1">{{item.status_name}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-[24px] py-[20px] text-[#6D7278]">
|
||||
<p class="text-[14px]">店铺编号:{{item.site_id}}</p>
|
||||
<p class="text-[14px] mt-[2px]">店铺套餐:{{item.group_name || '--'}}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!site.tableData.length && !site.loading" class="m-auto">
|
||||
<img src="@/app/assets/images/site_empty.png"/>
|
||||
<p class="text-center text-gray-400">暂无店铺</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-[16px] flex justify-end">
|
||||
<el-pagination v-model:current-page="site.params.page" v-model:page-size="site.params.limit"
|
||||
layout="total, prev, pager, next, jumper" :total="site.total"
|
||||
@current-change="getHomeSiteFn" :hide-on-single-page="true"/>
|
||||
</div>
|
||||
</el-dialog>
|
||||
<user-info-edit ref="userInfoEditRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { UserFilled } from '@element-plus/icons-vue'
|
||||
import { reactive, ref, computed } from 'vue'
|
||||
import { img } from '@/utils/common'
|
||||
import { RouteLocationRaw, useRouter } from 'vue-router'
|
||||
import { FormInstance, FormRules, ElNotification } from 'element-plus'
|
||||
import userInfoEdit from '@/app/components/user-info-edit/index.vue'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
import { setUserInfo } from '@/app/api/personal'
|
||||
import { getHomeSite } from '@/app/api/home'
|
||||
import { t } from '@/lang'
|
||||
import storage from '@/utils/storage'
|
||||
const userStore = useUserStore()
|
||||
|
||||
const siteInfo = userStore.siteInfo
|
||||
const router = useRouter()
|
||||
|
||||
const clickEvent = (command: string) => {
|
||||
switch (command) {
|
||||
case 'logout':
|
||||
userStore.logout()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const userInfoEditRef = ref(null)
|
||||
const getUserInfoFn = () => {
|
||||
userInfoEditRef.value?.open()
|
||||
}
|
||||
|
||||
const info = computed(() => {
|
||||
return userInfoEditRef.value?.saveInfo
|
||||
})
|
||||
|
||||
// 修改密码 --- start
|
||||
const changePasswordDialog = ref(false)
|
||||
const formRef = ref<FormInstance>()
|
||||
// 提交信息
|
||||
const saveInfo = reactive({
|
||||
original_password: '',
|
||||
password: '',
|
||||
password_copy: ''
|
||||
})
|
||||
// 表单验证规则
|
||||
const formRules = reactive<FormRules>({
|
||||
original_password: [
|
||||
{ required: true, message: t('originalPasswordPlaceholder'), trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: t('passwordPlaceholder'), trigger: 'blur' }
|
||||
],
|
||||
password_copy: [
|
||||
{ required: true, message: t('passwordPlaceholder'), trigger: 'blur' }
|
||||
]
|
||||
})
|
||||
const submitForm = (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
formEl.validate((valid) => {
|
||||
if (valid) {
|
||||
let msg = ''
|
||||
if (saveInfo.password && !saveInfo.original_password) msg = t('originalPasswordHint')
|
||||
if (saveInfo.password && saveInfo.original_password && !saveInfo.password_copy) msg = t('newPasswordHint')
|
||||
if (saveInfo.password && saveInfo.original_password && saveInfo.password_copy && saveInfo.password != saveInfo.password_copy) msg = t('doubleCipherHint')
|
||||
if (msg) {
|
||||
ElNotification({
|
||||
type: 'error',
|
||||
message: msg
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
setUserInfo(saveInfo).then((res: any) => {
|
||||
changePasswordDialog.value = false
|
||||
})
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
||||
// 修改密码 --- end
|
||||
// 切换店铺
|
||||
const changeSiteDialog = ref(false)
|
||||
const site = reactive({
|
||||
params: {
|
||||
page: 1,
|
||||
limit: 12
|
||||
},
|
||||
loading: false,
|
||||
tableData: [],
|
||||
total: 0
|
||||
})
|
||||
const getHomeSiteFn = (page: number = 1) => {
|
||||
site.params.page = page
|
||||
site.loading = true
|
||||
getHomeSite(site.params).then(res => {
|
||||
site.tableData = res.data.data
|
||||
site.total = res.data.total
|
||||
site.loading = false
|
||||
}).catch(() => {
|
||||
site.loading = false
|
||||
})
|
||||
}
|
||||
const changeSite = () => {
|
||||
getHomeSiteFn()
|
||||
changeSiteDialog.value = true
|
||||
}
|
||||
const selectSite = (site: any) => {
|
||||
storage.set({ key: 'siteId', data: site.site_id })
|
||||
storage.set({ key: 'siteInfo', data: site })
|
||||
storage.set({ key: 'comparisonSiteIdStorage', data: site.site_id })
|
||||
useUserStore().$patch((site) => {
|
||||
site.siteInfo = site
|
||||
})
|
||||
location.href = `${location.origin}/site/`
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.el-popper .el-dropdown-menu{
|
||||
width: 150px;
|
||||
}
|
||||
.home-item{
|
||||
box-shadow: 0 2px 4px 0 rgba(161,167,183,0.18);
|
||||
.items-tab span{
|
||||
transform: scale(0.9);
|
||||
}
|
||||
}
|
||||
.home-item:hover {
|
||||
border-color: var(--el-color-primary);
|
||||
.title {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
.home-item-head{
|
||||
background-color: #A1A7B7;
|
||||
span{
|
||||
color: #fff !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
137
admin/src/layout/routine/components/tabs.vue
Normal file
137
admin/src/layout/routine/components/tabs.vue
Normal file
@ -0,0 +1,137 @@
|
||||
<template>
|
||||
<div class="tab-wrap w-full px-[16px]">
|
||||
<el-tabs :closable="tabbarStore.tabLength > 1" :model-value="route.path" @tab-click="tabClick"
|
||||
@tab-remove="removeTab">
|
||||
<el-tab-pane v-for="(tab, key, index) in tabbarStore.tabs" :name="tab.path" :key="index">
|
||||
<template #label>
|
||||
<el-dropdown trigger="contextmenu" placement="bottom-start">
|
||||
<span :class="{ 'text-primary': route.path == tab.path }" class="tab-name">{{ tab.title }}</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item icon="Back" :disabled="index == 0" @click="closeLeft(tab.path)">{{t('tabs.closeLeft') }}</el-dropdown-item>
|
||||
<el-dropdown-item icon="Right" :disabled="index == (tabbarStore.tabLength - 1)" @click="closeRight(tab.path)">{{t('tabs.closeRight') }}</el-dropdown-item>
|
||||
<el-dropdown-item icon="Close" :disabled="tabbarStore.tabLength == 1" @click="closeOther(tab.path)">{{t('tabs.closeOther') }}</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { watch, onMounted } from 'vue'
|
||||
import useTabbarStore from '@/stores/modules/tabbar'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { t } from '@/lang'
|
||||
|
||||
const tabbarStore = useTabbarStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
onMounted(() => {
|
||||
tabbarStore.addTab(route)
|
||||
})
|
||||
|
||||
watch(route, (nval: any) => {
|
||||
tabbarStore.addTab(nval)
|
||||
})
|
||||
|
||||
/**
|
||||
* 添加tab
|
||||
* @param content
|
||||
*/
|
||||
const tabClick = (content: any) => {
|
||||
const tabRoute = tabbarStore.tabs[content.props.name]
|
||||
router.push({ path: tabRoute.path, query: tabRoute.query })
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除tab
|
||||
* @param content
|
||||
*/
|
||||
const removeTab = (content: any) => {
|
||||
if (route.path == content) {
|
||||
const tabs = Object.keys(tabbarStore.tabs)
|
||||
router.push({ path: tabs[tabs.indexOf(content) - 1] })
|
||||
}
|
||||
tabbarStore.removeTab(content)
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭左侧
|
||||
* @param path
|
||||
*/
|
||||
const closeLeft = (path: string) => {
|
||||
const tabs = Object.keys(tabbarStore.tabs)
|
||||
for (let i = tabs.indexOf(path) - 1; i >= 0; i--) {
|
||||
delete tabbarStore.tabs[tabs[i]]
|
||||
}
|
||||
router.push({ path })
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭右侧
|
||||
* @param path
|
||||
*/
|
||||
const closeRight = (path: string) => {
|
||||
const tabs = Object.keys(tabbarStore.tabs)
|
||||
for (let i = tabs.indexOf(path) + 1; i < tabs.length; i++) {
|
||||
delete tabbarStore.tabs[tabs[i]]
|
||||
}
|
||||
router.push({ path })
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭其他
|
||||
* @param path
|
||||
*/
|
||||
const closeOther = (path: string) => {
|
||||
const tabs = Object.keys(tabbarStore.tabs)
|
||||
tabs.forEach((key: string) => { key != path && delete tabbarStore.tabs[key] })
|
||||
router.push({ path })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-tabs) {
|
||||
.el-tabs--border-card {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.el-tabs__header {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.el-tabs__nav-wrap {
|
||||
margin-bottom: 0;
|
||||
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.el-tabs__content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.el-tabs__item {
|
||||
display: inline-flex !important;
|
||||
padding: 0 20px !important;
|
||||
align-items: center;
|
||||
|
||||
.tab-name:focus {
|
||||
outline: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.el-tabs__active-bar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.el-tabs__item.is-active {
|
||||
background-color: var(--el-color-primary-light-9);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
37
admin/src/layout/routine/index.vue
Normal file
37
admin/src/layout/routine/index.vue
Normal file
@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<div class="min-w-[1500px]">
|
||||
<el-header>
|
||||
<layout-header></layout-header>
|
||||
</el-header>
|
||||
<el-container>
|
||||
<layout-aside class="h-[calc(100vh-60px)]"></layout-aside>
|
||||
<el-main class="main-wrap h-[calc(100vh-60px)] p-0 bg-page relative">
|
||||
<el-scrollbar>
|
||||
<div class="p-[10px]">
|
||||
<router-view v-slot="{ Component, route }" v-if="appStore.routeRefreshTag ">
|
||||
<keep-alive :include="tabbarStore.tabNames">
|
||||
<component :is="Component" :key="route.fullPath"/>
|
||||
</keep-alive>
|
||||
</router-view>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import layoutHeader from './components/header/index.vue'
|
||||
import layoutAside from './components/aside/index.vue'
|
||||
import useAppStore from '@/stores/modules/app'
|
||||
import useTabbarStore from '@/stores/modules/tabbar'
|
||||
|
||||
const appStore = useAppStore()
|
||||
const tabbarStore = useTabbarStore()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-header){
|
||||
--el-header-height: 60px !important;
|
||||
}
|
||||
</style>
|
||||
4
admin/src/layout/routine/layout.json
Normal file
4
admin/src/layout/routine/layout.json
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
"layout": "routine",
|
||||
"cover": "/static/resource/images/system/layout_routine.png"
|
||||
}
|
||||
@ -196,13 +196,14 @@ export function findFirstValidRoute(routes: RouteRecordRaw[]): string | undefine
|
||||
* @param rules
|
||||
*/
|
||||
export function findRules(routes: Route[], rules :string[] = []) : string[] {
|
||||
let resultRules = [...rules]
|
||||
for (const route of routes) {
|
||||
if (route.auth && Array.isArray(route.auth)) {
|
||||
rules = rules.concat(route.auth)
|
||||
resultRules = resultRules.concat(route.auth)
|
||||
}
|
||||
if (route.children) {
|
||||
rules = findRules(route.children, rules)
|
||||
resultRules = findRules(route.children, resultRules)
|
||||
}
|
||||
}
|
||||
return rules
|
||||
return resultRules
|
||||
}
|
||||
|
||||
@ -52,12 +52,14 @@ const useSystemStore = defineStore('system', {
|
||||
async getWebsiteInfo() {
|
||||
await getWebConfig().then(({ data }) => {
|
||||
this.website = data
|
||||
}).catch()
|
||||
}).catch(() => {
|
||||
})
|
||||
},
|
||||
async getWebsiteLayout() {
|
||||
await getWebsiteLayout().then(({ data }) => {
|
||||
this.layoutConfig = data
|
||||
}).catch()
|
||||
}).catch(() => {
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@ -17,18 +17,18 @@ const useTabbarStore = defineStore('tabbar', {
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
addTab(roter: RouteLocationNormalizedLoaded) {
|
||||
if (roter.meta && roter.meta.type != 1) return
|
||||
if (this.tabs[roter.name]) {
|
||||
this.tabs[roter.name].query = roter.query || {}
|
||||
addTab(router: RouteLocationNormalizedLoaded) {
|
||||
if (router.meta && router.meta.type != 1) return
|
||||
if (this.tabs[router.name]) {
|
||||
this.tabs[router.name].query = router.query || {}
|
||||
return
|
||||
}
|
||||
this.tabs[roter.name] = {
|
||||
path: roter.path,
|
||||
title: roter.meta ? roter.meta.title : '',
|
||||
name: roter.name,
|
||||
query: roter.query || {},
|
||||
compomentName: roter.matched.at(-1).components.default.__name
|
||||
this.tabs[router.name] = {
|
||||
path: router.path,
|
||||
title: router.meta ? router.meta.title : '',
|
||||
name: router.name,
|
||||
query: router.query || {},
|
||||
componentName: router.matched.at(-1)?.components.default.__name
|
||||
}
|
||||
},
|
||||
removeTab(path: string) {
|
||||
@ -44,7 +44,7 @@ const useTabbarStore = defineStore('tabbar', {
|
||||
const name: any[] = []
|
||||
if (!useSystemStore().tab) return name
|
||||
Object.keys(state.tabs).forEach(key => {
|
||||
name.push(state.tabs[key].compomentName)
|
||||
name.push(state.tabs[key].componentName)
|
||||
})
|
||||
return name
|
||||
}
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
@font-face {
|
||||
font-family: "iconfont";
|
||||
/* Project id 3883393 */
|
||||
src: url('//at.alicdn.com/t/c/font_3883393_0604cbkh5j03.woff2?t=1762651161569') format('woff2'),
|
||||
url('//at.alicdn.com/t/c/font_3883393_0604cbkh5j03.woff?t=1762651161569') format('woff'),
|
||||
url('//at.alicdn.com/t/c/font_3883393_0604cbkh5j03.ttf?t=1762651161569') format('truetype');
|
||||
font-family: "iconfont"; /* Project id 3883393 */
|
||||
src: url('//at.alicdn.com/t/c/font_3883393_jquqyagjvf.woff2?t=1773643077865') format('woff2'),
|
||||
url('//at.alicdn.com/t/c/font_3883393_jquqyagjvf.woff?t=1773643077865') format('woff'),
|
||||
url('//at.alicdn.com/t/c/font_3883393_jquqyagjvf.ttf?t=1773643077865') format('truetype');
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
@ -14,6 +13,18 @@
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.iconjiang-right:before {
|
||||
content: "\e921";
|
||||
}
|
||||
|
||||
.iconpeisongshezhi:before {
|
||||
content: "\e920";
|
||||
}
|
||||
|
||||
.iconai:before {
|
||||
content: "\e91f";
|
||||
}
|
||||
|
||||
.icona-zhulixiangqingpc30:before {
|
||||
content: "\e914";
|
||||
}
|
||||
@ -3276,4 +3287,4 @@
|
||||
|
||||
.iconquanping:before {
|
||||
content: "\eb11";
|
||||
}
|
||||
}
|
||||
|
||||
@ -127,13 +127,14 @@ export function isUrl(str: string): boolean {
|
||||
* @returns
|
||||
*/
|
||||
export function img(path: string): string {
|
||||
if (!path) return ''
|
||||
|
||||
let imgDomain = import.meta.env.VITE_IMG_DOMAIN || location.origin
|
||||
|
||||
if (typeof path == 'string' && path.startsWith('/')) path = path.replace(/^\//, '')
|
||||
if (typeof imgDomain == 'string' && imgDomain.endsWith('/')) imgDomain = imgDomain.slice(0, -1)
|
||||
if(path){
|
||||
return isUrl(path) ? path : `${imgDomain}/${path}`
|
||||
}
|
||||
if (path.startsWith('/')) path = path.replace(/^\//, '')
|
||||
if (imgDomain.endsWith('/')) imgDomain = imgDomain.slice(0, -1)
|
||||
|
||||
return isUrl(path) ? path : `${imgDomain}/${path}`
|
||||
}
|
||||
|
||||
/**
|
||||
@ -409,4 +410,29 @@ export function distance(distance: string | number): string {
|
||||
const dist = typeof distance === 'string' ? parseFloat(distance) : distance;
|
||||
if (isNaN(dist)) return distance.toString();
|
||||
return dist < 1 ? parseInt((dist * 1000).toString()) + 'm' : dist.toFixed(1) + 'km'
|
||||
}
|
||||
|
||||
// 获取图片尺寸的函数
|
||||
export function getImageDimensions (url: string) {
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const imgObj = new Image()
|
||||
imgObj.onload = () => {
|
||||
// 成功加载
|
||||
const size = {
|
||||
width: imgObj.naturalWidth,
|
||||
height: imgObj.naturalHeight
|
||||
}
|
||||
resolve(size)
|
||||
}
|
||||
|
||||
imgObj.onerror = (err) => {
|
||||
// 加载失败
|
||||
resolve(null)
|
||||
}
|
||||
|
||||
// 设置图片源,开始加载
|
||||
// 注意:如果图片跨域且服务器未设置CORS,可能会触发onerror
|
||||
imgObj.src = img(url)
|
||||
})
|
||||
}
|
||||
@ -146,6 +146,9 @@ export const deleteGeometry = (key: string) => {
|
||||
* @param key
|
||||
*/
|
||||
export const selectGeometry = (key: string) => {
|
||||
if (!geometry[key] || !geometry[key].editor) {
|
||||
return
|
||||
}
|
||||
geometry[key].editor.select([key])
|
||||
}
|
||||
|
||||
|
||||
@ -183,15 +183,27 @@ class Request {
|
||||
}
|
||||
|
||||
private messageCache = new Map();
|
||||
private readonly CACHE_EXPIRY = 5000; // 5秒内重复内容不再弹出,可自定义过期时间
|
||||
private readonly MAX_CACHE_SIZE = 100;
|
||||
|
||||
private showElMessage(options: MessageParams) {
|
||||
const cacheKey = options.message
|
||||
const now = Date.now()
|
||||
const cachedMessage = this.messageCache.get(cacheKey);
|
||||
|
||||
if (!cachedMessage || Date.now() - cachedMessage.timestamp > 5000) { // 5秒内重复内容不再弹出,可自定义过期时间
|
||||
this.messageCache.set(cacheKey, { timestamp: Date.now() });
|
||||
if (!cachedMessage || now - cachedMessage.timestamp > this.CACHE_EXPIRY) {
|
||||
this.messageCache.set(cacheKey, { timestamp: now });
|
||||
ElMessage(options)
|
||||
}
|
||||
|
||||
// 定期清理过期缓存,防止内存泄漏
|
||||
if (this.messageCache.size > this.MAX_CACHE_SIZE) {
|
||||
for (const [key, value] of this.messageCache.entries()) {
|
||||
if (now - value.timestamp > this.CACHE_EXPIRY) {
|
||||
this.messageCache.delete(key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -38,10 +38,12 @@ class Storage {
|
||||
*/
|
||||
public get(key: string) {
|
||||
try {
|
||||
const json: any = window.localStorage.getItem(`${this.prefix}.${key}`)
|
||||
return JSON.parse(json)
|
||||
const value = window.localStorage.getItem(`${this.prefix}.${key}`)
|
||||
if (value === null) return null
|
||||
return JSON.parse(value)
|
||||
} catch (error) {
|
||||
return window.localStorage.getItem(`${this.prefix}.${key}`)
|
||||
const value = window.localStorage.getItem(`${this.prefix}.${key}`)
|
||||
return value !== null ? value : null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user