同步admin

This commit is contained in:
CQ 2026-03-19 14:18:25 +08:00
parent 6f1167fe29
commit b984a4c81c
68 changed files with 7484 additions and 2966 deletions

View File

@ -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 })
}
/**

View File

@ -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 })
}
/**

View File

@ -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`)
}

View File

@ -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`)
}

View File

@ -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 })
}

View File

@ -8,6 +8,14 @@ export function getWeappConfig() {
return request.get('weapp/config')
}
/**
*
* @returns
*/
export function getWeappAuthUrl() {
return request.get('weapp/auth_url')
}
/**
*
* @param params

View File

@ -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, () => {

View File

@ -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

View File

@ -17,7 +17,7 @@
"mobileOrUsernameNoEmpty": "普通注册方式至少需启用一种",
"loginPageSet": "界面设置",
"bgUrl": "背景图",
"bgUrlPlaceholder": "建议图片尺寸750*669像素图片格式jpg、png、jpeg",
"bgUrlPlaceholder": "前台快捷登录/注册页的背景图,建议图片尺寸750*669像素图片格式jpg、png、jpeg",
"desc": "描述",
"descPlaceholder": "请输入描述"
}

View File

@ -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": "站点登录背景图",

View File

@ -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 = () => {

View File

@ -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>

View File

@ -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

View File

@ -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();

View File

@ -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')))
}

View File

@ -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')))
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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 })

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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]">

View File

@ -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) => {

View File

@ -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]

View File

@ -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 })

View File

@ -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)
}

View File

@ -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)

View File

@ -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">

View File

@ -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>

View File

@ -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">

View File

@ -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 });
}

View File

@ -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>

View File

@ -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">

View 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>

View 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>

View 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>

View 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 = show1 && show1
// 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>

View File

@ -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>

View File

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

View File

@ -0,0 +1,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>

View File

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

View File

@ -0,0 +1,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>

View File

@ -0,0 +1,4 @@
{
"layout": "lightside",
"cover": "/static/resource/images/system/layout_lightside.png"
}

View File

@ -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)

View File

@ -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">

View 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>

View 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>

View 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>

View 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>

View File

@ -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>

View File

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

View File

@ -0,0 +1,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>

View 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>

View 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>

View File

@ -0,0 +1,4 @@
{
"layout": "routine",
"cover": "/static/resource/images/system/layout_routine.png"
}

View File

@ -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
}

View File

@ -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(() => {
})
}
}
})

View File

@ -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
}

View File

@ -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";
}
}

View File

@ -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)
})
}

View File

@ -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])
}

View File

@ -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)
}
}
}
}
}

View File

@ -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
}
}