🧹 清理重复配置文件
- 删除根目录中重复的 NestJS 配置文件 - 删除 tsconfig.json, tsconfig.build.json, eslint.config.mjs, .prettierrc - 保留 wwjcloud-nest/ 目录中的完整配置 - 避免配置冲突,确保项目结构清晰
This commit is contained in:
177
admin-vben/src/app/views/app/authorize.vue
Normal file
177
admin-vben/src/app/views/app/authorize.vue
Normal file
@@ -0,0 +1,177 @@
|
||||
<template>
|
||||
<!--授权信息-->
|
||||
<div class="main-container">
|
||||
<el-card class="box-card !border-none min-h-[300px]" shadow="never" v-loading="loading">
|
||||
<div v-if="!loading">
|
||||
<div class="title text-[16px] font-bold text-[#1D1F3A] mb-[30px]">授权信息</div>
|
||||
<div>
|
||||
<div class="flex items-center">
|
||||
<div class="w-[92px] h-[92px] rounded-[10px] flex justify-center items-center mr-[20px]">
|
||||
<img src="@/app/assets/images/tools/authorize.png" class="w-[92px] h-[92px]" />
|
||||
</div>
|
||||
<div class="flex flex-col justify-between font-500">
|
||||
<div class="flex flex-wrap items-center mb-[12px]">
|
||||
<span class="mr-[6px] text-[14px] text-[#666666] w-[70px] text-left">授权公司:</span>
|
||||
<span class="text-[14px] text-[#333]">{{ authinfo.company_name || "--" }}</span>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center mb-[12px]">
|
||||
<span class="mr-[6px] text-[14px] text-[#666666] w-[70px] text-left">授权域名:</span>
|
||||
<span class="text-[14px] text-[#333]">{{ authinfo.site_address || "--" }}</span>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center">
|
||||
<span class="mr-[6px] text-[14px] text-[#666666] w-[70px] text-left">授权码:</span>
|
||||
<span class="text-[14px] text-[#333]">
|
||||
<span class="mr-[10px]">{{ authinfo.auth_code ? (isCheck ? authinfo.auth_code : hideAuthCode(authinfo.auth_code)) : "--" }}</span>
|
||||
<el-icon v-if="!isCheck" @click="isCheck = !isCheck" class="text-[14px] cursor-pointer text-[#9699B6]">
|
||||
<View />
|
||||
</el-icon>
|
||||
<el-icon v-else @click="isCheck = !isCheck" class="text-[14px] cursor-pointer text-[#9699B6]"> <Hide /> </el-icon>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-[17px] ml-[110px]">
|
||||
<el-button class="!w-[140px] !h-[32px] mt-[8px] !rounded-[4px]" type="primary" @click="authCodeApproveFn">授权码认证</el-button>
|
||||
<el-popover ref="getAuthCodeDialog" placement="bottom-start" :width="478" trigger="click" class="mt-[8px]">
|
||||
<div class="px-[18px] py-[8px]">
|
||||
<p class="leading-[32px] text-[14px]">您在官方应用市场购买任意一款应用,即可获得授权码。输入正确授权码认证通过后,即可支持在线升级和其它相关服务</p>
|
||||
<div class="flex justify-end mt-[36px]">
|
||||
<el-button class="w-[182px] !h-[48px]" plain @click="market">去应用市场逛逛</el-button>
|
||||
<el-button class="w-[100px] !h-[48px]" plain @click="getAuthCodeDialog.hide()">关闭</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<template #reference>
|
||||
<el-button class="!w-[140px] !h-[32px] mt-[8px] !rounded-[4px] !text-[var(--el-color-primary)] hover:!text-[var(--el-color-primary)] !bg-transparent" plain type="primary">如何获取授权码?</el-button>
|
||||
</template>
|
||||
</el-popover>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="authCodeApproveDialog" title="授权码认证" width="400px">
|
||||
<el-form :model="formData" label-width="0" ref="formRef" :rules="formRules" class="page-form">
|
||||
<el-card class="box-card !border-none" shadow="never">
|
||||
<el-form-item prop="auth_code">
|
||||
<el-input v-model.trim="formData.auth_code" :placeholder="t('authCodePlaceholder')" class="input-width" clearable size="large" />
|
||||
</el-form-item>
|
||||
|
||||
<div class="mt-[20px]">
|
||||
<el-form-item prop="auth_secret">
|
||||
<el-input v-model.trim="formData.auth_secret" clearable :placeholder="t('authSecretPlaceholder')" class="input-width" size="large" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="text-sm mt-[10px] text-info">{{ t("authInfoTips") }}</div>
|
||||
|
||||
<div class="mt-[20px]">
|
||||
<el-button type="primary" class="w-full" size="large" :loading="saveLoading" @click="save(formRef)">{{ t("confirm") }}</el-button>
|
||||
</div>
|
||||
<div class="mt-[10px] text-right">
|
||||
<el-button type="primary" link @click="market">{{ t("notHaveAuth") }}</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-form>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from "vue"
|
||||
import { t } from "@/lang"
|
||||
import { getVersions } from "@/app/api/auth"
|
||||
import { getAuthInfo, setAuthInfo } from "@/app/api/module"
|
||||
import { FormInstance, FormRules } from "element-plus"
|
||||
import { cloneDeep } from "lodash-es"
|
||||
|
||||
const getAuthCodeDialog: Record<string, any> | null = ref(null)
|
||||
const authCodeApproveDialog = ref(false)
|
||||
const isCheck = ref(false)
|
||||
|
||||
const hideAuthCode = (res: any) => {
|
||||
const authCode = cloneDeep(res)
|
||||
const data = authCode.slice(0, authCode.length / 2) + authCode.slice(authCode.length / 2, authCode.length - 1).replace(/./g, "*")
|
||||
return data
|
||||
}
|
||||
|
||||
const authCodeApproveFn = () => {
|
||||
authCodeApproveDialog.value = true
|
||||
}
|
||||
|
||||
interface AuthInfo {
|
||||
company_name: string
|
||||
site_address: string
|
||||
auth_code: string
|
||||
}
|
||||
|
||||
const authinfo = ref<AuthInfo>({
|
||||
company_name: "",
|
||||
site_address: "",
|
||||
auth_code: ""
|
||||
})
|
||||
const loading = ref(true)
|
||||
const saveLoading = ref(false)
|
||||
const checkAppMange = () => {
|
||||
getAuthInfo().then((res) => {
|
||||
loading.value = false
|
||||
if (res.data.data && res.data.data.length != 0) {
|
||||
authinfo.value = res.data.data
|
||||
authCodeApproveDialog.value = false
|
||||
}
|
||||
}).catch(() => {
|
||||
loading.value = false
|
||||
authCodeApproveDialog.value = false
|
||||
})
|
||||
}
|
||||
|
||||
checkAppMange()
|
||||
|
||||
const formData = reactive<Record<string, string>>({
|
||||
auth_code: "",
|
||||
auth_secret: ""
|
||||
})
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = reactive<FormRules>({
|
||||
auth_code: [{ required: true, message: t("authCodePlaceholder"), trigger: "blur" }],
|
||||
auth_secret: [{ required: true, message: t("authSecretPlaceholder"), trigger: "blur" }]
|
||||
})
|
||||
|
||||
const save = async(formEl: FormInstance | undefined) => {
|
||||
if (saveLoading.value || !formEl) return
|
||||
|
||||
await formEl.validate(async(valid) => {
|
||||
if (valid) {
|
||||
saveLoading.value = true
|
||||
|
||||
setAuthInfo(formData).then(() => {
|
||||
saveLoading.value = false
|
||||
checkAppMange()
|
||||
}).catch(() => {
|
||||
saveLoading.value = false
|
||||
authCodeApproveDialog.value = false
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const market = () => {
|
||||
window.open("https://www.niucloud.com/app")
|
||||
}
|
||||
|
||||
const versions = ref("")
|
||||
const getVersionsInfo = () => {
|
||||
getVersions().then((res) => {
|
||||
versions.value = res.data.version.version
|
||||
})
|
||||
}
|
||||
getVersionsInfo()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-button){
|
||||
border-radius: 4px !important;
|
||||
}
|
||||
</style>
|
||||
92
admin-vben/src/app/views/app/index.vue
Normal file
92
admin-vben/src/app/views/app/index.vue
Normal file
@@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<!--应用管理-->
|
||||
<div class="main-container" v-loading="loading">
|
||||
<el-card class="box-card !border-none" shadow="never">
|
||||
|
||||
<template v-if="Object.keys(appList).length">
|
||||
|
||||
<template v-for="(item, index) in appList" :key="index + 'b'">
|
||||
<div class="flex justify-between items-center" v-if="item.list.length">
|
||||
<span class="text-page-title">{{ item.title }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap plug-list pb-10 plug-large" v-if="item.list.length">
|
||||
<div class="cursor-pointer mt-[20px] mr-4 bg-[#f7f7f7]" v-for="(childItem,childIndex) in item.list" :key="childIndex" @click="toLink(childItem)">
|
||||
<div class="w-[264px] flex py-[20px] px-[17px] app-item relative">
|
||||
<el-image class="w-[40px] h-[40px] mr-[10px]" :src="img(childItem.icon)" fit="contain">
|
||||
<template #error>
|
||||
<div class="image-slot">
|
||||
<img class="w-[40px] h-[40px]" src="@/app/assets/images/index/app_default.png" />
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
<div class="flex flex-col justify-between w-[180px]">
|
||||
<div class="text-[14px] flex items-center">
|
||||
<span class="app-text max-w-[170px]">{{ childItem.title }}</span>
|
||||
<!-- <span class="iconfont iconxiaochengxu2 text-[#00b240] ml-[4px] !text-[14px]"></span>-->
|
||||
</div>
|
||||
<!-- <el-icon color="#666">
|
||||
<QuestionFilled />
|
||||
</el-icon> -->
|
||||
<p class="app-text text-[12px] text-[#999]">{{childItem.desc}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<div class="empty flex items-center justify-center" v-if="!loading && !Object.keys(appList).length">
|
||||
<el-empty :description="t('emptyAppData')" />
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import { getSiteAddons, getShowApp } from '@/app/api/site'
|
||||
import { img } from '@/utils/common'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { t } from '@/lang'
|
||||
|
||||
const addonIndexRoute = useUserStore().addonIndexRoute
|
||||
const router = useRouter()
|
||||
const appList = ref<Record<string, any>[]>([])
|
||||
|
||||
const loading = ref(true)
|
||||
const getAppList = async () => {
|
||||
// const res = await getSiteAddons()
|
||||
// appList.value = res.data
|
||||
// loading.value = false
|
||||
|
||||
const res = await getShowApp()
|
||||
appList.value = res.data
|
||||
loading.value = false
|
||||
}
|
||||
getAppList()
|
||||
|
||||
const toLink = (item: any) => {
|
||||
if (item.url) {
|
||||
router.push(item.url)
|
||||
} else {
|
||||
addonIndexRoute[item.key] && router.push({ name: addonIndexRoute[item.key] })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.app-text {
|
||||
overflow: hidden;
|
||||
/* 超出部分隐藏 */
|
||||
white-space: nowrap;
|
||||
/* 禁止文本换行 */
|
||||
text-overflow: ellipsis;
|
||||
/* 显示省略号 */
|
||||
}
|
||||
.app-item:hover{
|
||||
transition: 0.5s;
|
||||
box-shadow: 0 2px 8px 0 rgba(0,0,0,0.1);
|
||||
}
|
||||
</style>
|
||||
94
admin-vben/src/app/views/app/marketing.vue
Normal file
94
admin-vben/src/app/views/app/marketing.vue
Normal file
@@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<!--营销管理-->
|
||||
<div class="main-container" v-loading="loading">
|
||||
<el-card class="box-card !border-none" shadow="never">
|
||||
|
||||
<template v-if="Object.keys(marketingList).length">
|
||||
|
||||
<template v-for="(item, index) in marketingList" :key="index + 'b'">
|
||||
<div class="flex justify-between items-center" v-if="item.list.length > 0">
|
||||
<span class="text-page-title">{{ item.title }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap plug-list pb-10 plug-large">
|
||||
<div class="cursor-pointer mt-[20px] mr-4 bg-[#f7f7f7]" v-for="(childItem,childIndex) in item.list" :key="childIndex" @click="toLink(childItem)">
|
||||
<div class="w-[264px] flex py-[20px] px-[17px] app-item relative">
|
||||
<el-image class="w-[40px] h-[40px] mr-[10px] rounded-[6px] overflow-hidden" :src="img(childItem.icon)" fit="contain">
|
||||
<template #error>
|
||||
<div class="image-slot">
|
||||
<img class="w-[40px] h-[40px]" src="@/app/assets/images/index/app_default.png" />
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
<div class="flex flex-col justify-between w-[180px]">
|
||||
<div class="text-[14px] flex items-center">
|
||||
<span class="app-text max-w-[170px]">{{ childItem.title }}</span>
|
||||
<!-- <span class="iconfont iconxiaochengxu2 text-[#00b240] ml-[4px] !text-[14px]"></span>-->
|
||||
</div>
|
||||
<!-- <el-icon color="#666">
|
||||
<QuestionFilled />
|
||||
</el-icon> -->
|
||||
<p class="app-text text-[12px] text-[#999]">{{childItem.desc}}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<div class="empty flex items-center justify-center" v-if="!loading && !Object.keys(marketingList).length">
|
||||
<el-empty :description="t('emptyAppData')" />
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import { getShowMarketing } from '@/app/api/site'
|
||||
import { img } from '@/utils/common'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { t } from '@/lang'
|
||||
|
||||
const addonIndexRoute = useUserStore().addonIndexRoute
|
||||
const router = useRouter()
|
||||
const marketingList = ref<Record<string, any>[]>([])
|
||||
|
||||
const loading = ref(true)
|
||||
const getMarketingList = async () => {
|
||||
const res = await getShowMarketing()
|
||||
marketingList.value = res.data
|
||||
loading.value = false
|
||||
}
|
||||
getMarketingList()
|
||||
|
||||
const toLink = (item: any) => {
|
||||
if (item.url) {
|
||||
// 判断如果携带is_target=true就通过新窗口打开
|
||||
if (item.url.indexOf('is_target=true') != -1) {
|
||||
const url = router.resolve(item.url)
|
||||
window.open(url.href)
|
||||
} else {
|
||||
router.push(item.url)
|
||||
}
|
||||
} else {
|
||||
addonIndexRoute[item.key] && router.push({ name: addonIndexRoute[item.key] })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.app-text {
|
||||
overflow: hidden;
|
||||
/* 超出部分隐藏 */
|
||||
white-space: nowrap;
|
||||
/* 禁止文本换行 */
|
||||
text-overflow: ellipsis;
|
||||
/* 显示省略号 */
|
||||
}
|
||||
.app-item:hover{
|
||||
transition: 0.5s;
|
||||
box-shadow: 0 2px 8px 0 rgba(0,0,0,0.1);
|
||||
}
|
||||
</style>
|
||||
278
admin-vben/src/app/views/app/upgrade.vue
Normal file
278
admin-vben/src/app/views/app/upgrade.vue
Normal file
@@ -0,0 +1,278 @@
|
||||
<template>
|
||||
<!--授权信息-->
|
||||
<div class="main-container">
|
||||
<el-card class="box-card !border-none min-h-[500px]" shadow="never" v-loading="loadingVersion">
|
||||
<div v-if="!loadingVersion">
|
||||
<div class="mb-[30px]" v-if="newVersion">
|
||||
<div class="title text-[16px] font-bold text-[#1D1F3A] mb-[20px]">版本信息</div>
|
||||
<div class="text-[14px] text-[#1D1F3A] mb-[20px]"><span>系统当前版本:</span><span class="font-bold ">v{{ version }}</span> </div>
|
||||
<div class="flex">
|
||||
<div class="w-[92px] h-[92px] rounded-[10px] flex justify-center items-center mr-[20px]">
|
||||
<img src="@/app/assets/images/tools/upgrade.png" class="w-[92px] h-[92px]" />
|
||||
</div>
|
||||
<div class="flex flex-col justify-between items-start">
|
||||
<div class="text-[14px] text-[#1D1F3A]">系统最新版本为</div>
|
||||
<div class="text-[14px] text-[#1D1F3A] font-bold">v{{ newVersion.version_no }}({{ versionCode }})</div>
|
||||
<div class="text-[#9699B6] text-[16px]" v-if="!shouldShowUpgradeButton">
|
||||
<span>已是最新</span>
|
||||
</div>
|
||||
<div v-else>
|
||||
<el-button class="w-[102px] !h-[32px]" type="primary" :loading="loading" @click="handleUpgrade" v-if="!(!newVersion || (newVersion && newVersion.version_no == version))">一键升级</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-title bg-[#F4F5F7] border-[#E6E6E6] border-solid border-b-[1px] h-[40px] flex items-center p-[10px]">
|
||||
<span class="text-[14px] font-500 text-[#1D1F3A]">升级记录</span>
|
||||
</div>
|
||||
<div >
|
||||
<div class="time-dialog" style="overflow: auto">
|
||||
<el-scrollbar>
|
||||
<el-timeline style="width: 100%">
|
||||
<el-timeline-item v-for="(item, index) in frameworkVersionList" :key="index" placement="left" :hollow="true">
|
||||
<el-collapse v-model="activeName" accordion>
|
||||
<el-collapse-item :name="index">
|
||||
<template #title>
|
||||
<div class="flex justify-between items-start flex-col">
|
||||
<span class="text-[#1D1F3A] text-[14px] leading-[20px]">版本: v{{ item.version_no }}</span>
|
||||
<span class="text-[#9699B6] text-[13px] mt-2">{{ item.release_time }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #icon="{ isActive }">
|
||||
<div class="ml-auto text-[#374151] flex items-center">
|
||||
<span class="text-[#374151] text-[14px]">{{ isActive ? '收起' : '更新内容' }}</span>
|
||||
<span class="iconfont iconjiantouxia ml-[4px] !text-[10px] transition-transform duration-300" v-if="!isActive"></span>
|
||||
<span class="iconfont iconjiantoushang ml-[4px] !text-[10px] transition-transform duration-300" v-if="isActive"></span>
|
||||
</div>
|
||||
</template>
|
||||
<div class="px-[20px] py-[20px] bg-overlay timeline-log-wrap whitespace-pre-wrap rounded-[4px] bg-[#F9F9FB] text-[#4F516D]" v-if="item['upgrade_log']">
|
||||
<div v-html="item['upgrade_log']"></div>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</el-timeline-item>
|
||||
</el-timeline>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-dialog v-model="authCodeApproveDialog" title="授权码认证" width="400px">
|
||||
<el-form :model="formData" label-width="0" ref="formRef" :rules="formRules" class="page-form">
|
||||
<el-card class="box-card !border-none" shadow="never">
|
||||
<el-form-item prop="auth_code">
|
||||
<el-input v-model.trim="formData.auth_code" :placeholder="t('authCodePlaceholder')" class="input-width" clearable size="large" />
|
||||
</el-form-item>
|
||||
|
||||
<div class="mt-[20px]">
|
||||
<el-form-item prop="auth_secret">
|
||||
<el-input v-model.trim="formData.auth_secret" clearable :placeholder="t('authSecretPlaceholder')" class="input-width" size="large" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div class="text-sm mt-[10px] text-info">{{ t("authInfoTips") }}</div>
|
||||
|
||||
<div class="mt-[20px]">
|
||||
<el-button type="primary" class="w-full" size="large" :loading="saveLoading" @click="save(formRef)">{{ t("confirm") }}</el-button>
|
||||
</div>
|
||||
<div class="mt-[10px] text-right">
|
||||
<el-button type="primary" link @click="market">{{ t("notHaveAuth") }}</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-form>
|
||||
</el-dialog>
|
||||
|
||||
<upgrade ref="upgradeRef" />
|
||||
<upgrade-log ref="upgradeLogRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, reactive } from "vue"
|
||||
import { t } from "@/lang"
|
||||
import { getVersions } from "@/app/api/auth"
|
||||
import { getAuthInfo, getFrameworkVersionList, setAuthInfo } from "@/app/api/module"
|
||||
import { ElMessage, FormInstance, FormRules } from "element-plus"
|
||||
import { useRouter } from "vue-router"
|
||||
import Upgrade from "@/app/components/upgrade/index.vue"
|
||||
import UpgradeLog from "@/app/components/upgrade-log/index.vue"
|
||||
|
||||
const upgradeRef = ref<any>(null)
|
||||
const upgradeLogRef = ref<any>(null)
|
||||
const authCodeApproveDialog = ref(false)
|
||||
const frameworkVersionList = ref([])
|
||||
const activeName = ref(0)
|
||||
const checkVersion = ref(false)
|
||||
|
||||
const formData = reactive<Record<string, string>>({
|
||||
auth_code: '',
|
||||
auth_secret: ''
|
||||
})
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = reactive<FormRules>({
|
||||
auth_code: [{ required: true, message: t('authCodePlaceholder'), trigger: 'blur' }],
|
||||
auth_secret: [{ required: true, message: t('authSecretPlaceholder'), trigger: 'blur' }]
|
||||
})
|
||||
|
||||
const saveLoading = ref(false)
|
||||
|
||||
const save = async(formEl: FormInstance | undefined) => {
|
||||
if (saveLoading.value || !formEl) return
|
||||
|
||||
await formEl.validate(async (valid) => {
|
||||
if (valid) {
|
||||
saveLoading.value = true
|
||||
|
||||
setAuthInfo(formData).then(() => {
|
||||
saveLoading.value = false
|
||||
checkAppMange()
|
||||
}).catch(() => {
|
||||
saveLoading.value = false
|
||||
authCodeApproveDialog.value = false
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
const loadingVersion = ref(false)
|
||||
const getFrameworkVersionListFn = () => {
|
||||
loadingVersion.value = true
|
||||
getFrameworkVersionList().then(({ data }) => {
|
||||
frameworkVersionList.value = data
|
||||
loadingVersion.value = false
|
||||
if (checkVersion.value) {
|
||||
if (!newVersion.value || (newVersion.value && newVersion.value.version_no == version.value)) {
|
||||
ElMessage({
|
||||
message: t('versionTips'),
|
||||
type: 'success'
|
||||
})
|
||||
}
|
||||
} else {
|
||||
checkVersion.value = true
|
||||
}
|
||||
}).catch(() => {
|
||||
loadingVersion.value = false
|
||||
})
|
||||
}
|
||||
getFrameworkVersionListFn()
|
||||
const shouldShowUpgradeButton = computed(() => {
|
||||
if (!newVersion.value || newVersion.value.version_no === version.value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 将版本号转为字符串再处理
|
||||
const currentVersionStr = String(version.value);
|
||||
const latestVersionStr = String(newVersion.value.version_no);
|
||||
// 移除点号并转为数字比较
|
||||
const currentVersionNum = parseInt(currentVersionStr.replace(/\./g, ''), 10);
|
||||
const latestVersionNum = parseInt(latestVersionStr.replace(/\./g, ''), 10);
|
||||
return latestVersionNum > currentVersionNum;
|
||||
});
|
||||
const newVersion: any = computed(() => {
|
||||
return frameworkVersionList.value.length ? frameworkVersionList.value[0] : null
|
||||
})
|
||||
|
||||
const authCodeApproveFn = () => {
|
||||
authCodeApproveDialog.value = true
|
||||
}
|
||||
|
||||
const version = ref('')
|
||||
const versionCode = ref('')
|
||||
|
||||
const getVersionsInfo = () => {
|
||||
getVersions().then((res) => {
|
||||
version.value = res.data.version.version
|
||||
versionCode.value = res.data.version.code
|
||||
})
|
||||
}
|
||||
getVersionsInfo()
|
||||
const timeSplit = (str: string) => {
|
||||
const [date, time] = str.split(" ")
|
||||
const [hours, minutes] = time.split(":")
|
||||
return [date, `${ hours }:${ minutes }`]
|
||||
}
|
||||
interface AuthInfo {
|
||||
company_name: string
|
||||
site_address: string
|
||||
auth_code: string
|
||||
}
|
||||
|
||||
const authInfo = ref<AuthInfo>({
|
||||
company_name: '',
|
||||
site_address: '',
|
||||
auth_code: ''
|
||||
})
|
||||
|
||||
const repeat = ref(false)
|
||||
const loading = ref(false)
|
||||
|
||||
/**
|
||||
* 升级
|
||||
*/
|
||||
const handleUpgrade = () => {
|
||||
if (!authInfo.value.auth_code) {
|
||||
authCodeApproveFn()
|
||||
return
|
||||
}
|
||||
if (repeat.value) return
|
||||
repeat.value = true
|
||||
loading.value = true
|
||||
upgradeRef.value?.open('', () => {
|
||||
repeat.value = false
|
||||
loading.value = false;
|
||||
})
|
||||
}
|
||||
|
||||
const checkAppMange = () => {
|
||||
getAuthInfo().then((res) => {
|
||||
if (res.data.data && res.data.data.length != 0) {
|
||||
authInfo.value = res.data.data
|
||||
authCodeApproveDialog.value = false
|
||||
}
|
||||
}).catch(() => {
|
||||
authCodeApproveDialog.value = false
|
||||
})
|
||||
}
|
||||
|
||||
checkAppMange()
|
||||
|
||||
const router = useRouter()
|
||||
const upgradeRecord = () => {
|
||||
router.push('/admin/tools/upgrade_records')
|
||||
}
|
||||
|
||||
const openUpgrade = () => {
|
||||
upgradeLogRef.value?.open()
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-timeline-item__node--normal){
|
||||
width: 16px !important;
|
||||
height: 16px !important;
|
||||
}
|
||||
:deep(.el-timeline-item__tail){
|
||||
left: 6px !important;
|
||||
}
|
||||
:deep(.el-timeline-item__node.is-hollow){
|
||||
background: #9699B6 !important;
|
||||
border-width: 3px !important;
|
||||
}
|
||||
:deep(.time-dialog .el-timeline-item__wrapper) {
|
||||
top: -2px !important;
|
||||
}
|
||||
:deep(.el-collapse-item__header){
|
||||
background: #F9F9FB !important;
|
||||
border: 1px solid #F1F1F8 !important;
|
||||
padding: 0 20px !important;
|
||||
line-height: normal !important;
|
||||
height: 70px !important;
|
||||
}
|
||||
:deep(.el-collapse-item__content){
|
||||
padding-bottom: 0 !important;
|
||||
}
|
||||
:deep(.el-button){
|
||||
border-radius: 4px !important;
|
||||
}
|
||||
</style>
|
||||
251
admin-vben/src/app/views/auth/components/edit-menu.vue
Normal file
251
admin-vben/src/app/views/auth/components/edit-menu.vue
Normal file
@@ -0,0 +1,251 @@
|
||||
<template>
|
||||
<el-dialog v-model="showDialog" :title="popTitle" width="500px" :destroy-on-close="true">
|
||||
<el-form :model="formData" label-width="90px" class="page-form" ref="formRef" :rules="formRules" v-loading="loading">
|
||||
<el-form-item :label="t('menuName')" prop="menu_name">
|
||||
<el-input v-model.trim="formData.menu_name" maxlength="10" show-word-limit :placeholder="t('menuNamePlaceholder')" class="input-width" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('menuKey')" prop="menu_key" v-if="!formData.id">
|
||||
<el-input v-model.trim="formData.menu_key" maxlength="50" show-word-limit :placeholder="t('menuKeyPlaceholder')" class="input-width" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('menuType')">
|
||||
<el-radio-group v-model="formData.menu_type">
|
||||
<el-radio :label="0">{{ t('menuTypeDir') }}</el-radio>
|
||||
<el-radio :label="1">{{ t('menuTypeMenu') }}</el-radio>
|
||||
<el-radio :label="2">{{ t('menuTypeButton') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('addon')" prop="addon" v-show="formData.app_type == 'site'">
|
||||
<el-select v-model="formData.addon" :placeholder="t('addon')" class="input-width" @change="addonChange">
|
||||
<el-option v-for="(item, index) in addonList" :label="item.title" :value="item.key" :key="index" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('parentMenu')" prop="parent_key">
|
||||
<el-tree-select class="input-width" v-if="formData.addon != ''" v-model="formData.parent_key"
|
||||
:props="{ label: 'menu_name', value: 'menu_key' }" :data="addonMenuList" check-strictly
|
||||
:render-after-expand="false" />
|
||||
<el-tree-select class="input-width" v-else v-model="formData.parent_key"
|
||||
:props="{ label: 'menu_name', value: 'menu_key' }" :data="sysMenuList" check-strictly
|
||||
:render-after-expand="false" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('routePath')" prop="router_path" v-show="formData.menu_type == 1">
|
||||
<el-input v-model.trim="formData.router_path" :placeholder="t('routePathPlaceholder')" class="input-width" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('viewPath')" prop="view_path" v-show="formData.menu_type == 1">
|
||||
<el-input v-model.trim="formData.view_path" :placeholder="t('viewPathPlaceholder')" class="input-width" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('authId')" prop="api_url" v-show="formData.menu_type != 0">
|
||||
<el-input v-model.trim="formData.api_url" :placeholder="t('authIdPlaceholder')" class="input-width">
|
||||
<template #append>
|
||||
<el-select class="border-none" style="width: 100px" v-model="formData.methods">
|
||||
<el-option label="POST" value="post" />
|
||||
<el-option label="GET" value="get" />
|
||||
<el-option label="PUT" value="put" />
|
||||
<el-option label="DELETE" value="delete" />
|
||||
</el-select>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('menuIcon')" prop="icon" v-show="formData.menu_type != 2">
|
||||
<div class="input-width">
|
||||
<select-icon v-model="formData.icon" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('status')" v-show="formData.menu_type != 2">
|
||||
<el-radio-group v-model="formData.status">
|
||||
<el-radio :label="1">{{ t('statusNormal') }}</el-radio>
|
||||
<el-radio :label="0">{{ t('statusDeactivate') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('isShow')" v-show="formData.menu_type != 2">
|
||||
<el-radio-group v-model="formData.is_show">
|
||||
<el-radio :label="1">{{ t('show') }}</el-radio>
|
||||
<el-radio :label="0">{{ t('hidden') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('menuShortName')">
|
||||
<el-input v-model.trim="formData.menu_short_name" :placeholder="t('menuShortNamePlaceholder')" class="input-width" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('sort')">
|
||||
<el-input-number v-model="formData.sort" :min="0"/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button>
|
||||
<el-button type="primary" :loading="loading" @click="confirm(formRef)">{{ t('confirm') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import { addMenu, editMenu, getMenuInfo, getSystemMenu, getAddonMenu } from '@/app/api/sys'
|
||||
import { getAddonDevelop } from '@/app/api/tools'
|
||||
|
||||
const showDialog = ref(false)
|
||||
const loading = ref(false)
|
||||
let popTitle: string = ''
|
||||
|
||||
/**
|
||||
* 表单数据
|
||||
*/
|
||||
const initialFormData = {
|
||||
id: 0,
|
||||
menu_name: '',
|
||||
menu_type: 0,
|
||||
parent_key: '',
|
||||
icon: '',
|
||||
api_url: '',
|
||||
router_path: '',
|
||||
view_path: '',
|
||||
methods: 'post',
|
||||
sort: '',
|
||||
status: 1,
|
||||
is_show: 1,
|
||||
menu_key: '',
|
||||
app_type: '',
|
||||
addon: '',
|
||||
menu_short_name: ''
|
||||
}
|
||||
const formData: Record<string, any> = reactive({ ...initialFormData })
|
||||
|
||||
const addonList = ref<Array<any>>([])
|
||||
const sysMenuList = ref<Array<any>>([])
|
||||
const addonMenuList = ref<Array<any>>([])
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
const validataMenuKey = (val: string) => {
|
||||
return /^([a-zA-Z_$])([a-zA-Z0-9_$])*$/.test(val)
|
||||
}
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = computed(() => {
|
||||
return {
|
||||
menu_name: [
|
||||
{ required: true, message: t('menuNamePlaceholder'), trigger: 'blur' }
|
||||
],
|
||||
menu_key: [
|
||||
{ required: true, message: t('menuKeyPlaceholder'), trigger: 'blur' },
|
||||
{
|
||||
validator: (rule: any, value: string, callback: any) => {
|
||||
if (!validataMenuKey(value)) {
|
||||
callback(new Error(t('menuKeyValidata')))
|
||||
}
|
||||
|
||||
callback()
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
router_path: [
|
||||
{ required: formData.menu_type == 1, message: t('routePathPlaceholder'), trigger: 'blur' }
|
||||
],
|
||||
view_path: [
|
||||
{ required: formData.menu_type == 1, message: t('viewPathPlaceholder'), trigger: 'blur' }
|
||||
],
|
||||
api_url: [
|
||||
{ required: formData.menu_type == 2, message: t('authIdPlaceholder'), trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
// 获取插件列表
|
||||
const getAddonDevelopFn = async () => {
|
||||
const { data } = await getAddonDevelop({})
|
||||
addonList.value = [{ title: '系统', key: '' }]
|
||||
addonList.value.push(...data)
|
||||
}
|
||||
|
||||
// 获取系统菜单列表
|
||||
const getSystemMenuFn = async () => {
|
||||
const { data } = await getSystemMenu()
|
||||
sysMenuList.value = [{ menu_name: '顶级', menu_key: '' }]
|
||||
sysMenuList.value.push(...data)
|
||||
}
|
||||
|
||||
// 获取系统应用列表
|
||||
const getAddonMenuFn = async (key: any) => {
|
||||
const { data } = await getAddonMenu(key)
|
||||
addonMenuList.value = data
|
||||
}
|
||||
|
||||
// 选择应用
|
||||
const addonChange = async (val: any) => {
|
||||
formData.parent_key = ''
|
||||
if (val != '') {
|
||||
await getAddonMenuFn(val)
|
||||
formData.parent_key = addonMenuList.value[0].menu_key
|
||||
}
|
||||
}
|
||||
|
||||
const emit = defineEmits(['complete'])
|
||||
|
||||
/**
|
||||
* 确认
|
||||
* @param formEl
|
||||
*/
|
||||
const confirm = async (formEl: FormInstance | undefined) => {
|
||||
if (loading.value || !formEl) return
|
||||
const save = formData.id ? editMenu : addMenu
|
||||
|
||||
await formEl.validate(async (valid, fields) => {
|
||||
if (valid) {
|
||||
loading.value = true
|
||||
|
||||
const data = formData
|
||||
|
||||
save(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)
|
||||
popTitle = t('addMenu')
|
||||
getAddonDevelopFn()
|
||||
getSystemMenuFn()
|
||||
if (row.menu_key) {
|
||||
popTitle = t('updateMenu')
|
||||
const data = await (await getMenuInfo(row.app_type, row.menu_key)).data
|
||||
Object.keys(formData).forEach((key: string) => {
|
||||
if (data[key] != undefined) formData[key] = data[key]
|
||||
})
|
||||
if (formData.addon != '') getAddonMenuFn(formData.addon)
|
||||
} else {
|
||||
Object.keys(formData).forEach((key: string) => {
|
||||
if (row[key] != undefined) formData[key] = row[key]
|
||||
})
|
||||
}
|
||||
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
showDialog,
|
||||
setFormData
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
210
admin-vben/src/app/views/auth/components/edit-role.vue
Normal file
210
admin-vben/src/app/views/auth/components/edit-role.vue
Normal file
@@ -0,0 +1,210 @@
|
||||
<template>
|
||||
<el-dialog v-model="showDialog" :title="popTitle" width="500px" :destroy-on-close="true">
|
||||
<el-form :model="formData" label-width="90px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
|
||||
<el-form-item :label="t('roleName')" prop="role_name">
|
||||
<el-input v-model.trim="formData.role_name" :placeholder="t('roleNamePlaceholder')" clearable :disabled="formData.uid" class="input-width" maxlength="10" :show-word-limit="true" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('status')">
|
||||
<el-radio-group v-model="formData.status">
|
||||
<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('permission')" prop="rules">
|
||||
<div class="flex items-center justify-between w-11/12">
|
||||
<div>
|
||||
<el-checkbox v-model="selectAll" :label="t('selectAll')" />
|
||||
<el-checkbox v-model="checkStrictly" :label="t('checkStrictly')" />
|
||||
</div>
|
||||
<el-button link type="primary" @click="menuAction()">{{ t('foldText') }}</el-button>
|
||||
|
||||
</div>
|
||||
<el-scrollbar height="35vh" class="w-full">
|
||||
<el-tree :data="menus" :props="{ label: 'menu_name' }" :default-checked-keys="formData.rules" :check-strictly="checkStrictly" show-checkbox default-expand-all @check-change="handleCheckChange" node-key="menu_key" ref="treeRef" />
|
||||
</el-scrollbar>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button>
|
||||
<el-button type="primary" :loading="loading" @click="confirm(formRef)">{{t('confirm')}}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup async>
|
||||
import { ref, reactive, computed, watch, toRaw, nextTick } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import { addRole, editRole, getRoleInfo, getSiteMenus } from '@/app/api/sys'
|
||||
import { debounce } from '@/utils/common'
|
||||
|
||||
const showDialog = ref(false)
|
||||
const loading = ref(false)
|
||||
const isOpen = ref(true)
|
||||
|
||||
let popTitle: string = ''
|
||||
|
||||
// 获取权限数据
|
||||
const menus = ref<Record<string, any>[]>([])
|
||||
getSiteMenus().then((res) => {
|
||||
menus.value = res.data
|
||||
})
|
||||
|
||||
// 全选
|
||||
const selectAll = ref(false)
|
||||
const checkStrictly = ref(false)
|
||||
const treeRef: Record<string, any> | null = ref(null)
|
||||
watch(selectAll, () => {
|
||||
nextTick(() => {
|
||||
if (selectAll.value) {
|
||||
treeRef.value.setCheckedNodes(toRaw(menus.value))
|
||||
} else {
|
||||
treeRef.value.setCheckedNodes([])
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const handleCheckChange = debounce((e) => {
|
||||
formData.rules = treeRef.value.getCheckedKeys()
|
||||
})
|
||||
|
||||
const menuAction = () => {
|
||||
if (isOpen.value) {
|
||||
collapseAll(menus.value)
|
||||
isOpen.value = false
|
||||
} else {
|
||||
unFoldAll(menus.value)
|
||||
isOpen.value = true
|
||||
}
|
||||
}
|
||||
|
||||
// 全部展开
|
||||
const unFoldAll = (data:any) => {
|
||||
Object.keys(data).forEach((key:string|any) => {
|
||||
treeRef.value.store.nodesMap[data[key].menu_key].expanded = true
|
||||
if (data[key].children && data[key].children.length > 0) collapseAll(data[key].children)
|
||||
})
|
||||
}
|
||||
// 全部折叠
|
||||
const collapseAll = (data:any) => {
|
||||
Object.keys(data).forEach((key:string|any) => {
|
||||
treeRef.value.store.nodesMap[data[key].menu_key].expanded = false
|
||||
if (data[key].children && data[key].children.length > 0) collapseAll(data[key].children)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 表单数据
|
||||
*/
|
||||
const initialFormData = {
|
||||
role_id: 0,
|
||||
role_name: '',
|
||||
status: 1,
|
||||
rules: []
|
||||
}
|
||||
const formData: Record<string, any> = reactive({ ...initialFormData })
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = computed(() => {
|
||||
return {
|
||||
role_name: [
|
||||
{ required: true, message: t('roleNamePlaceholder'), trigger: 'blur' }
|
||||
],
|
||||
rules: [
|
||||
{
|
||||
validator: (rule: any, value: string, callback: any) => {
|
||||
if (!value.length) callback(new Error(t('rulesPlaceholder')))
|
||||
else callback()
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['complete'])
|
||||
|
||||
/**
|
||||
* 确认
|
||||
* @param formEl
|
||||
*/
|
||||
const confirm = async (formEl: FormInstance | undefined) => {
|
||||
if (loading.value || !formEl) return
|
||||
const save = formData.role_id ? editRole : addRole
|
||||
|
||||
await formEl.validate(async (valid) => {
|
||||
if (valid) {
|
||||
loading.value = true
|
||||
|
||||
const data = Object.assign({}, formData)
|
||||
data.rules = data.rules.concat(treeRef.value.getHalfCheckedKeys())
|
||||
|
||||
save(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
|
||||
selectAll.value = false
|
||||
Object.assign(formData, initialFormData)
|
||||
popTitle = t('addRole')
|
||||
if (row) {
|
||||
popTitle = t('updateRole')
|
||||
const data = await (await getRoleInfo(row.role_id)).data
|
||||
Object.keys(formData).forEach((key: string) => {
|
||||
if (data[key] != undefined) {
|
||||
if (key == 'rules') {
|
||||
const arr = data.rules
|
||||
const newArr:any = []
|
||||
|
||||
Object.keys(data.rules).forEach((i) => {
|
||||
checked(data.rules[i], menus.value, newArr)
|
||||
})
|
||||
formData[key] = newArr
|
||||
} else {
|
||||
formData[key] = data[key]
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
function checked (menuKey:string, data:any, newArr:any) {
|
||||
Object.keys(data).forEach((key:string) => {
|
||||
const item = data[key]
|
||||
if (item.menu_key == menuKey) {
|
||||
if (!item.children || item.children.length == 0) {
|
||||
newArr.push(item.menu_key)
|
||||
}
|
||||
} else {
|
||||
if (item.children && item.children.length > 0) {
|
||||
checked(menuKey, item.children, newArr)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
showDialog,
|
||||
setFormData
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
217
admin-vben/src/app/views/auth/components/edit-user.vue
Normal file
217
admin-vben/src/app/views/auth/components/edit-user.vue
Normal file
@@ -0,0 +1,217 @@
|
||||
<template>
|
||||
<el-dialog v-model="showDialog" :title="popTitle" width="500px" :destroy-on-close="true">
|
||||
<el-form :model="formData" label-width="90px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
|
||||
|
||||
<!-- <el-form-item :label="t('accountNumber')" v-if="!formData.uid" prop="uid">
|
||||
<el-select :model-value="uid" :placeholder="t('accountNumberPlaceholder')" class="input-width" filterable clearable :allow-create="true" @change="selectUser" :default-first-option="true">
|
||||
<el-option v-for="item in userList" :key="item.uid" :label="item.username" :value="item.uid">
|
||||
<div class="flex items-center">
|
||||
<el-avatar :src="img(item.head_img)" size="small" class="mr-[10px]" v-if="item.head_img" />
|
||||
<img src="@/app/assets/images/member_head.png" alt="" class="mr-[10px] w-[24px]" v-else>
|
||||
{{ item.username }}
|
||||
</div>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item> -->
|
||||
|
||||
<el-form-item :label="t('accountNumber')" prop="username" >
|
||||
<el-input v-model.trim="formData.username" :placeholder="t('accountNumberPlaceholder')" clearable :disabled="formData.uid" class="input-width" maxlength="20" show-word-limit />
|
||||
</el-form-item>
|
||||
|
||||
<div v-if="needAddUserInfo">
|
||||
<el-form-item :label="t('headImg')">
|
||||
<upload-image v-model="formData.head_img" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('userRealName')" prop="real_name">
|
||||
<el-input v-model.trim="formData.real_name" :placeholder="t('userRealNamePlaceholder')" :readonly="real_name_input" @click="real_name_input = false" @blur="real_name_input = true" clearable class="input-width" maxlength="10" show-word-limit />
|
||||
</el-form-item>
|
||||
<div v-if="!formData.uid">
|
||||
<el-form-item :label="t('password')" prop="password">
|
||||
<el-input v-model.trim="formData.password" :placeholder="t('passwordPlaceholder')" :readonly="password_input" @click="password_input = false" @blur="password_input = true" type="password" :show-password="true" clearable class="input-width" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('confirmPassword')" prop="confirm_password">
|
||||
<el-input v-model.trim="formData.confirm_password" :placeholder="t('confirmPasswordPlaceholder')" :readonly="confirm_password_input" @click="confirm_password_input = false" @blur="confirm_password_input = true" type="password" :show-password="true" clearable class="input-width" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-form-item :label="t('userRoleName')" prop="role_ids" v-if="!formData.userrole.is_admin">
|
||||
<el-select v-model="formData.role_ids" :placeholder="t('userRolePlaceholder')" class="input-width" multiple collapse-tags collapse-tags-tooltip>
|
||||
<el-option :label="item.role_name" :value="item.role_id" v-for="(item, index) in roles" :key="index" :disabled="item.disabled" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('status')">
|
||||
<el-radio-group v-model="formData.status">
|
||||
<el-radio :label="1">{{ t('statusUnlock') }}</el-radio>
|
||||
<el-radio :label="0">{{ t('lock') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button>
|
||||
<el-button type="primary" :loading="loading" @click="confirm(formRef)">{{ t('confirm') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed, toRaw } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import { getAllUserList } from '@/app/api/user'
|
||||
import { getUserInfo, addUser, editUser } from '@/app/api/site'
|
||||
import { allRole } from '@/app/api/sys'
|
||||
import { img, deepClone } from '@/utils/common'
|
||||
import { AnyObject } from '@/types/global'
|
||||
|
||||
const userList = ref<AnyObject>([])
|
||||
const uid = ref<number | string>('')
|
||||
|
||||
const selectUser = (value: any) => {
|
||||
uid.value = value
|
||||
if (typeof value == 'string') formData.username = value
|
||||
}
|
||||
|
||||
const getUserList = () => {
|
||||
getAllUserList({}).then(({ data }) => {
|
||||
userList.value = data
|
||||
}).catch()
|
||||
}
|
||||
getUserList()
|
||||
const real_name_input = ref(true)
|
||||
const password_input = ref(true)
|
||||
const confirm_password_input = ref(true)
|
||||
const needAddUserInfo = computed(() => {
|
||||
if (formData.uid || !uid.value || typeof uid.value == 'string') {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
const showDialog = ref(false)
|
||||
const loading = ref(false)
|
||||
let popTitle: string = ''
|
||||
|
||||
/**
|
||||
* 表单数据
|
||||
*/
|
||||
const initialFormData = {
|
||||
uid: 0,
|
||||
username: '',
|
||||
head_img: '',
|
||||
real_name: '',
|
||||
password: '',
|
||||
confirm_password: '',
|
||||
status: 1,
|
||||
role_ids: [],
|
||||
userrole: {}
|
||||
}
|
||||
const formData: Record<string, any> = reactive({ ...initialFormData })
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = computed(() => {
|
||||
return {
|
||||
uid: [
|
||||
{
|
||||
validator: (rule: any, value: string, callback: any) => {
|
||||
if (!formData.uid && uid.value === '') callback(new Error(t('managerPlaceholder')))
|
||||
else callback()
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
username: [
|
||||
{ required: formData.uid == 0, message: t('accountNumberPlaceholder'), trigger: 'blur' }
|
||||
],
|
||||
real_name: [
|
||||
{ required: true, message: t('userRealNamePlaceholder'), trigger: 'blur' }
|
||||
],
|
||||
role_ids: [
|
||||
{ required: true, message: t('userRolePlaceholder'), trigger: 'blur' }
|
||||
],
|
||||
password: [
|
||||
{ required: formData.uid == 0, message: t('passwordPlaceholder'), trigger: 'blur' }
|
||||
],
|
||||
confirm_password: [
|
||||
{ required: formData.uid == 0, message: t('confirmPasswordPlaceholder'), trigger: 'blur' },
|
||||
{
|
||||
validator: (rule: any, value: string, callback: any) => {
|
||||
if (value != formData.password) callback(new Error(t('confirmPasswordError')))
|
||||
else callback()
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['complete'])
|
||||
|
||||
// 角色
|
||||
const roles = ref<Record<string, any>>([])
|
||||
allRole().then(res => {
|
||||
roles.value = res.data
|
||||
roles.value.forEach((element:any) => {
|
||||
element.role_id = element.role_id.toString()
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* 确认
|
||||
* @param formEl
|
||||
*/
|
||||
const confirm = async (formEl: FormInstance | undefined) => {
|
||||
if (loading.value || !formEl) return
|
||||
const save = formData.uid ? editUser : addUser
|
||||
|
||||
await formEl.validate(async (valid) => {
|
||||
if (valid) {
|
||||
loading.value = true
|
||||
|
||||
const data = deepClone(toRaw(formData))
|
||||
if (!formData.uid && typeof uid.value == 'number') data.uid = uid.value
|
||||
save(data).then(res => {
|
||||
loading.value = false
|
||||
showDialog.value = false
|
||||
!formData.uid && getUserList()
|
||||
emit('complete')
|
||||
}).catch(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const setFormData = async (row: any = null) => {
|
||||
loading.value = true
|
||||
uid.value = ''
|
||||
Object.assign(formData, initialFormData)
|
||||
popTitle = t('addUser')
|
||||
|
||||
if (row) {
|
||||
popTitle = t('updateUser')
|
||||
const data = await (await getUserInfo(row.uid)).data
|
||||
data.role_ids = data.role_ids || []
|
||||
Object.keys(formData).forEach((key: string) => {
|
||||
if (data[key] != undefined) formData[key] = data[key]
|
||||
})
|
||||
}
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
showDialog,
|
||||
setFormData
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<template v-if="prop.menu.menu_type != 2">
|
||||
<el-option :label="`${prop.menu.menu_name}`" :value="prop.menu.menu_key">
|
||||
<span v-html="`${menuLevel}${prop.menu.menu_name}`"></span>
|
||||
</el-option>
|
||||
<template v-if="prop.menu.children">
|
||||
<select-menu-item :menu="item" v-for="(item,index) in prop.menu.children" :level="prop.level + 1" :key="index" />
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const prop:any = defineProps({
|
||||
menu: Object,
|
||||
level: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
})
|
||||
|
||||
const menuLevel = computed(() => {
|
||||
let t = ''
|
||||
for (let i = 0; i < prop.level; i++) {
|
||||
t += i == 0 ? ' |--' : '--'
|
||||
}
|
||||
return t
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
67
admin-vben/src/app/views/auth/components/user-log-detail.vue
Normal file
67
admin-vben/src/app/views/auth/components/user-log-detail.vue
Normal file
@@ -0,0 +1,67 @@
|
||||
<template>
|
||||
<el-dialog v-model="showDialog" :title="t('detail')" width="500px" :destroy-on-close="true">
|
||||
<el-scrollbar height="400px" v-loading="loading">
|
||||
<el-descriptions :column="1">
|
||||
<el-descriptions-item :label="t('username')" label-align="right">{{logData.username}}</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('ip')" label-align="right">{{logData.ip}}</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('operation')" label-align="right">{{logData.operation}}</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('url')" label-align="right"><span class="break-all">{{logData.url}}</span></el-descriptions-item>
|
||||
<el-descriptions-item :label="t('type')" label-align="right">{{logData.type}}</el-descriptions-item>
|
||||
<el-descriptions-item :label="t('params')" label-align="right">
|
||||
<span class="break-all">{{logData.params}}</span>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-scrollbar>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import { getLogInfo } from '@/app/api/site'
|
||||
|
||||
const showDialog = ref(false)
|
||||
const loading = ref(false)
|
||||
|
||||
interface LogData {
|
||||
username: string
|
||||
ip: string,
|
||||
url: string,
|
||||
type: string,
|
||||
params:any
|
||||
}
|
||||
|
||||
const logData = ref<LogData>({
|
||||
username: '',
|
||||
ip: '',
|
||||
url: '',
|
||||
type: '',
|
||||
params: ''
|
||||
})
|
||||
const getLogDetail = async () => {
|
||||
logData.value = await (await getLogInfo(id)).data
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
let id = 0
|
||||
const setFormData = async (row: any = null) => {
|
||||
loading.value = true
|
||||
if (row) {
|
||||
id = row.id
|
||||
getLogDetail()
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
showDialog,
|
||||
setFormData
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
142
admin-vben/src/app/views/auth/log.vue
Normal file
142
admin-vben/src/app/views/auth/log.vue
Normal file
@@ -0,0 +1,142 @@
|
||||
<template>
|
||||
<!--操作日志-->
|
||||
<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-start mt-[20px]">
|
||||
<el-form :inline="true" :model="sysUserLogTableData.searchParam" ref="searchFormRef">
|
||||
<el-form-item :label="t('ip')" prop="ip">
|
||||
<el-input v-model.trim="sysUserLogTableData.searchParam.ip" :placeholder="t('ipPlaceholder')" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('username')" prop="username">
|
||||
<el-input v-model.trim="sysUserLogTableData.searchParam.username" :placeholder="t('usernamePlaceholder')" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('url')" prop="url">
|
||||
<el-input v-model.trim="sysUserLogTableData.searchParam.url" :placeholder="t('urlPlaceholder')" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="loadSysUserLogList()">{{ t('search') }}</el-button>
|
||||
<el-button @click="resetForm(searchFormRef)">{{ t('reset') }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="flex justify-end items-center w-[20%]">
|
||||
<div>
|
||||
<el-button type="primary" class="w-[100px]" @click="clearEvent()">{{ t('清空日志') }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<el-table :data="sysUserLogTableData.data" size="large" v-loading="sysUserLogTableData.loading">
|
||||
<template #empty>
|
||||
<span>{{ !sysUserLogTableData.loading ? t('emptyData') : '' }}</span>
|
||||
</template>
|
||||
<el-table-column prop="username" :label="t('username')" min-width="120" />
|
||||
<el-table-column prop="ip" :label="t('ip')" min-width="100" align="left"/>
|
||||
<el-table-column prop="operation" :label="t('operationLog')" min-width="200" align="left"/>
|
||||
<el-table-column prop="url" :label="t('url')" min-width="180" />
|
||||
<el-table-column prop="type" :label="t('type')" min-width="100" align="center"/>
|
||||
<el-table-column :label="t('createTime')" min-width="180" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ row.create_time || '' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('operation')" align="right" fixed="right" width="130">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="detailEvent(row)">{{ t('detail') }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="mt-[16px] flex justify-end">
|
||||
<el-pagination v-model:current-page="sysUserLogTableData.page" v-model:page-size="sysUserLogTableData.limit" layout="total, sizes, prev, pager, next, jumper" :total="sysUserLogTableData.total" @size-change="loadSysUserLogList()" @current-change="loadSysUserLogList" />
|
||||
</div>
|
||||
|
||||
<user-log-detail ref="userLogDetailDialog" @complete="loadSysUserLogList()" />
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import { getLogList, logDestroy } from '@/app/api/site'
|
||||
import UserLogDetail from '@/app/views/auth/components/user-log-detail.vue'
|
||||
import { FormInstance } from 'element-plus'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
const pageName = route.meta.title
|
||||
const sysUserLogTableData = reactive({
|
||||
page: 1,
|
||||
limit: 10,
|
||||
total: 0,
|
||||
loading: true,
|
||||
data: [],
|
||||
searchParam: {
|
||||
ip: '',
|
||||
username: '',
|
||||
url: ''
|
||||
}
|
||||
})
|
||||
|
||||
const searchFormRef = ref<FormInstance>()
|
||||
const resetForm = (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
|
||||
formEl.resetFields()
|
||||
loadSysUserLogList()
|
||||
}
|
||||
/**
|
||||
* 获取管理员操作记录表列表
|
||||
*/
|
||||
const loadSysUserLogList = (page: number = 1) => {
|
||||
sysUserLogTableData.loading = true
|
||||
sysUserLogTableData.page = page
|
||||
|
||||
getLogList({
|
||||
page: sysUserLogTableData.page,
|
||||
limit: sysUserLogTableData.limit,
|
||||
...sysUserLogTableData.searchParam
|
||||
}).then(res => {
|
||||
sysUserLogTableData.loading = false
|
||||
sysUserLogTableData.data = res.data.data
|
||||
sysUserLogTableData.total = res.data.total
|
||||
}).catch(() => {
|
||||
sysUserLogTableData.loading = false
|
||||
})
|
||||
}
|
||||
loadSysUserLogList()
|
||||
const userLogDetailDialog: Record<string, any> | null = ref(null)
|
||||
/**
|
||||
* 查看详情
|
||||
* @param data
|
||||
*/
|
||||
const detailEvent = (data: any) => {
|
||||
userLogDetailDialog.value.setFormData(data)
|
||||
userLogDetailDialog.value.showDialog = true
|
||||
}
|
||||
|
||||
const clearEvent = () => {
|
||||
ElMessageBox.confirm(t('确定要全部清空操作日志吗?'), t('提示'), {
|
||||
confirmButtonText: t('confirm'),
|
||||
cancelButtonText: t('cancel'),
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
logDestroy().then(() => {
|
||||
loadSysUserLogList()
|
||||
})
|
||||
}).catch(() => {
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
142
admin-vben/src/app/views/auth/menu.vue
Normal file
142
admin-vben/src/app/views/auth/menu.vue
Normal file
@@ -0,0 +1,142 @@
|
||||
<template>
|
||||
<!--平台菜单-->
|
||||
<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 class="flex items-center">
|
||||
<el-button type="primary" class="w-[100px]" @click="addEvent">
|
||||
{{ t('addMenu') }}
|
||||
</el-button>
|
||||
<el-button class="w-[100px]" @click="refreshMenu">
|
||||
{{ t('initializeMenu') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table class="mt-[20px]" :data="menusTableData.data" row-key="menu_key" size="large" v-loading="menusTableData.loading">
|
||||
<template #empty>
|
||||
<span>{{ !menusTableData.loading ? t('emptyData') : '' }}</span>
|
||||
</template>
|
||||
|
||||
<el-table-column prop="menu_name" :show-overflow-tooltip="true" :label="t('menuName')" min-width="150" />
|
||||
<el-table-column :label="t('icon')" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<icon v-if="row.icon" :name="row.icon" size="18px" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('menuType')" width="80">
|
||||
<template #default="{ row }">
|
||||
<div v-if="row.menu_type == 0">{{ t('menuTypeDir') }}</div>
|
||||
<div v-else-if="row.menu_type == 1">{{ t('menuTypeMenu') }}</div>
|
||||
<div v-else-if="row.menu_type == 2">{{ t('menuTypeButton') }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="api_url" :label="t('authId')" min-width="150" align="left" />
|
||||
<el-table-column :label="t('status')" min-width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag class="ml-2" type="success" v-if="row.status == 1">{{ t('statusNormal') }}</el-tag>
|
||||
<el-tag class="ml-2" type="error" v-if="row.status == 0">{{ t('statusDeactivate') }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="sort" :label="t('sort')" min-width="100" />
|
||||
<el-table-column :label="t('operation')" align="right" fixed="right" width="130">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="editEvent(row)">{{ t('edit') }}</el-button>
|
||||
<el-button type="primary" link @click="deleteEvent(row.menu_key)">{{ t('delete') }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<edit-menu ref="editMenuDialog" :menu-tree="menusTableData.data" @complete="getMenuList" app-type="admin" />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref,h } from 'vue'
|
||||
import { getMenus, deleteMenu,menuRefresh } from '@/app/api/sys'
|
||||
import { t } from '@/lang'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import EditMenu from '@/app/views/auth/components/edit-menu.vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
const pageName = route.meta.title
|
||||
const menusTableData = reactive({
|
||||
loading: true,
|
||||
data: []
|
||||
})
|
||||
|
||||
/**
|
||||
* 获取菜单
|
||||
*/
|
||||
const getMenuList = () => {
|
||||
menusTableData.loading = true
|
||||
getMenus('admin').then(res => {
|
||||
menusTableData.loading = false
|
||||
menusTableData.data = res.data
|
||||
}).catch(() => {
|
||||
})
|
||||
}
|
||||
getMenuList()
|
||||
// 重置菜单
|
||||
const refreshMenu = () => {
|
||||
ElMessageBox.confirm(h('div', null, [
|
||||
h('p', null, t('initializeMenuTipsOne')),
|
||||
h('p', null, t('initializeMenuTipsTwo')),
|
||||
]), t('warning'),
|
||||
{
|
||||
confirmButtonText: t('confirm'),
|
||||
cancelButtonText: t('cancel'),
|
||||
// type: 'warning'
|
||||
}
|
||||
).then(() => {
|
||||
menuRefresh({}).then(res => {
|
||||
location.reload()
|
||||
}).catch(() => {
|
||||
})
|
||||
}).catch(() => {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加菜单
|
||||
*/
|
||||
const editMenuDialog: Record<string, any> | null = ref(null)
|
||||
|
||||
const addEvent = () => {
|
||||
editMenuDialog.value.setFormData({ app_type: 'admin' })
|
||||
editMenuDialog.value.showDialog = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑菜单
|
||||
* @param data
|
||||
*/
|
||||
const editEvent = (data: any) => {
|
||||
editMenuDialog.value.setFormData(data)
|
||||
editMenuDialog.value.showDialog = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除菜单
|
||||
*/
|
||||
const deleteEvent = (key: string) => {
|
||||
ElMessageBox.confirm(t('menuDeleteTips'), t('warning'),
|
||||
{
|
||||
confirmButtonText: t('confirm'),
|
||||
cancelButtonText: t('cancel'),
|
||||
type: 'warning'
|
||||
}
|
||||
).then(() => {
|
||||
deleteMenu('admin', key).then(() => {
|
||||
getMenuList()
|
||||
}).catch(() => {
|
||||
})
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
160
admin-vben/src/app/views/auth/role.vue
Normal file
160
admin-vben/src/app/views/auth/role.vue
Normal file
@@ -0,0 +1,160 @@
|
||||
<template>
|
||||
<!--角色管理-->
|
||||
<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 mt-[20px]">
|
||||
<el-form :inline="true" :model="roleTableData.searchParam" ref="searchFormRef">
|
||||
<el-form-item :label="t('roleName')" prop="search">
|
||||
<el-input v-model.trim="roleTableData.searchParam.search" class="w-[240px]" :placeholder="t('roleNamePlaceholder')" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="loadRoleList()">{{ t('search') }}</el-button>
|
||||
<el-button @click="resetForm(searchFormRef)">{{ t('reset') }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-button type="primary" class="w-[100px] self-start" @click="addEvent">{{ t('addRole') }}</el-button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<el-table :data="roleTableData.data" size="large" v-loading="roleTableData.loading">
|
||||
<template #empty>
|
||||
<span>{{ !roleTableData.loading ? t('emptyData') : '' }}</span>
|
||||
</template>
|
||||
<el-table-column prop="role_name" :label="t('roleName')" />
|
||||
<el-table-column :label="t('status')">
|
||||
<template #default="{ row }">
|
||||
<el-tag type="success" v-if="row.status == 1" @click="modifyRoleStatusEvent(row.role_id, 0)" class="cursor-pointer">{{ row.status_name }}</el-tag>
|
||||
<el-tag type="error" v-else @click="modifyRoleStatusEvent(row.role_id, 1)" class="cursor-pointer">{{ row.status_name }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="create_time" :label="t('createTime')"></el-table-column>
|
||||
<el-table-column :label="t('operation')" align="right" fixed="right" width="130">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="editEvent(row)">{{ t('edit') }}</el-button>
|
||||
<el-button type="primary" link @click="deleteEvent(row.role_id)">{{ t('delete') }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="mt-[16px] flex justify-end">
|
||||
<el-pagination v-model:current-page="roleTableData.page" v-model:page-size="roleTableData.limit" layout="total, sizes, prev, pager, next, jumper" :total="roleTableData.total" @size-change="loadRoleList()" @current-change="loadRoleList" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<edit-role ref="editRoleDialog" @complete="loadRoleList()" />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import { getRoleList, deleteRole, modifyRoleStatus } from '@/app/api/sys'
|
||||
import { ElMessageBox, FormInstance } from 'element-plus'
|
||||
import EditRole from '@/app/views/auth/components/edit-role.vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
const pageName = route.meta.title
|
||||
const roleTableData = reactive({
|
||||
page: 1,
|
||||
limit: 10,
|
||||
total: 0,
|
||||
loading: true,
|
||||
data: [],
|
||||
searchParam: {
|
||||
search: ''
|
||||
}
|
||||
})
|
||||
|
||||
const searchFormRef = ref<FormInstance>()
|
||||
const resetForm = (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
|
||||
formEl.resetFields()
|
||||
loadRoleList()
|
||||
}
|
||||
/**
|
||||
* 获取角色列表
|
||||
*/
|
||||
const loadRoleList = (page: number = 1) => {
|
||||
roleTableData.loading = true
|
||||
roleTableData.page = page
|
||||
|
||||
getRoleList({
|
||||
page: roleTableData.page,
|
||||
limit: roleTableData.limit,
|
||||
role_name: roleTableData.searchParam.search
|
||||
}).then(res => {
|
||||
roleTableData.loading = false
|
||||
roleTableData.data = res.data.data
|
||||
roleTableData.total = res.data.total
|
||||
}).catch(() => {
|
||||
roleTableData.loading = false
|
||||
})
|
||||
}
|
||||
loadRoleList()
|
||||
|
||||
const editRoleDialog: Record<string, any> | null = ref(null)
|
||||
|
||||
/**
|
||||
* 添加角色
|
||||
*/
|
||||
const addEvent = () => {
|
||||
editRoleDialog.value.setFormData()
|
||||
editRoleDialog.value.showDialog = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑角色
|
||||
* @param data
|
||||
*/
|
||||
const editEvent = (data: any) => {
|
||||
editRoleDialog.value.setFormData(data)
|
||||
editRoleDialog.value.showDialog = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除角色
|
||||
*/
|
||||
const deleteEvent = (id: number) => {
|
||||
ElMessageBox.confirm(t('roleDeleteTips'), t('warning'),
|
||||
{
|
||||
confirmButtonText: t('confirm'),
|
||||
cancelButtonText: t('cancel'),
|
||||
type: 'warning'
|
||||
}
|
||||
).then(() => {
|
||||
deleteRole(id).then(() => {
|
||||
loadRoleList()
|
||||
}).catch(() => {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const isRepeat = ref(false)
|
||||
|
||||
// 修改状态
|
||||
const modifyRoleStatusEvent = (role_id: any, status: any) => {
|
||||
if (isRepeat.value) return
|
||||
isRepeat.value = true
|
||||
|
||||
modifyRoleStatus({
|
||||
role_id,
|
||||
status
|
||||
}).then((res) => {
|
||||
loadRoleList()
|
||||
isRepeat.value = false
|
||||
}).catch(() => {
|
||||
isRepeat.value = false
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
191
admin-vben/src/app/views/auth/site_menu.vue
Normal file
191
admin-vben/src/app/views/auth/site_menu.vue
Normal file
@@ -0,0 +1,191 @@
|
||||
<template>
|
||||
<!--站点菜单-->
|
||||
<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 class="flex items-center">
|
||||
<el-button type="primary" class="w-[100px]" @click="addEvent">
|
||||
{{ t('addMenu') }}
|
||||
</el-button>
|
||||
<el-button class="w-[100px]" @click="refreshMenu">
|
||||
{{ t('initializeMenu') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-tabs v-model="active">
|
||||
<el-tab-pane :label="t('system')" name="system">
|
||||
<el-table :data="menusTableData.system" row-key="menu_key" size="large" v-loading="menusTableData.loading">
|
||||
<template #empty>
|
||||
<span>{{ !menusTableData.loading ? t('emptyData') : '' }}</span>
|
||||
</template>
|
||||
<el-table-column prop="menu_name" :show-overflow-tooltip="true" :label="t('menuName')" min-width="150" />
|
||||
<el-table-column :label="t('icon')" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<icon v-if="row.icon" :name="row.icon" size="18px" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('menuType')" width="80">
|
||||
<template #default="{ row }">
|
||||
<div v-if="row.menu_type == 0">{{ t('menuTypeDir') }}</div>
|
||||
<div v-else-if="row.menu_type == 1">{{ t('menuTypeMenu') }}</div>
|
||||
<div v-else-if="row.menu_type == 2">{{ t('menuTypeButton') }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="api_url" :label="t('authId')" min-width="150" align="center" />
|
||||
<el-table-column :label="t('status')" min-width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag class="ml-2" type="success" v-if="row.status == 1">{{ t('statusNormal') }}</el-tag>
|
||||
<el-tag class="ml-2" type="error" v-if="row.status == 0">{{ t('statusDeactivate') }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="sort" :label="t('sort')" min-width="100" />
|
||||
<el-table-column :label="t('operation')" align="right" fixed="right" width="130">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="editEvent(row)">{{ t('edit') }}</el-button>
|
||||
<el-button type="primary" link @click="deleteEvent(row.menu_key)">{{ t('delete') }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="t('application')" name="application">
|
||||
<el-table :data="menusTableData.application" row-key="menu_key" size="large" v-loading="menusTableData.loading">
|
||||
<template #empty>
|
||||
<span>{{ !menusTableData.loading ? t('emptyData') : '' }}</span>
|
||||
</template>
|
||||
<el-table-column prop="menu_name" :show-overflow-tooltip="true" :label="t('menuName')" min-width="150" />
|
||||
<el-table-column :label="t('icon')" width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
<icon v-if="row.icon" :name="row.icon" size="18px" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('menuType')" width="80">
|
||||
<template #default="{ row }">
|
||||
<div v-if="row.menu_type == 0">{{ t('menuTypeDir') }}</div>
|
||||
<div v-else-if="row.menu_type == 1">{{ t('menuTypeMenu') }}</div>
|
||||
<div v-else-if="row.menu_type == 2">{{ t('menuTypeButton') }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="api_url" :label="t('authId')" min-width="150" align="center" />
|
||||
<el-table-column :label="t('status')" min-width="120" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag class="ml-2" type="success" v-if="row.status == 1">{{ t('statusNormal') }}</el-tag>
|
||||
<el-tag class="ml-2" type="error" v-if="row.status == 0">{{ t('statusDeactivate') }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="sort" :label="t('sort')" min-width="100" />
|
||||
<el-table-column :label="t('operation')" align="right" fixed="right" width="130">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="editEvent(row)">{{ t('edit') }}</el-button>
|
||||
<el-button type="primary" link @click="deleteEvent(row.menu_key)">{{ t('delete') }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
|
||||
<edit-menu ref="editMenuDialog" :menu-tree="menusTableData.data" @complete="getMenuList" />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref, h } from 'vue'
|
||||
import { getMenus, deleteMenu, menuRefresh } from '@/app/api/sys'
|
||||
import { t } from '@/lang'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import EditMenu from '@/app/views/auth/components/edit-menu.vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
const active = ref('system')
|
||||
const pageName = route.meta.title
|
||||
|
||||
const menusTableData = reactive<Record<string, any>>({
|
||||
loading: true,
|
||||
system: [],
|
||||
application: []
|
||||
})
|
||||
|
||||
/**
|
||||
* 获取菜单
|
||||
*/
|
||||
const getMenuList = () => {
|
||||
menusTableData.loading = true
|
||||
getMenus('site').then(({ data }) => {
|
||||
menusTableData.loading = false
|
||||
const system: Record<string, any>[] = []
|
||||
const application: Record<string, any> = []
|
||||
data.forEach((item: any) => {
|
||||
item.addon == '' ? system.push(item) : application.push(item)
|
||||
})
|
||||
menusTableData.system = system
|
||||
menusTableData.application = application
|
||||
}).catch(() => {
|
||||
|
||||
})
|
||||
}
|
||||
getMenuList()
|
||||
// 重置菜单
|
||||
const refreshMenu = () => {
|
||||
ElMessageBox.confirm(h('div', null, [
|
||||
h('p', null, t('initializeMenuTipsOne')),
|
||||
h('p', null, t('initializeMenuTipsTwo'))
|
||||
]), t('warning'),
|
||||
{
|
||||
confirmButtonText: t('confirm'),
|
||||
cancelButtonText: t('cancel')
|
||||
// type: 'warning'
|
||||
}
|
||||
).then(() => {
|
||||
menuRefresh({}).then(res => {
|
||||
location.reload()
|
||||
}).catch(() => {
|
||||
})
|
||||
}).catch(() => {
|
||||
})
|
||||
}
|
||||
/**
|
||||
* 添加菜单
|
||||
*/
|
||||
const editMenuDialog: Record<string, any> | null = ref(null)
|
||||
|
||||
const addEvent = () => {
|
||||
editMenuDialog.value.setFormData({ app_type: 'site' })
|
||||
editMenuDialog.value.showDialog = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑菜单
|
||||
* @param data
|
||||
*/
|
||||
const editEvent = (data: any) => {
|
||||
editMenuDialog.value.setFormData(data)
|
||||
editMenuDialog.value.showDialog = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除菜单
|
||||
*/
|
||||
const deleteEvent = (key: string) => {
|
||||
ElMessageBox.confirm(t('menuDeleteTips'), t('warning'),
|
||||
{
|
||||
confirmButtonText: t('confirm'),
|
||||
cancelButtonText: t('cancel'),
|
||||
type: 'warning'
|
||||
}
|
||||
).then(() => {
|
||||
deleteMenu('site', key).then(res => {
|
||||
getMenuList()
|
||||
}).catch(() => {
|
||||
})
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
227
admin-vben/src/app/views/auth/user.vue
Normal file
227
admin-vben/src/app/views/auth/user.vue
Normal file
@@ -0,0 +1,227 @@
|
||||
<template>
|
||||
<!--管理员-->
|
||||
<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 mt-[20px]">
|
||||
<el-form :inline="true" :model="userTableData.searchParam" ref="searchFormRef">
|
||||
<el-form-item :label="t('accountNumber')" prop="search">
|
||||
<el-input v-model.trim="userTableData.searchParam.search" class="input-width" :placeholder="t('accountNumberPlaceholder')" />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="loadUserList()">{{ t('search') }}</el-button>
|
||||
<el-button @click="resetForm(searchFormRef)">{{ t('reset') }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-button type="primary" class="w-[100px] self-start" @click="addEvent">{{ t('addUser') }}</el-button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<el-table :data="userTableData.data" size="large" v-loading="userTableData.loading">
|
||||
<template #empty>
|
||||
<span>{{ !userTableData.loading ? t('emptyData') : '' }}</span>
|
||||
</template>
|
||||
|
||||
<el-table-column :label="t('headImg')" width="100" align="left">
|
||||
<template #default="{ row }">
|
||||
<div class="w-[35px] h-[35px] flex items-center justify-center">
|
||||
<img v-if="row.head_img" :src="img(row.head_img)" class="w-[35px] rounded-full" />
|
||||
<img v-else src="@/app/assets/images/member_head.png" class="w-[35px] rounded-full" />
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="username" :label="t('accountNumber')" min-width="120" show-overflow-tooltip />
|
||||
<el-table-column prop="real_name" :label="t('userRealName')" min-width="120" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.real_name ? row.real_name :'--' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('userRoleName')" min-width="120" show-overflow-tooltip>
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.is_admin">{{ t('administrator') }}</span>
|
||||
<span v-else-if="row.role_array.length">{{ row.role_array.join(' | ') }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('status')" min-width="90" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-tag class="ml-2" type="success" v-if="row.status == 1">{{ t('statusUnlock') }}</el-tag>
|
||||
<el-tag class="ml-2" type="error" v-if="row.status == 0">{{ t('statusLock') }}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="last_time" :label="t('lastLoginTime')" min-width="180" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ row.last_time || '' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('lastLoginIP')" min-width="180" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ row.last_ip || '' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('operation')" align="right" fixed="right" width="160">
|
||||
<template #default="{ row }">
|
||||
<div v-if="row.is_admin != 1">
|
||||
<el-button type="primary" link @click="editEvent(row)">{{ t('edit') }}</el-button>
|
||||
<el-button type="primary" link @click="lockEvent(row.uid)" v-if="row.status">{{ t('lock') }}</el-button>
|
||||
<el-button type="primary" link @click="unlockEvent(row.uid)" v-else>{{ t('unlock') }}</el-button>
|
||||
<el-button type="primary" link @click="deleteEvent(row.uid)">{{ t('delete') }}</el-button>
|
||||
</div>
|
||||
<div v-else>
|
||||
<el-button link disabled>{{ t('adminDisabled') }}</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="mt-[16px] flex justify-end">
|
||||
<el-pagination v-model:current-page="userTableData.page" v-model:page-size="userTableData.limit"
|
||||
layout="total, sizes, prev, pager, next, jumper" :total="userTableData.total"
|
||||
@size-change="loadUserList()" @current-change="loadUserList" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- <el-tabs v-model="activeName" class="mt-[20px]">
|
||||
<el-tab-pane :label="t('管理员')" name="userList">
|
||||
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="t('管理员角色')" name="userRole">
|
||||
<userRole></userRole>
|
||||
</el-tab-pane>
|
||||
</el-tabs> -->
|
||||
<edit-user ref="editUserDialog" @complete="loadUserList()" />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import { getUserList, lockUser, unlockUser, deleteUser } from '@/app/api/site'
|
||||
import EditUser from '@/app/views/auth/components/edit-user.vue'
|
||||
import userRole from '@/app/views/auth/role.vue'
|
||||
import { img } from '@/utils/common'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
const pageName = route.meta.title
|
||||
const userTableData = reactive({
|
||||
page: 1,
|
||||
limit: 10,
|
||||
total: 0,
|
||||
loading: true,
|
||||
data: [],
|
||||
searchParam: {
|
||||
search: '',
|
||||
user_type: ''
|
||||
}
|
||||
})
|
||||
const activeName = ref('userList')
|
||||
const searchFormRef = ref<FormInstance>()
|
||||
|
||||
const resetForm = (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
|
||||
formEl.resetFields()
|
||||
loadUserList()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户列表
|
||||
*/
|
||||
const loadUserList = (page: number = 1) => {
|
||||
userTableData.loading = true
|
||||
userTableData.page = page
|
||||
|
||||
getUserList({
|
||||
page: userTableData.page,
|
||||
limit: userTableData.limit,
|
||||
username: userTableData.searchParam.search,
|
||||
user_type: userTableData.searchParam.user_type
|
||||
}).then(res => {
|
||||
userTableData.loading = false
|
||||
userTableData.data = res.data.data
|
||||
userTableData.total = res.data.total
|
||||
}).catch(() => {
|
||||
userTableData.loading = false
|
||||
})
|
||||
}
|
||||
loadUserList()
|
||||
|
||||
const editUserDialog: Record<string, any> | null = ref(null)
|
||||
|
||||
/**
|
||||
* 添加用户
|
||||
*/
|
||||
const addEvent = () => {
|
||||
editUserDialog.value.setFormData()
|
||||
editUserDialog.value.showDialog = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑用户
|
||||
* @param data
|
||||
*/
|
||||
const editEvent = (data: any) => {
|
||||
editUserDialog.value.setFormData(data)
|
||||
editUserDialog.value.showDialog = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 锁定用户
|
||||
*/
|
||||
const lockEvent = (id: number) => {
|
||||
ElMessageBox.confirm(t('userLockTips'), t('warning'),
|
||||
{
|
||||
confirmButtonText: t('confirm'),
|
||||
cancelButtonText: t('cancel'),
|
||||
type: 'warning'
|
||||
}
|
||||
).then(() => {
|
||||
lockUser(id).then(() => {
|
||||
loadUserList()
|
||||
}).catch(() => {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 解锁用户
|
||||
*/
|
||||
const unlockEvent = (id: number) => {
|
||||
ElMessageBox.confirm(t('userUnlockTips'), t('warning'),
|
||||
{
|
||||
confirmButtonText: t('confirm'),
|
||||
cancelButtonText: t('cancel'),
|
||||
type: 'warning'
|
||||
}
|
||||
).then(() => {
|
||||
unlockUser(id).then(() => {
|
||||
loadUserList()
|
||||
}).catch(() => {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const deleteEvent = (uid: number) => {
|
||||
ElMessageBox.confirm(t('userDeleteTips'), t('warning'),
|
||||
{
|
||||
confirmButtonText: t('confirm'),
|
||||
cancelButtonText: t('cancel'),
|
||||
type: 'warning'
|
||||
}
|
||||
).then(() => {
|
||||
deleteUser(uid).then(() => {
|
||||
loadUserList()
|
||||
}).catch(() => {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
118
admin-vben/src/app/views/channel/aliapp/access.vue
Normal file
118
admin-vben/src/app/views/channel/aliapp/access.vue
Normal file
@@ -0,0 +1,118 @@
|
||||
<template>
|
||||
<!--支付宝小程序-->
|
||||
<div class="main-container">
|
||||
<el-card class="card !border-none" shadow="never">
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-page-title">{{ pageName }}</span>
|
||||
</div>
|
||||
|
||||
<el-tabs v-model="activeName" class="my-[20px]" @tab-change="handleClick">
|
||||
<el-tab-pane :label="t('weappAccessFlow')" name="/channel/aliapp" />
|
||||
</el-tabs>
|
||||
|
||||
<div class="p-[20px]">
|
||||
<h3 class="panel-title !text-sm">{{ t("weappInlet") }}</h3>
|
||||
|
||||
<el-row>
|
||||
<el-col :span="20">
|
||||
<el-steps :active="4" direction="vertical">
|
||||
<el-step>
|
||||
<template #title>
|
||||
<p class="text-[14px] font-[700]">
|
||||
{{ t("weappAttestation") }}
|
||||
</p>
|
||||
</template>
|
||||
<template #description>
|
||||
<span class="text-[#999]">{{ t("weappAttest") }}</span>
|
||||
<div class="mt-[20px] mb-[40px] h-[32px]">
|
||||
<el-button type="primary" @click="linkEvent('https://open.alipay.com/develop/manage')">{{ t("clickAccess") }}</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-step>
|
||||
|
||||
<el-step>
|
||||
<template #title>
|
||||
<p class="text-[14px] font-[700]">
|
||||
{{ t("weappSetting") }}
|
||||
</p>
|
||||
</template>
|
||||
<template #description>
|
||||
<span class="text-[#999]">{{ t("emplace") }}</span>
|
||||
<div class="mt-[20px] mb-[40px] h-[32px]">
|
||||
<el-button type="primary" plain @click="router.push('/channel/aliapp/config')">{{ t("weappSettingBtn") }}</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-step>
|
||||
|
||||
<el-step>
|
||||
<template #title>
|
||||
<p class="text-[14px] font-[700]">
|
||||
{{ t("uploadVersion") }}
|
||||
</p>
|
||||
</template>
|
||||
<template #description>
|
||||
<span class="text-[#999]">{{ t("releaseCourse") }}</span>
|
||||
<div class="mt-[20px] mb-[40px] h-[32px]"></div>
|
||||
</template>
|
||||
</el-step>
|
||||
|
||||
<el-step>
|
||||
<template #title>
|
||||
<p class="text-[14px] font-[700]">
|
||||
{{ t("completeAccess") }}
|
||||
</p>
|
||||
</template>
|
||||
<template #description>
|
||||
<span class="text-[#999]">{{ t("releaseCourse") }}</span>
|
||||
</template>
|
||||
</el-step>
|
||||
</el-steps>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="4">
|
||||
<div class="flex justify-center">
|
||||
<el-image class="w-[180px] h-[180px]" :src="qrCode ? img(qrCode) : ''">
|
||||
<template #error>
|
||||
<div class="w-[100%] h-[100%] flex items-center justify-center bg-[#f5f7fa]">
|
||||
<span>{{ qrCode ? t('fileErr') : t('emptyQrCode') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
</div>
|
||||
<div class="mt-[22px] text-center">
|
||||
<p class=" text-[12px]">{{ t('clickAccess2') }}</p>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import { img } from '@/utils/common'
|
||||
import { getAliappConfig } from '@/app/api/aliapp'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const pageName = route.meta.title
|
||||
|
||||
const activeName = ref('/channel/aliapp')
|
||||
const qrCode = ref<string>('')
|
||||
onMounted(async () => {
|
||||
const res = await getAliappConfig()
|
||||
qrCode.value = res.data.qr_code
|
||||
})
|
||||
const linkEvent = (url: string) => {
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
const handleClick = (val: any) => {
|
||||
router.push({ path: activeName.value })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
179
admin-vben/src/app/views/channel/aliapp/config.vue
Normal file
179
admin-vben/src/app/views/channel/aliapp/config.vue
Normal file
@@ -0,0 +1,179 @@
|
||||
<template>
|
||||
<!--支付宝配置-->
|
||||
<div class="main-container">
|
||||
|
||||
<el-card class="card !border-none" shadow="never">
|
||||
<el-page-header :content="pageName" :icon="ArrowLeft" @back="back()" />
|
||||
</el-card>
|
||||
|
||||
<el-form class="page-form mt-[15px]" :model="formData" label-width="150px" ref="formRef" v-loading="loading">
|
||||
<el-card class="box-card !border-none" shadow="never">
|
||||
<h3 class="panel-title !text-sm">{{ t('aliappSet') }}</h3>
|
||||
|
||||
<el-form-item :label="t('aliappName')">
|
||||
<el-input v-model.trim="formData.name" :placeholder="t('aliappNamePlaceholder')" class="input-width" clearable />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('aliappQrcode')">
|
||||
<upload-image v-model="formData.qrcode" />
|
||||
<div class="form-tip">{{ t('aliappQrcodeTips') }}</div>
|
||||
</el-form-item>
|
||||
</el-card>
|
||||
|
||||
<el-card class="box-card !border-none mt-[15px]" shadow="never">
|
||||
<h3 class="panel-title !text-sm">{{ t('aliappDevelopInfo') }}</h3>
|
||||
|
||||
<el-form-item :label="t('aliappOriginal')">
|
||||
<el-input v-model.trim="formData.private_key" :placeholder="t('aliappOriginalPlaceholder')" class="input-width" clearable />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('aliappAppid')">
|
||||
<el-input v-model.trim="formData.app_id" :placeholder="t('appidPlaceholder')" class="input-width" clearable />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('countersignType')">
|
||||
{{ t('certificate') }}
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('publicKey')">
|
||||
<div class="input-width">
|
||||
<upload-file v-model="formData.public_key_crt" api="sys/document/aliyun" />
|
||||
</div>
|
||||
<div class="form-tip">{{ t('publicKeyTips') }}</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('alipayPublicKey')">
|
||||
<div class="input-width">
|
||||
<upload-file v-model="formData.alipay_public_key_crt" api="sys/document/aliyun" />
|
||||
</div>
|
||||
<div class="form-tip">{{ t('alipayPublicKeyTips') }}</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('alipayWithCrt')">
|
||||
<div class="input-width">
|
||||
<upload-file v-model="formData.alipay_with_crt" api="sys/document/aliyun" />
|
||||
</div>
|
||||
<div class="form-tip">{{ t('alipayWithCrtTips') }}</div>
|
||||
</el-form-item>
|
||||
</el-card>
|
||||
|
||||
<el-card class="box-card !border-none mt-[15px]" shadow="never">
|
||||
<h3 class="panel-title !text-sm">{{ t('theServerSetting') }}</h3>
|
||||
|
||||
<el-form-item label="AESKey">
|
||||
<el-input v-model.trim="formData.aes_key" :placeholder="t('AESKeyPlaceholder')" class="input-width" show-word-limit clearable />
|
||||
</el-form-item>
|
||||
</el-card>
|
||||
|
||||
<el-card class="box-card !border-none mt-[15px]" shadow="never">
|
||||
<h3 class="panel-title !text-sm">{{ t('functionSetting') }}</h3>
|
||||
|
||||
<el-form-item :label="t('serveWhiteList')">
|
||||
<el-input :model-value="formData.request_url" class="input-width" :readonly="true">
|
||||
<template #append>
|
||||
<div class="cursor-pointer" @click="copyEvent(formData.request_url)">{{ t('copy') }}</div>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</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>
|
||||
import { reactive, ref, watch } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import { setAliappConfig, getAliappConfig, getAliappStatic } from '@/app/api/aliapp'
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
import { ElMessage, FormInstance } from 'element-plus'
|
||||
import { ArrowLeft } from '@element-plus/icons-vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const pageName = route.meta.title
|
||||
const back = () => {
|
||||
router.push('/channel/aliapp')
|
||||
}
|
||||
|
||||
const loading = ref(true)
|
||||
|
||||
const formData = reactive<Record<string, string>>({
|
||||
name: '',
|
||||
qrcode: '',
|
||||
private_key: '',
|
||||
app_id: '',
|
||||
aes_key: '',
|
||||
public_key_crt: '',
|
||||
alipay_public_key_crt: '',
|
||||
alipay_with_crt: '',
|
||||
request_url: ''
|
||||
})
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
/**
|
||||
* 获取支付宝配置
|
||||
*/
|
||||
getAliappConfig().then(res => {
|
||||
Object.assign(formData, res.data)
|
||||
loading.value = false
|
||||
})
|
||||
/**
|
||||
* 获取支付宝静态资源
|
||||
*/
|
||||
getAliappStatic().then(res => {
|
||||
formData.request_url = res.data.domain
|
||||
})
|
||||
|
||||
/**
|
||||
* 复制
|
||||
*/
|
||||
const { copy, isSupported, copied } = useClipboard()
|
||||
const copyEvent = (text: string) => {
|
||||
if (!isSupported.value) {
|
||||
ElMessage({
|
||||
message: t('notSupportCopy'),
|
||||
type: 'warning'
|
||||
})
|
||||
return
|
||||
}
|
||||
copy(text)
|
||||
}
|
||||
|
||||
watch(copied, () => {
|
||||
if (copied.value) {
|
||||
ElMessage({
|
||||
message: t('copySuccess'),
|
||||
type: 'success'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 保存
|
||||
*/
|
||||
const save = async (formEl: FormInstance | undefined) => {
|
||||
if (loading.value || !formEl) return
|
||||
|
||||
await formEl.validate(async (valid) => {
|
||||
if (valid) {
|
||||
loading.value = true
|
||||
setAliappConfig(formData).then(() => {
|
||||
loading.value = false
|
||||
}).catch(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
132
admin-vben/src/app/views/channel/aliapp/course.vue
Normal file
132
admin-vben/src/app/views/channel/aliapp/course.vue
Normal file
@@ -0,0 +1,132 @@
|
||||
<template>
|
||||
<!--配置教程-->
|
||||
<div class="main-container">
|
||||
|
||||
<el-card class="card !border-none" shadow="never">
|
||||
<el-page-header :content="pageName" :icon="ArrowLeft" @back="back()" />
|
||||
</el-card>
|
||||
|
||||
<el-card class="box-card mt-[15px] !border-none" shadow="never">
|
||||
<div class="flex">
|
||||
<div class="min-w-[60px]">
|
||||
<span class="flex justify-center items-center block w-[40px] h-[40px] border-[1px] border-primary rounded-[999px] text-primary">1</span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="flex items-center text-[14px]">{{ t('alipayCourseTipsOne1') }}--<el-button link type="primary" @click="linkEvent">{{ t('alipayCourseTipsOne2') }}</el-button>, {{ t('alipayCourseTipsOne3') }}</p>
|
||||
<div class="w-[100%] mt-[10px]">
|
||||
<img class="w-[100%]" src="@/app/assets/images/setting/alipay1.png" />
|
||||
</div>
|
||||
<p class="flex items-center text-[14px] mt-[20px]">{{ t('alipayCourseTipsTwo1') }}</p>
|
||||
<div class="w-[100%] mt-[10px]">
|
||||
<img class="w-[100%]" src="@/app/assets/images/setting/alipay2.png" />
|
||||
</div>
|
||||
<div class="w-[100%] mt-[10px]">
|
||||
<img class="w-[100%]" src="@/app/assets/images/setting/alipay3.png" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex mt-[40px]">
|
||||
<div class="min-w-[60px]">
|
||||
<span class="flex justify-center items-center block w-[40px] h-[40px] border-[1px] border-primary rounded-[999px] text-primary">2</span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="flex items-center text-[14px]">{{ t('alipayCourseTipsTwo2') }}</p>
|
||||
<div class="w-[100%] mt-[10px] flex flex-wrap">
|
||||
<div class="w-[100%]">
|
||||
<img class="w-[100%]" src="@/app/assets/images/setting/alipay4.png" />
|
||||
</div>
|
||||
<div>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="6">
|
||||
<div class="w-[100%]">
|
||||
<img class="w-[100%]" src="@/app/assets/images/setting/alipay4_1.jpg" />
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="w-[100%]">
|
||||
<img class="w-[100%]" src="@/app/assets/images/setting/alipay4_2.jpg" />
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="w-[100%]">
|
||||
<img class="w-[100%]" src="@/app/assets/images/setting/alipay4_3.jpg" />
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="w-[100%]">
|
||||
<img class="w-[100%]" src="@/app/assets/images/setting/alipay4_4.jpg" />
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex mt-[40px]">
|
||||
<div class="min-w-[60px]">
|
||||
<span class="flex justify-center items-center block w-[40px] h-[40px] border-[1px] border-primary rounded-[999px] text-primary">3</span>
|
||||
</div>
|
||||
<div>
|
||||
<!-- <span class="text-primary">{{ t('alipayCourseTipsThree2') }}</span> -->
|
||||
<p class="flex items-center text-[14px]">{{ t('alipayCourseTipsThree1') }}
|
||||
</p>
|
||||
<div class="w-[100%] mt-[10px]">
|
||||
<img class="w-[100%]" src="@/app/assets/images/setting/alipay5.png" />
|
||||
</div>
|
||||
<div class="w-[100%] mt-[10px]">
|
||||
<img class="w-[100%]" src="@/app/assets/images/setting/alipay6.png" />
|
||||
</div>
|
||||
<p class="flex items-center text-[14px] mt-[20px]">{{ t('alipayCourseTipsThree2') }}</p>
|
||||
<div class="w-[100%] mt-[10px]">
|
||||
<img class="w-[100%]" src="@/app/assets/images/setting/alipay7.png" />
|
||||
</div>
|
||||
<p class="flex items-center text-[14px] mt-[20px]">{{ t('alipayCourseTipsThree3') }}</p>
|
||||
<div class="w-[100%] mt-[10px]">
|
||||
<img class="w-[100%]" src="@/app/assets/images/setting/alipay8.png" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import { getWechatConfig } from '@/app/api/wechat'
|
||||
import { ArrowLeft } from '@element-plus/icons-vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const back = () => {
|
||||
router.push('/channel/aliapp')
|
||||
}
|
||||
const pageName = route.meta.title
|
||||
const loading = ref(true)
|
||||
const formData = reactive<Record<string, string>>({
|
||||
wechat_name: '',
|
||||
wechat_original: '',
|
||||
app_id: '',
|
||||
app_secret: '',
|
||||
qr_code: '',
|
||||
token: '',
|
||||
encoding_aes_key: '',
|
||||
encryption_type: 'not_encrypt'
|
||||
})
|
||||
|
||||
/**
|
||||
* 获取微信配置
|
||||
*/
|
||||
getWechatConfig().then(res => {
|
||||
Object.assign(formData, res.data)
|
||||
loading.value = false
|
||||
})
|
||||
|
||||
const linkEvent = () => {
|
||||
window.open('https://open.alipay.com/develop/manage', '_blank')
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
130
admin-vben/src/app/views/channel/app/access.vue
Normal file
130
admin-vben/src/app/views/channel/app/access.vue
Normal file
@@ -0,0 +1,130 @@
|
||||
<template>
|
||||
<!--微信公众号-->
|
||||
<div class="main-container">
|
||||
<el-card class="card !border-none" shadow="never">
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-page-title">{{ pageName }}</span>
|
||||
</div>
|
||||
|
||||
<el-tabs v-model="activeName" class="my-[20px]" @tab-change="handleClick">
|
||||
<el-tab-pane :label="t('accessFlow')" name="/channel/app" />
|
||||
<el-tab-pane :label="t('versionManage')" name="/channel/app/version" />
|
||||
</el-tabs>
|
||||
|
||||
<div class="p-[20px]">
|
||||
<h3 class="panel-title !text-sm">{{ t("appInlet") }}</h3>
|
||||
|
||||
<el-row>
|
||||
<el-col :span="20">
|
||||
<el-steps class="!mt-[10px]" :active="3" direction="vertical">
|
||||
<el-step>
|
||||
<template #title>
|
||||
<p class="text-[14px] font-[700]">
|
||||
{{ t("uniappApp") }}
|
||||
</p>
|
||||
</template>
|
||||
<template #description>
|
||||
<span class="text-[#999]">{{ t("appAttestation1") }}</span>
|
||||
<div class="mt-[20px] mb-[40px] h-[32px]">
|
||||
<el-button type="primary" @click="linkEvent('https://dcloud.io/')">{{ t("toCreate") }}</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-step>
|
||||
|
||||
<el-step>
|
||||
<template #title>
|
||||
<p class="text-[14px] font-[700]">
|
||||
{{ t("appSetting") }}
|
||||
</p>
|
||||
</template>
|
||||
<template #description>
|
||||
<!-- <span class="text-[#999]">{{ t("wechatSetting1") }}</span>-->
|
||||
<div class="mt-[20px] mb-[40px] h-[32px]">
|
||||
<el-button type="primary" @click="router.push('/channel/app/config')">{{ t("settingInfo") }}</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-step>
|
||||
|
||||
<el-step>
|
||||
<template #title>
|
||||
<p class="text-[14px] font-[700]">
|
||||
{{ t("versionManage") }}
|
||||
</p>
|
||||
</template>
|
||||
<template #description>
|
||||
<!-- <span class="text-[#999]">{{ t("wechatAccess") }}</span>-->
|
||||
<div class="mt-[20px] mb-[40px] h-[32px]">
|
||||
<el-button type="primary" plain @click="router.push('/channel/app/version')">{{ t("releaseVersion") }}</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-step>
|
||||
</el-steps>
|
||||
</el-col>
|
||||
|
||||
</el-row>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import { img } from '@/utils/common'
|
||||
import { getWechatConfig } from '@/app/api/wechat'
|
||||
import { getAuthorizationUrl } from '@/app/api/wxoplatform'
|
||||
import { getWxoplatform } from '@/app/api/sys'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const pageName = route.meta.title
|
||||
|
||||
const activeName = ref('/channel/app')
|
||||
const qrcode = ref('')
|
||||
const wechatConfig = ref({})
|
||||
const oplatformConfig = ref({})
|
||||
|
||||
const onShowGetWechatConfig = async () => {
|
||||
await getWechatConfig().then(({ data }) => {
|
||||
wechatConfig.value = data
|
||||
qrcode.value = data.qr_code
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await onShowGetWechatConfig()
|
||||
|
||||
await getWxoplatform().then(({ data }) => {
|
||||
oplatformConfig.value = data
|
||||
})
|
||||
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
onShowGetWechatConfig()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('visibilitychange', () => {
|
||||
})
|
||||
})
|
||||
|
||||
const linkEvent = (url: string) => {
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
|
||||
const handleClick = (val: any) => {
|
||||
router.push({ path: activeName.value })
|
||||
}
|
||||
|
||||
const authBindWechat = () => {
|
||||
getAuthorizationUrl().then(({ data }) => {
|
||||
window.open(data)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,285 @@
|
||||
<template>
|
||||
<el-dialog v-model="showDialog" :title="formData.id ? t('updateAppVersion') : t('addAppVersion')" width="60%" class="diy-dialog-wrap" :destroy-on-close="true">
|
||||
|
||||
<el-form :model="formData" label-width="120px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
|
||||
|
||||
<div v-show="step == 1" class="h-[400px]">
|
||||
<el-form-item :label="t('versionName')" prop="version_name">
|
||||
<el-input v-model="formData.version_name" clearable :placeholder="t('versionNamePlaceholder')" class="input-width" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('versionCode')" prop="version_code">
|
||||
<el-input v-model="formData.version_code" clearable :placeholder="t('versionCodePlaceholder')" class="input-width" />
|
||||
<div class="form-tip">{{ t('versionCodeTips') }}</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('versionDesc')" prop="version_desc">
|
||||
<el-input v-model="formData.version_desc" type="textarea" rows="6" clearable :placeholder="t('versionDescPlaceholder')" class="input-width" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('platform')" prop="platform">
|
||||
<el-radio-group v-model="formData.platform">
|
||||
<el-radio :label="key" size="large" v-for="(item, key) in appPlatform">{{ item }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('isForcedUpgrade')" prop="is_forced_upgrade">
|
||||
<el-switch v-model="formData.is_forced_upgrade" :active-value="1" :inactive-value="0"></el-switch>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div v-show="step == 2" class="h-[400px]">
|
||||
<el-scrollbar>
|
||||
<el-form-item :label="t('upgradeType')">
|
||||
<el-radio-group v-model="formData.upgrade_type">
|
||||
<el-radio label="app" size="large">APP整包升级</el-radio>
|
||||
<el-radio label="hot" size="large">wgt资源包升级</el-radio>
|
||||
<el-radio label="market" size="large">应用市场升级</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('resourceFile')" v-show="formData.upgrade_type == 'app'">
|
||||
<el-radio-group v-model="formData.package_type">
|
||||
<el-radio label="file" size="large">上传资源包</el-radio>
|
||||
<el-radio label="cloud" size="large">云打包</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<div v-show="formData.package_type == 'file' && formData.upgrade_type != 'market'">
|
||||
<el-form-item label="" prop="package_path">
|
||||
<div class="input-width" >
|
||||
<upload-file v-model="formData.package_path" :accept="accept" api="sys/document/app_package"></upload-file>
|
||||
</div>
|
||||
<div class="form-tip" v-if="formData.upgrade_type == 'app'">{{ t('androidResourceFileTips') }}</div>
|
||||
<div class="form-tip" v-if="formData.upgrade_type == 'hot'">{{ t('iosResourceFileTips') }}</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div v-show="formData.upgrade_type == 'market'">
|
||||
<el-form-item label="应用市场链接" prop="package_path">
|
||||
<el-input v-model="formData.package_path" clearable class="input-width" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div v-show="formData.package_type == 'cloud'">
|
||||
<el-form-item :label="t('icon')">
|
||||
<div class="input-width" >
|
||||
<upload-file v-model="formData.build.icon" accept=".zip" api="sys/document/applet"></upload-file>
|
||||
</div>
|
||||
<div class="form-tip !leading-[1.5]">应用图标和启动界面图片 icon.png为应用的图标 push.png为推送消息的图标 splash.png为应用启动页的图标 将icon.png、push.png、splash.png放置到drawable,drawable-ldpi,drawable-mdpi,drawable-hdpi,drawable-xhdpi,drawable-xxhdpi文件夹下压缩成压缩包上传,
|
||||
具体详情可查看 <span class="text-primary cursor-pointer" @click="windowOpen('https://nativesupport.dcloud.net.cn/AppDocs/usesdk/android.html')">uniapp App离线打包</span>中“配置应用图标和启动界面”片段</div>
|
||||
<div class="form-tip !leading-[1.5]">只支持上传.zip 在drawable的根目录进行压缩</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('certType')">
|
||||
<el-radio-group v-model="formData.cert.type">
|
||||
<el-radio label="public" size="large">公共证书</el-radio>
|
||||
<el-radio label="private" size="large">自有证书</el-radio>
|
||||
</el-radio-group>
|
||||
<div class="form-tip">{{ t('publicCertTips') }}</div>
|
||||
<div class="form-tip !leading-[1.5]">{{ t('privateCertTips') }}<span class="text-primary cursor-pointer" @click="windowOpen('https://ask.dcloud.net.cn/article/35777')">《Android平台签名证书说明》</span></div>
|
||||
<div class="form-tip flex items-center">证书可以自己生成也可通过niucloud提供的<span class="text-primary cursor-pointer" @click="generateSingCertRef.open()">证书生成工具生成</span></div>
|
||||
</el-form-item>
|
||||
<div v-show="formData.cert.type == 'private'">
|
||||
<el-form-item :label="t('certFile')" prop="cert.cert_file">
|
||||
<div class="input-width" >
|
||||
<upload-file v-model="formData.cert.file" accept="" api="sys/document/android_cert"></upload-file>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('certAlias')" prop="cert.key_alias">
|
||||
<el-input v-model="formData.cert.key_alias" clearable :placeholder="t('versionDescPlaceholder')" class="input-width" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('certKeyPassword')" prop="cert.key_password">
|
||||
<el-input v-model="formData.cert.key_password" clearable :placeholder="t('versionDescPlaceholder')" class="input-width" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('certStorePassword')" prop="cert.store_password">
|
||||
<el-input v-model="formData.cert.store_password" clearable :placeholder="t('versionDescPlaceholder')" class="input-width" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button>
|
||||
<view v-show="step == 1">
|
||||
<el-button type="primary" class="ml-3" @click="step = 2">{{
|
||||
t('next')
|
||||
}}</el-button>
|
||||
</view>
|
||||
<view v-show="step == 2">
|
||||
<el-button type="primary" class="ml-3" @click="step = 1">{{
|
||||
t('prev')
|
||||
}}</el-button>
|
||||
<el-button type="primary" :loading="loading" @click="confirm(formRef)">{{ t('confirm') }}</el-button>
|
||||
</view>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<generate-sing-cert ref="generateSingCertRef"/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed, watch } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import { addVersion, editVersion, getVersionInfo, getAppPlatform } from '@/app/api/app'
|
||||
import GenerateSingCert from '@/app/views/channel/app/components/generate-sing-cert.vue'
|
||||
|
||||
const showDialog = ref(false)
|
||||
const loading = ref(false)
|
||||
const appPlatform = ref({})
|
||||
const step = ref(1)
|
||||
const generateSingCertRef = ref(null)
|
||||
|
||||
const getAppPlatformFn = async () => {
|
||||
await getAppPlatform().then(({ data }) => {
|
||||
appPlatform.value = data
|
||||
initialFormData.platform = Object.keys(data)[0]
|
||||
})
|
||||
}
|
||||
getAppPlatformFn()
|
||||
|
||||
const accept = computed(() => {
|
||||
if (formData.upgrade_type == 'app') return '.apk'
|
||||
if (formData.upgrade_type == 'hot') return '.wgt'
|
||||
return ''
|
||||
})
|
||||
|
||||
/**
|
||||
* 表单数据
|
||||
*/
|
||||
const initialFormData = {
|
||||
id: '',
|
||||
version_code: '',
|
||||
version_name: '',
|
||||
version_desc: '',
|
||||
platform: '',
|
||||
is_forced_upgrade: 0,
|
||||
package_path: '',
|
||||
package_type: 'file',
|
||||
upgrade_type: 'app',
|
||||
build: {
|
||||
icon: '',
|
||||
},
|
||||
cert: {
|
||||
type: 'public',
|
||||
key_alias: '',
|
||||
key_password: '',
|
||||
store_password: ''
|
||||
}
|
||||
}
|
||||
const formData: Record<string, any> = reactive({ ...initialFormData })
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
watch(() => formData.upgrade_type, () => {
|
||||
if (formData.upgrade_type == 'app' || formData.upgrade_type == 'hot') {
|
||||
formData.package_type = 'file'
|
||||
}
|
||||
formData.package_path = ''
|
||||
formData.cert.type = 'public'
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = computed(() => {
|
||||
return {
|
||||
version_code: [
|
||||
{ required: true, message: t('versionCodePlaceholder'), trigger: 'blur' },
|
||||
{
|
||||
trigger: 'blur',
|
||||
validator: (rule: any, value: any, callback: any) => {
|
||||
if (isNaN(value) || !/^\d{0,10}$/.test(value)) {
|
||||
callback(new Error(t('versionCodeTips')))
|
||||
} else if (value < 0) {
|
||||
callback(new Error(t('versionCodeTips')))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
version_name: [
|
||||
{ required: true, message: t('versionNamePlaceholder'), trigger: 'blur' }
|
||||
],
|
||||
package_path: [
|
||||
{ required: formData.upgrade_type != 'market' && formData.package_type == 'file', message: '请上传资源文件', trigger: 'blur' },
|
||||
{ required: formData.upgrade_type == 'market', message: '请输入应用市场链接', trigger: 'blur' }
|
||||
],
|
||||
'build.icon': [
|
||||
{ required: formData.package_type == 'cloud', message: '请上传图标文件', trigger: 'blur' },
|
||||
],
|
||||
'cert.cert_file': [
|
||||
{ required: formData.package_type == 'cloud' && formData.cert.type == 'private', message: '请上传证书文件', trigger: 'blur' }
|
||||
],
|
||||
'cert.key_alias': [
|
||||
{ required: formData.package_type == 'cloud' && formData.cert.type == 'private', message: '请输入证书别名', trigger: 'blur' }
|
||||
],
|
||||
'cert.key_password': [
|
||||
{ required: formData.package_type == 'cloud' && formData.cert.type == 'private', message: '请上传证书密码', trigger: 'blur' }
|
||||
],
|
||||
'cert.store_password': [
|
||||
{ required: formData.package_type == 'cloud' && formData.cert.type == 'private', message: '请上传证书库密码', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['complete'])
|
||||
|
||||
/**
|
||||
* 确认
|
||||
* @param formEl
|
||||
*/
|
||||
const confirm = async (formEl: FormInstance | undefined) => {
|
||||
if (loading.value || !formEl) return
|
||||
const save = formData.id ? editVersion : addVersion
|
||||
|
||||
await formEl.validate(async (valid) => {
|
||||
if (valid) {
|
||||
loading.value = true
|
||||
|
||||
save(formData).then(res => {
|
||||
loading.value = false
|
||||
showDialog.value = false
|
||||
emit('complete')
|
||||
}).catch(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const setFormData = async (row: any = null) => {
|
||||
Object.assign(formData, initialFormData)
|
||||
loading.value = true
|
||||
if (row) {
|
||||
const data = await (await getVersionInfo(row.id)).data
|
||||
if (data) Object.keys(formData).forEach((key: string) => {
|
||||
if (data[key] != undefined) formData[key] = data[key]
|
||||
})
|
||||
}
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
watch(() => showDialog.value, () => {
|
||||
step.value = 1
|
||||
})
|
||||
|
||||
const windowOpen = (url: string) => {
|
||||
window.open(url)
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
showDialog,
|
||||
setFormData
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
<style lang="scss">
|
||||
.diy-dialog-wrap .el-form-item__label{
|
||||
height: auto !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,156 @@
|
||||
<template>
|
||||
<el-dialog v-model="showDialog" title="生成Android证书" width="50%" class="diy-dialog-wrap" :destroy-on-close="true">
|
||||
<el-form :model="formData" label-width="120px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
|
||||
|
||||
<el-form-item label="证书别名" prop="key_alias">
|
||||
<el-input v-model="formData.key_alias" clearable placeholder="" class="input-width" />
|
||||
<div class="form-tip">只支持字母,从证书文件中读取证书时需要别名</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="证书密码" prop="key_password">
|
||||
<el-input v-model="formData.key_password" clearable placeholder="" class="input-width" />
|
||||
<div class="form-tip">只支持字母或数字,密码至少 6 位,设置好后请牢记密码</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="证书库密码" prop="store_password">
|
||||
<el-input v-model="formData.store_password" clearable placeholder="" class="input-width" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="有效期" prop="limit">
|
||||
<div class="flex items-center">
|
||||
<el-input v-model="formData.limit" clearable placeholder="" class="w-[100px]" />
|
||||
<div class="form-tip ml-2">年</div>
|
||||
</div>
|
||||
<div class="form-tip">限 1 - 100 年之间</div>
|
||||
</el-form-item>
|
||||
|
||||
<div class="text-primary cursor-pointer pl-[120px] my-[10px]" @click="moreInfo = !moreInfo">
|
||||
{{ moreInfo ? '点击收起' : '点击展开填写更多信息 '}}
|
||||
</div>
|
||||
|
||||
<view v-show="moreInfo">
|
||||
<el-form-item label="域名">
|
||||
<el-input v-model="formData.cn" clearable placeholder="" class="input-width" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="组织名称">
|
||||
<el-input v-model="formData.o" clearable placeholder="" class="input-width" />
|
||||
<div class="form-tip">如公司名称或者其他名称</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="部门">
|
||||
<el-input v-model="formData.ou" clearable placeholder="" class="input-width" />
|
||||
<div class="form-tip">部门名称,如 IT 部、研发部等。</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="国家地区">
|
||||
<el-input v-model="formData.c" clearable placeholder="" class="input-width" />
|
||||
<div class="form-tip">输入国家/地区代号(两个字母),中国为CN</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="省份">
|
||||
<el-input v-model="formData.st" clearable placeholder="" class="input-width" />
|
||||
<div class="form-tip">所在省份名称,如Beijing</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="城市">
|
||||
<el-input v-model="formData.l" clearable placeholder="" class="input-width" />
|
||||
<div class="form-tip">所在城市名称,如Beijing</div>
|
||||
</el-form-item>
|
||||
</view>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="showDialog = false">取消</el-button>
|
||||
<el-button type="primary" :loading="loading" @click="confirm(formRef)">生成</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import { generateSingCert } from '@/app/api/app'
|
||||
import { img } from '@/utils/common'
|
||||
|
||||
const showDialog = ref(false)
|
||||
const moreInfo = ref(false)
|
||||
const loading = ref(false)
|
||||
|
||||
const initialFormData = {
|
||||
key_alias: '',
|
||||
key_password: '',
|
||||
store_password: '',
|
||||
limit: 30,
|
||||
cn: '',
|
||||
o: '',
|
||||
ou: '',
|
||||
c: '',
|
||||
st: '',
|
||||
l: ''
|
||||
}
|
||||
const formData: Record<string, any> = reactive({ ...initialFormData })
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = computed(() => {
|
||||
return {
|
||||
key_alias: [
|
||||
{ required: true, message: '请输入证书别名', trigger: 'blur' }
|
||||
],
|
||||
key_password: [
|
||||
{ required: true, message: '请输入证书密码', trigger: 'blur' }
|
||||
],
|
||||
store_password: [
|
||||
{ required: true, message: '请输入证书库密码', trigger: 'blur' }
|
||||
],
|
||||
limit: [
|
||||
{ required: true, message: '请输入有效期', trigger: 'blur' },
|
||||
{
|
||||
trigger: 'blur',
|
||||
validator: (rule: any, value: any, callback: any) => {
|
||||
if (isNaN(value) || !/^\d{0,10}$/.test(value)) {
|
||||
callback(new Error('有效期必须是数字'))
|
||||
} else if (value < 0) {
|
||||
callback(new Error('有效期必须为 1 - 100 之间的数字'))
|
||||
} else if (value > 100) {
|
||||
callback(new Error('有效期必须为 1 - 100 之间的数字'))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
organization_name: [
|
||||
{ required: true, message: '请输入所有者', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
const confirm = async (formEl: FormInstance | undefined) => {
|
||||
if (formEl) {
|
||||
await formEl.validate()
|
||||
loading.value = true
|
||||
|
||||
generateSingCert(formData).then(res => {
|
||||
loading.value = false
|
||||
showDialog.value = false
|
||||
window.open(img(res.data), '_blank')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const open = async (row: any = null) => {
|
||||
Object.assign(formData, initialFormData)
|
||||
showDialog.value = true
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
open
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
129
admin-vben/src/app/views/channel/app/config.vue
Normal file
129
admin-vben/src/app/views/channel/app/config.vue
Normal file
@@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<!--小程序配置-->
|
||||
<div class="main-container">
|
||||
|
||||
<el-card class="card !border-none" shadow="never">
|
||||
<el-page-header :content="pageName" :icon="ArrowLeft" @back="back()" />
|
||||
</el-card>
|
||||
|
||||
<el-form class="page-form mt-[15px]" :model="formData" label-width="170px" ref="formRef" :rules="formRules" v-loading="loading">
|
||||
<el-card class="box-card !border-none mt-[15px]" shadow="never">
|
||||
<h3 class="panel-title !text-sm">{{ t('appInfo') }}</h3>
|
||||
|
||||
<el-form-item :label="t('uniAppId')" prop="uni_app_id">
|
||||
<el-input v-model.trim="formData.uni_app_id" placeholder="" class="input-width" clearable/>
|
||||
<div class="form-tip flex items-center">
|
||||
{{ t('uniAppIdTips') }}
|
||||
<el-button link type="primary" @click="windowOpen('https://www.dcloud.io/')">{{ t('toCreate') }}</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('appName')" prop="app_name">
|
||||
<el-input v-model.trim="formData.app_name" placeholder="" class="input-width" clearable/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('androidAppKey')" prop="android_app_key">
|
||||
<el-input v-model.trim="formData.android_app_key" placeholder="" class="input-width" clearable/>
|
||||
<div class="form-tip">
|
||||
{{ t('androidAppKeyTips') }}
|
||||
<span class="text-primary cursor-pointer" @click="windowOpen('https://nativesupport.dcloud.net.cn/AppDocs/usesdk/appkey.html')">查看详情</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('applicationId')" prop="application_id">
|
||||
<el-input v-model.trim="formData.application_id" placeholder="" class="input-width" clearable/>
|
||||
<div class="form-tip">
|
||||
{{ t('applicationIdTips') }}
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-card>
|
||||
<el-card class="box-card !border-none mt-[15px]" shadow="never">
|
||||
<h3 class="panel-title !text-sm">{{ t('wechatAppInfo') }}</h3>
|
||||
|
||||
<el-form-item :label="t('wechatAppid')" prop="app_id">
|
||||
<el-input v-model.trim="formData.wechat_app_id" :placeholder="t('appidPlaceholder')" class="input-width" clearable/>
|
||||
<div class="form-tip">
|
||||
{{ t('wechatAppidTips') }}
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('wechatAppsecret')" prop="app_secret">
|
||||
<el-input v-model.trim="formData.wechat_app_secret" :placeholder="t('appSecretPlaceholder')" class="input-width" clearable />
|
||||
</el-form-item>
|
||||
</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>
|
||||
import { reactive, ref, watch, computed } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import { getAppConfig, setAppConfig } from '@/app/api/app'
|
||||
import { FormInstance } from "element-plus"
|
||||
import { useRoute, useRouter } from "vue-router"
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const pageName = route.meta.title
|
||||
const loading = ref(true)
|
||||
|
||||
const formData = reactive<Record<string, any>>({
|
||||
uni_app_id: '',
|
||||
app_name: '',
|
||||
android_app_key: '',
|
||||
application_id: '',
|
||||
wechat_app_id: '',
|
||||
wechat_app_secret: ''
|
||||
})
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = computed(() => {
|
||||
return {
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 获取app配置
|
||||
*/
|
||||
getAppConfig().then(res => {
|
||||
Object.assign(formData, res.data)
|
||||
loading.value = false
|
||||
})
|
||||
|
||||
/**
|
||||
* 保存
|
||||
*/
|
||||
const save = async (formEl: FormInstance | undefined) => {
|
||||
if (loading.value || !formEl) return
|
||||
|
||||
await formEl.validate(async (valid) => {
|
||||
if (valid) {
|
||||
loading.value = true
|
||||
setAppConfig(formData).then(() => {
|
||||
loading.value = false
|
||||
}).catch(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const windowOpen = (url: string) => {
|
||||
window.open(url)
|
||||
}
|
||||
|
||||
const back = () => {
|
||||
router.push('/channel/app')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
237
admin-vben/src/app/views/channel/app/version.vue
Normal file
237
admin-vben/src/app/views/channel/app/version.vue
Normal file
@@ -0,0 +1,237 @@
|
||||
<template>
|
||||
<div class="main-container">
|
||||
<el-card class="box-card !border-none" shadow="never">
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-lg">{{pageName}}</span>
|
||||
</div>
|
||||
|
||||
<el-tabs v-model="activeName" class="my-[20px]" @tab-change="handleClick">
|
||||
<el-tab-pane :label="t('accessFlow')" name="/channel/app" />
|
||||
<el-tab-pane :label="t('versionManage')" name="/channel/app/version" />
|
||||
</el-tabs>
|
||||
|
||||
<el-alert type="info">
|
||||
<template #default>
|
||||
<div class="flex items-center">
|
||||
<div>
|
||||
<p>使用云打包提交成功后请先不要离开该页面稍待几分钟等待打包结果的返回</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-alert>
|
||||
|
||||
<div class="mt-[20px]">
|
||||
<el-button type="primary" @click="addEvent">{{ t('addAppVersion') }}</el-button>
|
||||
</div>
|
||||
|
||||
<div class="mt-[10px]">
|
||||
<el-table :data="appVersionTable.data" size="large" v-loading="appVersionTable.loading">
|
||||
<template #empty>
|
||||
<span>{{ !appVersionTable.loading ? t('emptyData') : '' }}</span>
|
||||
</template>
|
||||
|
||||
<el-table-column type="index" width="90" :label="t('index')" />
|
||||
|
||||
<el-table-column prop="version_code" :label="t('versionCode')" min-width="120" :show-overflow-tooltip="true"/>
|
||||
|
||||
<el-table-column prop="version_name" :label="t('versionName')" min-width="120" :show-overflow-tooltip="true"/>
|
||||
|
||||
<el-table-column prop="version_desc" :label="t('versionDesc')" min-width="120" :show-overflow-tooltip="true"/>
|
||||
|
||||
<el-table-column prop="platform_name" :label="t('platform')" min-width="120" :show-overflow-tooltip="true"/>
|
||||
|
||||
<el-table-column prop="status_name" :label="t('status')" min-width="120" :show-overflow-tooltip="true"/>
|
||||
|
||||
<el-table-column prop="status" :label="t('isForcedUpgradeTitle')" min-width="120" align="center" :show-overflow-tooltip="true">
|
||||
<template #default="{ row }">
|
||||
{{ row.is_forced_upgrade ? '是' : '否' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="package_path" :label="t('packagePath')" min-width="120" :show-overflow-tooltip="true"/>
|
||||
|
||||
<el-table-column :label="t('releaseTime')" min-width="120" :show-overflow-tooltip="true">
|
||||
<template #default="{ row }">
|
||||
<text v-if="row.release_time != 0">
|
||||
{{ row.release_time }}
|
||||
</text>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="t('operation')" fixed="right" align="right" min-width="200px">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link v-if="row.release_time == 0" @click="editEvent(row)">{{ t('edit') }}</el-button>
|
||||
<el-button type="primary" link v-if="row.status == 'upload_success'" @click="releaseEvent(row)">{{ t('release') }}</el-button>
|
||||
<el-button type="primary" link v-if="row.status == 'create_fail'" @click="handleFailReason(row)">{{ t('failReason') }}</el-button>
|
||||
<el-button type="primary" link v-if="row.package_path && row.upgrade_type != 'market'" @click="downloadEvent(row)">{{ t('download') }}</el-button>
|
||||
<el-button type="primary" link @click="deleteEvent(row)">{{ t('delete') }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
</el-table>
|
||||
<div class="mt-[16px] flex justify-end">
|
||||
<el-pagination v-model:current-page="appVersionTable.page" v-model:page-size="appVersionTable.limit"
|
||||
layout="total, sizes, prev, pager, next, jumper" :total="appVersionTable.total"
|
||||
@size-change="loadAppVersionList()" @current-change="loadAppVersionList" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<edit ref="editAppVersionDialog" @complete="loadAppVersionList" />
|
||||
</el-card>
|
||||
|
||||
<el-dialog v-model="failReasonDialogVisible" :title="t('failReason')" width="60%">
|
||||
<el-scrollbar class="h-[60vh] w-full whitespace-pre-wrap p-[20px]">
|
||||
<div v-html="failReason"></div>
|
||||
</el-scrollbar>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import { img } from '@/utils/common'
|
||||
import { ElMessageBox, FormInstance } from 'element-plus'
|
||||
import { getVersionList, getBuildLog, deleteVersion, releaseVersion } from '@/app/api/app'
|
||||
import Edit from '@/app/views/channel/app/components/app-version-edit.vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const pageName = route.meta.title
|
||||
const activeName = ref('/channel/app/version')
|
||||
|
||||
const appVersionTable = reactive({
|
||||
page: 1,
|
||||
limit: 10,
|
||||
total: 0,
|
||||
loading: true,
|
||||
data: [],
|
||||
searchParam: {
|
||||
platfrom: ''
|
||||
}
|
||||
})
|
||||
|
||||
const handleClick = (val: any) => {
|
||||
router.push({ path: activeName.value })
|
||||
}
|
||||
|
||||
const searchFormRef = ref<FormInstance>()
|
||||
|
||||
/**
|
||||
* 获取app版本管理列表
|
||||
*/
|
||||
const loadAppVersionList = (page: number = 1) => {
|
||||
appVersionTable.loading = true
|
||||
appVersionTable.page = page
|
||||
|
||||
getVersionList({
|
||||
page: appVersionTable.page,
|
||||
limit: appVersionTable.limit,
|
||||
...appVersionTable.searchParam
|
||||
}).then(res => {
|
||||
appVersionTable.loading = false
|
||||
appVersionTable.data = res.data.data
|
||||
appVersionTable.total = res.data.total
|
||||
if (page == 1 && appVersionTable.data.length && appVersionTable.data[0].status == 'creating') getAppBuildLogFn(appVersionTable.data[0].task_key)
|
||||
}).catch(() => {
|
||||
appVersionTable.loading = false
|
||||
})
|
||||
}
|
||||
loadAppVersionList()
|
||||
|
||||
const editAppVersionDialog: Record<string, any> | null = ref(null)
|
||||
|
||||
/**
|
||||
* 添加app版本管理
|
||||
*/
|
||||
const addEvent = () => {
|
||||
editAppVersionDialog.value.setFormData()
|
||||
editAppVersionDialog.value.showDialog = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑app版本管理
|
||||
* @param data
|
||||
*/
|
||||
const editEvent = (data: any) => {
|
||||
editAppVersionDialog.value.setFormData(data)
|
||||
editAppVersionDialog.value.showDialog = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除app版本管理
|
||||
*/
|
||||
const deleteEvent = (id: number) => {
|
||||
ElMessageBox.confirm(t('appVersionDeleteTips'), t('warning'),
|
||||
{
|
||||
confirmButtonText: t('confirm'),
|
||||
cancelButtonText: t('cancel'),
|
||||
type: 'warning'
|
||||
}
|
||||
).then(() => {
|
||||
deleteVersion({ id }).then(() => {
|
||||
loadAppVersionList()
|
||||
}).catch(() => {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const releaseEvent = (data: any) => {
|
||||
ElMessageBox.confirm(t('appVersionReleaseTips'), t('warning'),
|
||||
{
|
||||
confirmButtonText: t('confirm'),
|
||||
cancelButtonText: t('cancel'),
|
||||
type: 'warning'
|
||||
}
|
||||
).then(() => {
|
||||
releaseVersion(data.id).then(() => {
|
||||
loadAppVersionList()
|
||||
}).catch(() => {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const getAppBuildLogFn = (key: string) => {
|
||||
getBuildLog(key).then(res => {
|
||||
if (res.data) {
|
||||
if (res.data.status == '') {
|
||||
setTimeout(() => {
|
||||
getAppBuildLogFn(key)
|
||||
}, 2000)
|
||||
} else {
|
||||
loadAppVersionList()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const failReason = ref('')
|
||||
const failReasonDialogVisible = ref(false)
|
||||
const handleFailReason = (data: any) => {
|
||||
failReason.value = data.fail_reason
|
||||
failReasonDialogVisible.value = true
|
||||
}
|
||||
|
||||
const downloadEvent = (data: any) => {
|
||||
window.open(img(data.package_path), '_blank')
|
||||
}
|
||||
|
||||
const resetForm = (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
formEl.resetFields()
|
||||
loadAppVersionList()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* 多行超出隐藏 */
|
||||
.multi-hidden {
|
||||
word-break: break-all;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
</style>
|
||||
124
admin-vben/src/app/views/channel/h5/config.vue
Normal file
124
admin-vben/src/app/views/channel/h5/config.vue
Normal file
@@ -0,0 +1,124 @@
|
||||
<template>
|
||||
<!--H5端-->
|
||||
<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>
|
||||
|
||||
<el-form class="page-form mt-[20px]" :model="formData" label-width="150px" ref="formRef">
|
||||
<el-form-item :label="t('isOpen')">
|
||||
<el-switch v-model="formData.is_open"/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('h5DomainName')">
|
||||
<el-input :model-value="formData.request_url" class="input-width" :readonly="true">
|
||||
<template #append>
|
||||
<div class="cursor-pointer" @click="copyEvent(formData.request_url)">{{ t('copy') }}</div>
|
||||
</template>
|
||||
</el-input>
|
||||
<span class="ml-2 cursor-pointer visit-btn" @click="visitFn">{{t('clickVisit')}}</span>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<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>
|
||||
import { reactive, ref, watch } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import { setH5Config, getH5Config } from '@/app/api/h5'
|
||||
import { getUrl } from '@/app/api/sys'
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
import { ElMessage, FormInstance } from 'element-plus'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
const pageName = route.meta.title
|
||||
|
||||
const loading = ref(true)
|
||||
|
||||
const formData = reactive<Record<string, string | boolean | any>>({
|
||||
is_open: true,
|
||||
request_url: ''
|
||||
})
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
/**
|
||||
* 获取h5配置
|
||||
*/
|
||||
getH5Config().then(res => {
|
||||
Object.assign(formData, res.data)
|
||||
formData.is_open = Boolean(Number(formData.is_open))
|
||||
loading.value = false
|
||||
})
|
||||
/**
|
||||
* 获取h5域名
|
||||
*/
|
||||
getUrl().then(res => {
|
||||
formData.request_url = res.data.wap_url + '/'
|
||||
})
|
||||
|
||||
/**
|
||||
* 复制
|
||||
*/
|
||||
const { copy, isSupported, copied } = useClipboard()
|
||||
const copyEvent = (text: string) => {
|
||||
if (!isSupported.value) {
|
||||
ElMessage({
|
||||
message: t('notSupportCopy'),
|
||||
type: 'warning'
|
||||
})
|
||||
return
|
||||
}
|
||||
copy(text)
|
||||
}
|
||||
|
||||
watch(copied, () => {
|
||||
if (copied.value) {
|
||||
ElMessage({
|
||||
message: t('copySuccess'),
|
||||
type: 'success'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 点击访问
|
||||
const visitFn = () => {
|
||||
window.open(formData.request_url)
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存
|
||||
*/
|
||||
const save = async (formEl: FormInstance | undefined) => {
|
||||
if (loading.value || !formEl) return
|
||||
|
||||
await formEl.validate(async (valid) => {
|
||||
if (valid) {
|
||||
loading.value = true
|
||||
const data = { ...formData }
|
||||
data.is_open = Number(data.is_open)
|
||||
setH5Config(data).then(() => {
|
||||
loading.value = false
|
||||
}).catch(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.visit-btn{
|
||||
color:var(--el-color-primary);
|
||||
}
|
||||
</style>
|
||||
129
admin-vben/src/app/views/channel/pc/config.vue
Normal file
129
admin-vben/src/app/views/channel/pc/config.vue
Normal file
@@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<!--PC端-->
|
||||
<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>
|
||||
|
||||
<el-form class="page-form mt-[20px]" :model="formData" label-width="150px" ref="formRef">
|
||||
<!-- <el-form-item :label="t('preview')" prop="weapp_name">
|
||||
<img class="w-[500px]" src="@/app/assets/images/channel/preview.png" alt="">
|
||||
</el-form-item> -->
|
||||
|
||||
<el-form-item :label="t('isOpen')">
|
||||
<el-switch v-model="formData.is_open"/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('pCDomainName')">
|
||||
<el-input :model-value="formData.request_url" class="input-width" :readonly="true">
|
||||
<template #append>
|
||||
<div class="cursor-pointer" @click="copyEvent(formData.request_url)">{{ t('copy') }}</div>
|
||||
</template>
|
||||
</el-input>
|
||||
<span class="ml-2 cursor-pointer visit-btn" @click="visitFn">{{t('clickVisit')}}</span>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<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>
|
||||
import { reactive, ref, watch } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import { getUrl } from '@/app/api/sys'
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
import { ElMessage, FormInstance } from 'element-plus'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { getPcConfig, setPcConfig } from "@/app/api/pc"
|
||||
|
||||
const route = useRoute()
|
||||
const pageName = route.meta.title
|
||||
const loading = ref(true)
|
||||
|
||||
const formData = reactive<Record<string, string | boolean | any>>({
|
||||
is_open: false,
|
||||
request_url: ''
|
||||
})
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
/**
|
||||
* 获取pc域名
|
||||
*/
|
||||
getUrl().then(res => {
|
||||
formData.request_url = res.data.web_url + '/'
|
||||
loading.value = false
|
||||
})
|
||||
|
||||
/**
|
||||
* 获取pc配置
|
||||
*/
|
||||
getPcConfig().then(res => {
|
||||
Object.assign(formData, res.data)
|
||||
formData.is_open = Boolean(Number(formData.is_open))
|
||||
loading.value = false
|
||||
})
|
||||
|
||||
/**
|
||||
* 复制
|
||||
*/
|
||||
const { copy, isSupported, copied } = useClipboard()
|
||||
const copyEvent = (text: string) => {
|
||||
if (!isSupported.value) {
|
||||
ElMessage({
|
||||
message: t('notSupportCopy'),
|
||||
type: 'warning'
|
||||
})
|
||||
return
|
||||
}
|
||||
copy(text)
|
||||
}
|
||||
|
||||
watch(copied, () => {
|
||||
if (copied.value) {
|
||||
ElMessage({
|
||||
message: t('copySuccess'),
|
||||
type: 'success'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// 点击访问
|
||||
const visitFn = () => {
|
||||
window.open(formData.request_url)
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存
|
||||
*/
|
||||
const save = async (formEl: FormInstance | undefined) => {
|
||||
if (loading.value || !formEl) return
|
||||
|
||||
await formEl.validate(async (valid) => {
|
||||
if (valid) {
|
||||
loading.value = true
|
||||
const data = { ...formData }
|
||||
data.is_open = Number(data.is_open)
|
||||
setPcConfig(data).then(() => {
|
||||
loading.value = false
|
||||
}).catch(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.visit-btn {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
</style>
|
||||
163
admin-vben/src/app/views/channel/weapp/access.vue
Normal file
163
admin-vben/src/app/views/channel/weapp/access.vue
Normal file
@@ -0,0 +1,163 @@
|
||||
<template>
|
||||
<!--微信小程序-->
|
||||
<div class="main-container">
|
||||
<el-card class="card !border-none" shadow="never">
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-page-title">{{ pageName }}</span>
|
||||
</div>
|
||||
|
||||
<el-tabs v-model="activeName" class="mt-[20px]" @tab-change="handleClick">
|
||||
<el-tab-pane :label="t('weappAccessFlow')" name="/channel/weapp" />
|
||||
<el-tab-pane :label="t('subscribeMessage')" name="/channel/weapp/message" />
|
||||
<el-tab-pane :label="t('weappRelease')" name="/channel/weapp/code" />
|
||||
</el-tabs>
|
||||
|
||||
<div class="p-[20px]">
|
||||
<h3 class="panel-title !text-sm">{{ t("weappInlet") }}</h3>
|
||||
|
||||
<el-row>
|
||||
<el-col :span="20">
|
||||
<el-steps class="!mt-[10px]" :active="4" direction="vertical">
|
||||
<el-step>
|
||||
<template #title>
|
||||
<p class="text-[14px] font-[700]">
|
||||
{{ t("weappAttestation") }}
|
||||
</p>
|
||||
</template>
|
||||
<template #description>
|
||||
<span class="text-[#999]">{{ t("weappAttest") }}</span>
|
||||
<div class="mt-[20px] mb-[40px] h-[32px]">
|
||||
<el-button type="primary" @click="linkEvent('https://mp.weixin.qq.com/')">{{ t("clickAccess") }}</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-step>
|
||||
|
||||
<el-step>
|
||||
<template #title>
|
||||
<p class="text-[14px] font-[700]">
|
||||
{{ t("weappSetting") }}
|
||||
</p>
|
||||
</template>
|
||||
<template #description>
|
||||
<span class="text-[#999]">{{ t("emplace") }}</span>
|
||||
<div class="mt-[20px] mb-[40px] h-[32px]">
|
||||
<template v-if="oplatformConfig.app_id && oplatformConfig.app_secret">
|
||||
<el-button type="primary" @click="router.push('/channel/weapp/config')">{{ weappConfig.app_id ? t("seeConfig") : t("weappSettingBtn") }}</el-button>
|
||||
<el-button type="primary" plain @click="authBindWeapp">{{ weappConfig.is_authorization ? t("refreshAuth") : t("authWeapp") }}</el-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-button type="primary" @click="router.push('/channel/weapp/config')">{{ t("weappSettingBtn") }}</el-button>
|
||||
<el-button type="primary" plain @click="router.push('/channel/weapp/course')">配置教程</el-button>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</el-step>
|
||||
|
||||
<el-step>
|
||||
<template #title>
|
||||
<p class="text-[14px] font-[700]">
|
||||
{{ t("uploadVersion") }}
|
||||
</p>
|
||||
</template>
|
||||
<template #description>
|
||||
<span class="text-[#999]">{{ t("releaseCourse") }}</span>
|
||||
<div class="mt-[20px] mb-[40px] h-[32px]">
|
||||
<el-button type="primary" plain @click="router.push('/channel/weapp/code')">{{ t("weappRelease") }}</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-step>
|
||||
|
||||
<el-step>
|
||||
<template #title>
|
||||
<p class="text-[14px] font-[700]">
|
||||
{{ t("completeAccess") }}
|
||||
</p>
|
||||
</template>
|
||||
<template #description>
|
||||
<span class="text-[#999]">{{ t("releaseCourse") }}</span>
|
||||
<div class="mt-[20px] mb-[40px] h-[32px]"></div>
|
||||
</template>
|
||||
</el-step>
|
||||
</el-steps>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="4">
|
||||
<div class="flex justify-center">
|
||||
<el-image class="w-[180px] h-[180px]" :src="qrCode ? img(qrCode) : ''">
|
||||
<template #error>
|
||||
<div class="w-[100%] h-[100%] flex items-center justify-center bg-[#f5f7fa]">
|
||||
<span>{{ qrCode ? t('fileErr') : t('emptyQrCode') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
</div>
|
||||
<div class="mt-[22px] text-center">
|
||||
<p class=" text-[12px]">{{ t('clickAccess2') }}</p>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import { img } from '@/utils/common'
|
||||
import { getWeappConfig } from '@/app/api/weapp'
|
||||
import { getAuthorizationUrl } from '@/app/api/wxoplatform'
|
||||
import { getWxoplatform } from '@/app/api/sys'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const pageName = route.meta.title
|
||||
|
||||
const activeName = ref('/channel/weapp')
|
||||
const qrCode = ref('')
|
||||
const weappConfig = ref({})
|
||||
const oplatformConfig = ref({})
|
||||
|
||||
const onShowGetWeappConfig = async () => {
|
||||
await getWeappConfig().then(({ data }) => {
|
||||
weappConfig.value = data
|
||||
qrCode.value = data.qr_code
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await onShowGetWeappConfig()
|
||||
|
||||
await getWxoplatform().then(({ data }) => {
|
||||
oplatformConfig.value = data
|
||||
})
|
||||
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
onShowGetWeappConfig()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('visibilitychange', () => {
|
||||
|
||||
})
|
||||
})
|
||||
|
||||
const linkEvent = (url: string) => {
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
const handleClick = (val: any) => {
|
||||
router.push({ path: activeName.value })
|
||||
}
|
||||
const authBindWeapp = () => {
|
||||
getAuthorizationUrl().then(({ data }) => {
|
||||
window.open(data)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
332
admin-vben/src/app/views/channel/weapp/code.vue
Normal file
332
admin-vben/src/app/views/channel/weapp/code.vue
Normal file
@@ -0,0 +1,332 @@
|
||||
<template>
|
||||
<!--小程序发布-->
|
||||
<div class="main-container">
|
||||
<el-card class="card !border-none" shadow="never">
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-page-title">{{ pageName }}</span>
|
||||
</div>
|
||||
|
||||
<el-tabs v-model="activeName" class="my-[20px]" @tab-change="handleClick">
|
||||
<el-tab-pane :label="t('weappAccessFlow')" name="/channel/weapp" />
|
||||
<el-tab-pane :label="t('subscribeMessage')" name="/channel/weapp/message" />
|
||||
<el-tab-pane :label="t('weappRelease')" name="/channel/weapp/code" />
|
||||
</el-tabs>
|
||||
|
||||
<div class="mt-[20px]" v-if="!weappConfig.is_authorization">
|
||||
<el-button type="primary" @click="insert" :loading="uploading" :disabled="loading">{{ t('cloudRelease') }}</el-button>
|
||||
<el-button @click="localInsert" :disabled="loading">{{ t('localRelease') }}</el-button>
|
||||
</div>
|
||||
<div class="mt-[20px]" v-else>
|
||||
<el-button type="primary" @click="againUpload" :loading="uploading" :disabled="loading">{{ t('uploadWeapp') }}</el-button>
|
||||
</div>
|
||||
|
||||
<el-table class="mt-[15px]" :data="weappTableData.data" v-loading="weappTableData.loading" size="default">
|
||||
<template #empty>
|
||||
<span>{{ t('emptyData') }}</span>
|
||||
</template>
|
||||
|
||||
<el-table-column prop="version" :label="t('code')" align="left" />
|
||||
<el-table-column prop="status_name" :label="t('status')" align="left">
|
||||
<template #default="{ row }">
|
||||
<div>{{ row.status_name }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="create_time" :label="t('createTime')" align="center" />
|
||||
<el-table-column :label="t('operation')" fixed="right" align="right" min-width="120">
|
||||
<template #default="{ row, $index }">
|
||||
<template v-if="previewContent && $index == 0 && (row.status == 1 || row.status == 2) && weappTableData.page == 1">
|
||||
<el-tooltip :content="previewContent" raw-content effect="light">
|
||||
<el-button type="primary" link>{{ t('preview') }}</el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<el-button type="primary" link v-if="row.status == -1 || row.status == -2" @click="handleFailReason(row)">{{ t('failReason') }}</el-button>
|
||||
<el-button type="primary" link v-if="row.status == -2" @click="againUpload(row)" :loading="uploading">{{ t('againUpload') }}</el-button>
|
||||
<el-button type="primary" link v-if="row.status == 2" @click="undoAuditFn(row)">{{ t('undoAudit') }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="mt-[16px] flex justify-end">
|
||||
<el-pagination v-model:current-page="weappTableData.page" v-model:page-size="weappTableData.limit"
|
||||
layout="total, sizes, prev, pager, next, jumper" :total="weappTableData.total"
|
||||
@size-change="getWeappVersionListFn()" @current-change="getWeappVersionListFn" />
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-dialog v-model="dialogVisible" :title="t('codeDownTwoDesc')" width="30%" :before-close="handleClose">
|
||||
<el-form ref="ruleFormRef" :model="form" label-width="120px">
|
||||
<el-form-item prop="code" :label="t('code')">
|
||||
<el-input v-model.trim="form.code" :placeholder="t('codePlaceholder')" onkeyup="this.value = this.value.replace(/[^\d\.]/g,'');" />
|
||||
</el-form-item>
|
||||
<el-form-item prop="path" :label="t('path')">
|
||||
<upload-file v-model="form.path" :api="'weapp/upload'" :accept="'.zip'" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('content')">
|
||||
<el-input type="textarea" v-model.trim="form.content" :placeholder="t('contentPlaceholder')" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">{{ t('cancel') }}</el-button>
|
||||
<el-button type="primary" @click="insert">
|
||||
{{ t('confirm') }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="failReasonDialogVisible" :title="t('failReason')" width="60%">
|
||||
<el-scrollbar class="h-[60vh] w-full whitespace-pre-wrap p-[20px]">
|
||||
<div v-html="failReason"></div>
|
||||
</el-scrollbar>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog v-model="uploadSuccessShowDialog" :title="t('warning')" width="500px" draggable>
|
||||
<span v-html="t('uploadSuccessTips')"></span>
|
||||
<template #footer>
|
||||
<div class="flex justify-end">
|
||||
<el-button @click="knownToKnow" type="primary">{{ t('knownToKnow') }}</el-button>
|
||||
<el-button @click="uploadSuccessShowDialog = false" type="primary" plain>{{ t('confirm') }}</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from 'vue'
|
||||
import { setWeappVersion, getWeappPreview, getWeappVersionList, getWeappUploadLog, getWeappConfig } from '@/app/api/weapp'
|
||||
import { t } from '@/lang'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { getAuthInfo } from '@/app/api/module'
|
||||
import { getAppType } from '@/utils/common'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import { AnyObject } from '@/types/global'
|
||||
import Storage from '@/utils/storage'
|
||||
import { siteWeappCommit, undoAudit } from "@/app/api/wxoplatform";
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const pageName = route.meta.title
|
||||
const dialogVisible = ref(false)
|
||||
const loading = ref(true)
|
||||
const weappTableData:{
|
||||
page: number,
|
||||
limit: number,
|
||||
total: number,
|
||||
loading: boolean,
|
||||
data: AnyObject
|
||||
} = reactive({
|
||||
page: 1,
|
||||
limit: 10,
|
||||
total: 0,
|
||||
loading: false,
|
||||
data: []
|
||||
})
|
||||
const form = ref({
|
||||
desc: '',
|
||||
code: '',
|
||||
path: '',
|
||||
content: ''
|
||||
})
|
||||
const uploadSuccessShowDialog = ref(false)
|
||||
const authCode = ref('')
|
||||
|
||||
getAuthInfo().then(res => {
|
||||
if (res.data.data && res.data.data.auth_code) {
|
||||
authCode.value = res.data.data.auth_code
|
||||
getWeappPreviewImage()
|
||||
}
|
||||
loading.value = false
|
||||
}).catch(() => {
|
||||
loading.value = false
|
||||
})
|
||||
|
||||
const weappConfig = ref<{
|
||||
app_id:string,
|
||||
app_secret:string,
|
||||
is_authorization: number
|
||||
}>({
|
||||
app_id: '',
|
||||
app_secret: '',
|
||||
is_authorization: 0
|
||||
})
|
||||
getWeappConfig().then(res => {
|
||||
weappConfig.value = res.data
|
||||
})
|
||||
|
||||
const activeName = ref('/channel/weapp/code')
|
||||
const handleClick = (val: any) => {
|
||||
router.push({ path: activeName.value })
|
||||
}
|
||||
const ruleFormRef = ref<any>(null)
|
||||
|
||||
/**
|
||||
* 获取版本列表
|
||||
*/
|
||||
const getWeappVersionListFn = (page: number = 1) => {
|
||||
weappTableData.loading = true
|
||||
weappTableData.page = page
|
||||
|
||||
getWeappVersionList({
|
||||
page: weappTableData.page,
|
||||
limit: weappTableData.limit
|
||||
}).then(res => {
|
||||
weappTableData.loading = false
|
||||
weappTableData.data = res.data.data
|
||||
weappTableData.total = res.data.total
|
||||
if (page == 1 && weappTableData.data.length && weappTableData.data[0].status == 0) getWeappUploadLogFn(weappTableData.data[0].task_key)
|
||||
}).catch(() => {
|
||||
weappTableData.loading = false
|
||||
})
|
||||
}
|
||||
|
||||
getWeappVersionListFn()
|
||||
|
||||
const handleClose = () => {
|
||||
ruleFormRef.value.clearValidate()
|
||||
}
|
||||
|
||||
const uploading = ref(false)
|
||||
const insert = () => {
|
||||
if (!authCode.value) {
|
||||
authElMessageBox()
|
||||
return
|
||||
}
|
||||
if (!weappConfig.value.app_id) {
|
||||
configElMessageBox()
|
||||
return
|
||||
}
|
||||
|
||||
if (uploading.value) return
|
||||
uploading.value = true
|
||||
|
||||
previewContent.value = ''
|
||||
|
||||
setWeappVersion(form.value).then(res => {
|
||||
getWeappVersionListFn()
|
||||
getWeappPreviewImage()
|
||||
uploading.value = false
|
||||
}).catch(() => {
|
||||
uploading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
const localInsert = () => {
|
||||
ElMessageBox.alert(t('localInsertTips'), t('warning'), {
|
||||
confirmButtonText: t('confirm')
|
||||
})
|
||||
}
|
||||
|
||||
const previewContent = ref('')
|
||||
const getWeappPreviewImage = () => {
|
||||
if (!authCode.value) return
|
||||
getWeappPreview().then(res => {
|
||||
if (res.data) previewContent.value = `<img src="${ res.data }" class="w-[150px]">`
|
||||
}).catch()
|
||||
}
|
||||
|
||||
const getWeappUploadLogFn = (key: string) => {
|
||||
getWeappUploadLog(key).then(res => {
|
||||
const data = res.data.data ?? []
|
||||
if (data[0] && data[0].length) {
|
||||
const last = data[0][data[0].length - 1]
|
||||
if (last.code == 0) {
|
||||
getWeappVersionListFn()
|
||||
return
|
||||
}
|
||||
if (last.code == 1 && last.percent == 100) {
|
||||
getWeappVersionListFn()
|
||||
getWeappPreviewImage()
|
||||
!Storage.get('weappUploadTipsLock') && (uploadSuccessShowDialog.value = true)
|
||||
return
|
||||
}
|
||||
setTimeout(() => {
|
||||
getWeappUploadLogFn(key)
|
||||
}, 2000)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const authElMessageBox = () => {
|
||||
if (getAppType() == 'admin') {
|
||||
ElMessageBox.confirm(
|
||||
t('authTips'),
|
||||
t('warning'),
|
||||
{
|
||||
distinguishCancelAndClose: true,
|
||||
confirmButtonText: t('toBind'),
|
||||
cancelButtonText: t('toNiucloud')
|
||||
}
|
||||
).then(() => {
|
||||
router.push({ path: '/app/authorize' })
|
||||
}).catch((action: string) => {
|
||||
if (action === 'cancel') {
|
||||
window.open('https://www.niucloud.com/app')
|
||||
}
|
||||
})
|
||||
} else {
|
||||
ElMessageBox.alert(t('siteAuthTips'), t('warning'))
|
||||
}
|
||||
}
|
||||
|
||||
const configElMessageBox = () => {
|
||||
ElMessageBox.confirm(
|
||||
t('weappTips'),
|
||||
t('warning'),
|
||||
{
|
||||
confirmButtonText: t('toSetting'),
|
||||
cancelButtonText: t('cancel')
|
||||
}
|
||||
).then(() => {
|
||||
router.push({ path: '/channel/weapp/config' })
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 撤回代码审核
|
||||
* @param data
|
||||
*/
|
||||
const undoAuditFn = (data: any) => {
|
||||
ElMessageBox.confirm(
|
||||
t('undoAuditTips'),
|
||||
t('warning'),
|
||||
{
|
||||
confirmButtonText: t('confirm'),
|
||||
cancelButtonText: t('cancel')
|
||||
}
|
||||
).then(() => {
|
||||
undoAudit({ id: data.id }).then(() => {
|
||||
getWeappVersionListFn()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const failReason = ref('')
|
||||
const failReasonDialogVisible = ref(false)
|
||||
const handleFailReason = (data: any) => {
|
||||
failReason.value = data.fail_reason
|
||||
failReasonDialogVisible.value = true
|
||||
}
|
||||
|
||||
const knownToKnow = () => {
|
||||
Storage.set({ key: 'weappUploadTipsLock', data: true })
|
||||
uploadSuccessShowDialog.value = false
|
||||
}
|
||||
|
||||
const againUpload = () => {
|
||||
if (uploading.value) return
|
||||
uploading.value = true
|
||||
|
||||
siteWeappCommit().then(() => {
|
||||
getWeappVersionListFn()
|
||||
getWeappPreviewImage()
|
||||
uploading.value = false
|
||||
}).catch(() => {
|
||||
uploading.value = false
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,163 @@
|
||||
<template>
|
||||
<el-dialog v-model="showDialog" :title="t('functionSetting')" width="700px" :destroy-on-close="true">
|
||||
<el-form :model="formData" label-width="180px" ref="formRef" :rules="formRules" class="page-form pr-[100px]" v-loading="loading">
|
||||
<el-form-item :label="t('requestUrl')" prop="requestdomain">
|
||||
<el-input v-model="formData.requestdomain" :placeholder="t('requestdomainPlaceholder')" type="textarea">
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('socketUrl')" prop="wsrequestdomain">
|
||||
<el-input v-model="formData.wsrequestdomain" :placeholder="t('wsrequestdomainPlaceholder')" type="textarea">
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('uploadUrl')" prop="uploaddomain">
|
||||
<el-input v-model="formData.uploaddomain" :placeholder="t('uploaddomainPlaceholder')" type="textarea">
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('downloadUrl')" prop="downloaddomain">
|
||||
<el-input v-model="formData.downloaddomain" :placeholder="t('downloaddomainPlaceholder')" type="textarea">
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('udpUrl')" prop="udpdomain">
|
||||
<el-input v-model="formData.udpdomain" :placeholder="t('udpdomainPlaceholder')" type="textarea">
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('tcpUrl')" prop="tcpdomain">
|
||||
<el-input v-model="formData.tcpdomain" :placeholder="t('tcpdomainPlaceholder')" type="textarea">
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button>
|
||||
<el-button type="primary" :loading="loading" @click="confirm(formRef)">{{t('confirm')}}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import { setWeappDomain } from '@/app/api/weapp'
|
||||
import Test from '@/utils/test'
|
||||
|
||||
const showDialog = ref(false)
|
||||
const loading = ref(false)
|
||||
|
||||
/**
|
||||
* 表单数据
|
||||
*/
|
||||
const initialFormData = {
|
||||
requestdomain: '',
|
||||
wsrequestdomain: '',
|
||||
uploaddomain: '',
|
||||
downloaddomain: '',
|
||||
tcpdomain: '',
|
||||
udpdomain: ''
|
||||
}
|
||||
const formData: Record<string, any> = reactive({ ...initialFormData })
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
const emit = defineEmits(['complete'])
|
||||
|
||||
const formRules = computed(() => {
|
||||
return {
|
||||
requestdomain: [
|
||||
{
|
||||
validator: (rule: any, value: any, callback: any) => validatorProtocol(rule, value, callback, 'https://'),
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
uploaddomain: [
|
||||
{
|
||||
validator: (rule: any, value: any, callback: any) => validatorProtocol(rule, value, callback, 'https://'),
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
downloaddomain: [
|
||||
{
|
||||
validator: (rule: any, value: any, callback: any) => validatorProtocol(rule, value, callback, 'https://'),
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
wsrequestdomain: [
|
||||
{
|
||||
validator: (rule: any, value: any, callback: any) => validatorProtocol(rule, value, callback, 'wss://'),
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
tcpdomain: [
|
||||
{
|
||||
validator: (rule: any, value: any, callback: any) => validatorProtocol(rule, value, callback, 'tcp://'),
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
udpdomain: [
|
||||
{
|
||||
validator: (rule: any, value: any, callback: any) => validatorProtocol(rule, value, callback, 'udp://'),
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
const validatorProtocol = (rule: any, value: any, callback: any, protocol: string) => {
|
||||
if (!Test.empty(value)) {
|
||||
let flag = true
|
||||
value.split(';').forEach((item: string) => {
|
||||
if (!item.startsWith(protocol)) {
|
||||
flag = false
|
||||
callback(new Error(t('domainError')))
|
||||
}
|
||||
})
|
||||
if (flag) callback()
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认
|
||||
* @param formEl
|
||||
*/
|
||||
const confirm = async (formEl: FormInstance | undefined) => {
|
||||
if (loading.value || !formEl) return
|
||||
|
||||
await formEl.validate(async (valid) => {
|
||||
if (valid) {
|
||||
if (loading.value) return
|
||||
loading.value = true
|
||||
|
||||
const data = formData
|
||||
|
||||
setWeappDomain(data).then(res => {
|
||||
loading.value = false
|
||||
showDialog.value = false
|
||||
emit('complete', data)
|
||||
}).catch(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const setFormData = async (data: any = null) => {
|
||||
loading.value = false
|
||||
Object.assign(formData, initialFormData)
|
||||
if (data) {
|
||||
Object.keys(formData).forEach((key: string) => {
|
||||
if (data[key] != undefined) formData[key] = data[key]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
showDialog,
|
||||
setFormData
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,222 @@
|
||||
<template>
|
||||
<el-dialog v-model="showDialog" :title="t('privacyAgreementTitle')" width="900px" :destroy-on-close="true">
|
||||
<div class="h-[60vh]">
|
||||
<el-scrollbar>
|
||||
<el-form :model="formData" label-width="auto" label-position="left" ref="formRef" :rules="formRules"
|
||||
class="page-form w-[700px] mx-auto" v-loading="loading">
|
||||
<h3 class="text-center text-xl font-bold my-[20px]">{{ config.weapp_name }} 小程序隐私保护指引</h3>
|
||||
<h4 class="text-lg my-[10px]">1. 开发者处理的信息</h4>
|
||||
<div class="mb-[8px]">根据法律规定,开发者仅处理实现小程序功能所必要的信息。</div>
|
||||
<div class="setting-list">
|
||||
<setting-list v-model="formData.setting_list" ref="settingListRef"/>
|
||||
</div>
|
||||
<div>
|
||||
<el-button type="primary" link @click="settingListRef.addSettingList()">{{ t('addSettingType') }}</el-button>
|
||||
</div>
|
||||
<h4 class="text-lg my-[10px]">2. 第三方插件信息/SDK信息</h4>
|
||||
<div class="mb-[8px]">
|
||||
为实现特定功能,开发者可能会接入由第三方提供的插件/SDK。第三方插件/SDK的个人信息处理规则,请以其公示的官方说明为准。{{ config.weapp_name }}小程序接入的第三方插件信息/SDK信息如下:
|
||||
</div>
|
||||
<div>
|
||||
<div v-for="(item, index) in formData.sdk_privacy_info_list" class="mb-[15px]">
|
||||
<el-form-item label="SDK名称" class="!mb-[8px]">
|
||||
<el-input v-model="formData.sdk_privacy_info_list[index].sdk_name" class="input-width" placeholder="请输入SDK名称" />
|
||||
<el-button type="primary" class="ml-[10px]" link @click="delSdk(index)">{{ t('delete') }}</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item label="SDK提供方名称" class="!mb-[8px]">
|
||||
<el-input v-model="formData.sdk_privacy_info_list[index].sdk_biz_name" class="input-width" placeholder="请输入SDK提供方名称" />
|
||||
</el-form-item>
|
||||
<setting-list v-model="formData.sdk_privacy_info_list[index].sdk_list" ref="sdkSettingListRef"/>
|
||||
<el-form-item label="" class="!mb-[8px]">
|
||||
<el-button type="primary" link @click="sdkSettingListRef[index].addSettingList()">{{ t('addSdkSettingList') }}</el-button>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<el-button type="primary" link @click="addSdk">{{ t('addSdkInfo') }}</el-button>
|
||||
</div>
|
||||
<h4 class="text-lg my-[10px]">3. 你的权益</h4>
|
||||
<div class="mb-[8px]">3.1
|
||||
关于收集你的位置信息,你可以通过以下路径:小程序主页右上角“…”—“设置”—点击特定信息—点击“不允许”,撤回对开发者的授权。</div>
|
||||
<div class="mb-[8px]">3.2 关于收集你的微信昵称、头像、收集你的手机号,你可以通过以下路径:小程序主页右上角“...” — “设置” —
|
||||
“小程序已获取的信息” — 点击特定信息 —
|
||||
点击“通知开发者删除”,开发者承诺收到通知后将删除信息。法律法规另有规定的,开发者承诺将停止除存储和采取必要的安全保护措施之外的处理。</div>
|
||||
<div class="mb-[8px]">3.3 关于你的个人信息,你可以通过以下方式与开发者联系,行使查阅、复制、更正、删除等法定权利。</div>
|
||||
<div class="mb-[8px]">3.4
|
||||
若你在小程序中注册了账号,你可以通过以下方式与开发者联系,申请注销你在小程序中使用的账号。在受理你的申请后,开发者承诺在十五个工作日内完成核查和处理,并按照法律法规要求处理你的相关信息。</div>
|
||||
<div>
|
||||
<el-form-item label="电话" class="!mb-[8px]">
|
||||
<el-input v-model="formData.owner_setting.contact_phone" class="input-width" placeholder="请输入开发者的手机号" />
|
||||
</el-form-item>
|
||||
<el-form-item label="邮箱" class="!mb-[8px]">
|
||||
<el-input v-model="formData.owner_setting.contact_email" class="input-width" placeholder="请输入开发者的邮箱" />
|
||||
</el-form-item>
|
||||
<el-form-item label="微信号" class="!mb-[8px]">
|
||||
<el-input v-model="formData.owner_setting.contact_weixin" class="input-width" placeholder="请输入开发者的微信号" />
|
||||
</el-form-item>
|
||||
<el-form-item label="qq号" class="!mb-[8px]">
|
||||
<el-input v-model="formData.owner_setting.contact_qq" class="input-width" placeholder="请输入开发者的qq号" />
|
||||
</el-form-item>
|
||||
<div class="form-tip">信息收集方(开发者)的联系方式,4种联系方式至少要填一种</div>
|
||||
</div>
|
||||
<h4 class="text-lg my-[10px]">4. 开发者对信息的存储</h4>
|
||||
<div>
|
||||
<el-radio-group v-model="formData.store_expire_type">
|
||||
<div>
|
||||
<el-radio :label="1">
|
||||
固定存储期限
|
||||
<div class="!w-[200px] inline-block mx-[5px]"><el-input v-model="formData.store_expire_timestamp"/></div>
|
||||
</el-radio>
|
||||
</div>
|
||||
<div>
|
||||
<el-radio :label="0">开发者承诺,除法律法规另有规定外,开发者对你的信息的保存期限应当为实现处理目的所必要的最短时间。</el-radio>
|
||||
</div>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<h4 class="text-lg my-[10px]">5. 信息的使用规则</h4>
|
||||
<div class="mb-[8px]">5.1 开发者将会在本指引所明示的用途内使用收集的信息</div>
|
||||
<div class="mb-[8px]">
|
||||
5.2 如开发者使用你的信息超出本指引目的或合理范围,开发者必须在变更使用目的或范围前,再次以
|
||||
<div class="!w-[180px] inline-block">
|
||||
<el-input v-model="formData.owner_setting.notice_method"/>
|
||||
</div>
|
||||
方式告知并征得你的明示同意。</div>
|
||||
<h4 class="text-lg my-[10px]">6. 信息对外提供</h4>
|
||||
<div class="mb-[8px]">6.1
|
||||
开发者承诺,不会主动共享或转让你的信息至任何第三方,如存在确需共享或转让时,开发者应当直接征得或确认第三方征得你的单独同意。</div>
|
||||
<div class="mb-[8px]">6.2
|
||||
开发者承诺,不会对外公开披露你的信息,如必须公开披露时,开发者应当向你告知公开披露的目的、披露信息的类型及可能涉及的信息,并征得你的单独同意。</div>
|
||||
<h4 class="text-lg my-[10px]">7.
|
||||
你认为开发者未遵守上述约定,或有其他的投诉建议、或未成年人个人信息保护相关问题,可通过以下方式与开发者联系;或者向微信进行投诉。</h4>
|
||||
<!-- <h4 class="text-lg my-[10px]">8. 补充文档</h4>-->
|
||||
</el-form>
|
||||
</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(formRef)">{{ t('confirm') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import SettingList from './setting-list.vue'
|
||||
import { getWeappPrivacySetting, setWeappPrivacySetting } from '@/app/api/weapp'
|
||||
|
||||
const showDialog = ref(false)
|
||||
const loading = ref(false)
|
||||
const settingListRef = ref(null)
|
||||
const sdkSettingListRef = ref(null)
|
||||
|
||||
const props = defineProps({
|
||||
config: {
|
||||
type: Object,
|
||||
default: () => {}
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 表单数据
|
||||
*/
|
||||
const initialFormData = {
|
||||
setting_list: [
|
||||
{
|
||||
privacy_key: 'UserInfo',
|
||||
privacy_text: ''
|
||||
},
|
||||
{
|
||||
privacy_key: 'Location',
|
||||
privacy_text: ''
|
||||
},
|
||||
{
|
||||
privacy_key: 'PhoneNumber',
|
||||
privacy_text: ''
|
||||
}
|
||||
],
|
||||
owner_setting: {
|
||||
notice_method: ''
|
||||
},
|
||||
sdk_privacy_info_list: [],
|
||||
store_expire_type: 0,
|
||||
store_expire_timestamp: ''
|
||||
}
|
||||
|
||||
const formData: Record<string, any> = reactive({ ...initialFormData })
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
const emit = defineEmits(['complete'])
|
||||
|
||||
const formRules = computed(() => {
|
||||
return {}
|
||||
})
|
||||
|
||||
const addSdk = () => {
|
||||
formData.sdk_privacy_info_list.push({
|
||||
sdk_name: '',
|
||||
sdk_biz_name: '',
|
||||
sdk_list: []
|
||||
})
|
||||
}
|
||||
|
||||
const delSdk = (index: number) => {
|
||||
formData.sdk_privacy_info_list.splice(index, 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认
|
||||
* @param formEl
|
||||
*/
|
||||
const confirm = async (formEl: FormInstance | undefined) => {
|
||||
if (loading.value || !formEl) return
|
||||
|
||||
await formEl.validate(async (valid) => {
|
||||
if (valid) {
|
||||
if (loading.value) return
|
||||
loading.value = true
|
||||
|
||||
const data = formData
|
||||
|
||||
if (!data.store_expire_type) data.owner_setting.store_expire_timestamp = data.store_expire_timestamp
|
||||
|
||||
setWeappPrivacySetting(data).then(res => {
|
||||
loading.value = false
|
||||
showDialog.value = false
|
||||
emit('complete', data)
|
||||
}).catch(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const setFormData = async () => {
|
||||
getWeappPrivacySetting().then(({ data }) => {
|
||||
loading.value = false
|
||||
Object.assign(formData, initialFormData)
|
||||
if (data) {
|
||||
Object.keys(formData).forEach((key: string) => {
|
||||
if (data[key] != undefined) formData[key] = data[key]
|
||||
})
|
||||
if (data.owner_setting.store_expire_timestamp) {
|
||||
formData.store_expire_type = 1
|
||||
formData.store_expire_timestamp = data.owner_setting.store_expire_timestamp
|
||||
}
|
||||
}
|
||||
showDialog.value = true
|
||||
})
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
showDialog,
|
||||
setFormData
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,221 @@
|
||||
<template>
|
||||
<div v-for="(item, index) in value">
|
||||
<div class="flex items-center mb-[8px]" v-if="item.privacy_key == 'UserInfo'">
|
||||
为了<div class="!w-[200px] inline-block mx-[5px]"><el-input v-model="value[index].privacy_text" :placeholder="t('settingPlaceholder')"/></div>,开发者将在获取你的明示同意后,收集你的微信昵称、头像。
|
||||
<icon name="element Remove" @click="removeSettingList(index)" color="red" class="cursor-pointer"></icon>
|
||||
</div>
|
||||
<div class="flex items-center mb-[8px]" v-if="item.privacy_key == 'Location'">
|
||||
为了<div class="!w-[200px] inline-block mx-[5px]"><el-input v-model="value[index].privacy_text" :placeholder="t('settingPlaceholder')"/></div>,开发者将在获取你的明示同意后,收集你的位置信息。
|
||||
<icon name="element Remove" @click="removeSettingList(index)" color="red" class="cursor-pointer"></icon>
|
||||
</div>
|
||||
<div class="flex items-center mb-[8px]" v-if="item.privacy_key == 'PhoneNumber'">
|
||||
为了<div class="!w-[200px] inline-block mx-[5px]"><el-input v-model="value[index].privacy_text" :placeholder="t('settingPlaceholder')"/></div>,开发者将在获取你的明示同意后,收集你的手机号。
|
||||
<icon name="element Remove" @click="removeSettingList(index)" color="red" class="cursor-pointer"></icon>
|
||||
</div>
|
||||
<div class="flex items-center mb-[8px]" v-if="item.privacy_key == 'Address'">
|
||||
开发者收集你的地址,用于<div class="!w-[200px] inline-block mx-[5px]"><el-input v-model="value[index].privacy_text" :placeholder="t('settingPlaceholder')"/></div>
|
||||
<icon name="element Remove" @click="removeSettingList(index)" color="red" class="cursor-pointer"></icon>
|
||||
</div>
|
||||
<div class="flex items-center mb-[8px]" v-if="item.privacy_key == 'Record'">
|
||||
为了<div class="!w-[200px] inline-block mx-[5px]"><el-input v-model="value[index].privacy_text" :placeholder="t('settingPlaceholder')"/></div>,开发者将在获取你的明示同意后,访问你的麦克风。
|
||||
<icon name="element Remove" @click="removeSettingList(index)" color="red" class="cursor-pointer"></icon>
|
||||
</div>
|
||||
<div class="flex items-center mb-[8px]" v-if="item.privacy_key == 'Contact'">
|
||||
开发者使用你的通讯录(仅写入)权限,用于<div class="!w-[200px] inline-block mx-[5px]"><el-input v-model="value[index].privacy_text" :placeholder="t('settingPlaceholder')"/></div>
|
||||
<icon name="element Remove" @click="removeSettingList(index)" color="red" class="cursor-pointer"></icon>
|
||||
</div>
|
||||
<div class="flex items-center mb-[8px]" v-if="item.privacy_key == 'EXOrderInfo'">
|
||||
开发者收集你的订单信息,用于<div class="!w-[200px] inline-block mx-[5px]"><el-input v-model="value[index].privacy_text" :placeholder="t('settingPlaceholder')"/></div>
|
||||
<icon name="element Remove" @click="removeSettingList(index)" color="red" class="cursor-pointer"></icon>
|
||||
</div>
|
||||
<div class="flex items-center mb-[8px]" v-if="item.privacy_key == 'EXUserOpLog'">
|
||||
开发者收集你的操作日志,用于<div class="!w-[200px] inline-block mx-[5px]"><el-input v-model="value[index].privacy_text" :placeholder="t('settingPlaceholder')"/></div>
|
||||
<icon name="element Remove" @click="removeSettingList(index)" color="red" class="cursor-pointer"></icon>
|
||||
</div>
|
||||
<div class="flex items-center mb-[8px]" v-if="item.privacy_key == 'BlueTooth'">
|
||||
开发者访问你的蓝牙,用于<div class="!w-[200px] inline-block mx-[5px]"><el-input v-model="value[index].privacy_text" :placeholder="t('settingPlaceholder')"/></div>
|
||||
<icon name="element Remove" @click="removeSettingList(index)" color="red" class="cursor-pointer"></icon>
|
||||
</div>
|
||||
<div class="flex items-center mb-[8px]" v-if="item.privacy_key == 'MessageFile'">
|
||||
开发者收集你选中的文件,用于<div class="!w-[200px] inline-block mx-[5px]"><el-input v-model="value[index].privacy_text" :placeholder="t('settingPlaceholder')"/></div>
|
||||
<icon name="element Remove" @click="removeSettingList(index)" color="red" class="cursor-pointer"></icon>
|
||||
</div>
|
||||
<div class="flex items-center mb-[8px]" v-if="item.privacy_key == 'Compass'">
|
||||
开发者调用你的磁场传感器,用于<div class="!w-[200px] inline-block mx-[5px]"><el-input v-model="value[index].privacy_text" :placeholder="t('settingPlaceholder')"/></div>
|
||||
<icon name="element Remove" @click="removeSettingList(index)" color="red" class="cursor-pointer"></icon>
|
||||
</div>
|
||||
<div class="flex items-center mb-[8px]" v-if="item.privacy_key == 'Clipboard'">
|
||||
开发者读取你的剪切板,用于<div class="!w-[200px] inline-block mx-[5px]"><el-input v-model="value[index].privacy_text" :placeholder="t('settingPlaceholder')"/></div>
|
||||
<icon name="element Remove" @click="removeSettingList(index)" color="red" class="cursor-pointer"></icon>
|
||||
</div>
|
||||
<div class="flex items-center mb-[8px]" v-if="item.privacy_key == 'DeviceMotion'">
|
||||
开发者调用你的方向传感器,用于<div class="!w-[200px] inline-block mx-[5px]"><el-input v-model="value[index].privacy_text" :placeholder="t('settingPlaceholder')"/></div>
|
||||
<icon name="element Remove" @click="removeSettingList(index)" color="red" class="cursor-pointer"></icon>
|
||||
</div>
|
||||
<div class="flex items-center mb-[8px]" v-if="item.privacy_key == 'ChooseLocation'">
|
||||
开发者获取你选择的位置信息,用于<div class="!w-[200px] inline-block mx-[5px]"><el-input v-model="value[index].privacy_text" :placeholder="t('settingPlaceholder')"/></div>
|
||||
<icon name="element Remove" @click="removeSettingList(index)" color="red" class="cursor-pointer"></icon>
|
||||
</div>
|
||||
<div class="flex items-center mb-[8px]" v-if="item.privacy_key == 'CalendarWriteOnly'">
|
||||
开发者使用你的日历(仅写入)权限,用于<div class="!w-[200px] inline-block mx-[5px]"><el-input v-model="value[index].privacy_text" :placeholder="t('settingPlaceholder')"/></div>
|
||||
<icon name="element Remove" @click="removeSettingList(index)" color="red" class="cursor-pointer"></icon>
|
||||
</div>
|
||||
<div class="flex items-center mb-[8px]" v-if="item.privacy_key == 'AlbumWriteOnly'">
|
||||
为了<div class="!w-[200px] inline-block mx-[5px]"><el-input v-model="value[index].privacy_text" :placeholder="t('settingPlaceholder')"/></div> ,开发者将在获取你的明示同意后,使用你的相册(仅写入)权限。
|
||||
<icon name="element Remove" @click="removeSettingList(index)" color="red" class="cursor-pointer"></icon>
|
||||
</div>
|
||||
<div class="flex items-center mb-[8px]" v-if="item.privacy_key == 'EXUserPublishContent'">
|
||||
开发者收集你的发布内容,用于<div class="!w-[200px] inline-block mx-[5px]"><el-input v-model="value[index].privacy_text" :placeholder="t('settingPlaceholder')"/></div>
|
||||
<icon name="element Remove" @click="removeSettingList(index)" color="red" class="cursor-pointer"></icon>
|
||||
</div>
|
||||
<div class="flex items-center mb-[8px]" v-if="item.privacy_key == 'DeviceInfo'">
|
||||
开发者收集你的设备信息,用于<div class="!w-[200px] inline-block mx-[5px]"><el-input v-model="value[index].privacy_text" :placeholder="t('settingPlaceholder')"/></div>
|
||||
<icon name="element Remove" @click="removeSettingList(index)" color="red" class="cursor-pointer"></icon>
|
||||
</div>
|
||||
<div class="flex items-center mb-[8px]" v-if="item.privacy_key == 'Album'">
|
||||
开发者收集你选中的照片或视频信息,用于<div class="!w-[200px] inline-block mx-[5px]"><el-input v-model="value[index].privacy_text" :placeholder="t('settingPlaceholder')"/></div>
|
||||
<icon name="element Remove" @click="removeSettingList(index)" color="red" class="cursor-pointer"></icon>
|
||||
</div>
|
||||
<div class="flex items-center mb-[8px]" v-if="item.privacy_key == 'Invoice'">
|
||||
开发者收集你的发票信息,用于<div class="!w-[200px] inline-block mx-[5px]"><el-input v-model="value[index].privacy_text" :placeholder="t('settingPlaceholder')"/></div>
|
||||
<icon name="element Remove" @click="removeSettingList(index)" color="red" class="cursor-pointer"></icon>
|
||||
</div>
|
||||
<div class="flex items-center mb-[8px]" v-if="item.privacy_key == 'RunData'">
|
||||
为了<div class="!w-[200px] inline-block mx-[5px]"><el-input v-model="value[index].privacy_text" :placeholder="t('settingPlaceholder')"/></div>,开发者将在获取你的明示同意后,收集你的微信运动步数。
|
||||
<icon name="element Remove" @click="removeSettingList(index)" color="red" class="cursor-pointer"></icon>
|
||||
</div>
|
||||
<div class="flex items-center mb-[8px]" v-if="item.privacy_key == 'Camera'">
|
||||
为了<div class="!w-[200px] inline-block mx-[5px]"><el-input v-model="value[index].privacy_text" :placeholder="t('settingPlaceholder')"/></div>,开发者将在获取你的明示同意后,访问你的摄像头。
|
||||
<icon name="element Remove" @click="removeSettingList(index)" color="red" class="cursor-pointer"></icon>
|
||||
</div>
|
||||
<div class="flex items-center mb-[8px]" v-if="item.privacy_key == 'EXUserFollowAcct'">
|
||||
开发者收集你的所关注账号,用于<div class="!w-[200px] inline-block mx-[5px]"><el-input v-model="value[index].privacy_text" :placeholder="t('settingPlaceholder')"/></div>
|
||||
<icon name="element Remove" @click="removeSettingList(index)" color="red" class="cursor-pointer"></icon>
|
||||
</div>
|
||||
<div class="flex items-center mb-[8px]" v-if="item.privacy_key == 'EXIDNumber'">
|
||||
开发者收集你的身份证号码,用于<div class="!w-[200px] inline-block mx-[5px]"><el-input v-model="value[index].privacy_text" :placeholder="t('settingPlaceholder')"/></div>
|
||||
<icon name="element Remove" @click="removeSettingList(index)" color="red" class="cursor-pointer"></icon>
|
||||
</div>
|
||||
<div class="flex items-center mb-[8px]" v-if="item.privacy_key == 'LicensePlate'">
|
||||
为了<div class="!w-[200px] inline-block mx-[5px]"><el-input v-model="value[index].privacy_text" :placeholder="t('settingPlaceholder')"/></div>,开发者将在获取你的明示同意后,收集你的车牌号。
|
||||
<icon name="element Remove" @click="removeSettingList(index)" color="red" class="cursor-pointer"></icon>
|
||||
</div>
|
||||
<div class="flex items-center mb-[8px]" v-if="item.privacy_key == 'Email'">
|
||||
开发者收集你的邮箱,用于<div class="!w-[200px] inline-block mx-[5px]"><el-input v-model="value[index].privacy_text" :placeholder="t('settingPlaceholder')"/></div>
|
||||
<icon name="element Remove" @click="removeSettingList(index)" color="red" class="cursor-pointer"></icon>
|
||||
</div>
|
||||
<div class="flex items-center mb-[8px]" v-if="item.privacy_key == 'Accelerometer'">
|
||||
开发者调用你的加速传感器,用于<div class="!w-[200px] inline-block mx-[5px]"><el-input v-model="value[index].privacy_text" :placeholder="t('settingPlaceholder')"/></div>
|
||||
<icon name="element Remove" @click="removeSettingList(index)" color="red" class="cursor-pointer"></icon>
|
||||
</div>
|
||||
<div class="flex items-center mb-[8px]" v-if="item.privacy_key == 'Gyroscope'">
|
||||
开发者调用你的陀螺仪传感器,用于<div class="!w-[200px] inline-block mx-[5px]"><el-input v-model="value[index].privacy_text" :placeholder="t('settingPlaceholder')"/></div>
|
||||
<icon name="element Remove" @click="removeSettingList(index)" color="red" class="cursor-pointer"></icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="settingTypeDialog" :title="t('settingTypeTitle')" width="500px" :destroy-on-close="true">
|
||||
<el-checkbox-group v-model="checkList">
|
||||
<template v-for="(item, index) in privacyList">
|
||||
<el-checkbox :label="item.privacy_key" v-if="!checkIsSelected(item.privacy_key)">{{ item.privacy_text }}</el-checkbox>
|
||||
</template>
|
||||
</el-checkbox-group>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="settingTypeDialog = false">{{ t('cancel') }}</el-button>
|
||||
<el-button type="primary" @click="selectSettingType()">{{ t('confirm') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Array,
|
||||
default: () => {
|
||||
return []
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const settingTypeDialog = ref(false)
|
||||
|
||||
const privacyList = ref([
|
||||
{ privacy_key: 'UserInfo', privacy_text: '用户信息(微信昵称、头像)' },
|
||||
{ privacy_key: 'Location', privacy_text: '位置信息' },
|
||||
{ privacy_key: 'Address', privacy_text: '地址' },
|
||||
{ privacy_key: 'Invoice', privacy_text: '发票信息' },
|
||||
{ privacy_key: 'RunData', privacy_text: '微信运动数据' },
|
||||
{ privacy_key: 'Record', privacy_text: '麦克风' },
|
||||
{ privacy_key: 'Album', privacy_text: '选中的照片或视频信息' },
|
||||
{ privacy_key: 'Camera', privacy_text: '摄像头' },
|
||||
{ privacy_key: 'PhoneNumber', privacy_text: '手机号码' },
|
||||
{ privacy_key: 'Contact', privacy_text: '通讯录(仅写入)权限' },
|
||||
{ privacy_key: 'DeviceInfo', privacy_text: '设备信息' },
|
||||
{ privacy_key: 'EXIDNumber', privacy_text: '身份证号码' },
|
||||
{ privacy_key: 'EXOrderInfo', privacy_text: '订单信息' },
|
||||
{ privacy_key: 'EXUserPublishContent', privacy_text: '发布内容' },
|
||||
{ privacy_key: 'EXUserFollowAcct', privacy_text: '所关注账号' },
|
||||
{ privacy_key: 'EXUserOpLog', privacy_text: '操作日志' },
|
||||
{ privacy_key: 'AlbumWriteOnly', privacy_text: '相册(仅写入)权限' },
|
||||
{ privacy_key: 'LicensePlate', privacy_text: '车牌号' },
|
||||
{ privacy_key: 'BlueTooth', privacy_text: '蓝牙' },
|
||||
{ privacy_key: 'CalendarWriteOnly', privacy_text: '日历(仅写入)权限' },
|
||||
{ privacy_key: 'Email', privacy_text: '邮箱' },
|
||||
{ privacy_key: 'MessageFile', privacy_text: '选中的文件' },
|
||||
{ privacy_key: 'ChooseLocation', privacy_text: '选择的位置信息' },
|
||||
{ privacy_key: 'Accelerometer', privacy_text: '加速传感器' },
|
||||
{ privacy_key: 'Compass', privacy_text: '磁场传感器' },
|
||||
{ privacy_key: 'DeviceMotion', privacy_text: '方向传感器' },
|
||||
{ privacy_key: 'Gyroscope', privacy_text: '陀螺仪传感器' },
|
||||
{ privacy_key: 'Clipboard', privacy_text: '剪切板' }
|
||||
])
|
||||
|
||||
const emits = defineEmits(['update:modelValue', 'change'])
|
||||
|
||||
const value = computed({
|
||||
get () {
|
||||
return props.modelValue
|
||||
},
|
||||
set (value) {
|
||||
emits('update:modelValue', value)
|
||||
}
|
||||
})
|
||||
|
||||
const removeSettingList = (index: number) => {
|
||||
value.value.splice(index, 1)
|
||||
}
|
||||
|
||||
const checkList = ref([])
|
||||
const selectSettingType = () => {
|
||||
checkList.value.forEach((item: string) => {
|
||||
value.value.push({
|
||||
privacy_key: item,
|
||||
privacy_text: ''
|
||||
})
|
||||
})
|
||||
settingTypeDialog.value = false
|
||||
checkList.value = []
|
||||
}
|
||||
|
||||
const addSettingList = () => {
|
||||
settingTypeDialog.value = true
|
||||
}
|
||||
|
||||
const selectedSettingType = computed(() => {
|
||||
return value.value.map((item: any) => item.privacy_key)
|
||||
})
|
||||
const checkIsSelected = (key: string) => {
|
||||
return selectedSettingType.value.includes(key)
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
addSettingList
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
311
admin-vben/src/app/views/channel/weapp/config.vue
Normal file
311
admin-vben/src/app/views/channel/weapp/config.vue
Normal file
@@ -0,0 +1,311 @@
|
||||
<template>
|
||||
<!--小程序配置-->
|
||||
<div class="main-container">
|
||||
|
||||
<el-card class="card !border-none" shadow="never">
|
||||
<el-page-header :content="pageName" :icon="ArrowLeft" @back="back()" />
|
||||
</el-card>
|
||||
|
||||
<el-form class="page-form mt-[15px]" :model="formData" label-width="170px" ref="formRef" :rules="formRules" v-loading="loading">
|
||||
<el-card class="box-card !border-none" shadow="never">
|
||||
<h3 class="panel-title !text-sm">{{ t('weappInfo') }}</h3>
|
||||
|
||||
<el-form-item :label="t('weappName')" prop="weapp_name">
|
||||
<el-input v-model.trim="formData.weapp_name" :placeholder="t('weappNamePlaceholder')" class="input-width" clearable :readonly="formData.is_authorization"/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('weappOriginal')" prop="weapp_original">
|
||||
<el-input v-model.trim="formData.weapp_original" :placeholder="t('weappOriginalPlaceholder')" class="input-width" clearable :readonly="formData.is_authorization"/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('weappQrcode')" prop="qr_code">
|
||||
<upload-image v-model="formData.qr_code" />
|
||||
<div class="form-tip">{{ t('weappQrcodeTips') }}</div>
|
||||
</el-form-item>
|
||||
</el-card>
|
||||
|
||||
<el-card class="box-card !border-none mt-[15px]" shadow="never">
|
||||
<h3 class="panel-title !text-sm">{{ t('weappDevelopInfo') }}</h3>
|
||||
|
||||
<el-form-item :label="t('weappAppid')" prop="app_id">
|
||||
<el-input v-model.trim="formData.app_id" :placeholder="t('appidPlaceholder')" class="input-width" clearable :readonly="formData.is_authorization"/>
|
||||
<div class="form-tip">{{ t('weappAppidTips') }}</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('weappAppsecret')" prop="app_secret" v-if="!formData.is_authorization">
|
||||
<el-input v-model.trim="formData.app_secret" :placeholder="t('appSecretPlaceholder')" class="input-width" clearable />
|
||||
<div class="form-tip">{{ t('weappAppsecretTips') }}</div>
|
||||
</el-form-item>
|
||||
</el-card>
|
||||
|
||||
<el-card class="box-card !border-none mt-[15px]" shadow="never" v-if="!formData.is_authorization">
|
||||
<h3 class="panel-title !text-sm">{{ t('weappUpload') }}</h3>
|
||||
|
||||
<el-form-item :label="t('uploadKey')" prop="upload_private_key">
|
||||
<div class="input-width">
|
||||
<upload-file v-model="formData.upload_private_key" api="sys/document/wechat" />
|
||||
</div>
|
||||
<div class="form-tip">{{ t('uploadKeyTips') }}</div>
|
||||
<div class="form-tip">{{ t('uploadIpTips') }}{{ formData.upload_ip }}</div>
|
||||
</el-form-item>
|
||||
</el-card>
|
||||
|
||||
<el-card class="box-card !border-none mt-[15px]" shadow="never" v-show="!formData.is_authorization">
|
||||
<h3 class="panel-title !text-sm">{{ t('theServerSetting') }}</h3>
|
||||
|
||||
<el-form-item label="URL">
|
||||
<el-input :model-value="formData.serve_url" placeholder="Please input" class="input-width" :readonly="true">
|
||||
<template #append>
|
||||
<div class="cursor-pointer" @click="copyEvent(formData.serve_url)">{{ t('copy') }}</div>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="Token" prop="token">
|
||||
<el-input v-model.trim="formData.token" :placeholder="t('tokenPlaceholder')" class="input-width" maxlength="32" show-word-limit clearable />
|
||||
<div class="form-tip">{{ t('tokenTips') }}</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="EncodingAESKey" prop="encoding_aes_key">
|
||||
<el-input v-model.trim="formData.encoding_aes_key" :placeholder="t('encodingAesKeyPlaceholder')" class="input-width" maxlength="43" show-word-limit clearable />
|
||||
<div class="form-tip">{{ t('encodingAESKeyTips') }}</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('encryptionType')" prop="encryption_type">
|
||||
<el-radio-group v-model="formData.encryption_type">
|
||||
<el-radio label="not_encrypt">{{ t('cleartextMode') }}</el-radio>
|
||||
<el-radio label="compatible">{{ t('compatibleMode') }}</el-radio>
|
||||
<el-radio label="safe">{{ t('safeMode') }}</el-radio>
|
||||
</el-radio-group>
|
||||
<div class="form-tip">{{ t('cleartextModeTips') }}</div>
|
||||
<div class="form-tip">{{ t('compatibleModeTips') }}</div>
|
||||
<div class="form-tip">{{ t('safeModeTips') }}</div>
|
||||
</el-form-item>
|
||||
</el-card>
|
||||
|
||||
<el-card class="box-card !border-none mt-[15px]" shadow="never">
|
||||
<div class="flex items-start justify-between">
|
||||
<h3 class="panel-title !text-sm">{{ t('functionSetting') }}</h3>
|
||||
<el-button type="primary" link @click="modifyDomainFn" v-if="formData.is_authorization">{{ t('update') }}</el-button>
|
||||
</div>
|
||||
|
||||
<div v-if="!formData.is_authorization">
|
||||
<el-form-item :label="t('requestUrl')">
|
||||
<el-input :model-value="formData.request_url" placeholder="Please input" class="input-width" :readonly="true">
|
||||
<template #append>
|
||||
<div class="cursor-pointer" @click="copyEvent(formData.request_url)">{{ t('copy') }}</div>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('socketUrl')">
|
||||
<el-input :model-value="formData.socket_url" placeholder="Please input" class="input-width" :readonly="true">
|
||||
<template #append>
|
||||
<div class="cursor-pointer" @click="copyEvent(formData.socket_url)">{{ t('copy') }}</div>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('uploadUrl')">
|
||||
<el-input :model-value="formData.upload_url" placeholder="Please input" class="input-width" :readonly="true">
|
||||
<template #append>
|
||||
<div class="cursor-pointer" @click="copyEvent(formData.upload_url)">{{ t('copy') }}</div>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('downloadUrl')">
|
||||
<el-input :model-value="formData.download_url" placeholder="Please input" class="input-width" :readonly="true">
|
||||
<template #append>
|
||||
<div class="cursor-pointer" @click="copyEvent(formData.download_url)">{{ t('copy') }}</div>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<div v-else>
|
||||
<el-form-item :label="t('requestUrl')">
|
||||
<div v-if="formData.domain.requestdomain">{{ formData.domain.requestdomain }}</div>
|
||||
<div v-else>-</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('socketUrl')">
|
||||
<div v-if="formData.domain.wsrequestdomain">{{ formData.domain.wsrequestdomain }}</div>
|
||||
<div v-else>-</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('uploadUrl')">
|
||||
<div v-if="formData.domain.uploaddomain">{{ formData.domain.uploaddomain }}</div>
|
||||
<div v-else>-</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('downloadUrl')">
|
||||
<div v-if="formData.domain.downloaddomain">{{ formData.domain.downloaddomain }}</div>
|
||||
<div v-else>-</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('udpUrl')">
|
||||
<div v-if="formData.domain.udpdomain">{{ formData.domain.udpdomain }}</div>
|
||||
<div v-else>-</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('tcpUrl')">
|
||||
<div v-if="formData.domain.tcpdomain">{{ formData.domain.tcpdomain }}</div>
|
||||
<div v-else>-</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card class="box-card !border-none mt-[15px]" shadow="never" v-if="formData.is_authorization">
|
||||
<div class="flex items-start justify-between">
|
||||
<h3 class="panel-title !text-sm">{{ t('serviceContentStatement') }}</h3>
|
||||
</div>
|
||||
<el-form-item :label="t('privacyAgreement')">
|
||||
<div class="flex items-center">
|
||||
<div class="form-tip !mt-0">{{ t('privacyAgreementTips') }}</div>
|
||||
<el-button type="primary" link @click="modifyPrivacyAgreementFn">{{ t('setting') }}</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</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>
|
||||
|
||||
<modify-domain ref="modifyDomainRef" @complete="modifyDomainComplete"/>
|
||||
<modify-privacy-agreement :config="formData" ref="modifyPrivacyAgreementRef"/>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref, watch, computed } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import { getWeappConfig, setWeappConfig } from '@/app/api/weapp'
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
import { ElMessage, FormInstance } from 'element-plus'
|
||||
import { ArrowLeft } from '@element-plus/icons-vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import ModifyDomain from '@/app/views/channel/weapp/components/modify-domain.vue'
|
||||
import ModifyPrivacyAgreement from '@/app/views/channel/weapp/components/modify-privacy-agreement.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const pageName = route.meta.title
|
||||
const back = () => {
|
||||
router.push('/channel/weapp')
|
||||
}
|
||||
const loading = ref(true)
|
||||
|
||||
const formData = reactive<Record<string, any>>({
|
||||
weapp_name: '',
|
||||
weapp_original: '',
|
||||
app_id: '',
|
||||
app_secret: '',
|
||||
qr_code: '',
|
||||
token: '',
|
||||
encoding_aes_key: '',
|
||||
encryption_type: 'not_encrypt',
|
||||
serve_url: '',
|
||||
request_url: '',
|
||||
socket_url: '',
|
||||
upload_url: '',
|
||||
download_url: '',
|
||||
upload_private_key: '',
|
||||
is_authorization: 0,
|
||||
domain: {
|
||||
requestdomain: '',
|
||||
wsrequestdomain: '',
|
||||
uploaddomain: '',
|
||||
downloaddomain: '',
|
||||
tcpdomain: '',
|
||||
udpdomain: ''
|
||||
}
|
||||
})
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = computed(() => {
|
||||
return {
|
||||
weapp_name: [
|
||||
{ required: true, message: t('weappNamePlaceholder'), trigger: 'blur' }
|
||||
],
|
||||
weapp_original: [
|
||||
{ required: true, message: t('weappOriginalPlaceholder'), trigger: 'blur' }
|
||||
],
|
||||
app_id: [
|
||||
{ required: true, message: t('appidPlaceholder'), trigger: 'blur' }
|
||||
],
|
||||
app_secret: [
|
||||
{ required: !formData.is_authorization, message: t('appSecretPlaceholder'), trigger: 'blur' }
|
||||
],
|
||||
token: [
|
||||
{ required: !formData.is_authorization, message: t('tokenPlaceholder'), trigger: 'blur' }
|
||||
],
|
||||
encoding_aes_key: [
|
||||
{ required: !formData.is_authorization, message: t('encodingAesKeyPlaceholder'), trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 获取微信配置
|
||||
*/
|
||||
getWeappConfig().then(res => {
|
||||
Object.assign(formData, res.data)
|
||||
loading.value = false
|
||||
})
|
||||
|
||||
/**
|
||||
* 复制
|
||||
*/
|
||||
const { copy, isSupported, copied } = useClipboard()
|
||||
const copyEvent = (text: string) => {
|
||||
if (!isSupported.value) {
|
||||
ElMessage({
|
||||
message: t('notSupportCopy'),
|
||||
type: 'warning'
|
||||
})
|
||||
return
|
||||
}
|
||||
copy(text)
|
||||
}
|
||||
|
||||
watch(copied, () => {
|
||||
if (copied.value) {
|
||||
ElMessage({
|
||||
message: t('copySuccess'),
|
||||
type: 'success'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 保存
|
||||
*/
|
||||
const save = async (formEl: FormInstance | undefined) => {
|
||||
if (loading.value || !formEl) return
|
||||
|
||||
await formEl.validate(async (valid) => {
|
||||
if (valid) {
|
||||
loading.value = true
|
||||
setWeappConfig(formData).then(() => {
|
||||
loading.value = false
|
||||
}).catch(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const modifyDomainRef = ref(null)
|
||||
const modifyDomainFn = () => {
|
||||
modifyDomainRef.value.setFormData(formData.domain)
|
||||
modifyDomainRef.value.showDialog = true
|
||||
}
|
||||
|
||||
const modifyDomainComplete = (data) => {
|
||||
formData.domain = data
|
||||
}
|
||||
|
||||
const modifyPrivacyAgreementRef = ref(null)
|
||||
const modifyPrivacyAgreementFn = () => {
|
||||
modifyPrivacyAgreementRef.value.setFormData()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
75
admin-vben/src/app/views/channel/weapp/course.vue
Normal file
75
admin-vben/src/app/views/channel/weapp/course.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<!--配置教程-->
|
||||
<div class="main-container">
|
||||
|
||||
<el-card class="card !border-none" shadow="never">
|
||||
<el-page-header :content="pageName" :icon="ArrowLeft" @back="back()" />
|
||||
</el-card>
|
||||
|
||||
<el-card class="box-card mt-[15px] !border-none" shadow="never">
|
||||
<div class="flex">
|
||||
<div class="min-w-[60px]">
|
||||
<span class="flex justify-center items-center block w-[40px] h-[40px] border-[1px] border-primary rounded-[999px] text-primary">1</span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="flex items-center text-[14px]">{{ t('writingTipsOne1') }}<el-button link type="primary" @click="linkEvent">{{ t("writingTipsOne2") }}</el-button>,{{ t('writingTipsOne3') }}</p>
|
||||
<div class="w-[100%] mt-[10px]">
|
||||
<img class="w-[100%]" src="@/app/assets/images/setting/weapp_1.png" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex mt-[40px]">
|
||||
<div class="min-w-[60px]">
|
||||
<span class="flex justify-center items-center block w-[40px] h-[40px] border-[1px] border-primary rounded-[999px] text-primary">2</span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="flex items-center text-[14px]">{{ t('writingTipsTwo1') }}</p>
|
||||
<div class="w-[100%] mt-[10px]">
|
||||
<img class="w-[100%]" src="@/app/assets/images/setting/weapp_2.png" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex mt-[40px]">
|
||||
<div class="min-w-[60px]">
|
||||
<span class="flex justify-center items-center block w-[40px] h-[40px] border-[1px] border-primary rounded-[999px] text-primary">3</span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="flex items-center text-[14px]">{{ t('writingTipsThree1') }}<span class="text-primary">{{ t('writingTipsThree2') }}</span></p>
|
||||
<div class="w-[100%] mt-[10px]">
|
||||
<img class="w-[100%]" src="@/app/assets/images/setting/weapp_3.png" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex mt-[40px]">
|
||||
<div class="min-w-[60px]">
|
||||
<span class="flex justify-center items-center block w-[40px] h-[40px] border-[1px] border-primary rounded-[999px] text-primary">4</span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="flex items-center text-[14px]">{{ t('writingTipsFour1') }}<span class="text-primary">URL / Token / EncondingAESKey</span>{{ t('writingTipsFour2') }}</p>
|
||||
<div class="w-[100%] mt-[10px]">
|
||||
<img class="w-[100%]" src="@/app/assets/images/setting/weapp_4.png" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { t } from '@/lang'
|
||||
import { ArrowLeft } from '@element-plus/icons-vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const pageName = route.meta.title
|
||||
const back = () => {
|
||||
router.push('/channel/weapp')
|
||||
}
|
||||
const linkEvent = () => {
|
||||
window.open('https://mp.weixin.qq.com/', '_blank')
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
170
admin-vben/src/app/views/channel/weapp/template.vue
Normal file
170
admin-vben/src/app/views/channel/weapp/template.vue
Normal file
@@ -0,0 +1,170 @@
|
||||
<template>
|
||||
<!--订阅消息-->
|
||||
<div class="main-container">
|
||||
<el-card class="card !border-none" shadow="never">
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-page-title">{{ pageName }}</span>
|
||||
</div>
|
||||
|
||||
<el-tabs v-model="activeName" class="my-[20px]" @tab-change="handleClick">
|
||||
<el-tab-pane :label="t('weappAccessFlow')" name="/channel/weapp" />
|
||||
<el-tab-pane :label="t('subscribeMessage')" name="/channel/weapp/message" />
|
||||
<el-tab-pane :label="t('weappRelease')" name="/channel/weapp/code" />
|
||||
</el-tabs>
|
||||
|
||||
<el-alert :title="t('operationTipTwo')" 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 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.weapp.tips" v-if="row.weapp.tips" placement="top">
|
||||
<icon name="element WarningFilled" />
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</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>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="t('isStart')" min-width="100" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ row.is_weapp == 1 ? t('startUsing') : t('statusDeactivate') }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="weapp_template_id" :label="t('serialNumber')" min-width="180" />
|
||||
|
||||
<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_weapp == 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>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import { getTemplateList, getBatchAcquisition } from '@/app/api/weapp'
|
||||
import { editNoticeStatus } from '@/app/api/notice'
|
||||
import { ElLoading } from 'element-plus'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { AnyObject } from '@/types/global'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const pageName = route.meta.title
|
||||
const activeName = ref('/channel/weapp/message')
|
||||
const handleClick = (val: any) => {
|
||||
router.push({ path: activeName.value })
|
||||
}
|
||||
const cronTableData = reactive({
|
||||
loading: true,
|
||||
data: []
|
||||
})
|
||||
|
||||
/**
|
||||
* 获取消息模板列表
|
||||
*/
|
||||
const loadCronList = (page: number = 1) => {
|
||||
cronTableData.loading = true
|
||||
|
||||
getTemplateList().then(res => {
|
||||
cronTableData.loading = false
|
||||
let data = []
|
||||
res.data.forEach(item => {
|
||||
if (item.notice.length) {
|
||||
const addons = []
|
||||
Object.keys(item.notice).forEach((key, index) => {
|
||||
const notice = item.notice[key]
|
||||
notice.addon_name = item.title
|
||||
addons.push(notice)
|
||||
})
|
||||
if (addons.length) {
|
||||
addons[0].rowspan = addons.length
|
||||
data = data.concat(addons)
|
||||
}
|
||||
}
|
||||
})
|
||||
cronTableData.data = data
|
||||
}).catch(() => {
|
||||
cronTableData.loading = false
|
||||
})
|
||||
}
|
||||
loadCronList()
|
||||
|
||||
const templateSpan = (row : any) => {
|
||||
if (row.columnIndex === 0) {
|
||||
if (row.row.rowspan) {
|
||||
return {
|
||||
rowspan: row.row.rowspan,
|
||||
colspan: 1
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
rowspan: 0,
|
||||
colspan: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量获取
|
||||
*/
|
||||
const batchAcquisitionFn = (row: AnyObject | null = null) => {
|
||||
const loading = ElLoading.service({ lock: true, background: 'rgba(0, 0, 0, 0)' })
|
||||
getBatchAcquisition({ keys: row ? [row.key] : [] }).then(() => {
|
||||
loadCronList()
|
||||
loading.close()
|
||||
}).catch(() => {
|
||||
loading.close()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 开启或关闭订阅消息
|
||||
*/
|
||||
interface Switch {
|
||||
key: string;
|
||||
type: string;
|
||||
status: number
|
||||
}
|
||||
const infoSwitch = (res:any) => {
|
||||
const data = ref<Switch>({
|
||||
key: '',
|
||||
type: '',
|
||||
status: 0
|
||||
})
|
||||
data.value.status = res.is_weapp ? 0 : 1
|
||||
data.value.key = res.key
|
||||
data.value.type = 'weapp'
|
||||
cronTableData.loading = true
|
||||
editNoticeStatus(data.value).then(res => {
|
||||
loadCronList()
|
||||
}).catch(() => {
|
||||
cronTableData.loading = false
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
152
admin-vben/src/app/views/channel/wechat/access.vue
Normal file
152
admin-vben/src/app/views/channel/wechat/access.vue
Normal file
@@ -0,0 +1,152 @@
|
||||
<template>
|
||||
<!--微信公众号-->
|
||||
<div class="main-container">
|
||||
<el-card class="card !border-none" shadow="never">
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-page-title">{{ pageName }}</span>
|
||||
</div>
|
||||
|
||||
<el-tabs v-model="activeName" class="my-[20px]" @tab-change="handleClick">
|
||||
<el-tab-pane :label="t('wechatAccessFlow')" name="/channel/wechat" />
|
||||
<el-tab-pane :label="t('customMenu')" name="/channel/wechat/menu" />
|
||||
<el-tab-pane :label="t('wechatTemplate')" name="/channel/wechat/message" />
|
||||
<el-tab-pane :label="t('reply')" name="/channel/wechat/reply" />
|
||||
</el-tabs>
|
||||
|
||||
<div class="p-[20px]">
|
||||
<h3 class="panel-title !text-sm">{{ t("wechatInlet") }}</h3>
|
||||
|
||||
<el-row>
|
||||
<el-col :span="20">
|
||||
<el-steps class="!mt-[10px]" :active="3" direction="vertical">
|
||||
<el-step>
|
||||
<template #title>
|
||||
<p class="text-[14px] font-[700]">
|
||||
{{ t("wechatAttestation") }}
|
||||
</p>
|
||||
</template>
|
||||
<template #description>
|
||||
<span class="text-[#999]">{{ t("wechatAttestation1") }}</span>
|
||||
<div class="mt-[20px] mb-[40px] h-[32px]">
|
||||
<el-button type="primary" @click="linkEvent('https://mp.weixin.qq.com/')">{{ t("clickAccess") }}</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-step>
|
||||
|
||||
<el-step>
|
||||
<template #title>
|
||||
<p class="text-[14px] font-[700]">
|
||||
{{ t("wechatSetting") }}
|
||||
</p>
|
||||
</template>
|
||||
<template #description>
|
||||
<span class="text-[#999]">{{ t("wechatSetting1") }}</span>
|
||||
<div class="mt-[20px] mb-[40px] h-[32px]">
|
||||
<template v-if="oplatformConfig.app_id && oplatformConfig.app_secret">
|
||||
<el-button type="primary" @click="router.push('/channel/wechat/config')">{{ wechatConfig.app_id ? t("seeConfig") : t("clickSetting") }}</el-button>
|
||||
<el-button type="primary" plain @click="authBindWechat">{{ wechatConfig.is_authorization ? t("refreshAuth") : t("authWechat") }}</el-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-button type="primary" @click="router.push('/channel/wechat/config')">{{ t("clickSetting") }}</el-button>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</el-step>
|
||||
|
||||
<el-step>
|
||||
<template #title>
|
||||
<p class="text-[14px] font-[700]">
|
||||
{{ t("wechatAccess") }}
|
||||
</p>
|
||||
</template>
|
||||
<template #description>
|
||||
<span class="text-[#999]">{{ t("wechatAccess") }}</span>
|
||||
<div class="mt-[20px] mb-[40px] h-[32px]">
|
||||
<el-button type="primary" plain @click="router.push('/channel/wechat/course')">{{ t("releaseCourse") }}</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-step>
|
||||
</el-steps>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="4">
|
||||
<div class="flex justify-center">
|
||||
<el-image class="w-[180px] h-[180px]" :src="qrcode ? img(qrcode) : ''">
|
||||
<template #error>
|
||||
<div class="w-[100%] h-[100%] flex items-center justify-center bg-[#f5f7fa]">
|
||||
<span>{{ qrcode ? t('fileErr') : t('emptyQrCode') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
</div>
|
||||
<div class="mt-[22px] text-center">
|
||||
<p class="text-[12px]">{{ t('clickAccess2') }}</p>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import { img } from '@/utils/common'
|
||||
import { getWechatConfig } from '@/app/api/wechat'
|
||||
import { getAuthorizationUrl } from '@/app/api/wxoplatform'
|
||||
import { getWxoplatform } from '@/app/api/sys'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const pageName = route.meta.title
|
||||
|
||||
const activeName = ref('/channel/wechat')
|
||||
const qrcode = ref('')
|
||||
const wechatConfig = ref({})
|
||||
const oplatformConfig = ref({})
|
||||
|
||||
const onShowGetWechatConfig = async () => {
|
||||
await getWechatConfig().then(({ data }) => {
|
||||
wechatConfig.value = data
|
||||
qrcode.value = data.qr_code
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await onShowGetWechatConfig()
|
||||
|
||||
await getWxoplatform().then(({ data }) => {
|
||||
oplatformConfig.value = data
|
||||
})
|
||||
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
onShowGetWechatConfig()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('visibilitychange', () => {
|
||||
})
|
||||
})
|
||||
|
||||
const linkEvent = (url: string) => {
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
|
||||
const handleClick = (val: any) => {
|
||||
router.push({ path: activeName.value })
|
||||
}
|
||||
|
||||
const authBindWechat = () => {
|
||||
getAuthorizationUrl().then(({ data }) => {
|
||||
window.open(data)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
132
admin-vben/src/app/views/channel/wechat/components/menu-form.vue
Normal file
132
admin-vben/src/app/views/channel/wechat/components/menu-form.vue
Normal file
@@ -0,0 +1,132 @@
|
||||
<template>
|
||||
<div class="panel-title">{{ buttonData.sub_button ? t('menuNameInfo') : t('subMenuNameInfo') }}</div>
|
||||
|
||||
<el-form :model="buttonData" label-width="140px" ref="formRef" :rules="formRules" class="page-form mt-[30px]">
|
||||
|
||||
<el-form-item :label="t('menuName')" prop="name">
|
||||
<el-input v-model.trim="buttonData.name" :placeholder="t('menuNamePlaceholder')" class="input-width" clearable />
|
||||
<div class="form-tip">{{ buttonData.sub_button ? t('menuNameTips') : t('subMenuNameTips') }}</div>
|
||||
</el-form-item>
|
||||
|
||||
<template v-if="!buttonData.sub_button || !buttonData.sub_button.length">
|
||||
<el-form-item :label="t('messageType')">
|
||||
<el-radio-group v-model="buttonData.type">
|
||||
<el-radio label="view">{{ t('skipWebpage') }}</el-radio>
|
||||
<el-radio label="miniprogram">{{ t('skipWeapp') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('webpageUrl')" prop="url">
|
||||
<el-input v-model.trim="buttonData.url" :placeholder="t('webpageUrlPlaceholder')" class="input-width" clearable />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('weappAppid')" prop="appid" v-show="buttonData.type == 'miniprogram'">
|
||||
<el-input v-model.trim="buttonData.appid" :placeholder="t('weappAppidPlaceholder')" class="input-width" clearable />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('weappPage')" prop="pagepath" v-show="buttonData.type == 'miniprogram'">
|
||||
<el-input v-model.trim="buttonData.pagepath" :placeholder="t('weappPagePlaceholder')" class="input-width" clearable />
|
||||
</el-form-item>
|
||||
|
||||
</template>
|
||||
|
||||
<div class="mt-[40px]">
|
||||
<el-button type="primary" link @click="deleteButton">{{ t('deleteMemu') }}</el-button>
|
||||
</div>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import { strByteLength, isUrl } from '@/utils/common'
|
||||
|
||||
const prop = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => { }
|
||||
},
|
||||
index: {
|
||||
type: Number,
|
||||
default: 0
|
||||
},
|
||||
subIndex: {
|
||||
type: Number,
|
||||
default: -1
|
||||
}
|
||||
})
|
||||
|
||||
const formRef = ref()
|
||||
|
||||
const buttonData = computed({
|
||||
get () {
|
||||
return prop.data
|
||||
},
|
||||
set (value) {
|
||||
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 验证规则
|
||||
*/
|
||||
const formRules = computed(() => {
|
||||
return {
|
||||
name: [
|
||||
{ required: true, message: t('menuNamePlaceholder'), trigger: 'blur' },
|
||||
{
|
||||
validator: (rule: any, value: string, callback: any) => {
|
||||
if (buttonData.value.sub_button && strByteLength(value) > 8) callback(new Error(t('menuNameTips')))
|
||||
else if (!buttonData.value.sub_button && strByteLength(value) > 16) callback(new Error(t('subMenuNameTips')))
|
||||
else callback()
|
||||
},
|
||||
trigger: ['blur', 'change']
|
||||
}
|
||||
],
|
||||
url: [
|
||||
{ required: !buttonData.value.sub_button || !buttonData.value.sub_button.length, message: t('webpageUrlPlaceholder'), trigger: 'blur' },
|
||||
{
|
||||
validator: (rule: any, value: string, callback: any) => {
|
||||
if (!buttonData.value.sub_button || !buttonData.value.sub_button.length) {
|
||||
if (!isUrl(value)) {
|
||||
callback(new Error(t('menuUrlErrorTips')))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
appid: [
|
||||
{ required: ((!buttonData.value.sub_button || !buttonData.value.sub_button.length) && buttonData.value.type == 'miniprogram'), message: t('weappAppidPlaceholder'), trigger: 'blur' }
|
||||
],
|
||||
pagepath: [
|
||||
{ required: ((!buttonData.value.sub_button || !buttonData.value.sub_button.length) && buttonData.value.type == 'miniprogram'), message: t('weappPagePlaceholder'), trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['delete'])
|
||||
|
||||
const deleteButton = () => {
|
||||
emit('delete')
|
||||
}
|
||||
|
||||
const validate = async () => {
|
||||
let validate = false
|
||||
await formRef.value.validate(async (valid: boolean) => {
|
||||
validate = valid
|
||||
})
|
||||
return validate
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
validate,
|
||||
index: prop.index,
|
||||
subIndex: prop.subIndex
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<div class="attachment-item text-sm mr-[10px] mb-[10px] w-[280px] rounded-lg overflow-hidden border border-color" v-if="data">
|
||||
<div class="relative" @mouseover="hover = true" @mouseout="hover = false">
|
||||
<div class="w-full h-[130px] relative">
|
||||
<el-image :src="data.value.news_item[0].thumb_url" class="w-full h-full"/>
|
||||
<div class="absolute left-0 bottom-0 p-[10px] w-full truncate text-white leading-none" v-if="data.value.news_item.length > 1">
|
||||
{{ data.value.news_item[0].title }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="data.value.news_item.length > 1">
|
||||
<template v-for="(newsItem, newsIndex) in data.value.news_item">
|
||||
<div class="px-[15px] py-[10px] flex" :class="{'border-b border-color' : newsIndex < data.value.news_item.length - 1 }" v-if="newsIndex > 0">
|
||||
<div class="flex-1 w-0 truncate">
|
||||
{{ newsItem.title }}
|
||||
</div>
|
||||
<div class="w-[50px] h-[50px] ml-[10px]">
|
||||
<el-image :src="newsItem.thumb_url" class="w-full h-full"/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="px-[15px] py-[10px]" v-else>
|
||||
{{ data.value.news_item[0].title }}
|
||||
</div>
|
||||
<div class="absolute z-[1] flex items-center justify-center w-full h-full inset-0 bg-black bg-opacity-60 cursor-pointer" @click="data = null" v-show="hover && props.mode == 'select'">
|
||||
<icon name="element Delete" color="#fff" size="40px" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {}
|
||||
}
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
default: 'select'
|
||||
}
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const hover = ref(false)
|
||||
const data = computed({
|
||||
get () {
|
||||
return props.modelValue
|
||||
},
|
||||
set (value) {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
})
|
||||
|
||||
const meta = document.createElement('meta')
|
||||
meta.content = 'same-origin'
|
||||
meta.name = 'referrer'
|
||||
document.getElementsByTagName('head')[0].appendChild(meta)
|
||||
</script>
|
||||
@@ -0,0 +1,285 @@
|
||||
<template>
|
||||
<div class="border border-br-light rounded">
|
||||
<div class="py-[10px] px-[30px] flex text-sm border-0 border-b border-br-light text-tx-regular">
|
||||
<div class="pr-[25px] cursor-pointer flex items-center" :class="{'text-primary': formData.msgtype == 'text'}" @click="switchMsgType('text')">
|
||||
<icon name="iconfont iconxingzhuang-wenzi" size="18" class="mr-[5px]"/>
|
||||
文本
|
||||
</div>
|
||||
<div class="pr-[25px] cursor-pointer flex items-center" :class="{'text-primary': formData.msgtype == 'image'}" @click="switchMsgType('image')">
|
||||
<icon name="iconfont icontupian" size="18px" class="mr-[5px]"/>
|
||||
图片
|
||||
</div>
|
||||
<div class="pr-[25px] cursor-pointer flex items-center" :class="{'text-primary': formData.msgtype == 'video'}" @click="switchMsgType('video')">
|
||||
<icon name="iconfont iconshipin1" size="18" class="mr-[5px]"/>
|
||||
视频
|
||||
</div>
|
||||
<div class="pr-[25px] cursor-pointer flex items-center" :class="{'text-primary': formData.msgtype == 'mpnewsarticle'}" @click="switchMsgType('mpnewsarticle')">
|
||||
<icon name="iconfont icontuwendaohang2" size="13px" class="mr-[5px]"/>
|
||||
图文
|
||||
</div>
|
||||
<div class="pr-[25px] cursor-pointer flex items-center" :class="{'text-primary': formData.msgtype == 'miniprogrampage'}" @click="switchMsgType('miniprogrampage')">
|
||||
<icon name="iconfont iconxiaochengxu" size="14px" class="mr-[5px]"/>
|
||||
小程序卡片
|
||||
</div>
|
||||
</div>
|
||||
<div class="py-[20px] px-[30px] h-[350px]">
|
||||
<div v-if="formData.msgtype == 'text'">
|
||||
<el-input v-model.trim="formData.text.content" :rows="5" type="textarea" placeholder="" maxlength="600" :show-word-limit="true" resize="none" input-style="box-shadow: none;height:300px" />
|
||||
</div>
|
||||
<div v-if="formData.msgtype == 'image'" class="flex w-full h-full justify-center items-center image-media">
|
||||
<div class="w-full h-full" v-if="formData.image.url">
|
||||
<upload-image :limit="1" width="150px" height="150px" v-model="formData.image.url"/>
|
||||
</div>
|
||||
<div v-else class="flex w-full h-full justify-center items-center image-media">
|
||||
<div class="flex flex-1 h-full border border-br-light cursor-pointer select-media">
|
||||
<select-wechat-media type="image" @success="setImageMedia">
|
||||
<div class="flex items-center justify-center flex-col">
|
||||
<icon name="element Plus" size="20px" color="var(--el-text-color-secondary)" />
|
||||
<div class="leading-none text-xs mt-[10px] text-secondary">从素材库选择</div>
|
||||
</div>
|
||||
</select-wechat-media>
|
||||
</div>
|
||||
<div class="flex flex-1 h-full ml-[20px] border border-br-light cursor-pointer">
|
||||
<upload-media type="image" class="w-full h-full flex items-center justify-center" @success="setImageMedia">
|
||||
<div class="flex items-center justify-center flex-col">
|
||||
<icon name="element Plus" size="20px" color="var(--el-text-color-secondary)" />
|
||||
<div class="leading-none text-xs mt-[10px] text-secondary">上传图片</div>
|
||||
</div>
|
||||
</upload-media>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="formData.msgtype == 'video'" class="flex w-full h-full justify-center items-center video-media">
|
||||
<div class="w-full h-full" v-if="formData.video.url">
|
||||
<upload-video :limit="1" width="150px" height="150px" v-model="formData.video.url"/>
|
||||
</div>
|
||||
<div v-else class="flex w-full h-full justify-center items-center video-media">
|
||||
<div class="flex flex-1 h-full border border-br-light cursor-pointer select-media">
|
||||
<select-wechat-media type="video" @success="setVideoMedia">
|
||||
<div class="flex items-center justify-center flex-col">
|
||||
<icon name="element Plus" size="20px" color="var(--el-text-color-secondary)" />
|
||||
<div class="leading-none text-xs mt-[10px] text-secondary">从素材库选择</div>
|
||||
</div>
|
||||
</select-wechat-media>
|
||||
</div>
|
||||
<div class="flex flex-1 h-full ml-[20px] border border-br-light cursor-pointer">
|
||||
<upload-media type="video" class="w-full h-full flex items-center justify-center" @success="setVideoMedia">
|
||||
<div class="flex items-center justify-center flex-col">
|
||||
<icon name="element Plus" size="20px" color="var(--el-text-color-secondary)" />
|
||||
<div class="leading-none text-xs mt-[10px] text-secondary">上传视频</div>
|
||||
</div>
|
||||
</upload-media>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="formData.msgtype == 'mpnewsarticle'" class="flex w-full h-full justify-center items-center image-media">
|
||||
<div class="w-full h-full" v-if="formData.mpnewsarticle">
|
||||
<news-card v-model="formData.mpnewsarticle"/>
|
||||
</div>
|
||||
<div v-else class="flex w-full h-full justify-center items-center image-media">
|
||||
<div class="flex flex-1 h-full border border-br-light cursor-pointer select-media">
|
||||
<select-wechat-media type="news" @success="setNewsMedia">
|
||||
<div class="flex items-center justify-center flex-col">
|
||||
<icon name="element Plus" size="20px" color="var(--el-text-color-secondary)" />
|
||||
<div class="leading-none text-xs mt-[10px] text-secondary">从素材库选择</div>
|
||||
</div>
|
||||
</select-wechat-media>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="formData.msgtype == 'miniprogrampage'">
|
||||
<el-form :model="formData.miniprogrampage" label-width="140px" class="page-form" ref="formRef" :rules="formRules">
|
||||
<el-form-item label="小程序APPID" prop="appid">
|
||||
<el-input v-model.trim="formData.miniprogrampage.appid" class="input-width"/>
|
||||
<div class="form-tip">小程序需已经与公众号关联</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="小程序卡片标题" prop="title">
|
||||
<el-input v-model.trim="formData.miniprogrampage.title" class="input-width"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="小程序的页面路径" prop="pagepath">
|
||||
<el-input v-model.trim="formData.miniprogrampage.pagepath" class="input-width"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="小程序卡片图片" prop="thumb_media_url">
|
||||
<upload-image :limit="1" width="100px" height="100px" v-model="formData.miniprogrampage.thumb_media_url" v-if="formData.miniprogrampage.thumb_media_url"/>
|
||||
<select-wechat-media type="image" @success="setWeappImageMedia" v-else>
|
||||
<div class="rounded cursor-pointer overflow-hidden relative border border-solid border-color image-wrap mr-[10px] w-[100px] h-[100px]">
|
||||
<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">{{ t('upload.root') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</select-wechat-media>
|
||||
<div class="form-tip">小程序卡片图片建议大小为520*416</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref, watch } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import UploadMedia from '@/app/views/channel/wechat/components/upload-media.vue'
|
||||
import SelectWechatMedia from '@/app/views/channel/wechat/components/select-wechat-media.vue'
|
||||
import Test from '@/utils/test'
|
||||
import { ElMessage, FormInstance, FormRules } from 'element-plus'
|
||||
import NewsCard from '@/app/views/channel/wechat/components/news-card.vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const formData = ref({
|
||||
msgtype: 'text',
|
||||
text: {
|
||||
content: ''
|
||||
},
|
||||
image: {
|
||||
media_id: '',
|
||||
url: ''
|
||||
},
|
||||
video: {
|
||||
media_id: '',
|
||||
url: ''
|
||||
},
|
||||
miniprogrampage: {
|
||||
appid: '',
|
||||
title: '',
|
||||
pagepath: '',
|
||||
thumb_media_url: '',
|
||||
thumb_media_id: ''
|
||||
},
|
||||
mpnewsarticle: null
|
||||
})
|
||||
|
||||
const value = computed({
|
||||
get () {
|
||||
return props.modelValue
|
||||
},
|
||||
set (value) {
|
||||
emit('update:modelValue', value)
|
||||
}
|
||||
})
|
||||
|
||||
watch(() => value.value, (nval, oval) => {
|
||||
if ((!oval || !Object.keys(oval).length) && Object.keys(nval).length) {
|
||||
formData.value = value.value
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
watch(() => formData.value, () => {
|
||||
value.value = formData.value
|
||||
}, { deep: true })
|
||||
|
||||
const switchMsgType = (type: string) => {
|
||||
formData.value.msgtype = type
|
||||
}
|
||||
|
||||
const setImageMedia = (data: any) => {
|
||||
formData.value.image.media_id = data.media_id
|
||||
formData.value.image.url = data.value
|
||||
}
|
||||
|
||||
const setVideoMedia = (data: any) => {
|
||||
formData.value.video.media_id = data.media_id
|
||||
formData.value.video.url = data.value
|
||||
}
|
||||
|
||||
const setWeappImageMedia = (data: any) => {
|
||||
formData.value.miniprogrampage.thumb_media_id = data.media_id
|
||||
formData.value.miniprogrampage.thumb_media_url = data.value
|
||||
}
|
||||
|
||||
const setNewsMedia = (data: any) => {
|
||||
formData.value.mpnewsarticle = {
|
||||
article_id: data.media_id,
|
||||
value: data.value
|
||||
}
|
||||
}
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = reactive<FormRules>({
|
||||
appid: [
|
||||
{ required: true, message: '请填写小程序appid', trigger: 'blur' }
|
||||
],
|
||||
title: [
|
||||
{ required: true, message: '请填写小程序卡片标题', trigger: 'blur' }
|
||||
],
|
||||
pagepath: [
|
||||
{ required: true, message: '请填写小程序卡片跳转页面', trigger: 'blur' }
|
||||
],
|
||||
thumb_media_url: [
|
||||
{ required: true, message: '请上传小程序卡片封面', trigger: 'blur' }
|
||||
]
|
||||
})
|
||||
|
||||
/**
|
||||
* 验证数据
|
||||
*/
|
||||
const verify = async () => {
|
||||
let verify = true
|
||||
switch (formData.value.msgtype) {
|
||||
case 'text':
|
||||
if (Test.empty(formData.value.text.content)) {
|
||||
ElMessage({ message: '请输入回复内容', type: 'warning' })
|
||||
verify = false
|
||||
}
|
||||
break
|
||||
case 'image':
|
||||
if (Test.empty(formData.value.image.url)) {
|
||||
ElMessage({ message: '请上传回复图片', type: 'warning' })
|
||||
verify = false
|
||||
}
|
||||
break
|
||||
case 'video':
|
||||
if (Test.empty(formData.value.video.url)) {
|
||||
ElMessage({ message: '请上传回复视频', type: 'warning' })
|
||||
verify = false
|
||||
}
|
||||
break
|
||||
case 'miniprogrampage':
|
||||
await formRef.value.validate(async (valid) => {
|
||||
verify = valid
|
||||
})
|
||||
break
|
||||
case 'mpnewsarticle':
|
||||
if (Test.empty(formData.value.mpnewsarticle)) {
|
||||
ElMessage({ message: '请选择图文', type: 'warning' })
|
||||
verify = false
|
||||
}
|
||||
break
|
||||
}
|
||||
return verify
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
verify
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.image-media, .video-media) {
|
||||
.el-upload {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
:deep(.select-media) {
|
||||
& > div {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,214 @@
|
||||
<template>
|
||||
<div @click="openDialog">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<el-dialog v-model="showDialog" :title="t('upload.select' + type)" width="60%" class="attachment-dialog" :destroy-on-close="true">
|
||||
<div class="flex border-t border-b main-wrap border-color w-full h-[40vh]">
|
||||
<!-- 素材 -->
|
||||
<div class="attachment-list-wrap flex flex-col p-[15px] flex-1 overflow-hidden">
|
||||
<el-row :gutter="15" class="h-[32px]">
|
||||
<el-col :span="10">
|
||||
<div class="flex" v-if="prop.type != 'news'">
|
||||
<upload-media :type="prop.type" @success="getAttachmentList()">
|
||||
<el-button type="primary">{{ t('upload.upload' + type) }}</el-button>
|
||||
</upload-media>
|
||||
</div>
|
||||
<div class="flex" v-else>
|
||||
<el-button type="primary" :loading="syncLoading" @click="syncWechatNews">
|
||||
{{ syncLoading ? '同步中' : '同步微信图文' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<div class="flex-1 my-[15px] h-0" v-loading="attachment.loading">
|
||||
<el-scrollbar>
|
||||
<!-- 素材管理 -->
|
||||
<div v-if="attachment.data.length">
|
||||
<div class="flex flex-wrap" v-if="prop.type != 'news'">
|
||||
<div class="attachment-item mr-[10px] mb-[10px] w-[120px]"
|
||||
v-for="(item, index) in attachment.data" :key="index" @click="selectedFile = item">
|
||||
<div
|
||||
class="attachment-wrap w-full rounded cursor-pointer overflow-hidden relative flex items-center justify-center h-[120px]">
|
||||
<el-image :src="img(item.value)" fit="contain" v-if="type == 'image'" :preview-src-list="item.image_list" />
|
||||
<video :src="img(item.value)" v-else-if="type == 'video'"></video>
|
||||
<div class="absolute z-[1] flex items-center justify-center w-full h-full inset-0 bg-black bg-opacity-60" v-show="selectedFile.id == item.id">
|
||||
<icon name="element Select" color="#fff" size="40px" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative" ref="waterfallContainerRef" v-else>
|
||||
<div ref="waterfallItemRef"
|
||||
class="absolute attachment-item mr-[10px] mb-[10px] w-[280px] rounded-lg overflow-hidden border border-color"
|
||||
v-for="(item, index) in attachment.data"
|
||||
:style="{ left: listPosition[index] ? listPosition[index].left : '', top: listPosition[index] ? listPosition[index].top : '' }"
|
||||
:key="index" @click="selectedFile = item">
|
||||
<div class="relative">
|
||||
<div class="w-full h-[130px] relative">
|
||||
<el-image :src="item.value.news_item[0].thumb_url" class="w-full h-full" />
|
||||
<div class="absolute left-0 bottom-0 p-[10px] w-full truncate text-white leading-none" v-if="item.value.news_item.length > 1">
|
||||
{{ item.value.news_item[0].title }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="item.value.news_item.length > 1">
|
||||
<template v-for="(newsItem, newsIndex) in item.value.news_item">
|
||||
<div class="px-[15px] py-[10px] flex" :class="{'border-b border-color' : newsIndex < item.value.news_item.length - 1 }" v-if="newsIndex > 0">
|
||||
<div class="flex-1 w-0 truncate">{{ newsItem.title }}</div>
|
||||
<div class="w-[50px] h-[50px] ml-[10px]">
|
||||
<el-image :src="newsItem.thumb_url" class="w-full h-full" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div class="px-[15px] py-[10px]" v-else>{{ item.value.news_item[0].title }}</div>
|
||||
<div class="absolute z-[1] flex items-center justify-center w-full h-full inset-0 bg-black bg-opacity-60" v-show="selectedFile.id == item.id">
|
||||
<icon name="element Select" color="#fff" size="40px" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-center" v-else>
|
||||
<el-empty v-if="!attachment.loading" :description="t('upload.mediaEmpty')" :image-size="100" />
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
<el-row :gutter="20">
|
||||
<el-col span="24">
|
||||
<div class="flex h-full justify-end items-center">
|
||||
<el-pagination v-model:current-page="attachment.page" :small="true"
|
||||
v-model:page-size="attachment.limit" :page-sizes="[10, 20, 30, 40, 60]"
|
||||
layout="total, sizes, prev, pager, next, jumper" :total="attachment.total"
|
||||
@size-change="getAttachmentList()" @current-change="getAttachmentList" />
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button>
|
||||
<el-button type="primary" @click="confirm">{{ t('confirm') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref, nextTick } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import UploadMedia from './upload-media.vue'
|
||||
import { img, debounce } from '@/utils/common'
|
||||
import { getMediaList, syncNews } from '@/app/api/wechat'
|
||||
|
||||
const prop = defineProps({
|
||||
type: {
|
||||
type: String,
|
||||
default: 'image'
|
||||
}
|
||||
})
|
||||
|
||||
const showDialog = ref(false)
|
||||
|
||||
const openDialog = () => {
|
||||
prop.type == 'news' && waterfall()
|
||||
showDialog.value = true
|
||||
}
|
||||
|
||||
const attachment: Record<string, any> = reactive({
|
||||
loading: true,
|
||||
page: 1,
|
||||
total: 0,
|
||||
limit: 10,
|
||||
data: []
|
||||
})
|
||||
|
||||
/**
|
||||
* 查询素材
|
||||
*/
|
||||
const getAttachmentList = (page: number = 1) => {
|
||||
attachment.loading = true
|
||||
attachment.page = page
|
||||
getMediaList({
|
||||
page: attachment.page,
|
||||
limit: attachment.limit,
|
||||
type: prop.type
|
||||
}).then(res => {
|
||||
attachment.data = res.data.data
|
||||
attachment.total = res.data.total
|
||||
attachment.loading = false
|
||||
prop.type == 'news' && waterfall()
|
||||
}).catch(() => {
|
||||
attachment.loading = false
|
||||
})
|
||||
}
|
||||
getAttachmentList()
|
||||
|
||||
const emits = defineEmits(['success'])
|
||||
const selectedFile: Record<string, any> = ref({})
|
||||
|
||||
const confirm = () => {
|
||||
emits('success', selectedFile.value)
|
||||
}
|
||||
|
||||
const syncLoading = ref(false)
|
||||
const syncWechatNews = () => {
|
||||
if (syncLoading.value) return
|
||||
syncLoading.value = true
|
||||
|
||||
syncNews().then(() => {
|
||||
syncLoading.value = false
|
||||
getAttachmentList()
|
||||
}).catch(() => {
|
||||
syncLoading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
const meta = document.createElement('meta')
|
||||
meta.content = 'same-origin'
|
||||
meta.name = 'referrer'
|
||||
document.getElementsByTagName('head')[0].appendChild(meta)
|
||||
|
||||
// 瀑布流计算
|
||||
const waterfallContainerRef = ref(null)
|
||||
const waterfallItemRef = ref([])
|
||||
const listPosition = ref([])
|
||||
const waterfall = debounce(() => {
|
||||
nextTick(() => {
|
||||
const containerWidth = waterfallContainerRef.value.clientWidth
|
||||
const column = parseInt(containerWidth / 292)
|
||||
const heights = []
|
||||
const positions = []
|
||||
|
||||
waterfallItemRef.value.forEach((item, i) => {
|
||||
if (i < column) {
|
||||
const position = {}
|
||||
position.top = '0px'
|
||||
if (i % column == 0) {
|
||||
position.left = item.clientWidth * i + 'px'
|
||||
} else {
|
||||
position.left = item.clientWidth * i + (i % column * 10) + 'px'
|
||||
}
|
||||
positions[i] = position
|
||||
heights[i] = item.clientHeight + 10
|
||||
} else {
|
||||
const minHeight = Math.min(...heights) // 找到第一列的最小高度
|
||||
const minIndex = heights.findIndex(item => item === minHeight) // 找到最小高度的索引
|
||||
const position = {}
|
||||
position.top = minHeight + 10 + 'px'
|
||||
position.left = positions[minIndex].left
|
||||
positions[i] = position
|
||||
heights[minIndex] += item.clientHeight + 10
|
||||
}
|
||||
})
|
||||
listPosition.value = positions
|
||||
})
|
||||
}, 800)
|
||||
|
||||
// 重新布局,以适应窗口变化
|
||||
window.addEventListener('resize', () => waterfall())
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<el-upload v-bind="upload" ref="uploadRef">
|
||||
<slot></slot>
|
||||
</el-upload>
|
||||
</template>
|
||||
|
||||
<script lang='ts' setup>
|
||||
import { computed, ref } from 'vue'
|
||||
import { getToken } from '@/utils/common'
|
||||
import storage from '@/utils/storage'
|
||||
import { ElMessage, UploadFile, UploadFiles } from 'element-plus'
|
||||
|
||||
const prop = defineProps({
|
||||
type: {
|
||||
type: String,
|
||||
default: 'image'
|
||||
}
|
||||
})
|
||||
|
||||
const emits = defineEmits(['success'])
|
||||
|
||||
const uploadRef = ref<Record<string, any> | null>(null)
|
||||
// 上传文件
|
||||
const upload = computed(() => {
|
||||
const headers: Record<string, any> = {}
|
||||
headers[import.meta.env.VITE_REQUEST_HEADER_TOKEN_KEY] = getToken()
|
||||
headers[import.meta.env.VITE_REQUEST_HEADER_SITEID_KEY] = storage.get('siteId') || 0
|
||||
|
||||
return {
|
||||
action: `${import.meta.env.VITE_APP_BASE_URL}/wechat/media/${prop.type}`,
|
||||
multiple: true,
|
||||
headers,
|
||||
accept: prop.type == 'image' ? '.bmp,.png,.jpeg,.jpg,.gif' : '.mp4',
|
||||
onSuccess: (response: any, uploadFile: UploadFile, uploadFiles: UploadFiles) => {
|
||||
if (response.code >= 1) {
|
||||
emits('success', response.data)
|
||||
uploadRef.value?.handleRemove(uploadFile)
|
||||
} else {
|
||||
uploadFile.status = 'fail'
|
||||
uploadRef.value?.handleRemove(uploadFile)
|
||||
ElMessage({ message: response.msg, type: 'error' })
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
236
admin-vben/src/app/views/channel/wechat/config.vue
Normal file
236
admin-vben/src/app/views/channel/wechat/config.vue
Normal file
@@ -0,0 +1,236 @@
|
||||
<template>
|
||||
<!--公众号配置-->
|
||||
<div class="main-container">
|
||||
|
||||
<el-card class="card !border-none" shadow="never">
|
||||
<el-page-header :content="pageName" :icon="ArrowLeft" @back="back()" />
|
||||
</el-card>
|
||||
|
||||
<el-form class="page-form mt-[15px]" :model="formData" label-width="150px" ref="formRef" :rules="formRules" v-loading="loading">
|
||||
<el-card class="box-card !border-none" shadow="never">
|
||||
<h3 class="panel-title !text-sm">{{ t('wechatInfo') }}</h3>
|
||||
|
||||
<el-form-item :label="t('wechatName')" prop="wechat_name">
|
||||
<el-input v-model.trim="formData.wechat_name" :placeholder="t('wechatNamePlaceholder')" class="input-width" clearable :readonly="formData.is_authorization"/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('wechatOriginal')" prop="wechat_original">
|
||||
<el-input v-model.trim="formData.wechat_original" :placeholder="t('wechatOriginalPlaceholder')" class="input-width" clearable :readonly="formData.is_authorization"/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('wechatQrcode')" prop="qr_code">
|
||||
<upload-image v-model="formData.qr_code" />
|
||||
<div class="form-tip">{{ t('wechatQrcodeTips') }}</div>
|
||||
</el-form-item>
|
||||
</el-card>
|
||||
|
||||
<el-card class="box-card !border-none mt-[15px]" shadow="never">
|
||||
<h3 class="panel-title !text-sm">{{ t('wechatDevelopInfo') }}</h3>
|
||||
|
||||
<el-form-item :label="t('wechatAppid')" prop="app_id">
|
||||
<el-input v-model.trim="formData.app_id" :placeholder="t('appidPlaceholder')" class="input-width" clearable :readonly="formData.is_authorization"/>
|
||||
<div class="form-tip">{{ t('wechatAppidTips') }}</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('wechatAppsecret')" prop="app_secret" v-if="!formData.is_authorization">
|
||||
<el-input v-model.trim="formData.app_secret" :placeholder="t('appSecretPlaceholder')" class="input-width" clearable />
|
||||
<div class="form-tip">{{ t('wechatAppsecretTips') }}</div>
|
||||
</el-form-item>
|
||||
</el-card>
|
||||
|
||||
<el-card class="box-card !border-none mt-[15px]" shadow="never">
|
||||
<h3 class="panel-title !text-sm">{{ t('theServerSetting') }}</h3>
|
||||
|
||||
<el-form-item label="URL">
|
||||
<el-input :model-value="wechatStatic.serve_url" placeholder="Please input" class="input-width" :readonly="true">
|
||||
<template #append>
|
||||
<div class="cursor-pointer" @click="copyEvent(wechatStatic.serve_url)">{{ t('copy') }}</div>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="Token" prop="token">
|
||||
<el-input v-model.trim="formData.token" :placeholder="t('tokenPlaceholder')" class="input-width" maxlength="32" show-word-limit clearable />
|
||||
<div class="form-tip">{{ t('tokenTips') }}</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="EncodingAESKey" prop="encoding_aes_key">
|
||||
<el-input v-model.trim="formData.encoding_aes_key" :placeholder="t('encodingAesKeyPlaceholder')" class="input-width" maxlength="43" show-word-limit clearable />
|
||||
<div class="form-tip">{{ t('encodingAESKeyTips') }}</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('encryptionType')" prop="encryption_type">
|
||||
<el-radio-group v-model="formData.encryption_type">
|
||||
<el-radio label="not_encrypt">{{ t('cleartextMode') }}</el-radio>
|
||||
<el-radio label="compatible">{{ t('compatibleMode') }}</el-radio>
|
||||
<el-radio label="safe">{{ t('safeMode') }}</el-radio>
|
||||
</el-radio-group>
|
||||
<div class="form-tip">{{ t('cleartextModeTips') }}</div>
|
||||
<div class="form-tip">{{ t('compatibleModeTips') }}</div>
|
||||
<div class="form-tip">{{ t('safeModeTips') }}</div>
|
||||
</el-form-item>
|
||||
</el-card>
|
||||
|
||||
<el-card class="box-card !border-none mt-[15px]" shadow="never">
|
||||
<div class="flex">
|
||||
<h3 class="panel-title !text-sm">{{ t('functionSetting') }}</h3>
|
||||
</div>
|
||||
|
||||
<el-form-item label="">
|
||||
<div class="form-tip">{{ t('functionSettingTips') }}</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('businessDomain')">
|
||||
<el-input :model-value="wechatStatic.business_domain" placeholder="Please input" class="input-width" :readonly="true">
|
||||
<template #append>
|
||||
<div class="cursor-pointer" @click="copyEvent(wechatStatic.business_domain)">{{ t('copy') }}</div>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('jsSecureDomain')">
|
||||
<el-input :model-value="wechatStatic.js_secure_domain" placeholder="Please input" class="input-width" :readonly="true">
|
||||
<template #append>
|
||||
<div class="cursor-pointer" @click="copyEvent(wechatStatic.business_domain)">{{ t('copy') }}</div>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('webAuthDomain')">
|
||||
<el-input :model-value="wechatStatic.web_auth_domain" placeholder="Please input" class="input-width" :readonly="true">
|
||||
<template #append>
|
||||
<div class="cursor-pointer" @click="copyEvent(wechatStatic.business_domain)">{{ t('copy') }}</div>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</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>
|
||||
import { reactive, ref, watch, computed } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import { getWechatConfig, getWechatStatic, editWechatConfig } from '@/app/api/wechat'
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
import { ElMessage, FormInstance } from 'element-plus'
|
||||
import { ArrowLeft } from '@element-plus/icons-vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const pageName = route.meta.title
|
||||
const back = () => {
|
||||
router.push('/channel/wechat')
|
||||
}
|
||||
const loading = ref(true)
|
||||
|
||||
const formData = reactive<Record<string, any>>({
|
||||
wechat_name: '',
|
||||
wechat_original: '',
|
||||
app_id: '',
|
||||
app_secret: '',
|
||||
qr_code: '',
|
||||
token: '',
|
||||
encoding_aes_key: '',
|
||||
encryption_type: 'not_encrypt',
|
||||
is_authorization: 0
|
||||
})
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = computed(() => {
|
||||
return {
|
||||
wechat_name: [
|
||||
{ required: true, message: t('wechatNamePlaceholder'), trigger: 'blur' }
|
||||
],
|
||||
wechat_original: [
|
||||
{ required: true, message: t('wechatOriginalPlaceholder'), trigger: 'blur' }
|
||||
],
|
||||
app_id: [
|
||||
{ required: true, message: t('appidPlaceholder'), trigger: 'blur' }
|
||||
],
|
||||
app_secret: [
|
||||
{ required: !formData.is_authorization, message: t('appSecretPlaceholder'), trigger: 'blur' }
|
||||
],
|
||||
token: [
|
||||
{ required: true, message: t('tokenPlaceholder'), trigger: 'blur' }
|
||||
],
|
||||
encoding_aes_key: [
|
||||
{ required: true, message: t('encodingAesKeyPlaceholder'), trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 获取微信配置
|
||||
*/
|
||||
getWechatConfig().then(res => {
|
||||
Object.assign(formData, res.data)
|
||||
loading.value = false
|
||||
})
|
||||
|
||||
const wechatStatic = reactive<Record<string, string>>({
|
||||
business_domain: '',
|
||||
js_secure_domain: '',
|
||||
serve_url: '',
|
||||
web_auth_domain: ''
|
||||
})
|
||||
|
||||
getWechatStatic().then(res => {
|
||||
Object.assign(wechatStatic, res.data)
|
||||
loading.value = false
|
||||
})
|
||||
|
||||
/**
|
||||
* 复制
|
||||
*/
|
||||
const { copy, isSupported, copied } = useClipboard()
|
||||
const copyEvent = (text: string) => {
|
||||
if (!isSupported.value) {
|
||||
ElMessage({
|
||||
message: t('notSupportCopy'),
|
||||
type: 'warning'
|
||||
})
|
||||
return
|
||||
}
|
||||
copy(text)
|
||||
}
|
||||
|
||||
watch(copied, () => {
|
||||
if (copied.value) {
|
||||
ElMessage({
|
||||
message: t('copySuccess'),
|
||||
type: 'success'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* 保存
|
||||
*/
|
||||
const save = async (formEl: FormInstance | undefined) => {
|
||||
if (loading.value || !formEl) return
|
||||
|
||||
await formEl.validate(async (valid) => {
|
||||
if (valid) {
|
||||
loading.value = true
|
||||
editWechatConfig(formData).then(() => {
|
||||
loading.value = false
|
||||
}).catch(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
90
admin-vben/src/app/views/channel/wechat/course.vue
Normal file
90
admin-vben/src/app/views/channel/wechat/course.vue
Normal file
@@ -0,0 +1,90 @@
|
||||
<template>
|
||||
<!--发布教程-->
|
||||
<div class="main-container">
|
||||
|
||||
<el-card class="card !border-none" shadow="never">
|
||||
<el-page-header :content="pageName" :icon="ArrowLeft" @back="back()" />
|
||||
</el-card>
|
||||
|
||||
<el-card class="box-card mt-[15px] pt-[20px] !border-none" shadow="never">
|
||||
<div class="flex">
|
||||
<div class="min-w-[60px]">
|
||||
<span class="flex justify-center items-center block w-[40px] h-[40px] border-[1px] border-primary rounded-[999px] text-primary">1</span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="flex items-center text-[14px]">{{ t('writingTipsOne1') }}--<el-button link type="primary" @click="linkEvent">{{ t('writingTipsOne2') }}</el-button>, {{ t('writingTipsOne3') }}<span class="text-primary">URL / Token / EncondingAESKey</span>{{ t('writingTipsOne4') }}</p>
|
||||
<div class="w-[100%] mt-[10px]">
|
||||
<img class="w-[100%]" src="@/app/assets/images/setting/wechat_1.png" />
|
||||
</div>
|
||||
<p class="flex items-center text-[14px] mt-[20px]">{{ t('writingTipsOne5') }}</p>
|
||||
<div class="w-[100%] mt-[10px]">
|
||||
<img class="w-[100%]" src="@/app/assets/images/setting/wechat_4.png" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex mt-[40px]">
|
||||
<div class="min-w-[60px]">
|
||||
<span class="flex justify-center items-center block w-[40px] h-[40px] border-[1px] border-primary rounded-[999px] text-primary">2</span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="flex items-center text-[14px]">{{ t('writingTipsTwo1') }}</p>
|
||||
<div class="w-[100%] mt-[10px]">
|
||||
<img class="w-[100%]" src="@/app/assets/images/setting/wechat_2.png" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex mt-[40px]">
|
||||
<div class="min-w-[60px]">
|
||||
<span class="flex justify-center items-center block w-[40px] h-[40px] border-[1px] border-primary rounded-[999px] text-primary">3</span>
|
||||
</div>
|
||||
<div>
|
||||
<p class="flex items-center text-[14px]">{{ t('writingTipsThree1') }}<span class="text-primary">{{ t('writingTipsThree2') }}</span></p>
|
||||
<div class="w-[100%] mt-[10px]">
|
||||
<img class="w-[100%]" src="@/app/assets/images/setting/wechat_3.png" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import { getWechatConfig } from '@/app/api/wechat'
|
||||
import { ArrowLeft } from '@element-plus/icons-vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
const pageName = route.meta.title
|
||||
const router = useRouter()
|
||||
const back = () => {
|
||||
router.push('/channel/wechat')
|
||||
}
|
||||
const loading = ref(true)
|
||||
const formData = reactive<Record<string, string>>({
|
||||
wechat_name: '',
|
||||
wechat_original: '',
|
||||
app_id: '',
|
||||
app_secret: '',
|
||||
qr_code: '',
|
||||
token: '',
|
||||
encoding_aes_key: '',
|
||||
encryption_type: 'not_encrypt'
|
||||
})
|
||||
|
||||
/**
|
||||
* 获取微信配置
|
||||
*/
|
||||
getWechatConfig().then(res => {
|
||||
Object.assign(formData, res.data)
|
||||
loading.value = false
|
||||
})
|
||||
|
||||
const linkEvent = () => {
|
||||
window.open('https://mp.weixin.qq.com/', '_blank')
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
180
admin-vben/src/app/views/channel/wechat/keyword_reply_edit.vue
Normal file
180
admin-vben/src/app/views/channel/wechat/keyword_reply_edit.vue
Normal file
@@ -0,0 +1,180 @@
|
||||
<template>
|
||||
<!--关键字回复添加/编辑-->
|
||||
<div class="main-container">
|
||||
|
||||
<el-card class="card !border-none" shadow="never">
|
||||
<el-page-header :content="pageName" :icon="ArrowLeft" @back="back()" />
|
||||
</el-card>
|
||||
|
||||
<el-form class="page-form mt-[15px]" :model="formData" label-width="150px" ref="formRef" :rules="formRules" v-loading="loading">
|
||||
<el-card class="box-card !border-none" shadow="never">
|
||||
|
||||
<el-form-item :label="t('ruleName')" prop="name">
|
||||
<el-input v-model.trim="formData.name" :placeholder="t('ruleNamePlaceholder')" class="input-width" clearable maxlength="60"/>
|
||||
<div class="form-tip">{{ t('ruleNameTips') }}</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('keyword')" prop="keyword">
|
||||
<el-input v-model.trim="formData.keyword" :placeholder="t('keywordPlaceholder')" class="input-width" clearable >
|
||||
<template #prepend>
|
||||
<el-select v-model="formData.matching_type" style="width: 115px">
|
||||
<el-option :label="t('allMatching')" value="full" />
|
||||
<el-option :label="t('fuzzyMatching')" value="like" />
|
||||
</el-select>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('content')" prop="content">
|
||||
<div class="flex flex-col">
|
||||
<div class="flex items-center" v-for="(item, index) in formData.content">
|
||||
<div class="w-[300px] bg-page p-[10px] mr-[10px] mb-[10px] rounded leading-none" v-if="item.msgtype == 'text'">
|
||||
{{ item.text.content }}
|
||||
</div>
|
||||
<div class="w-[300px] bg-page p-[10px] mr-[10px] mb-[10px] rounded" v-if="item.msgtype == 'image'">
|
||||
<upload-image :limit="1" width="120px" height="120px" v-model="item.image.url"/>
|
||||
</div>
|
||||
<div class="w-[300px] bg-page p-[10px] mr-[10px] mb-[10px] rounded" v-if="item.msgtype == 'video'">
|
||||
<upload-video :limit="1" width="120px" height="120px" v-model="item.video.url"/>
|
||||
</div>
|
||||
<div class="w-[300px] bg-page p-[10px] mr-[10px] mb-[10px] rounded" v-if="item.msgtype == 'mpnewsarticle'">
|
||||
<news-card v-model="item.mpnewsarticle" mode="show"/>
|
||||
</div>
|
||||
<div class="w-[300px] bg-page p-[10px] mr-[10px] mb-[10px] rounded" v-if="item.msgtype == 'miniprogrampage'">
|
||||
小程序卡片【{{ item.miniprogrampage.appid }}】
|
||||
</div>
|
||||
<icon name="element Delete" class="cursor-pointer" @click="removeContent(index)"/>
|
||||
</div>
|
||||
<div class="mt-[10px]">
|
||||
<el-button type="primary" @click="showDialog = true">{{ t('addReplyContent') }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('replyMethod')" prop="reply_method">
|
||||
<el-radio-group v-model="formData.reply_method">
|
||||
<el-radio label="all">{{ t('replyMethodAll') }}</el-radio>
|
||||
<el-radio label="rand">{{ t('replyMethodRand') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</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>
|
||||
|
||||
<el-dialog v-model="showDialog" :title="t('addReplyContent')" width="60%" :destroy-on-close="true">
|
||||
<reply-form v-model="replyContent" ref="ReplyRef"/>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button>
|
||||
<el-button type="primary" @click="addReplyContent">{{ t('confirm') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import { getKeywordsReplyInfo, editKeywordsReply, addKeywordsReply } from '@/app/api/wechat'
|
||||
import { FormInstance, FormRules } from 'element-plus'
|
||||
import { ArrowLeft } from '@element-plus/icons-vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import ReplyForm from '@/app/views/channel/wechat/components/reply-form.vue'
|
||||
import NewsCard from '@/app/views/channel/wechat/components/news-card.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const pageName = route.meta.title
|
||||
const back = () => {
|
||||
router.push('/channel/wechat/reply')
|
||||
}
|
||||
|
||||
const showDialog = ref(false)
|
||||
|
||||
const formData: any = reactive({
|
||||
id: 0,
|
||||
name: '',
|
||||
keyword: '',
|
||||
content: [],
|
||||
matching_type: 'full',
|
||||
reply_method: 'all'
|
||||
})
|
||||
|
||||
const replyContent = ref({})
|
||||
const ReplyRef = ref(null)
|
||||
|
||||
const addReplyContent = () => {
|
||||
ReplyRef.value?.verify().then(res => {
|
||||
if (res) {
|
||||
formData.content.push(replyContent.value)
|
||||
replyContent.value = {}
|
||||
showDialog.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const removeContent = (index: number) => {
|
||||
formData.content.splice(index, 1)
|
||||
}
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = reactive<FormRules>({
|
||||
name: [
|
||||
{ required: true, message: t('ruleNamePlaceholder'), trigger: 'blur' }
|
||||
],
|
||||
keyword: [
|
||||
{ required: true, message: t('keywordPlaceholder'), trigger: 'blur' }
|
||||
],
|
||||
content: [
|
||||
{
|
||||
validator: (rule: any, value: any, callback: any) => {
|
||||
if (!formData.content.length) callback(new Error(t('contentPlaceholder')))
|
||||
callback()
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
if (route.query.id) {
|
||||
getKeywordsReplyInfo(route.query.id).then(({ data }) => {
|
||||
Object.keys(formData).forEach((key: string) => {
|
||||
if (data[key] != undefined) formData[key] = data[key]
|
||||
})
|
||||
loading.value = false
|
||||
}).catch()
|
||||
} else {
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存
|
||||
*/
|
||||
const save = async (formEl: FormInstance | undefined) => {
|
||||
if (loading.value || !formEl) return
|
||||
|
||||
await formEl.validate(async (valid) => {
|
||||
if (valid) {
|
||||
const api = formData.id ? editKeywordsReply : addKeywordsReply
|
||||
loading.value = true
|
||||
api(formData).then(() => {
|
||||
loading.value = false
|
||||
}).catch(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
</script>
|
||||
297
admin-vben/src/app/views/channel/wechat/menu.vue
Normal file
297
admin-vben/src/app/views/channel/wechat/menu.vue
Normal file
@@ -0,0 +1,297 @@
|
||||
<template>
|
||||
<!--自定义菜单-->
|
||||
<div class="main-container">
|
||||
<el-card class="card !border-none" shadow="never">
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-page-title">{{ pageName }}</span>
|
||||
</div>
|
||||
|
||||
<el-tabs v-model="activeName" class="my-[20px]" @tab-change="handleClick">
|
||||
<el-tab-pane :label="t('wechatAccessFlow')" name="/channel/wechat" />
|
||||
<el-tab-pane :label="t('customMenu')" name="/channel/wechat/menu" />
|
||||
<el-tab-pane :label="t('wechatTemplate')" name="/channel/wechat/message" />
|
||||
<el-tab-pane :label="t('reply')" name="/channel/wechat/reply" />
|
||||
</el-tabs>
|
||||
|
||||
<div class="flex" v-loading="loading">
|
||||
<div class="preview-wrap w-[300px] h-[550px] mr-[16px] bg-overlay rounded-md flex flex-col justify-between border border-color">
|
||||
<div class="head w-full h-[70px]"></div>
|
||||
|
||||
<div class="menu-list h-[70px] flex border-t border-color">
|
||||
<div class="py-[15px]">
|
||||
<div class="flex h-full px-[10px] items-center justify-center border-r border-color">
|
||||
<icon name="iconfont iconjianpan" size="20px" color="#b1b2b3" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 flex w-0">
|
||||
<div class="menu-item py-[15px] flex items-center justify-center cursor-pointer"
|
||||
:class="{ 'size-1': button.length == 1, 'size-2-3': button.length > 1, 'active': index == buttonIndex, 'curr': index == buttonIndex && subButtonIndex == -1 }"
|
||||
v-for="(item, index) in button" :key="index" @click="selectButton(index)">
|
||||
<div class="menu-name px-[10px] border-r border-color w-full leading-[40px] text-base truncate text-center">{{ item.name }}</div>
|
||||
<div class="active-shade"></div>
|
||||
|
||||
<!-- 子菜单 -->
|
||||
<div class="sub-menu-wrap w-full bg-overlay border border-color rounded">
|
||||
<div class="menu-item h-[50px] p-[10px] border-b border-color flex items-center justify-center cursor-pointer"
|
||||
:class="{ 'curr': subIndex == subButtonIndex }"
|
||||
v-for="(subItem, subIndex) in item.sub_button" :key="subIndex"
|
||||
@click.stop="selectBubButton(index, subIndex)">
|
||||
<div class="menu-name w-full text-base truncate text-center">{{ subItem.name }}</div>
|
||||
<div class="active-shade"></div>
|
||||
</div>
|
||||
<!-- 添加子菜单 -->
|
||||
<div class="add-menu flex items-center justify-center flex-1 cursor-pointer menu-item h-[50px]"
|
||||
v-show="!item.sub_button || item.sub_button.length < 5"
|
||||
@click.stop="addSubButton(index)">
|
||||
<icon name="element Plus" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 添加菜单 -->
|
||||
<div class="add-menu flex items-center justify-center flex-1 cursor-pointer menu-item" v-show="button.length < 3" @click="addButton">
|
||||
<icon name="element Plus" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1">
|
||||
<el-card class="box-card !border-none h-auto" shadow="never">
|
||||
<template v-if="button.length">
|
||||
|
||||
<div v-for="(item, index) in button" :key="index">
|
||||
<div v-show="index == buttonIndex && subButtonIndex == -1">
|
||||
<menu-form :data="item" @delete="deleteButton" :index="index" ref="formRef" />
|
||||
</div>
|
||||
|
||||
<div v-for="(subItem, subIndex) in item.sub_button" :key="subIndex">
|
||||
<div v-show="index == buttonIndex && subIndex == subButtonIndex">
|
||||
<menu-form :data="subItem" @delete="deleteButton" :index="index" :sub-index="subIndex" ref="formRef" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
<div v-else class="py-[20px] leading">尚未添加自定义菜单,点击左侧添加菜单为公众号创建菜单栏。</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<div class="fixed-footer-wrap">
|
||||
<div class="fixed-footer">
|
||||
<el-button type="primary" :loading="loading" @click="save()">{{ t('save') }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import { ElMessageBox, ElMessage } from 'element-plus'
|
||||
import { getWechatMenu, editWechatMenu } from '@/app/api/wechat'
|
||||
import menuForm from './components/menu-form.vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
const pageName = route.meta.title
|
||||
const router = useRouter()
|
||||
const loading = ref(true)
|
||||
const button = ref<Record<string, any>[]>([])
|
||||
const buttonIndex = ref<number>(0)
|
||||
const subButtonIndex = ref<number>(-1)
|
||||
const formRef = ref<Record<string, any>[] | null>(null)
|
||||
const activeName = ref('/channel/wechat/menu')
|
||||
const handleClick = (val: any) => {
|
||||
router.push({ path: activeName.value })
|
||||
}
|
||||
/**
|
||||
* 获取公众号菜单配置
|
||||
*/
|
||||
getWechatMenu().then((res) => {
|
||||
button.value = res.data
|
||||
loading.value = false
|
||||
})
|
||||
|
||||
/**
|
||||
* 添加一级菜单
|
||||
*/
|
||||
const addButton = () => {
|
||||
button.value.push({
|
||||
name: '菜单名称',
|
||||
type: 'view',
|
||||
url: '',
|
||||
appid: '',
|
||||
pagepath: '',
|
||||
sub_button: []
|
||||
})
|
||||
selectButton(button.value.length - 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加二级菜单
|
||||
* @param index
|
||||
*/
|
||||
const addSubButton = (index: number) => {
|
||||
!button.value[index].sub_button && (button.value[index].sub_button = [])
|
||||
button.value[index].sub_button.push({
|
||||
name: '子菜单名称',
|
||||
type: 'view',
|
||||
url: '',
|
||||
appid: '',
|
||||
pagepath: ''
|
||||
})
|
||||
selectBubButton(index, button.value[index].sub_button.length - 1)
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择一级菜单
|
||||
*/
|
||||
const selectButton = (index: number) => {
|
||||
buttonIndex.value = index
|
||||
subButtonIndex.value = -1
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择二级菜单
|
||||
* @param index
|
||||
* @param subIndex
|
||||
*/
|
||||
const selectBubButton = (index: number, subIndex: number) => {
|
||||
buttonIndex.value = index
|
||||
subButtonIndex.value = subIndex
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除菜单
|
||||
*/
|
||||
const deleteButton = () => {
|
||||
ElMessageBox.confirm(
|
||||
t('deleteMemuTips'),
|
||||
t('warning'),
|
||||
{
|
||||
confirmButtonText: t('confirm'),
|
||||
cancelButtonText: t('cancel'),
|
||||
type: 'warning'
|
||||
}
|
||||
).then(() => {
|
||||
if (subButtonIndex.value != -1) {
|
||||
button.value[buttonIndex.value].sub_button.splice(subButtonIndex.value, 1)
|
||||
subButtonIndex.value = button.value[buttonIndex.value].sub_button.length - 1
|
||||
// 如果子菜单被全部删除
|
||||
if (subButtonIndex.value == -1) {
|
||||
Object.assign(button.value[buttonIndex.value], {
|
||||
type: 'view',
|
||||
url: '',
|
||||
appid: '',
|
||||
pagepath: ''
|
||||
})
|
||||
}
|
||||
} else {
|
||||
button.value.splice(buttonIndex.value, 1)
|
||||
button.value.length && (buttonIndex.value = button.value.length - 1)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存
|
||||
*/
|
||||
const save = async () => {
|
||||
if (!formRef.value || !formRef.value) {
|
||||
ElMessage.error(t('menusEmptyTips'))
|
||||
return
|
||||
}
|
||||
for (let i = 0; i < formRef?.value.length; i++) {
|
||||
const item = formRef.value[i]
|
||||
const validate = await item.validate()
|
||||
if (!validate) {
|
||||
buttonIndex.value = item.index
|
||||
subButtonIndex.value = item.subIndex
|
||||
break
|
||||
}
|
||||
}
|
||||
if (loading.value) return
|
||||
loading.value = true
|
||||
editWechatMenu({ button: button.value }).then(() => {
|
||||
loading.value = false
|
||||
}).catch(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.preview-wrap {
|
||||
|
||||
.head {
|
||||
background: url('@/app/assets/images/wechat-menu-head-bg.png');
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
position: relative;
|
||||
|
||||
&.size-1 {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
&.size-2-3 {
|
||||
width: 33.33%;
|
||||
}
|
||||
|
||||
&:nth-child(3)>.menu-name {
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
.active-shade {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 1px solid var(--el-color-primary);
|
||||
display: none;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
&.curr {
|
||||
background: var(--el-color-primary-light-9);
|
||||
|
||||
>.menu-name {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
&>.active-shade {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
.sub-menu-wrap {
|
||||
display: block !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sub-menu-wrap {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
transform: translateY(calc(-100% - 15px));
|
||||
|
||||
.menu-item:nth-child(5) {
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dark {
|
||||
|
||||
.preview-wrap .head {
|
||||
background: url('@/app/assets/images/wechat-menu-head-dark-bg.png');
|
||||
background-size: cover;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
192
admin-vben/src/app/views/channel/wechat/reply.vue
Normal file
192
admin-vben/src/app/views/channel/wechat/reply.vue
Normal file
@@ -0,0 +1,192 @@
|
||||
<template>
|
||||
<!--自动回复-->
|
||||
<div class="main-container">
|
||||
<el-card class="card !border-none" shadow="never">
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-page-title">{{ t('title') }}</span>
|
||||
</div>
|
||||
|
||||
<el-tabs v-model="activeName" class="my-[20px]" @tab-change="handleClick">
|
||||
<el-tab-pane :label="t('wechatAccessFlow')" name="/channel/wechat" />
|
||||
<el-tab-pane :label="t('customMenu')" name="/channel/wechat/menu" />
|
||||
<el-tab-pane :label="t('wechatTemplate')" name="/channel/wechat/message" />
|
||||
<el-tab-pane :label="t('reply')" name="/channel/wechat/reply" />
|
||||
</el-tabs>
|
||||
|
||||
<div>
|
||||
<el-radio-group v-model="replyType" style="margin-bottom: 30px">
|
||||
<el-radio-button label="keyword">{{ t('keywordReply') }}</el-radio-button>
|
||||
<el-radio-button label="default">{{ t('defaultReply') }}</el-radio-button>
|
||||
<el-radio-button label="subscribe">{{ t('subscribeReply') }}</el-radio-button>
|
||||
</el-radio-group>
|
||||
|
||||
<div v-show="replyType == 'keyword'">
|
||||
<div class="flex justify-between items-center">
|
||||
<el-button type="primary" @click="addKeywordsReply">新建回复</el-button>
|
||||
</div>
|
||||
|
||||
<div class="mt-[10px]">
|
||||
<el-table :data="replyTableData.data" size="large" v-loading="replyTableData.loading">
|
||||
<template #empty>
|
||||
<span>{{ !replyTableData.loading ? t('emptyData') : '' }}</span>
|
||||
</template>
|
||||
<el-table-column prop="name" label="规则名称" min-width="120" />
|
||||
<el-table-column prop="keyword" label="关键字" min-width="120" />
|
||||
<el-table-column label="匹配规则" min-width="150" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ row.matching_type == 'full' ? '全匹配' : '模糊匹配' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="回复方式" min-width="150" align="center">
|
||||
<template #default="{ row }">
|
||||
{{ row.reply_method == 'all' ? '全部回复' : '随机回复一条' }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('operation')" align="right" fixed="right" width="180">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="editKeywordsReply(row)">{{ t('edit') }}</el-button>
|
||||
<el-button type="primary" link @click="deleteKeyword(row)">{{ t('delete') }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="mt-[16px] flex justify-end">
|
||||
<el-pagination v-model:current-page="replyTableData.page" v-model:page-size="replyTableData.limit"
|
||||
layout="total, sizes, prev, pager, next, jumper" :total="replyTableData.total"
|
||||
@size-change="loadKeywordsReplyList()" @current-change="loadKeywordsReplyList" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="replyType == 'default'">
|
||||
<reply-form v-model="defaultReply" ref="defaultReplyRef"/>
|
||||
<div class="mt-[20px]">
|
||||
<el-button type="primary" :loading="loading" @click="save()">{{ t('save') }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-show="replyType == 'subscribe'">
|
||||
<reply-form v-model="subscribeReply" ref="subscribeReplyRef"/>
|
||||
<div class="mt-[20px]">
|
||||
<el-button type="primary" :loading="loading" @click="save()">{{ t('save') }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { t } from '@/lang'
|
||||
import {
|
||||
getKeywordsReplyList,
|
||||
getDefaultReply,
|
||||
getSubscribeReply,
|
||||
setDefaultReply,
|
||||
setSubscribeReply,
|
||||
delKeywordsReply
|
||||
} from '@/app/api/wechat'
|
||||
import ReplyForm from '@/app/views/channel/wechat/components/reply-form.vue'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
|
||||
const router = useRouter()
|
||||
const activeName = ref('/channel/wechat/reply')
|
||||
const replyType = ref('keyword')
|
||||
|
||||
const addKeywordsReply = () => {
|
||||
router.push('/channel/wechat/keyword_reply_edit')
|
||||
}
|
||||
|
||||
const editKeywordsReply = (row: Object) => {
|
||||
router.push('/channel/wechat/keyword_reply_edit?id=' + row.id)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除菜单
|
||||
*/
|
||||
const deleteKeyword = (row: Object) => {
|
||||
ElMessageBox.confirm(t('replyDeleteTips'), t('warning'),
|
||||
{
|
||||
confirmButtonText: t('confirm'),
|
||||
cancelButtonText: t('cancel'),
|
||||
type: 'warning'
|
||||
}
|
||||
).then(() => {
|
||||
delKeywordsReply(row.id).then(() => {
|
||||
loadKeywordsReplyList()
|
||||
}).catch(() => {
|
||||
})
|
||||
})
|
||||
}
|
||||
const handleClick = (val: any) => {
|
||||
router.push({ path: activeName.value })
|
||||
}
|
||||
|
||||
const defaultReply = ref({})
|
||||
const subscribeReply = ref({})
|
||||
|
||||
getDefaultReply().then(({ data }) => {
|
||||
data.length != 0 && (defaultReply.value = data.content)
|
||||
}).catch()
|
||||
|
||||
getSubscribeReply().then(({ data }) => {
|
||||
data.length != 0 && (subscribeReply.value = data.content)
|
||||
}).catch()
|
||||
|
||||
const defaultReplyRef = ref(null)
|
||||
const subscribeReplyRef = ref(null)
|
||||
|
||||
const save = async () => {
|
||||
let verify = true,
|
||||
api,
|
||||
data = {}
|
||||
|
||||
switch (replyType.value) {
|
||||
case 'default':
|
||||
await defaultReplyRef.value?.verify().then(res => {
|
||||
verify = res
|
||||
})
|
||||
api = setDefaultReply
|
||||
data = defaultReply.value
|
||||
break
|
||||
case 'subscribe':
|
||||
await subscribeReplyRef.value?.verify().then(res => {
|
||||
verify = res
|
||||
})
|
||||
api = setSubscribeReply
|
||||
data = subscribeReply.value
|
||||
break
|
||||
}
|
||||
if (verify) {
|
||||
api({content: data}).then(() => {}).catch()
|
||||
}
|
||||
}
|
||||
|
||||
const replyTableData = reactive({
|
||||
page: 1,
|
||||
limit: 10,
|
||||
total: 0,
|
||||
loading: true,
|
||||
data: []
|
||||
})
|
||||
|
||||
const loadKeywordsReplyList = (page: number = 1) => {
|
||||
replyTableData.loading = true
|
||||
replyTableData.page = page
|
||||
|
||||
getKeywordsReplyList({
|
||||
page: replyTableData.page,
|
||||
limit: replyTableData.limit
|
||||
}).then(res => {
|
||||
replyTableData.loading = false
|
||||
replyTableData.data = res.data.data
|
||||
replyTableData.total = res.data.total
|
||||
}).catch(() => {
|
||||
replyTableData.loading = false
|
||||
})
|
||||
}
|
||||
loadKeywordsReplyList()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
199
admin-vben/src/app/views/channel/wechat/template.vue
Normal file
199
admin-vben/src/app/views/channel/wechat/template.vue
Normal file
@@ -0,0 +1,199 @@
|
||||
<template>
|
||||
<!--模板消息-->
|
||||
<div class="main-container">
|
||||
<el-card class="card !border-none" shadow="never">
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<span class="text-page-title">{{ pageName }}</span>
|
||||
<el-button type="primary" class="w-[100px]" @click="batchAcquisitionFn()">{{ t('batchAcquisition') }}</el-button>
|
||||
</div>
|
||||
|
||||
<el-tabs v-model="activeName" class="my-[20px]" @tab-change="handleClick">
|
||||
<el-tab-pane :label="t('wechatAccessFlow')" name="/channel/wechat" />
|
||||
<el-tab-pane :label="t('customMenu')" name="/channel/wechat/menu" />
|
||||
<el-tab-pane :label="t('wechatTemplate')" name="/channel/wechat/message" />
|
||||
<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>
|
||||
</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 :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('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 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>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import { getTemplateList, getBatchAcquisition } from '@/app/api/wechat'
|
||||
import { editNoticeStatus } from '@/app/api/notice'
|
||||
import { AnyObject } from '@/types/global'
|
||||
import { ElLoading } from 'element-plus'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const pageName = route.meta.title
|
||||
const cronTableData = reactive({
|
||||
loading: true,
|
||||
data: []
|
||||
})
|
||||
const activeName = ref('/channel/wechat/message')
|
||||
const handleClick = (val: any) => {
|
||||
router.push({ path: activeName.value })
|
||||
}
|
||||
/**
|
||||
* 获取消息模板列表
|
||||
*/
|
||||
const loadCronList = (page: number = 1) => {
|
||||
cronTableData.loading = true
|
||||
|
||||
getTemplateList().then(res => {
|
||||
cronTableData.loading = false
|
||||
let data = []
|
||||
res.data.forEach(item => {
|
||||
if (item.notice.length) {
|
||||
const addons = []
|
||||
Object.keys(item.notice).forEach((key, index) => {
|
||||
const notice = item.notice[key]
|
||||
notice.addon_name = item.title
|
||||
addons.push(notice)
|
||||
})
|
||||
if (addons.length) {
|
||||
addons[0].rowspan = addons.length
|
||||
data = data.concat(addons)
|
||||
}
|
||||
}
|
||||
})
|
||||
cronTableData.data = data
|
||||
}).catch((e) => {
|
||||
cronTableData.loading = false
|
||||
})
|
||||
}
|
||||
loadCronList()
|
||||
|
||||
const templateSpan = (row : any) => {
|
||||
if (row.columnIndex === 0) {
|
||||
if (row.row.rowspan) {
|
||||
return {
|
||||
rowspan: row.row.rowspan,
|
||||
colspan: 1
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
rowspan: 0,
|
||||
colspan: 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量获取
|
||||
*/
|
||||
const batchAcquisitionFn = (row: AnyObject | null = null) => {
|
||||
const loading = ElLoading.service({ lock: true, background: 'rgba(0, 0, 0, 0)' })
|
||||
getBatchAcquisition({ keys: row ? [row.key] : [] }).then(() => {
|
||||
loadCronList()
|
||||
loading.close()
|
||||
}).catch(() => {
|
||||
loading.close()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 开启或关闭模版消息
|
||||
*/
|
||||
interface Switch {
|
||||
key: string;
|
||||
type: string;
|
||||
status: number
|
||||
}
|
||||
|
||||
const infoSwitch = (res: AnyObject) => {
|
||||
const data = ref<Switch>({
|
||||
key: '',
|
||||
type: '',
|
||||
status: 0
|
||||
})
|
||||
data.value.status = res.is_wechat ? 0 : 1
|
||||
data.value.key = res.key
|
||||
data.value.type = 'wechat'
|
||||
cronTableData.loading = true
|
||||
editNoticeStatus(data.value).then(res => {
|
||||
loadCronList()
|
||||
}).catch(() => {
|
||||
cronTableData.loading = false
|
||||
})
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-tabs__item:hover) {
|
||||
border-bottom: 2px solid var(--el-color-primary);
|
||||
}
|
||||
|
||||
:deep(.el-tabs__item) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
:deep(.el-tabs__item+.el-tabs__item) {
|
||||
margin-right: 20px;
|
||||
margin-left: 20px;
|
||||
// border-bottom: 2px solid var(--el-color-primary);
|
||||
}
|
||||
|
||||
:deep(.el-tabs--top) {
|
||||
.el-tabs__active-bar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.el-tabs__item.is-active {
|
||||
border-bottom: 2px solid var(--el-color-primary);
|
||||
}
|
||||
|
||||
.el-tabs__item.is-top:nth-child(2) {
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
}</style>
|
||||
168
admin-vben/src/app/views/dict/components/dict.vue
Normal file
168
admin-vben/src/app/views/dict/components/dict.vue
Normal file
@@ -0,0 +1,168 @@
|
||||
<template>
|
||||
<el-dialog v-model="showDialog" :title="t('dictData')" width="60%" class="diy-dialog-wrap" :destroy-on-close="true">
|
||||
<div class="mb-[10px]">
|
||||
<el-button type="primary" @click="addEvent">
|
||||
{{ t('addDictData') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<el-table :data="tableDate" size="large" v-loading="loading">
|
||||
<el-table-column :label="t('dataName')" prop="name" />
|
||||
<el-table-column :label="t('dataValue')" prop="value" />
|
||||
<el-table-column :label="t('sort')" align="center" min-width="100px" prop="sort" />
|
||||
<el-table-column :label="t('memo')" prop="memo" />
|
||||
<el-table-column :label="t('operation')" align="right" fixed="right" width="120">
|
||||
<template #default="{ row, $index }">
|
||||
<el-button type="primary" link @click="editEvent(row, $index)">{{ t('edit') }}</el-button>
|
||||
<el-button type="primary" link @click="deleteEvent($index)">{{ t('delete') }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
</el-table>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button>
|
||||
<el-button type="primary" @click="confirm()">{{ t('confirm') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
<el-dialog v-model="dialogVisible" :title="type != 'edit' ? t('addDictData') : t('editDictData')" width="480" class="diy-dialog-wrap" :destroy-on-close="true">
|
||||
<el-form :model="formData" label-width="120px" ref="formRef" :rules="formRules" class="page-form">
|
||||
<el-form-item :label="t('name')">
|
||||
<el-input v-model.trim="name" disabled class="input-width" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('dataName')" prop="name">
|
||||
<el-input v-model.trim="formData.name" clearable :placeholder="t('dataNamePlaceholder')" class="input-width" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('dataValue')" prop="value">
|
||||
<el-input v-model.trim="formData.value" clearable :placeholder="t('dataValuePlaceholder')" class="input-width" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('sort')" prop="sort">
|
||||
<div>
|
||||
<el-input-number v-model="formData.sort" ::step="1" step-strictly :value-on-clear="0" :min="0" class="input-width" />
|
||||
<p class="text-[12px] text-[#a9a9a9] leading-normal mt-[5px]">{{ t('sortPlaceholder') }}</p>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('memo')">
|
||||
<el-input v-model.trim="formData.memo" type="textarea" clearable :placeholder="t('momePlaceholder')" class="input-width" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">{{ t('cancel') }}</el-button>
|
||||
<el-button type="primary" @click="submit(formRef)">{{ t('confirm') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import { setDictData, getDictInfo } from '@/app/api/dict'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
|
||||
const showDialog = ref(false)
|
||||
const loading = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
|
||||
const tableDate = ref<Array<any>>([])
|
||||
const id = ref()
|
||||
const type = ref('add')
|
||||
const formRef = ref()
|
||||
/**
|
||||
* 表单数据
|
||||
*/
|
||||
const name = ref('')
|
||||
const initialFormData = {
|
||||
name: '',
|
||||
value: '',
|
||||
sort: 0,
|
||||
memo: ''
|
||||
}
|
||||
const formData = ref({ ...initialFormData })
|
||||
// 表单验证规则
|
||||
const formRules = computed(() => {
|
||||
return {
|
||||
name: [
|
||||
{ required: true, message: t('dataNamePlaceholder'), trigger: 'blur' }
|
||||
],
|
||||
value: [
|
||||
{ required: true, message: t('dataValuePlaceholder'), trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
})
|
||||
const addEvent = () => {
|
||||
type.value = 'add'
|
||||
formData.value = cloneDeep(initialFormData)
|
||||
dialogVisible.value = true
|
||||
}
|
||||
const tableIndex = ref(0)
|
||||
const editEvent = (row: any, index: number) => {
|
||||
type.value = 'edit'
|
||||
tableIndex.value = index
|
||||
formData.value = cloneDeep(initialFormData)
|
||||
formData.value = Object.assign(formData.value, cloneDeep(row))
|
||||
dialogVisible.value = true
|
||||
}
|
||||
/**
|
||||
* 表单确认
|
||||
*/
|
||||
const submit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
await formEl.validate(async (valid) => {
|
||||
if (valid) {
|
||||
if (type.value != 'edit') {
|
||||
tableDate.value.push(cloneDeep(formData.value))
|
||||
} else {
|
||||
tableDate.value.splice(tableIndex.value, 1, cloneDeep(formData.value))
|
||||
}
|
||||
tableDate.value.sort(function (a, b) { return b.sort - a.sort })
|
||||
dialogVisible.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
const emit = defineEmits(['complete'])
|
||||
|
||||
/**
|
||||
*删除
|
||||
*/
|
||||
const deleteEvent = (index: number) => {
|
||||
tableDate.value.splice(index, 1)
|
||||
}
|
||||
/**
|
||||
* 确认
|
||||
*/
|
||||
const confirm = async () => {
|
||||
loading.value = true
|
||||
setDictData(id.value, { dictionary: JSON.stringify(tableDate.value) }).then(res => {
|
||||
loading.value = false
|
||||
showDialog.value = false
|
||||
emit('complete')
|
||||
}).catch(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
const setFormData = async (row: any = null) => {
|
||||
showDialog.value = true
|
||||
loading.value = true
|
||||
id.value = row.id
|
||||
name.value = row.name
|
||||
const data = await (await getDictInfo(row.id)).data
|
||||
tableDate.value = data.dictionary
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
showDialog,
|
||||
setFormData
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
<style lang="scss">
|
||||
.diy-dialog-wrap .el-form-item__label {
|
||||
height: auto !important;
|
||||
}
|
||||
</style>
|
||||
124
admin-vben/src/app/views/dict/components/edit.vue
Normal file
124
admin-vben/src/app/views/dict/components/edit.vue
Normal file
@@ -0,0 +1,124 @@
|
||||
<template>
|
||||
<el-dialog v-model="showDialog" :title="formData.id ? t('updateDict') : t('addDict')" width="480" class="diy-dialog-wrap" :destroy-on-close="true">
|
||||
<el-form :model="formData" label-width="120px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
|
||||
<el-form-item :label="t('name')" prop="name">
|
||||
<el-input v-model.trim="formData.name" clearable maxlength="40" show-word-limit :placeholder="t('namePlaceholder')" class="input-width" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('key')" prop="key">
|
||||
<el-input v-model.trim="formData.key" clearable maxlength="40" show-word-limit :placeholder="t('keyPlaceholder')" class="input-width" />
|
||||
<p class="form-tip">{{ t('keyFormatTips') }}</p>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('memo')">
|
||||
<el-input v-model.trim="formData.memo" type="textarea" clearable :placeholder="t('memoPlaceholder')" class="input-width" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button>
|
||||
<el-button type="primary" :loading="loading" @click="confirm(formRef)">{{ t('confirm') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import { addDict, editDict, getDictInfo } from '@/app/api/dict'
|
||||
|
||||
const showDialog = ref(false)
|
||||
const loading = ref(false)
|
||||
|
||||
/**
|
||||
* 表单数据
|
||||
*/
|
||||
const initialFormData = {
|
||||
id: '',
|
||||
name: '',
|
||||
key: '',
|
||||
memo: ''
|
||||
}
|
||||
const formData: Record<string, any> = reactive({ ...initialFormData })
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = computed(() => {
|
||||
return {
|
||||
name: [
|
||||
{ required: true, message: t('namePlaceholder'), trigger: 'blur' }
|
||||
],
|
||||
key: [
|
||||
{ required: true, message: t('keyPlaceholder'), trigger: 'blur' },
|
||||
{
|
||||
validator: (rule: any, value: any, callback: any) => {
|
||||
if (/^[a-zA-Z_]+$/.test(value)) {
|
||||
callback()
|
||||
} else {
|
||||
callback(new Error(t('keyFormatTips')))
|
||||
}
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
data: [
|
||||
{ required: true, message: t('dataPlaceholder'), trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['complete'])
|
||||
|
||||
/**
|
||||
* 确认
|
||||
* @param formEl
|
||||
*/
|
||||
const confirm = async (formEl: FormInstance | undefined) => {
|
||||
if (loading.value || !formEl) return
|
||||
const save = formData.id ? editDict : addDict
|
||||
|
||||
await formEl.validate(async (valid) => {
|
||||
if (valid) {
|
||||
loading.value = true
|
||||
|
||||
const data = formData
|
||||
|
||||
save(data).then(res => {
|
||||
loading.value = false
|
||||
showDialog.value = false
|
||||
emit('complete')
|
||||
}).catch(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const setFormData = async (row: any = null) => {
|
||||
Object.assign(formData, initialFormData)
|
||||
loading.value = true
|
||||
if (row) {
|
||||
const data = await (await getDictInfo(row.id)).data
|
||||
if (data) {
|
||||
Object.keys(formData).forEach((key: string) => {
|
||||
if (data[key] != undefined) formData[key] = data[key]
|
||||
})
|
||||
}
|
||||
}
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
showDialog,
|
||||
setFormData
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
<style lang="scss">
|
||||
.diy-dialog-wrap .el-form-item__label{
|
||||
height: auto !important;
|
||||
}
|
||||
</style>
|
||||
159
admin-vben/src/app/views/dict/list.vue
Normal file
159
admin-vben/src/app/views/dict/list.vue
Normal file
@@ -0,0 +1,159 @@
|
||||
<template>
|
||||
<!--数据字典-->
|
||||
<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>
|
||||
<el-button type="primary" @click="addEvent">
|
||||
{{ t('addDict') }}
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-card class="box-card !border-none my-[10px] table-search-wrap" shadow="never">
|
||||
<el-form :inline="true" :model="dictTable.searchParam" ref="searchFormRef">
|
||||
<el-form-item :label="t('name')" prop="name">
|
||||
<el-input v-model.trim="dictTable.searchParam.name" :placeholder="t('namePlaceholder')" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('key')" prop="key">
|
||||
<el-input v-model.trim="dictTable.searchParam.key" :placeholder="t('keyPlaceholder')" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="loadDictList()">{{ t('search') }}</el-button>
|
||||
<el-button @click="resetForm(searchFormRef)">{{ t('reset') }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<div class="mt-[10px]">
|
||||
<el-table :data="dictTable.data" size="large" v-loading="dictTable.loading">
|
||||
<template #empty>
|
||||
<span>{{ !dictTable.loading ? t('emptyData') : '' }}</span>
|
||||
</template>
|
||||
<el-table-column prop="name" :label="t('name')" min-width="120" />
|
||||
<el-table-column prop="key" :label="t('key')" min-width="120" />
|
||||
<el-table-column prop="memo" :label="t('memo')" min-width="120" />
|
||||
<el-table-column prop="create_time" :label="t('createTime')" min-width="120" />
|
||||
<el-table-column :label="t('operation')" align="right" fixed="right" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="dictData(row)">{{ t('dictData') }}</el-button>
|
||||
<el-button type="primary" link @click="editEvent(row)">{{ t('edit') }}</el-button>
|
||||
<el-button type="primary" link @click="deleteEvent(row.id)">{{ t('delete') }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
</el-table>
|
||||
<div class="mt-[16px] flex justify-end">
|
||||
<el-pagination v-model:current-page="dictTable.page" v-model:page-size="dictTable.limit"
|
||||
layout="total, sizes, prev, pager, next, jumper" :total="dictTable.total"
|
||||
@size-change="loadDictList()" @current-change="loadDictList" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<edit ref="editDictDialog" @complete="loadDictList" />
|
||||
<dict ref="dictDialog" @complete="loadDictList" />
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import { getDictList, deleteDict } from '@/app/api/dict'
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import Edit from '@/app/views/dict/components/edit.vue'
|
||||
import dict from '@/app/views/dict/components/dict.vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
const pageName = route.meta.title
|
||||
|
||||
const dictTable = reactive({
|
||||
page: 1,
|
||||
limit: 10,
|
||||
total: 0,
|
||||
loading: true,
|
||||
data: [],
|
||||
searchParam: {
|
||||
name: '',
|
||||
key: ''
|
||||
}
|
||||
})
|
||||
|
||||
const searchFormRef = ref<FormInstance>()
|
||||
|
||||
/**
|
||||
* 获取数据字典列表
|
||||
*/
|
||||
const loadDictList = (page: number = 1) => {
|
||||
dictTable.loading = true
|
||||
dictTable.page = page
|
||||
|
||||
getDictList({
|
||||
page: dictTable.page,
|
||||
limit: dictTable.limit,
|
||||
...dictTable.searchParam
|
||||
}).then(res => {
|
||||
dictTable.loading = false
|
||||
dictTable.data = res.data.data
|
||||
dictTable.total = res.data.total
|
||||
}).catch(() => {
|
||||
dictTable.loading = false
|
||||
})
|
||||
}
|
||||
loadDictList()
|
||||
|
||||
const editDictDialog: Record<string, any> | null = ref(null)
|
||||
|
||||
/**
|
||||
* 添加数据字典
|
||||
*/
|
||||
const addEvent = () => {
|
||||
editDictDialog.value.setFormData()
|
||||
editDictDialog.value.showDialog = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑数据字典
|
||||
* @param data
|
||||
*/
|
||||
const editEvent = (data: any) => {
|
||||
editDictDialog.value.setFormData(data)
|
||||
editDictDialog.value.showDialog = true
|
||||
}
|
||||
|
||||
const dictDialog: Record<string, any> | null = ref(null)
|
||||
|
||||
const dictData = (data: any) => {
|
||||
dictDialog.value.setFormData(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除数据字典
|
||||
*/
|
||||
const deleteEvent = (id: number) => {
|
||||
ElMessageBox.confirm(t('dictDeleteTips'), t('warning'),
|
||||
{
|
||||
confirmButtonText: t('confirm'),
|
||||
cancelButtonText: t('cancel'),
|
||||
type: 'warning'
|
||||
}
|
||||
).then(() => {
|
||||
deleteDict(id).then(() => {
|
||||
loadDictList()
|
||||
}).catch(() => {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const resetForm = (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
formEl.resetFields()
|
||||
loadDictList()
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
130
admin-vben/src/app/views/diy/components/add-theme.vue
Normal file
130
admin-vben/src/app/views/diy/components/add-theme.vue
Normal file
@@ -0,0 +1,130 @@
|
||||
<template>
|
||||
<el-dialog v-model="dialogThemeVisible" title="新增颜色" width="550px" align-center>
|
||||
<el-form :model="formData" label-width="120px" ref="formRef" :rules="formRules">
|
||||
<el-form-item label="名字" prop="title">
|
||||
<el-input v-model="formData.title" class="!w-[250px]" maxlength="7" placeholder="请输入颜色名称" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="颜色key值" prop="label">
|
||||
<el-input v-model="formData.label" class="!w-[250px]" maxlength="20" :disabled="type=='edit'" placeholder="请输入颜色key值" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="颜色value值" prop="value">
|
||||
<el-color-picker v-model="formData.value" show-alpha :predefine="diyStore.predefineColors"/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="颜色提示">
|
||||
<el-input v-model="formData.tip" class="!w-[250px]" placeholder="请输入颜色提示" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="dialogThemeVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="confirmFn(formRef)">保存</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import useDiyStore from '@/stores/modules/diy'
|
||||
const diyStore = useDiyStore()
|
||||
|
||||
const dialogThemeVisible = ref(false)
|
||||
const confirmRepeat = ref(false)
|
||||
const emit = defineEmits(['confirm'])
|
||||
/**
|
||||
* 表单数据
|
||||
*/
|
||||
const initialData = {
|
||||
title: '',
|
||||
label: '',
|
||||
value: '',
|
||||
tip: ''
|
||||
}
|
||||
let keyArr = [] // 存储现有颜色的key
|
||||
const type = ref('') // 区分编辑和添加
|
||||
const formData: Record<string, any> = reactive({ ...initialData })
|
||||
|
||||
const open = (option:any) => {
|
||||
keyArr = option.key
|
||||
type.value = ''
|
||||
// 恢复默认值
|
||||
for (const key in formData) {
|
||||
formData[key] = ''
|
||||
}
|
||||
if (option.data && Object.keys(option.data).length) {
|
||||
type.value = 'edit'
|
||||
Object.keys(formData).forEach((item, index) => {
|
||||
formData[item] = option.data[item] ? option.data[item] : ''
|
||||
})
|
||||
}
|
||||
dialogThemeVisible.value = true
|
||||
}
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = computed(() => {
|
||||
return {
|
||||
title: [
|
||||
{ required: true, message: '请输入颜色名称', trigger: 'blur' }
|
||||
],
|
||||
value: [
|
||||
{
|
||||
required: true,
|
||||
validator: (rule: any, value: any, callback: any) => {
|
||||
if (!value) {
|
||||
callback('请输入颜色value值')
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: ['blur', 'change']
|
||||
}
|
||||
],
|
||||
label: [
|
||||
{ required: true, message: '请输入颜色key值', trigger: 'blur' },
|
||||
{
|
||||
validator: (rule: any, value: any, callback: any) => {
|
||||
const regex = /^[a-zA-Z0-9-]+$/
|
||||
if (keyArr.indexOf(value) != -1) {
|
||||
callback('新增颜色key值与已存在颜色key值命名重复,请修改命名')
|
||||
} if (!regex.test(value)) {
|
||||
callback('颜色key值只能输入字母、数字和连字符')
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
const confirmFn = async (formEl: FormInstance | undefined) => {
|
||||
if (confirmRepeat.value) return
|
||||
await formRef.value?.validate(async (valid) => {
|
||||
if (confirmRepeat.value) return
|
||||
confirmRepeat.value = true
|
||||
if (valid) {
|
||||
confirmRepeat.value = false
|
||||
emit('confirm', cloneDeep(formData))
|
||||
dialogThemeVisible.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
dialogThemeVisible,
|
||||
open
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
612
admin-vben/src/app/views/diy/components/edit-active-cube.vue
Normal file
612
admin-vben/src/app/views/diy/components/edit-active-cube.vue
Normal file
@@ -0,0 +1,612 @@
|
||||
<template>
|
||||
<!-- 内容 -->
|
||||
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
|
||||
|
||||
<div class="edit-attr-item-wrap">
|
||||
<h3 class="mb-[10px]">{{ t('titleContent') }}</h3>
|
||||
<el-form label-width="80px" class="px-[10px]" @submit.prevent>
|
||||
<el-form-item :label="t('selectStyle')" class="flex">
|
||||
<span class="text-primary flex-1 cursor-pointer" @click="showTitleStyle">{{ diyStore.editComponent.titleStyle.title }}</span>
|
||||
<el-icon @click="showTitleStyle" class="cursor-pointer">
|
||||
<ArrowRight />
|
||||
</el-icon>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('title')" v-if="diyStore.editComponent && diyStore.editComponent.titleStyle && diyStore.editComponent.titleStyle.value != 'style-5'">
|
||||
<el-input v-model.trim="diyStore.editComponent.text" :placeholder="t('titlePlaceholder')" clearable maxlength="10" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('image')" v-else>
|
||||
<upload-image v-model="diyStore.editComponent.textImg" :limit="1" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('link')">
|
||||
<diy-link v-model="diyStore.editComponent.textLink" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('subTitle')">
|
||||
<el-input v-model.trim="diyStore.editComponent.subTitle.text" :placeholder="t('subTitlePlaceholder')" clearable maxlength="8" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('link')">
|
||||
<diy-link v-model="diyStore.editComponent.subTitle.link" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-dialog v-model="showTitleDialog" :title="t('selectStyle')" width="460px">
|
||||
|
||||
<div class="flex flex-wrap">
|
||||
<template v-for="(item,index) in titleStyleList" :key="index">
|
||||
<div :class="{ 'border-primary': selectTitleStyle.value == item.value }"
|
||||
@click="changeTitleStyle(item)"
|
||||
class="flex items-center justify-center overflow-hidden w-[200px] h-[100px] mr-[12px] mb-[12px] cursor-pointer border bg-[#eee]">
|
||||
<img :src="img(item.url)" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="showTitleDialog = false">{{ t('cancel') }}</el-button>
|
||||
<el-button type="primary" @click="confirmTitleStyle">{{ t('confirm') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
</el-dialog>
|
||||
</div>
|
||||
|
||||
<div class="edit-attr-item-wrap">
|
||||
<h3 class="mb-[10px]">{{ t('activeCubeBlockContent') }}</h3>
|
||||
<el-form label-width="90px" class="px-[10px]">
|
||||
|
||||
<el-form-item :label="t('selectStyle')" class="flex">
|
||||
<span class="text-primary flex-1 cursor-pointer"
|
||||
@click="showBlockStyle">{{ diyStore.editComponent.blockStyle.title }}</span>
|
||||
<el-icon @click="showBlockStyle" class="cursor-pointer">
|
||||
<ArrowRight />
|
||||
</el-icon>
|
||||
</el-form-item>
|
||||
|
||||
<el-dialog v-model="showListDialog" :title="t('selectStyle')" width="600px">
|
||||
<div class="flex flex-wrap">
|
||||
<template v-for="(item,index) in blockStyleList" :key="index">
|
||||
<div :class="{ 'border-primary': selectBlockStyle.value == item.value }"
|
||||
@click="changeBlockStyle(item)"
|
||||
class="flex items-center justify-center overflow-hidden w-[250px] h-[150px] mr-[12px] mb-[12px] cursor-pointer border bg-[#eee]">
|
||||
<img :src="img(item.url)" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="showListDialog = false">{{ t('cancel') }}</el-button>
|
||||
<el-button type="primary" @click="confirmBlockStyle">{{ t('confirm') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
</el-dialog>
|
||||
|
||||
<p class="text-sm text-gray-400 mb-[10px]">{{ t('dragMouseAdjustOrder') }}</p>
|
||||
|
||||
<div ref="blockBoxRef">
|
||||
<div v-for="(item,index) in diyStore.editComponent.list" :key="item.id"
|
||||
class="item-wrap p-[10px] pb-0 relative border border-dashed border-gray-300 mb-[16px]">
|
||||
<el-form-item :label="t('image')">
|
||||
<upload-image v-model="item.imageUrl" :limit="1" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('activeCubeTitle')">
|
||||
<el-input v-model.trim="item.title.text" :placeholder="t('activeCubeTitlePlaceholder')"
|
||||
clearable maxlength="4" show-word-limit />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('activeCubeSubTitleTextColor')" v-show="diyStore.editComponent.blockStyle.value == 'style-3'">
|
||||
<el-color-picker v-model="item.title.textColor" show-alpha :predefine="diyStore.predefineColors" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('activeCubeSubTitle')" v-if="diyStore.editComponent.blockStyle.value != 'style-3'">
|
||||
<el-input v-model.trim="item.subTitle.text" :placeholder="t('activeCubeSubTitlePlaceholder')" clearable maxlength="6" show-word-limit />
|
||||
</el-form-item>
|
||||
|
||||
<div v-show="diyStore.editComponent.blockStyle.value == 'style-4'">
|
||||
<el-form-item :label="t('activeCubeSubTitleTextColor')">
|
||||
<el-color-picker v-model="item.subTitle.textColor" show-alpha :predefine="diyStore.predefineColors" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('activeCubeSubTitleBgColor')">
|
||||
<el-color-picker v-model="item.subTitle.startColor" show-alpha :predefine="diyStore.predefineColors" />
|
||||
<icon name="iconfont iconmap-connect" size="20px" class="block !text-gray-400 mx-[5px]" />
|
||||
<el-color-picker v-model="item.subTitle.endColor" show-alpha :predefine="diyStore.predefineColors" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<el-form-item :label="t('activeListFrameColor')">
|
||||
<el-color-picker v-model="item.listFrame.startColor" show-alpha :predefine="diyStore.predefineColors" />
|
||||
<icon name="iconfont iconmap-connect" size="20px" class="block !text-gray-400 mx-[5px]" />
|
||||
<el-color-picker v-model="item.listFrame.endColor" show-alpha :predefine="diyStore.predefineColors" />
|
||||
</el-form-item>
|
||||
|
||||
<div
|
||||
v-show="diyStore.editComponent.blockStyle.value != 'style-4' && diyStore.editComponent.blockStyle.value != 'style-3'">
|
||||
<el-form-item :label="t('activeCubeButton')">
|
||||
<el-input v-model.trim="item.moreTitle.text" :placeholder="t('activeCubeButtonPlaceholder')" clearable maxlength="3" show-word-limit />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('activeCubeButtonColor')">
|
||||
<el-color-picker v-model="item.moreTitle.startColor" show-alpha :predefine="diyStore.predefineColors" />
|
||||
<icon name="iconfont iconmap-connect" size="20px" class="block !text-gray-400 mx-[5px]" />
|
||||
<el-color-picker v-model="item.moreTitle.endColor" show-alpha :predefine="diyStore.predefineColors" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<el-form-item :label="t('link')">
|
||||
<diy-link v-model="item.link" />
|
||||
</el-form-item>
|
||||
|
||||
<div class="del absolute cursor-pointer z-[2] top-[-8px] right-[-8px]"
|
||||
v-show="diyStore.editComponent.list.length > 1"
|
||||
@click="diyStore.editComponent.list.splice(index,1)">
|
||||
<icon name="element CircleCloseFilled" color="#bbb" size="20px" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-button v-show="diyStore.editComponent.list.length < 10" class="w-full" @click="addItem">
|
||||
{{ t('activeCubeAddItem') }}
|
||||
</el-button>
|
||||
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 样式 -->
|
||||
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
|
||||
|
||||
<div class="edit-attr-item-wrap" v-if="selectTitleStyle.value != 'style-5'">
|
||||
<h3 class="mb-[10px]">{{ t('titleStyle') }}</h3>
|
||||
<el-form label-width="90px" class="px-[10px]">
|
||||
<el-form-item :label="t('textColor')">
|
||||
<el-color-picker v-model="diyStore.editComponent.titleColor" show-alpha :predefine="diyStore.predefineColors" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="edit-attr-item-wrap">
|
||||
<h3 class="mb-[10px]">{{ t('subTitleStyle') }}</h3>
|
||||
<el-form label-width="90px" class="px-[10px]">
|
||||
<el-form-item :label="t('textColor')">
|
||||
<el-color-picker v-model="diyStore.editComponent.subTitle.textColor" show-alpha
|
||||
:predefine="diyStore.predefineColors" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('subTextBgColor')">
|
||||
<el-color-picker v-model="diyStore.editComponent.subTitle.startColor" show-alpha :predefine="diyStore.predefineColors" />
|
||||
<icon name="iconfont iconmap-connect" size="20px" class="block !text-gray-400 mx-[5px]" />
|
||||
<el-color-picker v-model="diyStore.editComponent.subTitle.endColor" show-alpha :predefine="diyStore.predefineColors" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="edit-attr-item-wrap">
|
||||
<h3 class="mb-[10px]">{{ t('activeCubeBlockStyle') }}</h3>
|
||||
<el-form label-width="80px" class="px-[10px]">
|
||||
<el-form-item :label="t('activeCubeBlockTextFontWeight')">
|
||||
<el-radio-group v-model="diyStore.editComponent.blockStyle.fontWeight">
|
||||
<el-radio :label="'normal'">{{ t('fontWeightNormal') }}</el-radio>
|
||||
<el-radio :label="'bold'">{{ t('fontWeightBold') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('activeCubeBlockBtnText')" class="flex">
|
||||
<el-radio-group v-model="diyStore.editComponent.blockStyle.btnText">
|
||||
<el-radio :label="'normal'">{{ t('btnTextNormal') }}</el-radio>
|
||||
<el-radio :label="'italics'">{{ t('btnTextItalics') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('topRounded')">
|
||||
<el-slider v-model="diyStore.editComponent.topElementRounded" show-input size="small" class="ml-[10px] diy-nav-slider" :max="50" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('bottomRounded')">
|
||||
<el-slider v-model="diyStore.editComponent.bottomElementRounded" show-input size="small" class="ml-[10px] diy-nav-slider" :max="50" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 组件样式 -->
|
||||
<slot name="style"></slot>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { t } from '@/lang'
|
||||
import useDiyStore from '@/stores/modules/diy'
|
||||
import { img } from '@/utils/common'
|
||||
import { ref, reactive, onMounted, nextTick } from 'vue'
|
||||
import Sortable from 'sortablejs'
|
||||
import { range } from 'lodash-es'
|
||||
|
||||
const diyStore = useDiyStore()
|
||||
diyStore.editComponent.ignore = [] // 忽略公共属性
|
||||
|
||||
// 组件验证
|
||||
diyStore.editComponent.verify = (index: number) => {
|
||||
const res = { code: true, message: '' }
|
||||
|
||||
if (diyStore.value[index].text == '') {
|
||||
res.code = false
|
||||
res.message = t('activeCubeTitlePlaceholder')
|
||||
return res
|
||||
}
|
||||
|
||||
diyStore.value[index].list.forEach((item: any) => {
|
||||
if (item.imageUrl === '') {
|
||||
res.code = false
|
||||
res.message = t('imageUrlTip')
|
||||
return res
|
||||
}
|
||||
if (item.title.text === '') {
|
||||
res.code = false
|
||||
res.message = t('activeCubeTitlePlaceholder')
|
||||
return res
|
||||
}
|
||||
if (['style-1', 'style-2', 'style-4'].indexOf(diyStore.value[index].blockStyle.value) != -1) {
|
||||
if (item.subTitle.text === '') {
|
||||
res.code = false
|
||||
res.message = t('activeCubeSubTitlePlaceholder')
|
||||
return res
|
||||
}
|
||||
}
|
||||
if (['style-1', 'style-2'].indexOf(diyStore.value[index].blockStyle.value) != -1) {
|
||||
if (item.moreTitle.text === '') {
|
||||
res.code = false
|
||||
res.message = t('activeCubeButtonPlaceholder')
|
||||
return res
|
||||
}
|
||||
}
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
diyStore.editComponent.list.forEach((item: any) => {
|
||||
if (!item.id) item.id = diyStore.generateRandom()
|
||||
})
|
||||
|
||||
// 标题风格样式
|
||||
const showTitleDialog = ref(false)
|
||||
|
||||
const showTitleStyle = () => {
|
||||
selectTitleStyle.title = diyStore.editComponent.titleStyle.title
|
||||
selectTitleStyle.value = diyStore.editComponent.titleStyle.value
|
||||
showTitleDialog.value = true
|
||||
}
|
||||
|
||||
const titleStyleList = reactive([
|
||||
{
|
||||
url: 'static/resource/images/diy/active_cube/title_style1.png',
|
||||
title: '风格1',
|
||||
value: 'style-1'
|
||||
}, {
|
||||
url: 'static/resource/images/diy/active_cube/title_style2.png',
|
||||
title: '风格2',
|
||||
value: 'style-2'
|
||||
}, {
|
||||
url: 'static/resource/images/diy/active_cube/title_style3.png',
|
||||
title: '风格3',
|
||||
value: 'style-3'
|
||||
}, {
|
||||
url: 'static/resource/images/diy/active_cube/title_style5.png',
|
||||
title: '风格4',
|
||||
value: 'style-4'
|
||||
}, {
|
||||
url: 'static/resource/images/diy/active_cube/title_style6.png',
|
||||
title: '风格5',
|
||||
value: 'style-5'
|
||||
}
|
||||
])
|
||||
|
||||
const selectTitleStyle = reactive({
|
||||
title: diyStore.editComponent.titleStyle.title,
|
||||
value: diyStore.editComponent.titleStyle.value
|
||||
})
|
||||
|
||||
const changeTitleStyle = (item: any) => {
|
||||
selectTitleStyle.title = item.title
|
||||
selectTitleStyle.value = item.value
|
||||
}
|
||||
|
||||
const confirmTitleStyle = () => {
|
||||
diyStore.editComponent.titleStyle.title = selectTitleStyle.title
|
||||
diyStore.editComponent.titleStyle.value = selectTitleStyle.value
|
||||
initTitleStyle(diyStore.editComponent.titleStyle.value)
|
||||
showTitleDialog.value = false
|
||||
}
|
||||
|
||||
const initTitleStyle = (style) => {
|
||||
if (diyStore.editComponent.titleStyle.value == 'style-1') {
|
||||
diyStore.editComponent.titleColor = '#F91700'
|
||||
diyStore.editComponent.subTitle.textColor = '#FFFFFF'
|
||||
diyStore.editComponent.subTitle.startColor = '#FB792F'
|
||||
diyStore.editComponent.subTitle.endColor = '#F91700'
|
||||
} else if (diyStore.editComponent.titleStyle.value == 'style-2') {
|
||||
diyStore.editComponent.titleColor = '#F91700'
|
||||
diyStore.editComponent.subTitle.textColor = '#FFFFFF'
|
||||
diyStore.editComponent.subTitle.startColor = '#FB792F'
|
||||
diyStore.editComponent.subTitle.endColor = '#F91700'
|
||||
} else if (diyStore.editComponent.titleStyle.value == 'style-3') {
|
||||
diyStore.editComponent.titleColor = '#F91700'
|
||||
diyStore.editComponent.subTitle.textColor = '#FFFFFF'
|
||||
diyStore.editComponent.subTitle.startColor = '#FB792F'
|
||||
diyStore.editComponent.subTitle.endColor = '#F91700'
|
||||
} else if (diyStore.editComponent.titleStyle.value == 'style-4') {
|
||||
diyStore.editComponent.titleColor = '#FFFFFF'
|
||||
diyStore.editComponent.subTitle.textColor = '#333333'
|
||||
diyStore.editComponent.subTitle.startColor = '#FFFFFF'
|
||||
diyStore.editComponent.subTitle.endColor = '#FFFFFF'
|
||||
} else if (diyStore.editComponent.titleStyle.value == 'style-5') {
|
||||
diyStore.editComponent.titleColor = ''
|
||||
diyStore.editComponent.subTitle.textColor = '#999999'
|
||||
diyStore.editComponent.subTitle.startColor = '#FFFFFF'
|
||||
diyStore.editComponent.subTitle.endColor = '#FFFFFF'
|
||||
}
|
||||
}
|
||||
|
||||
// 板块风格样式
|
||||
const showListDialog = ref(false)
|
||||
|
||||
const showBlockStyle = () => {
|
||||
showListDialog.value = true
|
||||
}
|
||||
|
||||
const blockStyleList = reactive([
|
||||
{
|
||||
url: 'static/resource/images/diy/active_cube/block_style1.png',
|
||||
title: '风格1',
|
||||
value: 'style-1'
|
||||
},
|
||||
{
|
||||
url: 'static/resource/images/diy/active_cube/block_style2.png',
|
||||
title: '风格2',
|
||||
value: 'style-2'
|
||||
},
|
||||
{
|
||||
url: 'static/resource/images/diy/active_cube/block_style3.png',
|
||||
title: '风格3',
|
||||
value: 'style-3'
|
||||
},
|
||||
{
|
||||
url: 'static/resource/images/diy/active_cube/block_style4.png',
|
||||
title: '风格4',
|
||||
value: 'style-4'
|
||||
}
|
||||
])
|
||||
|
||||
const selectBlockStyle = reactive({
|
||||
title: diyStore.editComponent.blockStyle.title,
|
||||
value: diyStore.editComponent.blockStyle.value
|
||||
})
|
||||
|
||||
const changeBlockStyle = (item: any) => {
|
||||
selectBlockStyle.title = item.title
|
||||
selectBlockStyle.value = item.value
|
||||
}
|
||||
|
||||
const confirmBlockStyle = () => {
|
||||
diyStore.editComponent.blockStyle.title = selectBlockStyle.title
|
||||
diyStore.editComponent.blockStyle.value = selectBlockStyle.value
|
||||
initBlockStyle(diyStore.editComponent.blockStyle.value)
|
||||
showListDialog.value = false
|
||||
}
|
||||
|
||||
const initBlockStyle = (style: any) => {
|
||||
if (style == 'style-1') {
|
||||
diyStore.editComponent.blockStyle.fontWeight = 'normal'
|
||||
diyStore.editComponent.blockStyle.btnText = 'normal'
|
||||
|
||||
diyStore.editComponent.list[0].title.textColor = '#303133'
|
||||
diyStore.editComponent.list[0].subTitle.textColor = '#999999'
|
||||
diyStore.editComponent.list[0].subTitle.startColor = ''
|
||||
diyStore.editComponent.list[0].subTitle.endColor = ''
|
||||
diyStore.editComponent.list[0].moreTitle.startColor = '#FEA715'
|
||||
diyStore.editComponent.list[0].moreTitle.endColor = '#FE1E00'
|
||||
diyStore.editComponent.list[0].listFrame.startColor = '#FFFAF5'
|
||||
diyStore.editComponent.list[0].listFrame.endColor = '#FFFFFF'
|
||||
|
||||
diyStore.editComponent.list[1].title.textColor = '#303133'
|
||||
diyStore.editComponent.list[1].subTitle.textColor = '#999999'
|
||||
diyStore.editComponent.list[1].subTitle.startColor = ''
|
||||
diyStore.editComponent.list[1].subTitle.endColor = ''
|
||||
diyStore.editComponent.list[1].moreTitle.startColor = '#FFBF50'
|
||||
diyStore.editComponent.list[1].moreTitle.endColor = '#FF9E03'
|
||||
diyStore.editComponent.list[1].listFrame.startColor = '#FFFAF5'
|
||||
diyStore.editComponent.list[1].listFrame.endColor = '#FFFFFF'
|
||||
|
||||
diyStore.editComponent.list[2].title.textColor = '#303133'
|
||||
diyStore.editComponent.list[2].subTitle.textColor = '#999999'
|
||||
diyStore.editComponent.list[2].subTitle.startColor = ''
|
||||
diyStore.editComponent.list[2].subTitle.endColor = ''
|
||||
diyStore.editComponent.list[2].moreTitle.startColor = '#A2E792'
|
||||
diyStore.editComponent.list[2].moreTitle.endColor = '#49CD2D'
|
||||
diyStore.editComponent.list[2].listFrame.startColor = '#FFFAF5'
|
||||
diyStore.editComponent.list[2].listFrame.endColor = '#FFFFFF'
|
||||
|
||||
diyStore.editComponent.list[3].title.textColor = '#303133'
|
||||
diyStore.editComponent.list[3].subTitle.textColor = '#999999'
|
||||
diyStore.editComponent.list[3].subTitle.startColor = ''
|
||||
diyStore.editComponent.list[3].subTitle.endColor = ''
|
||||
diyStore.editComponent.list[3].moreTitle.startColor = '#4AC1FF'
|
||||
diyStore.editComponent.list[3].moreTitle.endColor = '#1D7CFF'
|
||||
diyStore.editComponent.list[3].listFrame.startColor = '#FFFAF5'
|
||||
diyStore.editComponent.list[3].listFrame.endColor = '#FFFFFF'
|
||||
} else if (style == 'style-2') {
|
||||
diyStore.editComponent.blockStyle.fontWeight = 'normal'
|
||||
diyStore.editComponent.blockStyle.btnText = 'normal'
|
||||
|
||||
diyStore.editComponent.blockStyle.fontWeight = 'bold'
|
||||
diyStore.editComponent.blockStyle.btnText = 'italics'
|
||||
|
||||
diyStore.editComponent.list[0].title.textColor = '#303133'
|
||||
diyStore.editComponent.list[0].subTitle.textColor = '#999999'
|
||||
diyStore.editComponent.list[0].subTitle.startColor = ''
|
||||
diyStore.editComponent.list[0].subTitle.endColor = ''
|
||||
diyStore.editComponent.list[0].moreTitle.startColor = '#FFC051'
|
||||
diyStore.editComponent.list[0].moreTitle.endColor = '#FF9C00'
|
||||
diyStore.editComponent.list[0].listFrame.startColor = '#FFF1DB'
|
||||
diyStore.editComponent.list[0].listFrame.endColor = '#FFFBF4'
|
||||
|
||||
diyStore.editComponent.list[1].title.textColor = '#303133'
|
||||
diyStore.editComponent.list[1].subTitle.textColor = '#999999'
|
||||
diyStore.editComponent.list[1].subTitle.startColor = ''
|
||||
diyStore.editComponent.list[1].subTitle.endColor = ''
|
||||
diyStore.editComponent.list[1].moreTitle.startColor = '#A4E894'
|
||||
diyStore.editComponent.list[1].moreTitle.endColor = '#45CC2A'
|
||||
diyStore.editComponent.list[1].listFrame.startColor = '#E6F6E2'
|
||||
diyStore.editComponent.list[1].listFrame.endColor = '#F5FDF3'
|
||||
|
||||
diyStore.editComponent.list[2].title.textColor = '#303133'
|
||||
diyStore.editComponent.list[2].subTitle.textColor = '#999999'
|
||||
diyStore.editComponent.list[2].subTitle.startColor = ''
|
||||
diyStore.editComponent.list[2].subTitle.endColor = ''
|
||||
diyStore.editComponent.list[2].moreTitle.startColor = '#4BC2FF'
|
||||
diyStore.editComponent.list[2].moreTitle.endColor = '#1F7DFF'
|
||||
diyStore.editComponent.list[2].listFrame.startColor = '#E2F6FF'
|
||||
diyStore.editComponent.list[2].listFrame.endColor = '#F2FAFF'
|
||||
|
||||
diyStore.editComponent.list[3].title.textColor = '#303133'
|
||||
diyStore.editComponent.list[3].subTitle.textColor = '#999999'
|
||||
diyStore.editComponent.list[3].subTitle.startColor = ''
|
||||
diyStore.editComponent.list[3].subTitle.endColor = ''
|
||||
diyStore.editComponent.list[3].moreTitle.startColor = '#FB792F'
|
||||
diyStore.editComponent.list[3].moreTitle.endColor = '#F91700'
|
||||
diyStore.editComponent.list[3].listFrame.startColor = '#FFEAEA'
|
||||
diyStore.editComponent.list[3].listFrame.endColor = '#FFFCFB'
|
||||
} else if (style == 'style-3') {
|
||||
diyStore.editComponent.blockStyle.fontWeight = 'normal'
|
||||
diyStore.editComponent.blockStyle.btnText = 'normal'
|
||||
|
||||
diyStore.editComponent.list[0].title.textColor = '#FF1128'
|
||||
diyStore.editComponent.list[0].subTitle.textColor = ''
|
||||
diyStore.editComponent.list[0].subTitle.startColor = ''
|
||||
diyStore.editComponent.list[0].subTitle.endColor = ''
|
||||
diyStore.editComponent.list[0].moreTitle.startColor = ''
|
||||
diyStore.editComponent.list[0].moreTitle.endColor = ''
|
||||
diyStore.editComponent.list[0].listFrame.startColor = ''
|
||||
diyStore.editComponent.list[0].listFrame.endColor = ''
|
||||
|
||||
diyStore.editComponent.list[1].title.textColor = '#303133'
|
||||
diyStore.editComponent.list[1].subTitle.textColor = ''
|
||||
diyStore.editComponent.list[1].subTitle.startColor = ''
|
||||
diyStore.editComponent.list[1].subTitle.endColor = ''
|
||||
diyStore.editComponent.list[1].moreTitle.startColor = ''
|
||||
diyStore.editComponent.list[1].moreTitle.endColor = ''
|
||||
diyStore.editComponent.list[1].listFrame.startColor = ''
|
||||
diyStore.editComponent.list[1].listFrame.endColor = ''
|
||||
|
||||
diyStore.editComponent.list[2].title.textColor = '#303133'
|
||||
diyStore.editComponent.list[2].subTitle.textColor = ''
|
||||
diyStore.editComponent.list[2].subTitle.startColor = ''
|
||||
diyStore.editComponent.list[2].subTitle.endColor = ''
|
||||
diyStore.editComponent.list[2].moreTitle.startColor = ''
|
||||
diyStore.editComponent.list[2].moreTitle.endColor = ''
|
||||
diyStore.editComponent.list[2].listFrame.startColor = ''
|
||||
diyStore.editComponent.list[2].listFrame.endColor = ''
|
||||
|
||||
diyStore.editComponent.list[3].title.textColor = '#303133'
|
||||
diyStore.editComponent.list[3].subTitle.textColor = ''
|
||||
diyStore.editComponent.list[3].subTitle.startColor = ''
|
||||
diyStore.editComponent.list[3].subTitle.endColor = ''
|
||||
diyStore.editComponent.list[3].moreTitle.startColor = ''
|
||||
diyStore.editComponent.list[3].moreTitle.endColor = ''
|
||||
diyStore.editComponent.list[3].listFrame.startColor = ''
|
||||
diyStore.editComponent.list[3].listFrame.endColor = ''
|
||||
} else if (style == 'style-4') {
|
||||
diyStore.editComponent.blockStyle.fontWeight = 'bold'
|
||||
diyStore.editComponent.blockStyle.btnText = 'normal'
|
||||
|
||||
diyStore.editComponent.list[0].title.textColor = '#303133'
|
||||
diyStore.editComponent.list[0].subTitle.textColor = '#ED6E00'
|
||||
diyStore.editComponent.list[0].subTitle.startColor = '#FFE4D9'
|
||||
diyStore.editComponent.list[0].subTitle.endColor = '#FFE4D9'
|
||||
diyStore.editComponent.list[0].moreTitle.startColor = ''
|
||||
diyStore.editComponent.list[0].moreTitle.endColor = ''
|
||||
diyStore.editComponent.list[0].listFrame.startColor = '#FFAD4D'
|
||||
diyStore.editComponent.list[0].listFrame.endColor = '#F93D02'
|
||||
|
||||
diyStore.editComponent.list[1].title.textColor = '#303133'
|
||||
diyStore.editComponent.list[1].subTitle.textColor = '#2E59E9'
|
||||
diyStore.editComponent.list[1].subTitle.startColor = '#CAD7F8'
|
||||
diyStore.editComponent.list[1].subTitle.endColor = '#CAD7F8'
|
||||
diyStore.editComponent.list[1].moreTitle.startColor = ''
|
||||
diyStore.editComponent.list[1].moreTitle.endColor = ''
|
||||
diyStore.editComponent.list[1].listFrame.startColor = '#7CA7F4'
|
||||
diyStore.editComponent.list[1].listFrame.endColor = '#2B56E9'
|
||||
|
||||
diyStore.editComponent.list[2].title.textColor = '#303133'
|
||||
diyStore.editComponent.list[2].subTitle.textColor = '#F62F55'
|
||||
diyStore.editComponent.list[2].subTitle.startColor = '#FCD6D9'
|
||||
diyStore.editComponent.list[2].subTitle.endColor = '#FCD6D9'
|
||||
diyStore.editComponent.list[2].moreTitle.startColor = ''
|
||||
diyStore.editComponent.list[2].moreTitle.endColor = ''
|
||||
diyStore.editComponent.list[2].listFrame.startColor = '#FF7F48'
|
||||
diyStore.editComponent.list[2].listFrame.endColor = '#EE335B'
|
||||
|
||||
diyStore.editComponent.list[3].title.textColor = '#303133'
|
||||
diyStore.editComponent.list[3].subTitle.textColor = '#139B3C'
|
||||
diyStore.editComponent.list[3].subTitle.startColor = '#D3F1DA'
|
||||
diyStore.editComponent.list[3].subTitle.endColor = '#D3F1DA'
|
||||
diyStore.editComponent.list[3].moreTitle.startColor = ''
|
||||
diyStore.editComponent.list[3].moreTitle.endColor = ''
|
||||
diyStore.editComponent.list[3].listFrame.startColor = '#90D48C'
|
||||
diyStore.editComponent.list[3].listFrame.endColor = '#299F4F'
|
||||
}
|
||||
}
|
||||
|
||||
const addItem = () => {
|
||||
diyStore.editComponent.list.push({
|
||||
id: diyStore.generateRandom(),
|
||||
title: {
|
||||
title: '标题',
|
||||
textColor: '#000000'
|
||||
},
|
||||
subTitle: {
|
||||
text: '副标题',
|
||||
textColor: '#999999',
|
||||
startColor: '',
|
||||
endColor: ''
|
||||
},
|
||||
listFrame: {
|
||||
startColor: '#4AC1FF',
|
||||
endColor: '#1D7CFF'
|
||||
},
|
||||
moreTitle: {
|
||||
text: '去看看',
|
||||
startColor: '#FEA715',
|
||||
endColor: '#FE1E00'
|
||||
},
|
||||
imageUrl: '',
|
||||
link: { name: '' }
|
||||
})
|
||||
}
|
||||
|
||||
const blockBoxRef = ref()
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
const sortable = Sortable.create(blockBoxRef.value, {
|
||||
group: 'item-wrap',
|
||||
animation: 200,
|
||||
onEnd: event => {
|
||||
const temp = diyStore.editComponent.list[event.oldIndex!]
|
||||
diyStore.editComponent.list.splice(event.oldIndex!, 1)
|
||||
diyStore.editComponent.list.splice(event.newIndex!, 0, temp)
|
||||
sortable.sort(
|
||||
range(diyStore.editComponent.list.length).map(value => {
|
||||
return value.toString()
|
||||
})
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
defineExpose({})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
656
admin-vben/src/app/views/diy/components/edit-carousel-search.vue
Normal file
656
admin-vben/src/app/views/diy/components/edit-carousel-search.vue
Normal file
@@ -0,0 +1,656 @@
|
||||
<template>
|
||||
<!-- 内容 -->
|
||||
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
|
||||
|
||||
<div class="edit-attr-item-wrap">
|
||||
<h3 class="mb-[10px]">{{ t('carouselSearchShowPosition') }}</h3>
|
||||
<el-form label-width="100px" class="px-[10px]">
|
||||
<el-form-item :label="t('carouselSearchShowWay')">
|
||||
<el-radio-group v-model="diyStore.editComponent.positionWay">
|
||||
<el-radio label="static">{{ t('carouselSearchShowWayStatic') }}</el-radio>
|
||||
<el-radio label="fixed">{{ t('carouselSearchShowWayFixed') }}</el-radio>
|
||||
</el-radio-group>
|
||||
<div v-if="diyStore.editComponent.positionWay == 'fixed'" class="text-sm text-gray-400 mb-[10px]">滑动页面查看效果</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('carouselSearchFixedBgColor')" v-show="diyStore.editComponent.positionWay == 'fixed'">
|
||||
<el-color-picker v-model="diyStore.editComponent.fixedBgColor" show-alpha :predefine="diyStore.predefineColors" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('carouselSearchBgGradient')">
|
||||
<el-radio-group v-model="diyStore.editComponent.bgGradient">
|
||||
<el-radio :label="true">{{ t('carouselSearchOpen') }}</el-radio>
|
||||
<el-radio :label="false">{{ t('carouselSearchClose') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="edit-attr-item-wrap">
|
||||
<h3 class="mb-[10px]">{{ t('carouselSearchSet') }}</h3>
|
||||
<el-form label-width="100px" class="px-[10px]" @submit.prevent>
|
||||
<el-form-item :label="t('selectStyle')" class="flex">
|
||||
<span class="text-primary flex-1 cursor-pointer" @click="showSearchStyle">{{ diyStore.editComponent.search.styleName }}</span>
|
||||
<el-icon @click="showSearchStyle" class="cursor-pointer">
|
||||
<ArrowRight />
|
||||
</el-icon>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('carouselSearchSubTitle')" v-if="diyStore.editComponent.search.style == 'style-2'">
|
||||
<el-input v-model.trim="diyStore.editComponent.search.subTitle.text" :placeholder="t('carouselSearchSubTitlePlaceholder')" clearable maxlength="10" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('logo')">
|
||||
<upload-image v-model="diyStore.editComponent.search.logo" :limit="1" />
|
||||
<div class="text-sm text-gray-400 mb-[10px]">{{ t('carouselSearchLogoTips') }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('carouselSearchText')">
|
||||
<div>
|
||||
<el-input v-model.trim="diyStore.editComponent.search.text" :placeholder="t('carouselSearchPlaceholder')" clearable maxlength="20" show-word-limit />
|
||||
<p class="text-sm text-gray-400 mt-[10px] leading-[1.5]">{{ t('carouselSearchTextTips') }}</p>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('link')">
|
||||
<diy-link v-model="diyStore.editComponent.search.link" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-dialog v-model="showSearchDialog" :title="t('selectStyle')" width="500px">
|
||||
|
||||
<div class="flex flex-wrap">
|
||||
<template v-for="(item,index) in searchStyleList" :key="index">
|
||||
<div :class="{ 'border-primary': selectSearchStyle.value == item.value }" @click="changeSearchStyle(item)" class="flex items-center justify-center overflow-hidden w-[200px] h-[100px] m-[6px] cursor-pointer border bg-[#eee]">
|
||||
<img :src="img(item.url)" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="showSearchDialog = false">{{ t('cancel') }}</el-button>
|
||||
<el-button type="primary" @click="confirmSearchStyle">{{ t('confirm') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
</el-dialog>
|
||||
</div>
|
||||
|
||||
<div class="edit-attr-item-wrap mb-[20px]">
|
||||
<h3 class="mb-[10px]">{{ t('carouselSearchHotWordSet') }}</h3>
|
||||
<el-form label-width="100px" class="px-[10px]">
|
||||
|
||||
<el-form-item :label="t('carouselSearchHotWordInterval')">
|
||||
<el-slider v-model="diyStore.editComponent.search.hotWord.interval" show-input size="small" class="ml-[10px] diy-nav-slider" :min="1" :max="10" />
|
||||
</el-form-item>
|
||||
|
||||
<p class="text-sm text-gray-400 mb-[10px]">{{ t('dragMouseAdjustOrder') }}</p>
|
||||
|
||||
<div ref="searchHotWordTabBoxRef">
|
||||
<div v-for="(item,index) in diyStore.editComponent.search.hotWord.list" :key="item.id"
|
||||
class="item-wrap p-[10px] relative border border-dashed border-gray-300 mb-[16px]">
|
||||
|
||||
<el-form-item :label="t('carouselSearchHotWordText')" class="!mb-0">
|
||||
<el-input v-model.trim="item.text" :placeholder="t('carouselSearchHotWordTextPlaceholder')" clearable maxlength="4" show-word-limit />
|
||||
</el-form-item>
|
||||
|
||||
<div class="del absolute cursor-pointer z-[2] top-[-8px] right-[-8px]" @click="diyStore.editComponent.search.hotWord.list.splice(index,1)">
|
||||
<icon name="element CircleCloseFilled" color="#bbb" size="20px" />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<el-button v-show="diyStore.editComponent.search.hotWord.list.length < 50" class="w-full" @click="addHotWordItem">{{ t('carouselSearchAddHotWordItem') }}</el-button>
|
||||
</div>
|
||||
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<el-collapse v-model="activeNames" @change="handleChange" class="collapse-wrap">
|
||||
<el-collapse-item :title="t('carouselSearchTabSet')" name="tab">
|
||||
<div class="edit-attr-item-wrap">
|
||||
<el-form label-width="100px" class="px-[10px]" @submit.prevent>
|
||||
<el-form-item :label="t('carouselSearchTabControl')">
|
||||
<el-switch v-model="diyStore.editComponent.tab.control" />
|
||||
</el-form-item>
|
||||
|
||||
<p class="text-sm text-gray-400 mb-[10px]">{{ t('dragMouseAdjustOrder') }}</p>
|
||||
|
||||
<div ref="tabBoxRef">
|
||||
<div v-for="(item,index) in diyStore.editComponent.tab.list" :key="item.id" class="item-wrap p-[10px] pb-0 relative border border-dashed border-gray-300 mb-[16px]">
|
||||
|
||||
<el-form-item :label="t('carouselSearchTabCategoryText')">
|
||||
<el-input v-model.trim="item.text" :placeholder="t('carouselSearchTabCategoryTextPlaceholder')" clearable maxlength="4" show-word-limit />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('dataSources')">
|
||||
<el-input v-model.trim="item.diy_title" :placeholder="t('selectDiyPagePlaceholder')" readonly class="select-diy-page-input" @click="diyPageShowDialogOpen(index)">
|
||||
<template #suffix>
|
||||
<div @click.stop="tabClear(index)">
|
||||
<el-icon v-if="item.diy_title">
|
||||
<Close />
|
||||
</el-icon>
|
||||
<el-icon v-else>
|
||||
<ArrowRight />
|
||||
</el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<div class="del absolute cursor-pointer z-[2] top-[-8px] right-[-8px]"
|
||||
v-show="diyStore.editComponent.tab.list.length > 1"
|
||||
@click="diyStore.editComponent.tab.list.splice(index,1)">
|
||||
<icon name="element CircleCloseFilled" color="#bbb" size="20px" />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<el-button v-show="diyStore.editComponent.tab.list.length < 50" class="w-full" @click="addTabItem">{{ t('carouselSearchAddTabItem') }}</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 选择微页面弹出框 -->
|
||||
<el-dialog v-model="diyPageShowDialog" :title="t('selectSourcesDiyPage')" width="1000px" :close-on-press-escape="true" :destroy-on-close="true" :close-on-click-modal="false">
|
||||
<el-table :data="diyPageTable.data" ref="diyPageTableRef" size="large"
|
||||
v-loading="diyPageTable.loading" height="490px"
|
||||
@current-change="handleCurrentDiyPageChange" row-key="id" highlight-current-row>
|
||||
<template #empty>
|
||||
<span>{{ !diyPageTable.loading ? t('emptyData') : '' }}</span>
|
||||
</template>
|
||||
<el-table-column prop="page_title" :label="t('diyPageTitle')" min-width="120" />
|
||||
<el-table-column prop="addon_name" :label="t('diyPageTypeName')" min-width="80" />
|
||||
<el-table-column prop="type_name" :label="t('diyPageForAddon')" min-width="80" />
|
||||
</el-table>
|
||||
<div class="mt-[16px] flex justify-end">
|
||||
<el-pagination v-model:current-page="diyPageTable.page"
|
||||
v-model:page-size="diyPageTable.limit"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="diyPageTable.total"
|
||||
@size-change="loadDiyPageList" @current-change="loadDiyPageList" />
|
||||
</div>
|
||||
<div class="flex items-center justify-end mt-[15px]">
|
||||
<el-button type="primary" @click="saveDiyPageId">{{ t('confirm') }}</el-button>
|
||||
<el-button @click="diyPageShowDialog = false">{{ t('cancel') }}</el-button>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
</el-form>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
<el-collapse-item :title="t('carouselSearchSwiperSet')" name="swiper">
|
||||
<el-form label-width="100px" class="px-[10px]">
|
||||
<el-form-item :label="t('carouselSearchSwiperControl')">
|
||||
<el-switch v-model="diyStore.editComponent.swiper.control" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('carouselSearchSwiperInterval')">
|
||||
<el-slider v-model="diyStore.editComponent.swiper.interval" show-input size="small" class="ml-[10px] diy-nav-slider" :min="1" :max="10" />
|
||||
</el-form-item>
|
||||
|
||||
<div class="text-sm text-gray-400 mb-[10px]">{{ t('carouselSearchSwiperTips') }}</div>
|
||||
|
||||
<div ref="imageBoxRef">
|
||||
<div v-for="(item,index) in diyStore.editComponent.swiper.list" :key="item.id"
|
||||
class="item-wrap p-[10px] pb-0 relative border border-dashed border-gray-300 mb-[16px]">
|
||||
<el-form-item :label="t('image')">
|
||||
<upload-image v-model="item.imageUrl" :limit="1" @change="selectImg" />
|
||||
</el-form-item>
|
||||
|
||||
<div class="del absolute cursor-pointer z-[2] top-[-8px] right-[-8px]"
|
||||
v-show="diyStore.editComponent.swiper.list.length > 1"
|
||||
@click="diyStore.editComponent.swiper.list.splice(index,1)">
|
||||
<icon name="element CircleCloseFilled" color="#bbb" size="20px" />
|
||||
</div>
|
||||
|
||||
<el-form-item :label="t('link')">
|
||||
<diy-link v-model="item.link" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-button v-show="diyStore.editComponent.swiper.list.length < 10" class="w-full"
|
||||
@click="addImageAd">{{ t('addImageAd') }}
|
||||
</el-button>
|
||||
|
||||
</el-form>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 样式 -->
|
||||
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
|
||||
<div class="edit-attr-item-wrap" v-if="diyStore.editComponent.search.style == 'style-2'">
|
||||
<h3 class="mb-[10px]">{{ t('carouselSearchPositionStyle') }}</h3>
|
||||
<el-form label-width="100px" class="px-[10px]">
|
||||
<el-form-item :label="t('carouselSearchTextColor')">
|
||||
<el-color-picker v-model="diyStore.editComponent.search.positionColor" show-alpha :predefine="diyStore.predefineColors" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="edit-attr-item-wrap" v-if="diyStore.editComponent.search.style == 'style-2'">
|
||||
<h3 class="mb-[10px]">{{ t('carouselSearchSubTitleStyle') }}</h3>
|
||||
<el-form label-width="100px" class="px-[10px]">
|
||||
<el-form-item :label="t('carouselSearchTextColor')">
|
||||
<el-color-picker v-model="diyStore.editComponent.search.subTitle.textColor" show-alpha :predefine="diyStore.predefineColors" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('carouselSearchBgColor')">
|
||||
<el-color-picker v-model="diyStore.editComponent.search.subTitle.startColor" :predefine="diyStore.predefineColors" show-alpha />
|
||||
<icon name="iconfont iconmap-connect" size="20px" class="block !text-gray-400 mx-[5px]" />
|
||||
<el-color-picker v-model="diyStore.editComponent.search.subTitle.endColor" :predefine="diyStore.predefineColors" show-alpha />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="edit-attr-item-wrap">
|
||||
<h3 class="mb-[10px]">{{ t('carouselSearchStyle') }}</h3>
|
||||
<el-form label-width="100px" class="px-[10px]">
|
||||
<el-form-item :label="t('carouselSearchTextColor')">
|
||||
<el-color-picker v-model="diyStore.editComponent.search.color" show-alpha :predefine="diyStore.predefineColors" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('carouselSearchBgColor')">
|
||||
<el-color-picker v-model="diyStore.editComponent.search.bgColor" show-alpha :predefine="diyStore.predefineColors" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('carouselSearchBtnColor')">
|
||||
<el-color-picker v-model="diyStore.editComponent.search.btnColor" show-alpha :predefine="diyStore.predefineColors" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('carouselSearchBtnBgColor')">
|
||||
<el-color-picker v-model="diyStore.editComponent.search.btnBgColor" show-alpha :predefine="diyStore.predefineColors" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="edit-attr-item-wrap">
|
||||
<h3 class="mb-[10px]">{{ t('carouselSearchTabStyle') }}</h3>
|
||||
<el-form label-width="100px" class="px-[10px]">
|
||||
<el-form-item :label="t('noColor')">
|
||||
<el-color-picker v-model="diyStore.editComponent.tab.noColor" show-alpha :predefine="diyStore.predefineColors" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('selectColor')">
|
||||
<el-color-picker v-model="diyStore.editComponent.tab.selectColor" show-alpha :predefine="diyStore.predefineColors" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('fixedNoColor')">
|
||||
<el-color-picker v-model="diyStore.editComponent.tab.fixedNoColor" show-alpha :predefine="diyStore.predefineColors" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('fixedSelectColor')">
|
||||
<el-color-picker v-model="diyStore.editComponent.tab.fixedSelectColor" show-alpha :predefine="diyStore.predefineColors" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="edit-attr-item-wrap">
|
||||
<h3 class="mb-[10px]">{{ t('carouselSearchSwiperSet') }}</h3>
|
||||
<el-form label-width="100px" class="px-[10px]">
|
||||
<el-form-item :label="t('carouselSearchSwiperStyle')" @change="changeSwiperStyle">
|
||||
<el-radio-group v-model="diyStore.editComponent.swiper.swiperStyle">
|
||||
<el-radio label="style-1">{{ t('carouselSearchSwiperIndicatorStyle1') }}</el-radio>
|
||||
<el-radio label="style-2">{{ t('carouselSearchSwiperIndicatorStyle2') }}</el-radio>
|
||||
<el-radio label="style-3">{{ t('carouselSearchSwiperIndicatorStyle3') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('topRounded')">
|
||||
<el-slider v-model="diyStore.editComponent.swiper.topRounded" show-input size="small"
|
||||
class="ml-[10px] diy-nav-slider" :max="50" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('bottomRounded')">
|
||||
<el-slider v-model="diyStore.editComponent.swiper.bottomRounded" show-input size="small"
|
||||
class="ml-[10px] diy-nav-slider" :max="50" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="edit-attr-item-wrap">
|
||||
<h3 class="mb-[10px]">{{ t('carouselSearchSwiperIndicatorSet') }}</h3>
|
||||
<el-form label-width="100px" class="px-[10px]">
|
||||
<el-form-item :label="t('carouselSearchSwiperIndicatorStyle')">
|
||||
<el-radio-group v-model="diyStore.editComponent.swiper.indicatorStyle">
|
||||
<el-radio label="style-1">{{ t('carouselSearchSwiperIndicatorStyle1') }}</el-radio>
|
||||
<el-radio label="style-2">{{ t('carouselSearchSwiperIndicatorStyle2') }}</el-radio>
|
||||
<el-radio label="style-3">{{ t('carouselSearchSwiperIndicatorStyle3') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('carouselSearchSwiperIndicatorAlign')">
|
||||
<el-radio-group v-model="diyStore.editComponent.swiper.indicatorAlign">
|
||||
<el-radio label="left">{{ t('alignLeft') }}</el-radio>
|
||||
<el-radio label="center">{{ t('alignCenter') }}</el-radio>
|
||||
<el-radio label="right">{{ t('alignRight') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('noColor')">
|
||||
<el-color-picker v-model="diyStore.editComponent.swiper.indicatorColor" show-alpha :predefine="diyStore.predefineColors" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('selectColor')">
|
||||
<el-color-picker v-model="diyStore.editComponent.swiper.indicatorActiveColor" show-alpha :predefine="diyStore.predefineColors" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 组件样式 -->
|
||||
<!-- <slot name="style"></slot> -->
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { t } from '@/lang'
|
||||
import { img } from '@/utils/common'
|
||||
import useDiyStore from '@/stores/modules/diy'
|
||||
import { ref, reactive, watch, onMounted, nextTick } from 'vue'
|
||||
import { ElTable } from 'element-plus'
|
||||
import Sortable from 'sortablejs'
|
||||
import { range, cloneDeep } from 'lodash-es'
|
||||
|
||||
import { getDiyPageListByCarouselSearch } from '@/app/api/diy'
|
||||
|
||||
const diyStore = useDiyStore()
|
||||
diyStore.editComponent.ignore = ['componentBgColor', 'componentBgUrl', 'marginTop', 'marginBottom', 'topRounded', 'bottomRounded', 'pageBgColor', 'marginBoth'] // 忽略公共属性
|
||||
|
||||
// 组件验证
|
||||
diyStore.editComponent.verify = (index: number) => {
|
||||
const res = { code: true, message: '' }
|
||||
|
||||
diyStore.value[index].search.hotWord.list.forEach((item: any) => {
|
||||
if (item.text == '') {
|
||||
res.code = false
|
||||
res.message = t('carouselSearchHotWordTextPlaceholder')
|
||||
return res
|
||||
}
|
||||
})
|
||||
|
||||
diyStore.value[index].tab.list.forEach((item: any) => {
|
||||
if (item.text == '') {
|
||||
res.code = false
|
||||
res.message = t('carouselSearchTabCategoryTextPlaceholder')
|
||||
return res
|
||||
}
|
||||
// if(item.diy_id == ''){
|
||||
// res.code = false
|
||||
// res.message = t('selectDiyPagePlaceholder')
|
||||
// return res
|
||||
// }
|
||||
})
|
||||
|
||||
if (diyStore.value[index].swiper.control) {
|
||||
diyStore.value[index].swiper.list.forEach((item: any) => {
|
||||
if (item.imageUrl == '') {
|
||||
res.code = false
|
||||
res.message = t('imageUrlTip')
|
||||
return res
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
/** ************ 搜索框样式选择-start ********************/
|
||||
const selectSearchStyle = reactive({
|
||||
title: diyStore.editComponent.search.styleName,
|
||||
value: diyStore.editComponent.search.style
|
||||
})
|
||||
|
||||
const showSearchDialog = ref(false)
|
||||
|
||||
const showSearchStyle = () => {
|
||||
showSearchDialog.value = true
|
||||
selectSearchStyle.title = diyStore.editComponent.search.styleName
|
||||
selectSearchStyle.value = diyStore.editComponent.search.style
|
||||
}
|
||||
|
||||
const changeSearchStyle = (item: any) => {
|
||||
selectSearchStyle.title = item.title
|
||||
selectSearchStyle.value = item.value
|
||||
}
|
||||
|
||||
const confirmSearchStyle = () => {
|
||||
diyStore.editComponent.search.styleName = selectSearchStyle.title
|
||||
diyStore.editComponent.search.style = selectSearchStyle.value
|
||||
showSearchDialog.value = false
|
||||
}
|
||||
|
||||
const searchStyleList = reactive([
|
||||
{
|
||||
url: 'static/resource/images/diy/carousel_search/style_1.png',
|
||||
title: '风格1',
|
||||
value: 'style-1'
|
||||
},
|
||||
{
|
||||
url: 'static/resource/images/diy/carousel_search/style_2.png',
|
||||
title: '风格2',
|
||||
value: 'style-2'
|
||||
}
|
||||
])
|
||||
|
||||
/** ************ 搜索框样式选择-end ********************/
|
||||
|
||||
diyStore.editComponent.search.hotWord.list.forEach((item: any) => {
|
||||
if (!item.id) item.id = diyStore.generateRandom()
|
||||
})
|
||||
|
||||
diyStore.editComponent.tab.list.forEach((item: any) => {
|
||||
if (!item.id) item.id = diyStore.generateRandom()
|
||||
})
|
||||
|
||||
diyStore.editComponent.swiper.list.forEach((item: any) => {
|
||||
if (!item.id) item.id = diyStore.generateRandom()
|
||||
})
|
||||
|
||||
const activeNames = ref(['tab', 'swiper'])
|
||||
const handleChange = (val: string[]) => {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadDiyPageList()
|
||||
})
|
||||
|
||||
const addHotWordItem = () => {
|
||||
diyStore.editComponent.search.hotWord.list.push({
|
||||
id: diyStore.generateRandom(),
|
||||
text: '关键词'
|
||||
})
|
||||
}
|
||||
|
||||
const tabClear = (index: any) => {
|
||||
diyStore.editComponent.tab.list[index].diy_id = 0
|
||||
diyStore.editComponent.tab.list[index].diy_title = ''
|
||||
}
|
||||
|
||||
const addTabItem = () => {
|
||||
diyStore.editComponent.tab.list.push({
|
||||
id: diyStore.generateRandom(),
|
||||
text: '分类名称', // 最多4个字
|
||||
source: 'diy_page', // 数据源类型,微页面:diy_page
|
||||
diy_id: '',
|
||||
diy_title: ''
|
||||
})
|
||||
}
|
||||
|
||||
const searchHotWordTabBoxRef = ref()
|
||||
|
||||
const tabBoxRef = ref()
|
||||
|
||||
const imageBoxRef = ref()
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
const hotWordSortable = Sortable.create(searchHotWordTabBoxRef.value, {
|
||||
group: 'item-wrap',
|
||||
animation: 200,
|
||||
onEnd: event => {
|
||||
const temp = diyStore.editComponent.search.hotWord.list[event.oldIndex!]
|
||||
diyStore.editComponent.search.hotWord.list.splice(event.oldIndex!, 1)
|
||||
diyStore.editComponent.search.hotWord.list.splice(event.newIndex!, 0, temp)
|
||||
tabSortable.sort(
|
||||
range(diyStore.editComponent.search.hotWord.list.length).map(value => {
|
||||
return value.toString()
|
||||
})
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
const tabSortable = Sortable.create(tabBoxRef.value, {
|
||||
group: 'item-wrap',
|
||||
animation: 200,
|
||||
onEnd: event => {
|
||||
const temp = diyStore.editComponent.tab.list[event.oldIndex!]
|
||||
diyStore.editComponent.tab.list.splice(event.oldIndex!, 1)
|
||||
diyStore.editComponent.tab.list.splice(event.newIndex!, 0, temp)
|
||||
tabSortable.sort(
|
||||
range(diyStore.editComponent.tab.list.length).map(value => {
|
||||
return value.toString()
|
||||
})
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
const imageSortable = Sortable.create(imageBoxRef.value, {
|
||||
group: 'item-wrap',
|
||||
animation: 200,
|
||||
onEnd: event => {
|
||||
const temp = diyStore.editComponent.swiper.list[event.oldIndex!]
|
||||
diyStore.editComponent.swiper.list.splice(event.oldIndex!, 1)
|
||||
diyStore.editComponent.swiper.list.splice(event.newIndex!, 0, temp)
|
||||
imageSortable.sort(
|
||||
range(diyStore.editComponent.swiper.list.length).map(value => {
|
||||
return value.toString()
|
||||
})
|
||||
)
|
||||
handleHeight(true)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const diyPageShowDialog = ref(false)
|
||||
|
||||
const diyPageTable = reactive({
|
||||
page: 1,
|
||||
limit: 10,
|
||||
total: 0,
|
||||
loading: true,
|
||||
data: [],
|
||||
searchParam: {}
|
||||
})
|
||||
const diyPageTableRef = ref<InstanceType<typeof ElTable>>()
|
||||
|
||||
/**
|
||||
* 获取自定义页面列表
|
||||
*/
|
||||
const loadDiyPageList = (page: number = 1) => {
|
||||
diyPageTable.loading = true
|
||||
diyPageTable.page = page
|
||||
|
||||
getDiyPageListByCarouselSearch({
|
||||
page: diyPageTable.page,
|
||||
limit: diyPageTable.limit,
|
||||
...diyPageTable.searchParam
|
||||
}).then(res => {
|
||||
diyPageTable.loading = false
|
||||
const data = res.data.data
|
||||
let newData: any = []
|
||||
let isExistCount = 0
|
||||
|
||||
// 排除当前编辑的微页面以及存在 置顶组件的数据
|
||||
if (diyStore.id) {
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
if (data[i].id == diyStore.id) {
|
||||
isExistCount++
|
||||
} else {
|
||||
newData.push(data[i])
|
||||
}
|
||||
}
|
||||
} else {
|
||||
newData = cloneDeep(data) // 添加
|
||||
}
|
||||
if (isExistCount) {
|
||||
res.data.total = res.data.total - isExistCount
|
||||
}
|
||||
diyPageTable.data = newData
|
||||
diyPageTable.total = res.data.total
|
||||
}).catch(() => {
|
||||
diyPageTable.loading = false
|
||||
})
|
||||
}
|
||||
|
||||
// 选择微页面
|
||||
let currDiyPage: any = {}
|
||||
let currTabIndexForDiyPage = 0
|
||||
const handleCurrentDiyPageChange = (val: string | any[]) => {
|
||||
currDiyPage = val
|
||||
}
|
||||
|
||||
const saveDiyPageId = () => {
|
||||
diyStore.editComponent.tab.list[currTabIndexForDiyPage].diy_id = currDiyPage.id
|
||||
diyStore.editComponent.tab.list[currTabIndexForDiyPage].diy_title = currDiyPage.title
|
||||
diyPageShowDialog.value = false
|
||||
}
|
||||
|
||||
const diyPageShowDialogOpen = (index: any) => {
|
||||
diyPageShowDialog.value = true
|
||||
currTabIndexForDiyPage = index
|
||||
if (currDiyPage) {
|
||||
setTimeout(() => {
|
||||
diyPageTableRef.value!.setCurrentRow(currDiyPage)
|
||||
}, 200)
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => diyStore.editComponent.swiper.list,
|
||||
(newValue, oldValue) => {
|
||||
// 设置图片宽高
|
||||
handleHeight()
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
const addImageAd = () => {
|
||||
diyStore.editComponent.swiper.list.push({
|
||||
id: diyStore.generateRandom(),
|
||||
imageUrl: '',
|
||||
imgWidth: 0,
|
||||
imgHeight: 0,
|
||||
link: { name: '' }
|
||||
})
|
||||
}
|
||||
|
||||
const selectImg = (url: string) => {
|
||||
handleHeight(true)
|
||||
}
|
||||
|
||||
const changeSwiperStyle = (value: any) => {
|
||||
handleHeight(true)
|
||||
}
|
||||
|
||||
// 处理高度
|
||||
const handleHeight = (isCalcHeight: boolean = false) => {
|
||||
diyStore.editComponent.swiper.list.forEach((item: any, index: number) => {
|
||||
const image = new Image()
|
||||
image.src = img(item.imageUrl)
|
||||
image.onload = async () => {
|
||||
item.imgWidth = image.width
|
||||
item.imgHeight = image.height
|
||||
// 计算第一张图片高度
|
||||
if (isCalcHeight && index == 0) {
|
||||
const ratio = item.imgHeight / item.imgWidth
|
||||
if (diyStore.editComponent.swiper.swiperStyle == 'style-1') {
|
||||
item.width = 375 * 0.92 // 0.92:前端缩放比例
|
||||
} else {
|
||||
item.width = 355
|
||||
}
|
||||
item.height = item.width * ratio
|
||||
diyStore.editComponent.swiper.imageHeight = parseInt(item.height)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
defineExpose({})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
||||
<style lang="scss">
|
||||
.select-diy-page-input .el-input__inner {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.collapse-wrap {
|
||||
.el-collapse-item__header {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
166
admin-vben/src/app/views/diy/components/edit-float-btn.vue
Normal file
166
admin-vben/src/app/views/diy/components/edit-float-btn.vue
Normal file
@@ -0,0 +1,166 @@
|
||||
<template>
|
||||
<!-- 内容 -->
|
||||
<div class="content-wrap float-btn" v-show="diyStore.editTab == 'content'">
|
||||
|
||||
<div class="edit-attr-item-wrap">
|
||||
<h3 class="mb-[10px]">{{ t('floatBtnButton') }}</h3>
|
||||
<el-form label-width="80px" class="px-[10px]">
|
||||
<el-form-item :label="t('floatBtnButton')">
|
||||
<span>{{ selectTemplate.name }}</span>
|
||||
<ul class="ml-[10px] flex items-center">
|
||||
<template v-for="(item,i) in templateList" :key="i">
|
||||
<li v-if="diyStore.editComponent.style==='style-1'||(diyStore.editComponent.style==='style-2'&&i>1)"
|
||||
:class="['w-[50px] h-[32px] flex items-center justify-center border-solid border-[1px] border-[#eee] cursor-pointer', {'border-r-transparent': templateList.length != (i+1)}, (item.className == diyStore.editComponent.bottomPosition) ? '!border-[var(--el-color-primary)]' : '' ]"
|
||||
@click="changeTemplateList(item)">
|
||||
<span :class="['iconfont !text-[20px]', item.src]"></span>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('floatBtnOffset')">
|
||||
<el-slider v-model="diyStore.editComponent.offset" show-input size="small" class="ml-[10px] diy-nav-slider" :max="100" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('lateralBtnOffset')">
|
||||
<el-slider v-model="diyStore.editComponent.lateralOffset" show-input size="small" class="ml-[10px] diy-nav-slider" :max="15" :min="-10" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<div class="edit-attr-item-wrap">
|
||||
<h3 class="mb-[10px]">{{ t('floatBtnImageSet') }}</h3>
|
||||
<el-form label-width="80px" class="px-[10px]">
|
||||
<el-form-item :label="t('floatBtnImageSize')">
|
||||
<el-slider v-model="diyStore.editComponent.imageSize" show-input size="small" class="ml-[10px] diy-nav-slider" :min="30" :max="100" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('floatBtnAroundRadius')">
|
||||
<el-slider v-model="diyStore.editComponent.aroundRadius" show-input size="small" class="ml-[10px] diy-nav-slider" :max="50" />
|
||||
</el-form-item>
|
||||
|
||||
<div class="text-[12px] text-[#999] mb-[15px] mt-[5px]">{{ t('floatBtnImageSuggest') }}</div>
|
||||
<div ref="imageBoxRef">
|
||||
<div v-for="(item,index) in diyStore.editComponent.list" :key="item.id"
|
||||
class="item-wrap p-[10px] pb-0 relative border border-dashed border-gray-300 mb-[16px]">
|
||||
<el-form-item :label="t('image')">
|
||||
<upload-image v-model="item.imageUrl" :limit="1" />
|
||||
</el-form-item>
|
||||
|
||||
<div class="del absolute cursor-pointer z-[2] top-[-8px] right-[-8px]"
|
||||
v-show="diyStore.editComponent.list.length > 1"
|
||||
@click="diyStore.editComponent.list.splice(index,1)">
|
||||
<icon name="element CircleCloseFilled" color="#bbb" size="20px" />
|
||||
</div>
|
||||
|
||||
<el-form-item :label="t('link')">
|
||||
<diy-link v-model="item.link" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
|
||||
<el-button v-show="diyStore.editComponent.list.length < 3" class="w-full" @click="addImageAd">{{ t('addImageAd') }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 样式 -->
|
||||
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
|
||||
<!-- 组件样式 -->
|
||||
<slot name="style"></slot>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, onMounted, nextTick } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import Sortable from 'sortablejs'
|
||||
import useDiyStore from '@/stores/modules/diy'
|
||||
import { range } from 'lodash-es'
|
||||
|
||||
const diyStore = useDiyStore()
|
||||
diyStore.editComponent.ignore = ['pageBgColor', 'marginTop', 'marginBottom', 'marginBoth', 'componentBgUrl'] // 忽略公共属性
|
||||
|
||||
// 组件验证
|
||||
diyStore.editComponent.verify = (index: number) => {
|
||||
const res = { code: true, message: '' }
|
||||
diyStore.value[index].list.forEach((item: any) => {
|
||||
if (item.imageUrl === '') {
|
||||
res.code = false
|
||||
res.message = t('imageUrlTip')
|
||||
return res
|
||||
}
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
const templateList = ref([
|
||||
{
|
||||
name: '左上',
|
||||
src: 'iconzuoshangpc',
|
||||
className: 'upperLeft'
|
||||
},
|
||||
{
|
||||
name: '右上',
|
||||
src: 'iconyoushangpc',
|
||||
className: 'upperRight'
|
||||
},
|
||||
{
|
||||
name: '左下',
|
||||
src: 'iconzuoxiapc',
|
||||
className: 'lowerLeft'
|
||||
},
|
||||
{
|
||||
name: '右下',
|
||||
src: 'iconyouxiapc',
|
||||
className: 'lowerRight'
|
||||
}
|
||||
])
|
||||
|
||||
const selectTemplate = ref({})
|
||||
templateList.value.forEach((item) => {
|
||||
if (item.className == diyStore.editComponent.bottomPosition) {
|
||||
selectTemplate.value = item
|
||||
}
|
||||
})
|
||||
|
||||
const changeTemplateList = (data: any) => {
|
||||
selectTemplate.value = data
|
||||
diyStore.editComponent.bottomPosition = data.className
|
||||
}
|
||||
|
||||
const addImageAd = () => {
|
||||
diyStore.editComponent.list.push({
|
||||
id: diyStore.generateRandom(),
|
||||
imageUrl: '',
|
||||
link: { name: '' }
|
||||
})
|
||||
}
|
||||
|
||||
const imageBoxRef = ref()
|
||||
diyStore.editComponent.list.forEach((item: any) => {
|
||||
if (!item.id) item.id = diyStore.generateRandom()
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
const imageSortable = Sortable.create(imageBoxRef.value, {
|
||||
group: 'item-wrap',
|
||||
animation: 200,
|
||||
onEnd: event => {
|
||||
const temp = diyStore.editComponent.list[event.oldIndex!]
|
||||
diyStore.editComponent.list.splice(event.oldIndex!, 1)
|
||||
diyStore.editComponent.list.splice(event.newIndex!, 0, temp)
|
||||
imageSortable.sort(
|
||||
range(diyStore.editComponent.list.length).map(value => {
|
||||
return value.toString()
|
||||
})
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
defineExpose({})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
267
admin-vben/src/app/views/diy/components/edit-graphic-nav.vue
Normal file
267
admin-vben/src/app/views/diy/components/edit-graphic-nav.vue
Normal file
@@ -0,0 +1,267 @@
|
||||
<template>
|
||||
<!-- 内容 -->
|
||||
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
|
||||
<div class="edit-attr-item-wrap">
|
||||
<h3 class="mb-[10px]">{{ t('graphicNavModeTitle') }}</h3>
|
||||
<el-form label-width="90px" class="px-[10px]">
|
||||
|
||||
<el-form-item :label="t('layoutMode')">
|
||||
<el-radio-group v-model="diyStore.editComponent.layout">
|
||||
<el-radio :label="'horizontal'">{{ t('layoutModeHorizontal') }}</el-radio>
|
||||
<el-radio :label="'vertical'">{{ t('layoutModeVertical') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('graphicNavSelectMode')">
|
||||
<el-radio-group v-model="diyStore.editComponent.mode">
|
||||
<el-radio :label="'graphic'">{{ t('graphicNavModeGraphic') }}</el-radio>
|
||||
<el-radio :label="'img'">{{ t('graphicNavModeImg') }}</el-radio>
|
||||
<el-radio :label="'text'">{{ t('graphicNavModeText') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<view v-show="diyStore.editComponent.layout == 'horizontal'">
|
||||
|
||||
<el-form-item :label="t('graphicNavPageCount')">
|
||||
<el-radio-group v-model="diyStore.editComponent.pageCount" @change="changePageCount">
|
||||
<el-radio :label="1">{{ t('singleLine') }}</el-radio>
|
||||
<el-radio :label="2">{{ t('multiline') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('graphicNavShowStyle')">
|
||||
<el-radio-group v-model="diyStore.editComponent.showStyle" @change="changeShowStyle">
|
||||
<el-radio :label="'fixed'">{{ t('graphicNavStyleFixed') }}</el-radio>
|
||||
<el-radio :label="'singleSlide'">{{ diyStore.editComponent.pageCount == 2 ? t('graphicNavStyleMultiLine') : t('graphicNavStyleSingleSlide') }}</el-radio>
|
||||
<el-radio :label="'pageSlide'">{{ t('graphicNavStylePageSlide') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('graphicNavRowCount')">
|
||||
<el-radio-group v-model="diyStore.editComponent.rowCount">
|
||||
<el-radio :label="3">3{{ t('piece') }}</el-radio>
|
||||
<el-radio :label="4">4{{ t('piece') }}</el-radio>
|
||||
<el-radio :label="5">5{{ t('piece') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</view>
|
||||
</el-form>
|
||||
</div>
|
||||
<div class="edit-attr-item-wrap">
|
||||
<h3 class="mb-[10px]">{{ t('graphicNavSetLabel') }}</h3>
|
||||
<el-form label-width="90px" class="px-[10px]">
|
||||
|
||||
<p class="text-sm text-gray-400 mb-[10px]">{{ t('graphicNavTips') }}</p>
|
||||
|
||||
<div ref="imageBoxRef">
|
||||
<div v-for="(item,index) in diyStore.editComponent.list" :key="item.id"
|
||||
class="item-wrap p-[10px] pb-0 relative border border-dashed border-gray-300 mb-[16px]">
|
||||
<el-form-item :label="t('image')" v-show="diyStore.editComponent.mode === 'graphic' || diyStore.editComponent.mode === 'img'">
|
||||
<upload-image v-model="item.imageUrl" :limit="1" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('graphicNavTitle')" v-show="diyStore.editComponent.mode === 'graphic' || diyStore.editComponent.mode === 'text'">
|
||||
<el-input v-model.trim="item.title" :placeholder="t('graphicNavTitlePlaceholder')" clearable maxlength="20" show-word-limit />
|
||||
</el-form-item>
|
||||
|
||||
<div class="del absolute cursor-pointer z-[2] top-[-8px] right-[-8px]"
|
||||
v-show="diyStore.editComponent.list.length > 1"
|
||||
@click="diyStore.editComponent.list.splice(index,1)">
|
||||
<icon name="element CircleCloseFilled" color="#bbb" size="20px" />
|
||||
</div>
|
||||
|
||||
<el-form-item :label="t('link')">
|
||||
<diy-link v-model="item.link" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-button v-show="diyStore.editComponent.list.length < 50" class="w-full" @click="addGraphicNav">
|
||||
{{ t('addGraphicNav') }}
|
||||
</el-button>
|
||||
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 样式 -->
|
||||
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
|
||||
<div class="edit-attr-item-wrap" v-show="['graphic','img'].includes(diyStore.editComponent.mode)">
|
||||
<h3 class="mb-[10px]">{{ t('graphicNavImageSet') }}</h3>
|
||||
<el-form label-width="90px" class="px-[10px]">
|
||||
<el-form-item :label="t('graphicNavImageSize')">
|
||||
<el-slider v-model="diyStore.editComponent.imageSize" show-input size="small" class="ml-[10px] diy-nav-slider" :min="20" :max="60" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('graphicNavAroundRadius')">
|
||||
<el-slider v-model="diyStore.editComponent.aroundRadius" show-input size="small" class="ml-[10px] diy-nav-slider" :max="50" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<div class="edit-attr-item-wrap" v-show="['graphic','text'].includes(diyStore.editComponent.mode)">
|
||||
<h3 class="mb-[10px]">{{ t('textSet') }}</h3>
|
||||
<el-form label-width="90px" class="px-[10px]">
|
||||
<el-form-item :label="t('textFontSize')">
|
||||
<el-slider v-model="diyStore.editComponent.font.size" show-input size="small" class="ml-[10px] diy-nav-slider" :min="12" :max="16" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('textFontWeight')">
|
||||
<el-radio-group v-model="diyStore.editComponent.font.weight">
|
||||
<el-radio :label="'normal'">{{ t('fontWeightNormal') }}</el-radio>
|
||||
<el-radio :label="'bold'">{{ t('fontWeightBold') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('textColor')">
|
||||
<el-color-picker v-model="diyStore.editComponent.font.color" show-alpha :predefine="diyStore.predefineColors" />
|
||||
</el-form-item>
|
||||
|
||||
</el-form>
|
||||
</div>
|
||||
<div class="edit-attr-item-wrap" v-show="diyStore.editComponent.showStyle == 'pageSlide' && diyStore.editComponent.layout == 'horizontal'">
|
||||
<h3 class="mb-[10px]">{{ t('carouselSearchSwiperIndicatorSet') }}</h3>
|
||||
<el-form label-width="100px" class="px-[10px]">
|
||||
<el-form-item :label="t('carouselSearchSwiperIndicatorStyle')">
|
||||
<el-radio-group v-model="diyStore.editComponent.swiper.indicatorStyle">
|
||||
<el-radio label="style-1">{{ t('carouselSearchSwiperIndicatorStyle1') }}</el-radio>
|
||||
<el-radio label="style-2">{{ t('carouselSearchSwiperIndicatorStyle2') }}</el-radio>
|
||||
<el-radio label="style-3">{{ t('carouselSearchSwiperIndicatorStyle3') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('carouselSearchSwiperIndicatorAlign')">
|
||||
<el-radio-group v-model="diyStore.editComponent.swiper.indicatorAlign">
|
||||
<el-radio label="left">{{ t('alignLeft') }}</el-radio>
|
||||
<el-radio label="center">{{ t('alignCenter') }}</el-radio>
|
||||
<el-radio label="right">{{ t('alignRight') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('noColor')">
|
||||
<el-color-picker v-model="diyStore.editComponent.swiper.indicatorColor" show-alpha :predefine="diyStore.predefineColors" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('selectColor')">
|
||||
<el-color-picker v-model="diyStore.editComponent.swiper.indicatorActiveColor" show-alpha :predefine="diyStore.predefineColors" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 组件样式 -->
|
||||
<slot name="style"></slot>
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, onMounted, nextTick } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import Sortable from 'sortablejs'
|
||||
import { img } from '@/utils/common'
|
||||
import { range } from 'lodash-es'
|
||||
|
||||
import useDiyStore from '@/stores/modules/diy'
|
||||
|
||||
const diyStore = useDiyStore()
|
||||
diyStore.editComponent.ignore = [] // 忽略公共属性
|
||||
// 组件验证
|
||||
diyStore.editComponent.verify = (index: number) => {
|
||||
const res = { code: true, message: '' }
|
||||
|
||||
diyStore.value[index].list.forEach((item: any) => {
|
||||
if ((diyStore.value[index].mode === 'graphic' || diyStore.value[index].mode === 'img') && item.imageUrl === '') {
|
||||
res.code = false
|
||||
res.message = t('imageUrlTip')
|
||||
return res
|
||||
}
|
||||
if ((diyStore.value[index].mode === 'graphic' || diyStore.value[index].mode === 'text') && item.title === '') {
|
||||
res.code = false
|
||||
res.message = t('graphicNavTitlePlaceholder')
|
||||
return res
|
||||
}
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
diyStore.editComponent.list.forEach((item: any) => {
|
||||
if (!item.id) item.id = diyStore.generateRandom()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => diyStore.editComponent.list,
|
||||
(newValue, oldValue) => {
|
||||
// 设置图片宽高
|
||||
diyStore.editComponent.list.forEach((item: any) => {
|
||||
const image = new Image()
|
||||
image.src = img(item.imageUrl)
|
||||
image.onload = async () => {
|
||||
item.imgWidth = image.width
|
||||
item.imgHeight = image.height
|
||||
}
|
||||
})
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
const addGraphicNav = () => {
|
||||
diyStore.editComponent.list.push({
|
||||
id: diyStore.generateRandom(),
|
||||
title: '',
|
||||
imageUrl: '',
|
||||
imgWidth: 0,
|
||||
imgHeight: 0,
|
||||
link: { name: '' },
|
||||
label: {
|
||||
control: false,
|
||||
text: '热门',
|
||||
textColor: '#FFFFFF',
|
||||
bgColorStart: '#F83287',
|
||||
bgColorEnd: '#FE3423'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const imageBoxRef = ref()
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
const sortable = Sortable.create(imageBoxRef.value, {
|
||||
group: 'item-wrap',
|
||||
animation: 200,
|
||||
onEnd: event => {
|
||||
const temp = diyStore.editComponent.list[event.oldIndex!]
|
||||
diyStore.editComponent.list.splice(event.oldIndex!, 1)
|
||||
diyStore.editComponent.list.splice(event.newIndex!, 0, temp)
|
||||
sortable.sort(
|
||||
range(diyStore.editComponent.list.length).map(value => {
|
||||
return value.toString()
|
||||
})
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
const changePageCount = (value: any) => {
|
||||
if (value == '1') {
|
||||
diyStore.editComponent.showStyle = 'singleSlide'
|
||||
} else if (value == '2') {
|
||||
diyStore.editComponent.showStyle = 'fixed'
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.edit-graphic-nav {
|
||||
|
||||
.item-wrap {
|
||||
.del {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.del {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
34
admin-vben/src/app/views/diy/components/edit-horz-blank.vue
Normal file
34
admin-vben/src/app/views/diy/components/edit-horz-blank.vue
Normal file
@@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<!-- 内容 -->
|
||||
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
|
||||
<div class="edit-attr-item-wrap">
|
||||
<h3 class="mb-[10px]">{{ t('blankHeightSet') }}</h3>
|
||||
<el-form label-width="80px" class="px-[10px]">
|
||||
<el-form-item :label="t('blankHeight')">
|
||||
<el-slider v-model="diyStore.editComponent.height" show-input size="small" max="200" class="ml-[10px] diy-nav-slider" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 样式 -->
|
||||
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
|
||||
|
||||
<!-- 组件样式 -->
|
||||
<slot name="style"></slot>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { t } from '@/lang'
|
||||
import useDiyStore from '@/stores/modules/diy'
|
||||
|
||||
const diyStore = useDiyStore()
|
||||
diyStore.editComponent.ignore = ['pageBgColor', 'componentBgUrl'] // 忽略公共属性
|
||||
|
||||
defineExpose({})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
44
admin-vben/src/app/views/diy/components/edit-horz-line.vue
Normal file
44
admin-vben/src/app/views/diy/components/edit-horz-line.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<!-- 内容 -->
|
||||
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
|
||||
<div class="edit-attr-item-wrap">
|
||||
<h3 class="mb-[10px]">{{ t('horzLineStyle') }}</h3>
|
||||
<el-form label-width="80px" class="px-[10px]">
|
||||
<el-form-item :label="t('styleLabel')">
|
||||
<el-radio-group v-model="diyStore.editComponent.borderStyle">
|
||||
<el-radio label="solid">{{ t('horzLineStyleSolid') }}</el-radio>
|
||||
<el-radio label="dashed">{{ t('horzLineStyleDashed') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('horzLineBorderColor')">
|
||||
<el-color-picker v-model="diyStore.editComponent.borderColor" show-alpha :predefine="diyStore.predefineColors" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('horzLineBorderWidth')">
|
||||
<el-slider v-model="diyStore.editComponent.borderWidth" show-input size="small"
|
||||
class="ml-[10px] diy-nav-slider" :min="1" :max="10" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 样式 -->
|
||||
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
|
||||
|
||||
<!-- 组件样式 -->
|
||||
<slot name="style"></slot>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { t } from '@/lang'
|
||||
import useDiyStore from '@/stores/modules/diy'
|
||||
|
||||
const diyStore = useDiyStore()
|
||||
diyStore.editComponent.ignore = ['pageBgColor', 'componentBgColor', 'componentBgUrl', 'topRounded', 'bottomRounded'] // 忽略公共属性
|
||||
|
||||
defineExpose({})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
73
admin-vben/src/app/views/diy/components/edit-hot-area.vue
Normal file
73
admin-vben/src/app/views/diy/components/edit-hot-area.vue
Normal file
@@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<!-- 内容 -->
|
||||
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
|
||||
|
||||
<div class="edit-attr-item-wrap">
|
||||
<h3 class="mb-[10px]">{{ t('hotAreaSet') }}</h3>
|
||||
<el-form label-width="80px" class="px-[10px]">
|
||||
|
||||
<div ref="imageBoxRef">
|
||||
<div class="item-wrap p-[10px] pb-0 relative border border-dashed border-gray-300 mb-[16px]">
|
||||
<el-form-item :label="t('hotAreaBackground')">
|
||||
<upload-image v-model="diyStore.editComponent.imageUrl" :limit="1" @change="selectImg" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('hotAreaSet')">
|
||||
<heat-map v-model="diyStore.editComponent" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 样式 -->
|
||||
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
|
||||
|
||||
<!-- 组件样式 -->
|
||||
<slot name="style"></slot>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { t } from '@/lang'
|
||||
import useDiyStore from '@/stores/modules/diy'
|
||||
import { img } from '@/utils/common'
|
||||
|
||||
const diyStore = useDiyStore()
|
||||
diyStore.editComponent.ignore = ['componentBgUrl'] // 忽略公共属性
|
||||
|
||||
// 组件验证
|
||||
diyStore.editComponent.verify = (index: number) => {
|
||||
const res = { code: true, message: '' }
|
||||
if (diyStore.value[index].imageUrl === '') {
|
||||
res.code = false
|
||||
res.message = t('imageUrlTip')
|
||||
return res
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
const selectImg = (url: string) => {
|
||||
handleHeight()
|
||||
}
|
||||
|
||||
// 处理高度
|
||||
const handleHeight = () => {
|
||||
const image = new Image()
|
||||
image.src = img(diyStore.editComponent.imageUrl)
|
||||
image.onload = async() => {
|
||||
diyStore.editComponent.imgWidth = image.width
|
||||
diyStore.editComponent.imgHeight = image.height
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
179
admin-vben/src/app/views/diy/components/edit-image-ads.vue
Normal file
179
admin-vben/src/app/views/diy/components/edit-image-ads.vue
Normal file
@@ -0,0 +1,179 @@
|
||||
<template>
|
||||
<!-- 内容 -->
|
||||
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
|
||||
<div class="edit-attr-item-wrap">
|
||||
<h3 class="mb-[10px]">{{ t('imageSet') }}</h3>
|
||||
<el-form label-width="80px" class="px-[10px]">
|
||||
|
||||
<el-form-item :label="t('sameScreen')" v-if="diyStore.currentIndex == 0">
|
||||
<el-switch v-model="diyStore.editComponent.isSameScreen" />
|
||||
<div class="text-sm text-gray-400 leading-[1.4]">{{ t('imageAdsSameScreenTips') }}</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('imageHeight')" class="display-block">
|
||||
<el-input v-model.trim="diyStore.editComponent.imageHeight" :placeholder="t('imageHeightPlaceholder')" clearable maxlength="10" @blur="blurImageHeight">
|
||||
<template #append>px</template>
|
||||
</el-input>
|
||||
<div class="text-sm text-gray-400 mb-[10px]">{{ t('imageAdsTips') }}</div>
|
||||
</el-form-item>
|
||||
|
||||
<div ref="imageBoxRef">
|
||||
<div v-for="(item,index) in diyStore.editComponent.list" :key="item.id" class="item-wrap p-[10px] pb-0 relative border border-dashed border-gray-300 mb-[16px]">
|
||||
<el-form-item :label="t('image')">
|
||||
<upload-image v-model="item.imageUrl" :limit="1" @change="selectImg" />
|
||||
</el-form-item>
|
||||
|
||||
<div class="del absolute cursor-pointer z-[2] top-[-8px] right-[-8px]"
|
||||
v-show="diyStore.editComponent.list.length > 1"
|
||||
@click="diyStore.editComponent.list.splice(index,1)">
|
||||
<icon name="element CircleCloseFilled" color="#bbb" size="20px" />
|
||||
</div>
|
||||
|
||||
<el-form-item :label="t('link')">
|
||||
<diy-link v-model="item.link" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-button v-show="diyStore.editComponent.list.length < 10" class="w-full" @click="addImageAd">
|
||||
{{ t('addImageAd') }}
|
||||
</el-button>
|
||||
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 样式 -->
|
||||
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
|
||||
|
||||
<!-- 组件样式 -->
|
||||
<slot name="style"></slot>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, watch, onMounted, nextTick } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import Sortable from 'sortablejs'
|
||||
import { img } from '@/utils/common'
|
||||
import { range } from 'lodash-es'
|
||||
import useDiyStore from '@/stores/modules/diy'
|
||||
|
||||
const diyStore = useDiyStore()
|
||||
diyStore.editComponent.ignore = ['componentBgUrl'] // 忽略公共属性
|
||||
|
||||
// 组件验证
|
||||
diyStore.editComponent.verify = (index: number) => {
|
||||
const res = { code: true, message: '' }
|
||||
if (diyStore.value[index].imageHeight == 0) {
|
||||
res.code = false
|
||||
res.message = t('imageHeightPlaceholder')
|
||||
return res
|
||||
}
|
||||
if (!/^\d+.?\d{0,2}$/.test(diyStore.value[index].imageHeight)) {
|
||||
res.code = false
|
||||
res.message = t('imageHeightRegNum')
|
||||
return res
|
||||
}
|
||||
diyStore.value[index].list.forEach((item: any) => {
|
||||
if (item.imageUrl === '') {
|
||||
res.code = false
|
||||
res.message = t('imageUrlTip')
|
||||
return res
|
||||
}
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
diyStore.editComponent.list.forEach((item: any) => {
|
||||
if (!item.id) item.id = diyStore.generateRandom()
|
||||
})
|
||||
|
||||
watch(
|
||||
() => diyStore.editComponent.list,
|
||||
(newValue, oldValue) => {
|
||||
// 设置图片宽高
|
||||
handleHeight()
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
const addImageAd = () => {
|
||||
diyStore.editComponent.list.push({
|
||||
id: diyStore.generateRandom(),
|
||||
imageUrl: '',
|
||||
imgWidth: 0,
|
||||
imgHeight: 0,
|
||||
link: { name: '' }
|
||||
})
|
||||
}
|
||||
|
||||
const selectImg = (url: string) => {
|
||||
handleHeight(true)
|
||||
}
|
||||
|
||||
// 处理高度
|
||||
const handleHeight = (isCalcHeight: boolean = false) => {
|
||||
diyStore.editComponent.list.forEach((item: any, index: number) => {
|
||||
const image = new Image()
|
||||
image.src = img(item.imageUrl)
|
||||
image.onload = async() => {
|
||||
item.imgWidth = image.width
|
||||
item.imgHeight = image.height
|
||||
// 计算第一张图片高度
|
||||
if (isCalcHeight && index == 0) {
|
||||
const ratio = item.imgHeight / item.imgWidth
|
||||
item.width = 375 - (diyStore.editComponent.margin.both * 2)
|
||||
item.height = item.width * ratio
|
||||
diyStore.editComponent.imageHeight = parseInt(item.height)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const blurImageHeight = () => {
|
||||
diyStore.editComponent.imageHeight = diyStore.editComponent.imageHeight ? parseInt(diyStore.editComponent.imageHeight) : 0
|
||||
}
|
||||
|
||||
const imageBoxRef = ref()
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
const sortable = Sortable.create(imageBoxRef.value, {
|
||||
group: 'item-wrap',
|
||||
animation: 200,
|
||||
onEnd: event => {
|
||||
const temp = diyStore.editComponent.list[event.oldIndex!]
|
||||
diyStore.editComponent.list.splice(event.oldIndex!, 1)
|
||||
diyStore.editComponent.list.splice(event.newIndex!, 0, temp)
|
||||
sortable.sort(
|
||||
range(diyStore.editComponent.list.length).map(value => {
|
||||
return value.toString()
|
||||
})
|
||||
)
|
||||
handleHeight(true)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
defineExpose({})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.edit-image-ads {
|
||||
|
||||
.item-wrap {
|
||||
.del {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.del {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
41
admin-vben/src/app/views/diy/components/edit-member-info.vue
Normal file
41
admin-vben/src/app/views/diy/components/edit-member-info.vue
Normal file
@@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<!-- 内容 -->
|
||||
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
|
||||
<div class="edit-attr-item-wrap">
|
||||
<h3 class="mb-[10px]">{{ t('memberStyle') }}</h3>
|
||||
<el-form label-width="80px" class="px-[10px]">
|
||||
<el-form-item :label="t('bgUrl')">
|
||||
<upload-image v-model="diyStore.editComponent.bgUrl" :limit="1" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 样式 -->
|
||||
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
|
||||
<div class="edit-attr-item-wrap">
|
||||
<h3 class="mb-[10px]">{{ t('memberStyle') }}</h3>
|
||||
<el-form label-width="80px" class="px-[10px]">
|
||||
<el-form-item :label="t('textColor')">
|
||||
<el-color-picker v-model="diyStore.editComponent.textColor" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 组件样式 -->
|
||||
<slot name="style"></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { t } from '@/lang'
|
||||
import useDiyStore from '@/stores/modules/diy'
|
||||
|
||||
const diyStore = useDiyStore()
|
||||
diyStore.editComponent.ignore = ['componentBgUrl'] // 忽略公共属性
|
||||
|
||||
defineExpose({})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
135
admin-vben/src/app/views/diy/components/edit-member-level.vue
Normal file
135
admin-vben/src/app/views/diy/components/edit-member-level.vue
Normal file
@@ -0,0 +1,135 @@
|
||||
<template>
|
||||
<!-- 内容 -->
|
||||
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
|
||||
<div class="edit-attr-item-wrap">
|
||||
<h3 class="mb-[10px]">{{ t('selectStyle') }}</h3>
|
||||
<el-form label-width="80px" class="px-[10px]">
|
||||
<el-form-item :label="t('selectStyle')" class="flex">
|
||||
<span class="text-primary flex-1 cursor-pointer"
|
||||
@click="showStyle">{{ diyStore.editComponent.styleName }}</span>
|
||||
<el-icon @click="showStyle" class="cursor-pointer">
|
||||
<ArrowRight />
|
||||
</el-icon>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-dialog v-model="showDialog" :title="t('selectStyle')" width="660px">
|
||||
|
||||
<div class="flex flex-wrap">
|
||||
<template v-for="(item,index) in styleList" :key="index">
|
||||
<div
|
||||
:class="{ 'border-primary': selectStyle.value == item.value, '!mr-[0]': [(index+1)%3] == 0 }"
|
||||
@click="changeStyle(item)"
|
||||
class="flex my-[5px] items-center justify-center overflow-hidden w-[200px] h-[100px] mr-[12px] cursor-pointer border bg-gray-50">
|
||||
<img :src="img(item.url)" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button>
|
||||
<el-button type="primary" @click="confirmStyle">{{ t('confirm') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
</el-dialog>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 样式 -->
|
||||
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
|
||||
|
||||
<!-- 组件样式 -->
|
||||
<slot name="style"></slot>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { t } from '@/lang'
|
||||
import useDiyStore from '@/stores/modules/diy'
|
||||
import { img } from '@/utils/common'
|
||||
import { ref, reactive } from 'vue'
|
||||
|
||||
const diyStore = useDiyStore()
|
||||
diyStore.editComponent.ignore = ['componentBgColor', 'componentBgUrl'] // 忽略公共属性
|
||||
|
||||
const selectStyle = reactive({
|
||||
title: diyStore.editComponent.styleName,
|
||||
value: diyStore.editComponent.style
|
||||
})
|
||||
|
||||
// 风格样式
|
||||
const showDialog = ref(false)
|
||||
|
||||
const showStyle = () => {
|
||||
showDialog.value = true
|
||||
selectStyle.title = diyStore.editComponent.styleName
|
||||
selectStyle.value = diyStore.editComponent.style
|
||||
}
|
||||
|
||||
const styleList = reactive([
|
||||
{
|
||||
url: 'static/resource/images/diy/member/member_level_style1.jpg',
|
||||
title: '风格1',
|
||||
value: 'style-1'
|
||||
},
|
||||
{
|
||||
url: 'static/resource/images/diy/member/member_level_style2.png',
|
||||
title: '风格2',
|
||||
value: 'style-2'
|
||||
},
|
||||
{
|
||||
url: 'static/resource/images/diy/member/member_level_style3.jpg',
|
||||
title: '风格3',
|
||||
value: 'style-3'
|
||||
},
|
||||
{
|
||||
url: 'static/resource/images/diy/member/member_level_style4.png',
|
||||
title: '风格4',
|
||||
value: 'style-4'
|
||||
},
|
||||
{
|
||||
url: 'static/resource/images/diy/member/member_level_style5.png',
|
||||
title: '风格5',
|
||||
value: 'style-5'
|
||||
}
|
||||
])
|
||||
|
||||
const changeStyle = (item: any) => {
|
||||
selectStyle.title = item.title
|
||||
selectStyle.value = item.value
|
||||
}
|
||||
|
||||
const confirmStyle = () => {
|
||||
diyStore.editComponent.styleName = selectStyle.title
|
||||
diyStore.editComponent.style = selectStyle.value
|
||||
initStyle(diyStore.editComponent.style)
|
||||
showDialog.value = false
|
||||
}
|
||||
|
||||
const initStyle = (style: any) => {
|
||||
if (style == 'style-1') {
|
||||
diyStore.editComponent.bottomRounded = 0
|
||||
diyStore.editComponent.topRounded = 12
|
||||
} else if (style == 'style-2') {
|
||||
diyStore.editComponent.bottomRounded = 0
|
||||
diyStore.editComponent.topRounded = 12
|
||||
} else if (style == 'style-3') {
|
||||
diyStore.editComponent.bottomRounded = 12
|
||||
diyStore.editComponent.topRounded = 12
|
||||
} else if (style == 'style-4') {
|
||||
diyStore.editComponent.bottomRounded = 12
|
||||
diyStore.editComponent.topRounded = 12
|
||||
} else if (style == 'style-5') {
|
||||
diyStore.editComponent.bottomRounded = 12
|
||||
diyStore.editComponent.topRounded = 12
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
212
admin-vben/src/app/views/diy/components/edit-notice.vue
Normal file
212
admin-vben/src/app/views/diy/components/edit-notice.vue
Normal file
@@ -0,0 +1,212 @@
|
||||
<template>
|
||||
<!-- 内容 -->
|
||||
<div class="content-wrap notice-content-wrap" v-show="diyStore.editTab == 'content'">
|
||||
<div class="edit-attr-item-wrap">
|
||||
<h3 class="mb-[10px]">{{ t('noticeStyle') }}</h3>
|
||||
<el-form label-width="90px" class="px-[10px]">
|
||||
<el-form-item :label="t('noticeType')">
|
||||
<el-radio-group v-model="diyStore.editComponent.noticeType">
|
||||
<el-radio label="img">{{ t('noticeTypeImg') }}</el-radio>
|
||||
<el-radio label="text">{{ t('noticeTypeText') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<div class="flex items-center flex-wrap py-[8px] px-[10px] bg-[#f4f3f7] rounded mb-[18px] mx-[18px]" v-show="diyStore.editComponent.noticeType == 'img'">
|
||||
<div :class="['mr-[10px] rounded cursor-pointer border-[1px] border-solid', {'border-[var(--el-color-primary)]': diyStore.editComponent.systemUrl == 'style_1' && diyStore.editComponent.imgType == 'system'}]">
|
||||
<img src="@/app/assets/images/diy/notice/style_1.png" :class="['h-[28px] px-[10px] py-[5px]']" @click="changeStyle('style_1')" />
|
||||
</div>
|
||||
|
||||
<div :class="['mr-[10px] rounded cursor-pointer w-[100px] border-[1px] border-solid', {'border-[var(--el-color-primary)]': diyStore.editComponent.systemUrl == 'style_2' && diyStore.editComponent.imgType == 'system'}]">
|
||||
<img src="@/app/assets/images/diy/notice/style_2.png" class="px-[10px] py-[5px]" @click="changeStyle('style_2')" />
|
||||
</div>
|
||||
<div @click.stop="diyStore.editComponent.imgType = 'diy'"
|
||||
:class="['mr-[10px] rounded cursor-pointer diy-upload-img border-[1px] border-solid', {'border-[var(--el-color-primary)]': (diyStore.editComponent.imageUrl && diyStore.editComponent.imgType == 'diy') }]">
|
||||
<upload-image v-model="diyStore.editComponent.imageUrl" :limit="1" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-form-item :label="t('noticeTitle')" v-show="diyStore.editComponent.noticeType == 'text'">
|
||||
<el-input v-model.trim="diyStore.editComponent.noticeTitle" :placeholder="t('titlePlaceholder')" clearable maxlength="20" show-word-limit />
|
||||
</el-form-item>
|
||||
|
||||
</el-form>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="edit-attr-item-wrap">
|
||||
<h3 class="mb-[10px]">{{ t('noticeText') }}</h3>
|
||||
<el-form label-width="90px" class="px-[10px]">
|
||||
|
||||
<el-form-item :label="t('noticeScrollWay')">
|
||||
<el-radio-group v-model="diyStore.editComponent.scrollWay">
|
||||
<el-radio label="upDown">{{ t('noticeUpDown') }}</el-radio>
|
||||
<el-radio label="horizontal">{{ t('noticeHorizontal') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('noticeShowType')">
|
||||
<el-radio-group v-model="diyStore.editComponent.showType">
|
||||
<el-radio label="popup">{{ t('noticeShowPopUp') }}</el-radio>
|
||||
<el-radio label="link">{{ t('noticeShowLink') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<p class="text-sm text-gray-400 mb-[10px]">{{ t('dragMouseAdjustOrder') }}</p>
|
||||
|
||||
<div ref="noticeBoxRef">
|
||||
<div v-for="(item,index) in diyStore.editComponent.list" :key="item.id" class="item-wrap p-[10px] pb-0 relative border border-dashed border-gray-300 mb-[16px]">
|
||||
|
||||
<el-form-item :label="t('noticeText')">
|
||||
<el-input v-model.trim="item.text" :placeholder="t('noticePlaceholderText')" clearable maxlength="40" show-word-limit />
|
||||
</el-form-item>
|
||||
|
||||
<div class="del absolute cursor-pointer z-[2] top-[-8px] right-[-8px]"
|
||||
v-show="diyStore.editComponent.list.length > 1"
|
||||
@click="diyStore.editComponent.list.splice(index,1)">
|
||||
<icon name="element CircleCloseFilled" color="#bbb" size="20px" />
|
||||
</div>
|
||||
|
||||
<el-form-item :label="t('link')" v-if="diyStore.editComponent.showType == 'link'">
|
||||
<diy-link v-model="item.link" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-button class="w-full" @click="addNotice">{{ t('addNotice') }}</el-button>
|
||||
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 样式 -->
|
||||
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
|
||||
<div class="edit-attr-item-wrap">
|
||||
<h3 class="mb-[10px]">{{ t('textSet') }}</h3>
|
||||
<el-form label-width="80px" class="px-[10px]">
|
||||
<el-form-item :label="t('textFontSize')">
|
||||
<el-slider v-model="diyStore.editComponent.fontSize" show-input size="small" class="ml-[10px] diy-nav-slider" :min="12" :max="20" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('textFontWeight')">
|
||||
<el-radio-group v-model="diyStore.editComponent.fontWeight">
|
||||
<el-radio :label="'normal'">{{ t('fontWeightNormal') }}</el-radio>
|
||||
<el-radio :label="'bold'">{{ t('fontWeightBold') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('textColor')">
|
||||
<el-color-picker v-model="diyStore.editComponent.textColor" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<!-- 组件样式 -->
|
||||
<slot name="style"></slot>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { t } from '@/lang'
|
||||
import useDiyStore from '@/stores/modules/diy'
|
||||
import { ref, watch, onMounted, nextTick } from 'vue'
|
||||
import { range } from 'lodash-es'
|
||||
import Sortable from 'sortablejs'
|
||||
|
||||
const diyStore = useDiyStore()
|
||||
diyStore.editComponent.ignore = [] // 忽略公共属性
|
||||
|
||||
// 组件验证
|
||||
diyStore.editComponent.verify = (index: number) => {
|
||||
const res = { code: true, message: '' }
|
||||
|
||||
if (diyStore.value[index].noticeType == 'text') {
|
||||
if (diyStore.value[index].noticeTitle == '') {
|
||||
res.code = false
|
||||
res.message = t('noticeTypeTextPlaceholder')
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
diyStore.value[index].list.forEach((item: any) => {
|
||||
if (item.text == '') {
|
||||
res.code = false
|
||||
res.message = t('noticePlaceholderText')
|
||||
return res
|
||||
}
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
diyStore.editComponent.list.forEach((item: any) => {
|
||||
if (!item.id) item.id = diyStore.generateRandom()
|
||||
})
|
||||
|
||||
const changeStyle = (value: any) => {
|
||||
diyStore.editComponent.systemUrl = value
|
||||
diyStore.editComponent.imgType = 'system'
|
||||
}
|
||||
|
||||
watch(
|
||||
() => diyStore.editComponent.imageUrl,
|
||||
(newValue, oldValue) => {
|
||||
if (newValue) {
|
||||
diyStore.editComponent.imgType = 'diy'
|
||||
} else {
|
||||
changeStyle('style_1')
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const addNotice = () => {
|
||||
diyStore.editComponent.list.push({
|
||||
id: diyStore.generateRandom(),
|
||||
text: '公告',
|
||||
link: { name: '' }
|
||||
})
|
||||
}
|
||||
|
||||
const noticeBoxRef = ref()
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
const sortable = Sortable.create(noticeBoxRef.value, {
|
||||
group: 'item-wrap',
|
||||
animation: 200,
|
||||
onEnd: event => {
|
||||
const temp = diyStore.editComponent.list[event.oldIndex!]
|
||||
diyStore.editComponent.list.splice(event.oldIndex!, 1)
|
||||
diyStore.editComponent.list.splice(event.newIndex!, 0, temp)
|
||||
sortable.sort(
|
||||
range(diyStore.editComponent.list.length).map(value => {
|
||||
return value.toString()
|
||||
})
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
defineExpose({})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.notice-content-wrap {
|
||||
.add-notice-width {
|
||||
width: calc(100% - 20px);
|
||||
}
|
||||
|
||||
.diy-upload-img {
|
||||
.image-wrap {
|
||||
width: 50px !important;
|
||||
height: 50px !important;
|
||||
margin-right: 0 !important;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.content-wrap {
|
||||
div {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style lang="scss" scoped></style>
|
||||
252
admin-vben/src/app/views/diy/components/edit-page.vue
Normal file
252
admin-vben/src/app/views/diy/components/edit-page.vue
Normal file
@@ -0,0 +1,252 @@
|
||||
<template>
|
||||
<!-- 内容 -->
|
||||
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
|
||||
|
||||
<div class="edit-attr-item-wrap">
|
||||
<h3 class="mb-[10px]">{{ t('pageContent') }}</h3>
|
||||
<el-form label-width="80px" class="px-[10px]" @submit.prevent>
|
||||
<el-form-item :label="t('diyPageTitle')">
|
||||
<el-input v-model.trim="diyStore.pageTitle" :placeholder="t('diyPageTitlePlaceholder')" clearable
|
||||
maxlength="16" show-word-limit />
|
||||
<div class="text-sm text-gray-400">{{ t('pageTitleTips') }}</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 表单布局 页面设置 -->
|
||||
<slot name="content"></slot>
|
||||
|
||||
<div class="edit-attr-item-wrap" v-if="diyStore.global.topStatusBar.control">
|
||||
<h3 class="mb-[10px]">{{ t('statusBarContent') }}</h3>
|
||||
<el-form label-width="80px" class="px-[10px]" @submit.prevent>
|
||||
<el-form-item :label="t('topStatusBarNav')" class="display-block">
|
||||
<el-switch v-model="diyStore.global.topStatusBar.isShow" />
|
||||
<div class="text-sm text-gray-400">{{ t('statusBarSwitchTips') }}</div>
|
||||
</el-form-item>
|
||||
<template v-if="diyStore.global.topStatusBar.isShow">
|
||||
<el-form-item :label="t('diyTitle')">
|
||||
<el-input v-model.trim="diyStore.global.title" :placeholder="t('diyTitlePlaceholder')" clearable maxlength="12" show-word-limit />
|
||||
<div class="text-sm text-gray-400">{{ t('titleTips') }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('selectStyle')" class="display-block">
|
||||
<div class="flex">
|
||||
<span class="text-primary flex-1 cursor-pointer" @click="showStyle">{{ diyStore.global.topStatusBar.styleName }}</span>
|
||||
<el-icon>
|
||||
<ArrowRight />
|
||||
</el-icon>
|
||||
</div>
|
||||
<div class="text-sm text-gray-400 leading-[1.5]">{{ t('styleShowTips') }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('topStatusBarImg')" v-if="['style-2','style-3'].indexOf(diyStore.global.topStatusBar.style) > -1">
|
||||
<upload-image v-model="diyStore.global.topStatusBar.imgUrl" :limit="1" />
|
||||
<div class="text-sm text-gray-400 mt-[10px]">{{ t('topStatusBarImgTips') }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('topStatusBarSearchName')" v-if="'style-3' == diyStore.global.topStatusBar.style">
|
||||
<el-input v-model.trim="diyStore.global.topStatusBar.inputPlaceholder" :placeholder="t('topStatusBarSearchNamePlaceholder')" clearable maxlength="12" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('textAlign')" v-show="diyStore.global.topStatusBar.style == 'style-1'">
|
||||
<el-radio-group v-model="diyStore.global.topStatusBar.textAlign">
|
||||
<el-radio :label="'left'">{{ t('textAlignLeft') }}</el-radio>
|
||||
<el-radio :label="'center'">{{ t('textAlignCenter') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('link')" v-if="['style-2','style-3'].indexOf(diyStore.global.topStatusBar.style) > -1">
|
||||
<diy-link v-model="diyStore.global.topStatusBar.link" />
|
||||
</el-form-item>
|
||||
</template>
|
||||
</el-form>
|
||||
</div>
|
||||
<div class="edit-attr-item-wrap" v-if="diyStore.global.bottomTabBar.control">
|
||||
<h3 class="mb-[10px]">{{ t('bottomNavContent') }}</h3>
|
||||
<el-form label-width="80px" class="px-[10px]">
|
||||
<el-form-item :label="t('tabbar')" class="display-block">
|
||||
<el-switch v-model="diyStore.global.bottomTabBar.isShow" />
|
||||
<div class="text-sm text-gray-400">{{ t('tabbarSwitchTips') }}</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<div class="edit-attr-item-wrap">
|
||||
<h3 class="mb-[10px]">{{ t('popWindowAds') }}</h3>
|
||||
<el-form label-width="80px" class="px-[10px]">
|
||||
<el-form-item :label="t('popAdsIsEnabled')" class="display-block">
|
||||
<el-switch v-model="diyStore.global.popWindow.show" />
|
||||
</el-form-item>
|
||||
<div v-show="diyStore.global.popWindow.show">
|
||||
<el-form-item :label="t('popAdsType')">
|
||||
<el-radio-group v-model="diyStore.global.popWindow.count">
|
||||
<el-radio label="once">{{ t('firstPop') }}</el-radio>
|
||||
<el-radio label="always">{{ t('everyTimePops') }}</el-radio>
|
||||
</el-radio-group>
|
||||
<div class="text-sm text-gray-400">{{ t('popWindowCountTips') }}</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('popAdsImage')">
|
||||
<upload-image v-model="diyStore.global.popWindow.imgUrl" :limit="1" @change="selectImg" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('popAdsLink')">
|
||||
<diy-link v-model="diyStore.global.popWindow.link" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="showDialog" :title="t('selectStyle')" width="800px">
|
||||
|
||||
<div class="flex flex-wrap">
|
||||
<div class="flex items-center justify-center overflow-hidden w-[32%] h-[100px] mr-[2%] mb-[15px] cursor-pointer border bg-gray-50"
|
||||
:class="{ 'border-primary': selectStyle == 'style-1' }" @click="selectStyle = 'style-1'">
|
||||
<img class="max-w-[100%] max-h-[100%]" src="@/app/assets/images/diy/head/nav_style1.jpg" />
|
||||
</div>
|
||||
<div class="flex items-center justify-center overflow-hidden w-[32%] h-[100px] mr-[2%] mb-[15px] cursor-pointer border bg-gray-50"
|
||||
:class="{ 'border-primary': selectStyle == 'style-2' }" @click="selectStyle = 'style-2'">
|
||||
<img class="max-w-[100%] max-h-[100%]" src="@/app/assets/images/diy/head/nav_style2.jpg" />
|
||||
</div>
|
||||
<div class="flex items-center justify-center overflow-hidden w-[32%] h-[100px] mb-[15px] cursor-pointer border bg-gray-50"
|
||||
:class="{ 'border-primary': selectStyle == 'style-3' }" @click="selectStyle = 'style-3'">
|
||||
<img class="max-w-[100%] max-h-[100%]" src="@/app/assets/images/diy/head/nav_style3.jpg" />
|
||||
</div>
|
||||
<div class="flex items-center justify-center overflow-hidden w-[32%] h-[100px] mr-[2%] cursor-pointer border bg-gray-50"
|
||||
:class="{ 'border-primary': selectStyle == 'style-4' }" @click="selectStyle = 'style-4'">
|
||||
<img class="max-w-[100%] max-h-[100%]" src="@/app/assets/images/diy/head/nav_style4.jpg" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button>
|
||||
<el-button type="primary" @click="changeStyle">{{ t('confirm') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
</el-dialog>
|
||||
</div>
|
||||
|
||||
<!-- 样式 -->
|
||||
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
|
||||
<div class="edit-attr-item-wrap">
|
||||
<h3 class="mb-[10px]">{{ t('pageStyle') }}</h3>
|
||||
<el-form label-width="115px" class="px-[10px]">
|
||||
<el-form-item :label="t('pageBgColor')">
|
||||
<el-color-picker v-model="diyStore.editComponent.pageStartBgColor" show-alpha :predefine="diyStore.predefineColors" />
|
||||
<icon name="iconfont iconmap-connect" size="20px" class="block !text-gray-400 mx-[5px]" />
|
||||
<el-color-picker v-model="diyStore.editComponent.pageEndBgColor" show-alpha :predefine="diyStore.predefineColors" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('bgGradientAngle')">
|
||||
<el-radio-group v-model="diyStore.editComponent.pageGradientAngle">
|
||||
<el-radio label="to bottom">{{ t('topToBottom') }}</el-radio>
|
||||
<el-radio label="to right">{{ t('leftToRight') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('bgHeightScale')">
|
||||
<el-slider v-model="diyStore.global.bgHeightScale" show-input size="small" class="ml-[10px] diy-nav-slider" />
|
||||
</el-form-item>
|
||||
<div class="text-sm text-gray-400 ml-[80px] mb-[10px]">{{ t('bgHeightScaleTip') }}</div>
|
||||
<el-form-item :label="t('bgUrl')">
|
||||
<upload-image v-model="diyStore.global.bgUrl" :limit="1" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<div class="edit-attr-item-wrap" v-if="diyStore.global.topStatusBar.control">
|
||||
<h3 class="mb-[10px]">{{ t('statusBarStyle') }}</h3>
|
||||
<el-form label-width="115px" class="px-[10px]">
|
||||
<el-form-item :label="t('topStatusBarBgColor')" class="display-block">
|
||||
<el-color-picker v-model="diyStore.global.topStatusBar.bgColor" show-alpha />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('rollTopStatusBarBgColor')" class="display-block">
|
||||
<el-color-picker v-model="diyStore.global.topStatusBar.rollBgColor" show-alpha />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('topStatusBarTextColor')" class="display-block">
|
||||
<el-color-picker v-model="diyStore.global.topStatusBar.textColor" show-alpha />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('rollTopStatusBarTextColor')" class="display-block">
|
||||
<el-color-picker v-model="diyStore.global.topStatusBar.rollTextColor" show-alpha />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<div class="edit-attr-item-wrap">
|
||||
<h3 class="mb-[10px]">{{ t('marginSet') }}</h3>
|
||||
<el-form label-width="115px" class="px-[10px]">
|
||||
<el-form-item :label="t('marginBoth')">
|
||||
<el-slider v-model="diyStore.global.template.margin.both" show-input size="small" @input="inputBoth" class="ml-[10px] diy-nav-slider" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { t } from '@/lang'
|
||||
import { watch, ref } from 'vue'
|
||||
import useDiyStore from '@/stores/modules/diy'
|
||||
import { img } from '@/utils/common'
|
||||
|
||||
const diyStore = useDiyStore()
|
||||
|
||||
watch(
|
||||
() => diyStore.global.bgUrl,
|
||||
(newValue, oldValue) => {
|
||||
// 设置图片宽高
|
||||
const image = new Image()
|
||||
image.src = img(diyStore.global.bgUrl)
|
||||
image.onload = async () => {
|
||||
diyStore.global.imgWidth = image.width
|
||||
diyStore.global.imgHeight = image.height
|
||||
}
|
||||
if (!diyStore.global.bgUrl) {
|
||||
diyStore.global.imgWidth = ''
|
||||
diyStore.global.imgHeight = ''
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// 改变页面的左右边距时,更新所有组件的数值
|
||||
const inputBoth = (value: any) => {
|
||||
diyStore.value.forEach((item, index) => {
|
||||
item.margin.both = value
|
||||
})
|
||||
}
|
||||
|
||||
watch(
|
||||
() => diyStore.global,
|
||||
(newValue, oldValue) => {
|
||||
selectStyle.value = newValue.topStatusBar.style
|
||||
}, { deep: true }
|
||||
)
|
||||
|
||||
const showDialog = ref(false)
|
||||
const showStyle = () => {
|
||||
showDialog.value = true
|
||||
}
|
||||
|
||||
const selectStyle = ref('style-1')
|
||||
const changeStyle = () => {
|
||||
switch (selectStyle.value) {
|
||||
case 'style-1':
|
||||
diyStore.global.topStatusBar.styleName = '风格1'
|
||||
break
|
||||
case 'style-2':
|
||||
diyStore.global.topStatusBar.styleName = '风格2'
|
||||
break
|
||||
case 'style-3':
|
||||
diyStore.global.topStatusBar.styleName = '风格3'
|
||||
break
|
||||
case 'style-4':
|
||||
diyStore.global.topStatusBar.styleName = '风格4'
|
||||
break
|
||||
}
|
||||
diyStore.global.topStatusBar.style = selectStyle.value
|
||||
showDialog.value = false
|
||||
}
|
||||
|
||||
const selectImg = (url: any) => {
|
||||
const image = new Image()
|
||||
image.src = img(url)
|
||||
image.onload = async () => {
|
||||
diyStore.global.popWindow.imgWidth = image.width
|
||||
diyStore.global.popWindow.imgHeight = image.height
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
158
admin-vben/src/app/views/diy/components/edit-picture-show.vue
Normal file
158
admin-vben/src/app/views/diy/components/edit-picture-show.vue
Normal file
@@ -0,0 +1,158 @@
|
||||
<template>
|
||||
<!-- 内容 -->
|
||||
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
|
||||
|
||||
<div class="edit-attr-item-wrap">
|
||||
<h3 class="mb-[10px]">{{ t('pictureShowBlockOne') }}</h3>
|
||||
<el-form label-width="80px" class="px-[10px]">
|
||||
<el-form-item :label="t('image')">
|
||||
<upload-image v-model="diyStore.editComponent.moduleOne.head.textImg" :limit="1" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('subTitle')">
|
||||
<el-input v-model.trim="diyStore.editComponent.moduleOne.head.subText"
|
||||
:placeholder="t('subTitlePlaceholder')" clearable maxlength="8" show-word-limit />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('subTitleTextColor')">
|
||||
<el-color-picker v-model="diyStore.editComponent.moduleOne.head.subTextColor" show-alpha />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('pictureShowBgColor')">
|
||||
<el-color-picker v-model="diyStore.editComponent.moduleOne.listFrame.startColor" show-alpha />
|
||||
<icon name="iconfont iconmap-connect" size="20px" class="block !text-gray-400 mx-[5px]" />
|
||||
<el-color-picker v-model="diyStore.editComponent.moduleOne.listFrame.endColor" show-alpha />
|
||||
</el-form-item>
|
||||
|
||||
<div v-for="(item,index) in diyStore.editComponent.moduleOne.list" :key="item.id"
|
||||
class="item-wrap p-[10px] pb-0 relative border border-dashed border-gray-300 mb-[16px]">
|
||||
<el-form-item :label="t('image')">
|
||||
<upload-image v-model="item.imageUrl" :limit="1" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('pictureShowBtnText')">
|
||||
<el-input v-model.trim="item.btnTitle.text" :placeholder="t('activeCubeTitlePlaceholder')" clearable maxlength="4" show-word-limit />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('pictureShowBtnColor')">
|
||||
<el-color-picker v-model="item.btnTitle.color" show-alpha />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('pictureShowBtnBgColor')">
|
||||
<el-color-picker v-model="item.btnTitle.startColor" show-alpha />
|
||||
<icon name="iconfont iconmap-connect" size="20px" class="block !text-gray-400 mx-[5px]" />
|
||||
<el-color-picker v-model="item.btnTitle.endColor" show-alpha />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('link')">
|
||||
<diy-link v-model="item.link" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="edit-attr-item-wrap">
|
||||
<h3 class="mb-[10px]">{{ t('pictureShowBlockTwo') }}</h3>
|
||||
<el-form label-width="90px" class="px-[10px]">
|
||||
<el-form-item :label="t('image')">
|
||||
<upload-image v-model="diyStore.editComponent.moduleTwo.head.textImg" :limit="1" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('subTitle')">
|
||||
<el-input v-model.trim="diyStore.editComponent.moduleTwo.head.subText" :placeholder="t('subTitlePlaceholder')" clearable maxlength="8" show-word-limit />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('subTitleTextColor')">
|
||||
<el-color-picker v-model="diyStore.editComponent.moduleTwo.head.subTextColor" show-alpha />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('pictureShowBgColor')">
|
||||
<el-color-picker v-model="diyStore.editComponent.moduleTwo.listFrame.startColor" show-alpha />
|
||||
<icon name="iconfont iconmap-connect" size="20px" class="block !text-gray-400 mx-[5px]" />
|
||||
<el-color-picker v-model="diyStore.editComponent.moduleTwo.listFrame.endColor" show-alpha />
|
||||
</el-form-item>
|
||||
|
||||
<div v-for="(item,index) in diyStore.editComponent.moduleTwo.list" :key="item.id"
|
||||
class="item-wrap p-[10px] pb-0 relative border border-dashed border-gray-300 mb-[16px]">
|
||||
<el-form-item :label="t('image')">
|
||||
<upload-image v-model="item.imageUrl" :limit="1" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('pictureShowBtnText')">
|
||||
<el-input v-model.trim="item.btnTitle.text" :placeholder="t('activeCubeTitlePlaceholder')" clearable maxlength="4" show-word-limit />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('pictureShowBtnColor')">
|
||||
<el-color-picker v-model="item.btnTitle.color" show-alpha />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('pictureShowBtnBgColor')">
|
||||
<el-color-picker v-model="item.btnTitle.startColor" show-alpha />
|
||||
<icon name="iconfont iconmap-connect" size="20px" class="block !text-gray-400 mx-[5px]" />
|
||||
<el-color-picker v-model="item.btnTitle.endColor" show-alpha />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('link')">
|
||||
<diy-link v-model="item.link" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 样式 -->
|
||||
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
|
||||
<div class="edit-attr-item-wrap">
|
||||
<h3 class="mb-[10px]">{{ t('pictureShowBlockStyle') }}</h3>
|
||||
<el-form label-width="90px" class="px-[10px]">
|
||||
<el-form-item :label="t('topRounded')">
|
||||
<el-slider v-model="diyStore.editComponent.moduleRounded.topRounded" show-input size="small" class="ml-[10px] diy-nav-slider" :max="100" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('bottomRounded')">
|
||||
<el-slider v-model="diyStore.editComponent.moduleRounded.bottomRounded" show-input size="small" class="ml-[10px] diy-nav-slider" :max="100" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 组件样式 -->
|
||||
<slot name="style"></slot>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { t } from '@/lang'
|
||||
import useDiyStore from '@/stores/modules/diy'
|
||||
import { ref, reactive } from 'vue'
|
||||
|
||||
const diyStore = useDiyStore()
|
||||
diyStore.editComponent.ignore = ['componentBgUrl'] // 忽略公共属性
|
||||
|
||||
// 组件验证
|
||||
diyStore.editComponent.verify = (index: number) => {
|
||||
const res = { code: true, message: '' }
|
||||
diyStore.value[index].moduleOne.list.forEach((item: any) => {
|
||||
if (item.imageUrl === '') {
|
||||
res.code = false
|
||||
res.message = t('imageUrlTip')
|
||||
return res
|
||||
}
|
||||
})
|
||||
|
||||
diyStore.value[index].moduleTwo.list.forEach((item: any) => {
|
||||
if (item.imageUrl === '') {
|
||||
res.code = false
|
||||
res.message = t('imageUrlTip')
|
||||
return res
|
||||
}
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
defineExpose({})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
42
admin-vben/src/app/views/diy/components/edit-rich-text.vue
Normal file
42
admin-vben/src/app/views/diy/components/edit-rich-text.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<!-- 内容 -->
|
||||
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
|
||||
<div class="edit-attr-item-wrap">
|
||||
<h3 class="mb-[10px]">{{ t('richTextContentSet') }}</h3>
|
||||
<editor v-model="diyStore.editComponent.html" :height="600" class="editor-width" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 样式 -->
|
||||
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
|
||||
|
||||
<!-- 组件样式 -->
|
||||
<slot name="style"></slot>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { t } from '@/lang'
|
||||
import useDiyStore from '@/stores/modules/diy'
|
||||
|
||||
const diyStore = useDiyStore()
|
||||
diyStore.editComponent.ignore = [] // 忽略公共属性
|
||||
|
||||
// 组件验证
|
||||
diyStore.editComponent.verify = (index: number) => {
|
||||
const res = { code: true, message: '' }
|
||||
|
||||
if (diyStore.value[index].html == '<p><br></p>') {
|
||||
res.code = false
|
||||
res.message = t('richTextPlaceholder')
|
||||
return res
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
defineExpose({})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
644
admin-vben/src/app/views/diy/components/edit-rubik-cube.vue
Normal file
644
admin-vben/src/app/views/diy/components/edit-rubik-cube.vue
Normal file
@@ -0,0 +1,644 @@
|
||||
<template>
|
||||
<!-- 内容 -->
|
||||
<div class="content-wrap rubik-cube" v-show="diyStore.editTab == 'content'">
|
||||
|
||||
<div class="edit-attr-item-wrap">
|
||||
<h3 class="mb-[10px]">{{ t('selectStyle') }}</h3>
|
||||
<el-form label-width="80px" class="px-[10px]">
|
||||
<el-form-item :label="t('template')">
|
||||
<span>{{ selectTemplate.name }}</span>
|
||||
</el-form-item>
|
||||
<ul class="selected-template-list">
|
||||
<li v-for="(item,i) in templateList" :key="i"
|
||||
:class="[(item.className == diyStore.editComponent.mode) ? 'selected' : '' ]"
|
||||
@click="changeTemplateList(i)">
|
||||
<icon :name="'iconfont ' + item.src" size="16px" />
|
||||
</li>
|
||||
</ul>
|
||||
</el-form>
|
||||
</div>
|
||||
<div class="edit-attr-item-wrap">
|
||||
<h3 class="mb-[10px]">{{ t('rubikCubeLayout') }}</h3>
|
||||
<el-form label-width="80px" class="px-[10px]">
|
||||
|
||||
<ul class="layout">
|
||||
<li v-for="(li,i) in selectTemplate.dimensionScale" :key="i" :class="[selectTemplate.className]">
|
||||
<div class="have-preview-image" v-show="diyStore.editComponent.list[i].imageUrl && diyStore.editComponent.list[i].imageUrl != 'static/resource/images/diy/figure.png'">
|
||||
<img :src="img(diyStore.editComponent.list[i].imageUrl)" />
|
||||
</div>
|
||||
<div class="empty" :class="[selectTemplate.className]" v-show="!diyStore.editComponent.list[i].imageUrl || diyStore.editComponent.list[i].imageUrl == 'static/resource/images/diy/figure.png'">
|
||||
<p>{{ li.name }}</p>
|
||||
<p>{{ li.desc }}</p>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div v-for="(item) in diyStore.editComponent.list" :key="item.id" class="item-wrap p-[10px] pb-0 relative border border-dashed border-gray-300 mb-[16px]">
|
||||
<el-form-item :label="t('image')">
|
||||
<upload-image v-model="item.imageUrl" :limit="1" @change="selectImg" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('link')">
|
||||
<diy-link v-model="item.link" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
</el-form>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 样式 -->
|
||||
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
|
||||
|
||||
<div class="edit-attr-item-wrap">
|
||||
<h3 class="mb-[10px]">{{ t('rubikCubeStyle') }}</h3>
|
||||
<el-form label-width="80px" class="px-[10px]">
|
||||
<el-form-item :label="t('imageGap')">
|
||||
<el-slider v-model="diyStore.editComponent.imageGap" show-input size="small" class="ml-[10px] diy-nav-slider" :max="30" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('topRounded')">
|
||||
<el-slider v-model="diyStore.editComponent.topElementRounded" show-input size="small" class="ml-[10px] diy-nav-slider" :max="50" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('bottomRounded')">
|
||||
<el-slider v-model="diyStore.editComponent.bottomElementRounded" show-input size="small" class="ml-[10px] diy-nav-slider" :max="50" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 组件样式 -->
|
||||
<slot name="style"></slot>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import useDiyStore from '@/stores/modules/diy'
|
||||
import { img } from '@/utils/common'
|
||||
|
||||
const diyStore = useDiyStore()
|
||||
diyStore.editComponent.ignore = [] // 忽略公共属性
|
||||
|
||||
// 组件验证
|
||||
diyStore.editComponent.verify = (index: number) => {
|
||||
const res = { code: true, message: '' }
|
||||
diyStore.value[index].list.forEach((item: any) => {
|
||||
if (item.imageUrl === '') {
|
||||
res.code = false
|
||||
res.message = t('imageUrlTip')
|
||||
return res
|
||||
}
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
const templateList = ref([
|
||||
{
|
||||
name: '1行2个',
|
||||
src: 'iconyihang2gepc1',
|
||||
className: 'row1-of2',
|
||||
dimensionScale: [
|
||||
{
|
||||
desc: '宽度50%',
|
||||
size: '200px * 200px',
|
||||
name: '图一'
|
||||
},
|
||||
{
|
||||
desc: '宽度50%',
|
||||
size: '200px * 200px',
|
||||
name: '图二'
|
||||
}
|
||||
],
|
||||
descAux: '选定布局区域,在下方添加图片,建议添加尺寸一致的图片,宽度最小建议为:200px'
|
||||
},
|
||||
{
|
||||
name: '1行3个',
|
||||
src: 'iconyihang3gepc',
|
||||
className: 'row1-of3',
|
||||
dimensionScale: [
|
||||
{
|
||||
desc: '宽度33.33%',
|
||||
size: '200px * 200px',
|
||||
name: '图一'
|
||||
},
|
||||
{
|
||||
desc: '宽度33.33%',
|
||||
size: '200px * 200px',
|
||||
name: '图二'
|
||||
},
|
||||
{
|
||||
desc: '宽度33.33%',
|
||||
size: '200px * 200px',
|
||||
name: '图三'
|
||||
}
|
||||
],
|
||||
descAux: '选定布局区域,在下方添加图片,建议添加尺寸一致的图片,宽度最小建议为:130px'
|
||||
},
|
||||
{
|
||||
name: '1行4个',
|
||||
src: 'iconyihang4gepc',
|
||||
className: 'row1-of4',
|
||||
dimensionScale: [
|
||||
{
|
||||
desc: '宽度25%',
|
||||
size: '200px * 200px',
|
||||
name: '图一'
|
||||
},
|
||||
{
|
||||
desc: '宽度25%',
|
||||
size: '200px * 200px',
|
||||
name: '图二'
|
||||
},
|
||||
{
|
||||
desc: '宽度25%',
|
||||
size: '200px * 200px',
|
||||
name: '图三'
|
||||
},
|
||||
{
|
||||
desc: '宽度25%',
|
||||
size: '200px * 200px',
|
||||
name: '图四'
|
||||
}
|
||||
],
|
||||
descAux: '选定布局区域,在下方添加图片,建议添加尺寸一致的图片,宽度最小建议为:100px'
|
||||
},
|
||||
{
|
||||
name: '2左2右',
|
||||
src: 'iconliangzuoliangyoupc',
|
||||
className: 'row2-lt-of2-rt',
|
||||
dimensionScale: [
|
||||
{
|
||||
desc: '宽度50%',
|
||||
size: '200px * 200px',
|
||||
name: '图一'
|
||||
},
|
||||
{
|
||||
desc: '宽度50%',
|
||||
size: '200px * 200px',
|
||||
name: '图二'
|
||||
},
|
||||
{
|
||||
desc: '宽度50%',
|
||||
size: '200px * 200px',
|
||||
name: '图三'
|
||||
},
|
||||
{
|
||||
desc: '宽度50%',
|
||||
size: '200px * 200px',
|
||||
name: '图四'
|
||||
}
|
||||
],
|
||||
descAux: '选定布局区域,在下方添加图片,建议添加尺寸一致的图片,宽度最小建议为:200px'
|
||||
},
|
||||
{
|
||||
name: '1左2右',
|
||||
src: 'iconyizuoliangyoupc',
|
||||
className: 'row1-lt-of2-rt',
|
||||
dimensionScale: [
|
||||
{
|
||||
desc: '宽度50% * 高度100%',
|
||||
size: '200px * 400px',
|
||||
name: '图一'
|
||||
},
|
||||
{
|
||||
desc: '宽度50% * 高度50%',
|
||||
size: '200px * 200px',
|
||||
name: '图二'
|
||||
},
|
||||
{
|
||||
desc: '宽度50% * 高度50%',
|
||||
size: '200px * 200px',
|
||||
name: '图三'
|
||||
}
|
||||
],
|
||||
descAux: '选定布局区域,在下方添加图片,宽度最小建议为:200px,右侧两张图片高度一致,左侧图片高度为右侧两张图片高度之和(例:左侧图片尺寸:200px * 300px,右侧两张图片尺寸:200px * 150px)'
|
||||
},
|
||||
{
|
||||
name: '1上2下',
|
||||
src: 'iconyishangliangxiapc',
|
||||
className: 'row1-tp-of2-bm',
|
||||
dimensionScale: [
|
||||
{
|
||||
desc: '宽度100% * 高度50%',
|
||||
size: '400px * 200px',
|
||||
name: '图一'
|
||||
},
|
||||
{
|
||||
desc: '宽度50% * 高度50%',
|
||||
size: '200px * 200px',
|
||||
name: '图二'
|
||||
},
|
||||
{
|
||||
desc: '宽度50% * 高度50%',
|
||||
size: '200px * 200px',
|
||||
name: '图三'
|
||||
}
|
||||
],
|
||||
descAux: '选定布局区域,在下方添加图片,上方一张图片的宽度为下方两张图片宽度之和,下放两张图片尺寸一致,高度可根据实际需求自行确定(例:上方图片尺寸:400px * 150px,下方两张图片尺寸:200px * 150px)'
|
||||
},
|
||||
{
|
||||
name: '1左3右',
|
||||
src: 'iconyizuosanyoupc',
|
||||
className: 'row1-lt-of1-tp-of2-bm',
|
||||
dimensionScale: [
|
||||
{
|
||||
desc: '宽度50% * 高度100%',
|
||||
size: '200px * 400px',
|
||||
name: '图一'
|
||||
},
|
||||
{
|
||||
desc: '宽度50% * 高度50%',
|
||||
size: '200px * 200px',
|
||||
name: '图二'
|
||||
},
|
||||
{
|
||||
desc: '宽度25% * 高度50%',
|
||||
size: '100px * 200px',
|
||||
name: '图三'
|
||||
},
|
||||
{
|
||||
desc: '宽度25% * 高度50%',
|
||||
size: '100px * 200px',
|
||||
name: '图四'
|
||||
}
|
||||
],
|
||||
descAux: '选定布局区域,在下方添加图片,左右两侧内容宽高相同,右侧上下区域高度各占50%,右侧内容下半部分两张图片的宽度相同,各占右侧内容宽度的50%(例:左侧图片尺寸:200px * 400px,右侧上半部分图片尺寸:200px * 200px,右侧下半部分两张图片尺寸:100px * 200px)'
|
||||
}
|
||||
])
|
||||
|
||||
const selectTemplate = computed(() => {
|
||||
let data
|
||||
templateList.value.forEach((item) => {
|
||||
if (item.className == diyStore.editComponent.mode) {
|
||||
data = item
|
||||
}
|
||||
})
|
||||
return data
|
||||
})
|
||||
|
||||
const changeTemplateList = (v: number) => {
|
||||
for (let i = 0; i < templateList.value.length; i++) {
|
||||
if (i == v) {
|
||||
diyStore.editComponent.mode = templateList.value[i].className
|
||||
const count = templateList.value[i].dimensionScale.length
|
||||
|
||||
// 重置当前编辑的图片集合
|
||||
|
||||
// 数量不够,进行添加
|
||||
if (count > diyStore.editComponent.list.length) {
|
||||
for (let j = 0; j < count; j++) {
|
||||
if ((j + 1) > diyStore.editComponent.list.length) {
|
||||
diyStore.editComponent.list.push({
|
||||
imageUrl: '',
|
||||
imgWidth: 0,
|
||||
imgHeight: 0,
|
||||
link: { name: '' }
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 数量不相同时,并且数量超出,减去
|
||||
if (count != diyStore.editComponent.list.length) {
|
||||
for (let j = 0; j < diyStore.editComponent.list.length; j++) {
|
||||
if ((j + 1) > count) {
|
||||
diyStore.editComponent.list.splice(j, 1)
|
||||
j = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const selectImg = (url: string) => {
|
||||
handleHeight(true)
|
||||
}
|
||||
|
||||
// 处理高度
|
||||
const handleHeight = (isCalcHeight: boolean = false) => {
|
||||
diyStore.editComponent.list.forEach((item: any, index: number) => {
|
||||
const image = new Image()
|
||||
image.src = img(item.imageUrl)
|
||||
image.onload = async() => {
|
||||
item.imgWidth = image.width
|
||||
item.imgHeight = image.height
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
watch(() => diyStore.editComponent.list, () => {
|
||||
handleHeight(true)
|
||||
}, { deep: true })
|
||||
|
||||
defineExpose({})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.rubik-cube .selected-template-list {
|
||||
/*padding-left: 15px;*/
|
||||
margin-bottom: 20px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
li {
|
||||
color: #909399;
|
||||
width: 46px;
|
||||
height: 32px;
|
||||
text-align: center;
|
||||
line-height: 29px;
|
||||
border: 1px solid #e5e5e5;
|
||||
cursor: pointer;
|
||||
background: #ffffff;
|
||||
box-sizing: border-box;
|
||||
border-right: 1px transparent solid;
|
||||
|
||||
&:last-child {
|
||||
border-right: 1px solid #e5e5e5;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
color: var(--el-color-primary);
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
img {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
div {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.layout {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
margin-bottom: 15px;
|
||||
|
||||
li {
|
||||
float: left;
|
||||
color: #909399;
|
||||
border: 1px solid #e5e5e5;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
position: relative;
|
||||
|
||||
div.empty {
|
||||
left: 0;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
margin-top: -26px;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
line-height: 26px;
|
||||
}
|
||||
}
|
||||
|
||||
div.have-preview-image {
|
||||
box-sizing: border-box;
|
||||
|
||||
img {
|
||||
display: inline-block;
|
||||
width: auto;
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
// 1行2个
|
||||
&.row1-of2 {
|
||||
width: 49.2%;
|
||||
height: 160px;
|
||||
border-right: 1px transparent solid;
|
||||
|
||||
&:last-child {
|
||||
border-right: 1px solid #e5e5e5;
|
||||
}
|
||||
|
||||
div.empty {
|
||||
}
|
||||
|
||||
div.have-preview-image {
|
||||
text-align: center;
|
||||
height: 100%;
|
||||
line-height: 160px;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 1行3个
|
||||
&.row1-of3 {
|
||||
width: 32.5%;
|
||||
height: 100px;
|
||||
border-right: 1px transparent solid;
|
||||
|
||||
&:last-child {
|
||||
border-right: 1px solid #bdf;
|
||||
}
|
||||
|
||||
div.empty {
|
||||
}
|
||||
|
||||
div.have-preview-image {
|
||||
text-align: center;
|
||||
height: 100%;
|
||||
line-height: 100px;
|
||||
background: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
// 1行4个
|
||||
&.row1-of4 {
|
||||
width: 24.2%;
|
||||
height: 80px;
|
||||
border-right: 1px transparent solid;
|
||||
|
||||
&:last-child {
|
||||
border-right: 1px solid #bdf;
|
||||
}
|
||||
|
||||
div.empty {
|
||||
}
|
||||
|
||||
div.have-preview-image {
|
||||
text-align: center;
|
||||
height: 100%;
|
||||
line-height: 80px;
|
||||
background: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
// 2左2右
|
||||
&.row2-lt-of2-rt {
|
||||
width: 49.2%;
|
||||
height: 160px;
|
||||
|
||||
&:nth-child(1) {
|
||||
border-right: 1px transparent solid;
|
||||
border-bottom: 1px transparent solid;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
border-bottom: 1px transparent solid;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
border-right: 1px transparent solid;
|
||||
clear: both;
|
||||
}
|
||||
|
||||
div.empty {
|
||||
}
|
||||
|
||||
div.have-preview-image {
|
||||
text-align: center;
|
||||
height: 100%;
|
||||
line-height: 160px;
|
||||
background: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
// 1左2右
|
||||
&.row1-lt-of2-rt {
|
||||
width: 49.2%;
|
||||
font-size: 12px;
|
||||
|
||||
&:nth-child(1) {
|
||||
height: 322px;
|
||||
border-right: 1px transparent solid;
|
||||
|
||||
div.have-preview-image {
|
||||
text-align: center;
|
||||
height: 100%;
|
||||
line-height: 322px;
|
||||
background: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
height: 160px;
|
||||
border-bottom: 1px transparent solid;
|
||||
|
||||
div.have-preview-image {
|
||||
text-align: center;
|
||||
height: 100%;
|
||||
line-height: 160px;
|
||||
background: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
height: 160px;
|
||||
|
||||
div.have-preview-image {
|
||||
text-align: center;
|
||||
height: 100%;
|
||||
line-height: 160px;
|
||||
background: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
div.empty {
|
||||
}
|
||||
}
|
||||
|
||||
// 1上2下
|
||||
&.row1-tp-of2-bm {
|
||||
height: 160px;
|
||||
|
||||
&:nth-child(1) {
|
||||
width: 99.4%;
|
||||
border-bottom: 1px transparent solid;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
width: 49.2%;
|
||||
border-right: 1px transparent solid;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
width: 49.2%;
|
||||
}
|
||||
|
||||
div.empty {
|
||||
}
|
||||
|
||||
div.have-preview-image {
|
||||
text-align: center;
|
||||
height: 100%;
|
||||
line-height: 160px;
|
||||
background: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
// 1左3右
|
||||
&.row1-lt-of1-tp-of2-bm {
|
||||
&:nth-child(1) {
|
||||
height: 320px;
|
||||
width: 49.2%;
|
||||
border-right: 1px transparent solid;
|
||||
|
||||
div.have-preview-image {
|
||||
text-align: center;
|
||||
height: 100%;
|
||||
line-height: 320px;
|
||||
background: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
height: 160px;
|
||||
width: 49.2%;
|
||||
border-bottom: 1px transparent solid;
|
||||
|
||||
div.have-preview-image {
|
||||
text-align: center;
|
||||
height: 100%;
|
||||
line-height: 160px;
|
||||
background: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
height: 160px;
|
||||
width: 24.2%;
|
||||
border-right: 1px transparent solid;
|
||||
|
||||
div.have-preview-image {
|
||||
text-align: center;
|
||||
height: 100%;
|
||||
line-height: 160px;
|
||||
background: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
&:nth-child(4) {
|
||||
height: 160px;
|
||||
width: 24.2%;
|
||||
|
||||
div.have-preview-image {
|
||||
text-align: center;
|
||||
height: 100%;
|
||||
line-height: 160px;
|
||||
background: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
div.empty {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
</style>
|
||||
156
admin-vben/src/app/views/diy/components/edit-text.vue
Normal file
156
admin-vben/src/app/views/diy/components/edit-text.vue
Normal file
@@ -0,0 +1,156 @@
|
||||
<template>
|
||||
<!-- 内容 -->
|
||||
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
|
||||
<div class="edit-attr-item-wrap">
|
||||
<h3 class="mb-[10px]">{{ t('styleSet') }}</h3>
|
||||
<el-form label-width="80px" class="px-[10px]">
|
||||
<el-form-item :label="t('selectStyle')" class="flex">
|
||||
<span class="text-primary flex-1 cursor-pointer"
|
||||
@click="showStyle">{{ diyStore.editComponent.styleName }}</span>
|
||||
<el-icon @click="showStyle" class="cursor-pointer">
|
||||
<ArrowRight />
|
||||
</el-icon>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<div class="edit-attr-item-wrap">
|
||||
<h3 class="mb-[10px]">{{ t('titleContent') }}</h3>
|
||||
<el-form label-width="80px" class="px-[10px]">
|
||||
<el-form-item :label="t('title')">
|
||||
<el-input v-model.trim="diyStore.editComponent.text" :placeholder="t('titlePlaceholder')" clearable maxlength="15" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('link')">
|
||||
<diy-link v-model="diyStore.editComponent.link" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('textAlign')" v-show="diyStore.editComponent.style == 'style-1'">
|
||||
<el-radio-group v-model="diyStore.editComponent.textAlign">
|
||||
<el-radio :label="'left'">{{ t('textAlignLeft') }}</el-radio>
|
||||
<el-radio :label="'center'">{{ t('textAlignCenter') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="edit-attr-item-wrap" v-show="diyStore.editComponent.subTitle.control">
|
||||
<h3 class="mb-[10px]">{{ t('subTitleContent') }}</h3>
|
||||
<el-form label-width="80px" class="px-[10px]">
|
||||
<el-form-item :label="t('subTitle')">
|
||||
<el-input v-model.trim="diyStore.editComponent.subTitle.text" :placeholder="t('subTitlePlaceholder')" clearable maxlength="30" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('textFontSize')">
|
||||
<el-slider v-model="diyStore.editComponent.subTitle.fontSize" show-input size="small" class="ml-[10px] diy-nav-slider" :min="12" :max="16" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('textColor')">
|
||||
<el-color-picker v-model="diyStore.editComponent.subTitle.color" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="edit-attr-item-wrap" v-show="diyStore.editComponent.more.control">
|
||||
<h3 class="mb-[10px]">{{ t('moreContent') }}</h3>
|
||||
<el-form label-width="80px" class="px-[10px]">
|
||||
<el-form-item :label="t('more')">
|
||||
<el-input v-model.trim="diyStore.editComponent.more.text" :placeholder="t('morePlaceholder')" clearable maxlength="8" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('link')">
|
||||
<diy-link v-model="diyStore.editComponent.more.link" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('moreIsShow')">
|
||||
<el-switch v-model="diyStore.editComponent.more.isShow" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('textColor')">
|
||||
<el-color-picker v-model="diyStore.editComponent.more.color" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="showDialog" :title="t('selectStyle')" width="620px">
|
||||
|
||||
<div class="flex flex-wrap">
|
||||
<div
|
||||
class="flex items-center justify-center overflow-hidden w-[280px] h-[100px] mr-[12px] cursor-pointer border bg-gray-50"
|
||||
:class="{ 'border-primary': selectStyle == 'style-1' }" @click="selectStyle = 'style-1'">
|
||||
<img class="max-w-[280px] max-h-[220px]" src="@/app/assets/images/diy/text/style1.png" />
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center justify-center overflow-hidden w-[280px] h-[100px] cursor-pointer border bg-gray-50"
|
||||
:class="{ 'border-primary': selectStyle == 'style-2' }" @click="selectStyle = 'style-2'">
|
||||
<img class="max-w-[280px] max-h-[220px]" src="@/app/assets/images/diy/text/style2.png" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button>
|
||||
<el-button type="primary" @click="changeStyle">{{ t('confirm') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
</el-dialog>
|
||||
</div>
|
||||
|
||||
<!-- 样式 -->
|
||||
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
|
||||
|
||||
<div class="edit-attr-item-wrap">
|
||||
<h3 class="mb-[10px]">{{ t('titleStyle') }}</h3>
|
||||
<el-form label-width="80px" class="px-[10px]">
|
||||
<el-form-item :label="t('textFontSize')">
|
||||
<el-slider v-model="diyStore.editComponent.fontSize" show-input size="small" class="ml-[10px] diy-nav-slider" :min="12" :max="30" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('textFontWeight')">
|
||||
<el-radio-group v-model="diyStore.editComponent.fontWeight">
|
||||
<el-radio :label="'normal'">{{ t('fontWeightNormal') }}</el-radio>
|
||||
<el-radio :label="'bold'">{{ t('fontWeightBold') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('textColor')">
|
||||
<el-color-picker v-model="diyStore.editComponent.textColor" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 组件样式 -->
|
||||
<slot name="style"></slot>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { t } from '@/lang'
|
||||
import useDiyStore from '@/stores/modules/diy'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const diyStore = useDiyStore()
|
||||
diyStore.editComponent.ignore = [] // 忽略公共属性
|
||||
|
||||
const showDialog = ref(false)
|
||||
|
||||
const showStyle = () => {
|
||||
showDialog.value = true
|
||||
}
|
||||
|
||||
const selectStyle = ref(diyStore.editComponent.style)
|
||||
|
||||
const changeStyle = () => {
|
||||
switch (selectStyle.value) {
|
||||
case 'style-1':
|
||||
diyStore.editComponent.subTitle.control = false
|
||||
diyStore.editComponent.more.control = false
|
||||
diyStore.editComponent.styleName = '风格1'
|
||||
break
|
||||
case 'style-2':
|
||||
diyStore.editComponent.subTitle.control = true
|
||||
diyStore.editComponent.more.control = true
|
||||
diyStore.editComponent.styleName = '风格2'
|
||||
break
|
||||
}
|
||||
diyStore.editComponent.style = selectStyle.value
|
||||
showDialog.value = false
|
||||
}
|
||||
|
||||
defineExpose({})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
267
admin-vben/src/app/views/diy/components/edit-theme.vue
Normal file
267
admin-vben/src/app/views/diy/components/edit-theme.vue
Normal file
@@ -0,0 +1,267 @@
|
||||
<template>
|
||||
<el-dialog v-model="dialogThemeVisible" title="编辑色调" width="850px" align-center destroy-on-close="true">
|
||||
<el-form :model="openData" label-width="150px" :rules="formRules">
|
||||
<el-form-item label="色调名称" prop="title">
|
||||
<el-input v-model="openData.title" placeholder="请输入色调名称" maxlength="15" class="!w-[250px]" :disabled="openData.id != ''" @keydown.enter.native.prevent />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<el-form :model="formData" label-width="150px" class="h-[640px] overflow-auto" ref="formRef" @submit.prevent>
|
||||
|
||||
<el-form-item :label="item.title" v-for="(item, index) in formData" :key="index" :prop="`${index}.value`"
|
||||
:rules="[{ required: true, message: `请选择${item.title}色调`, trigger: 'blur' }]">
|
||||
<el-color-picker v-model="item.value" show-alpha :predefine="diyStore.predefineColors" @change="colorPickerChange($event, item)" />
|
||||
<div class="form-tip">{{ item.tip }}</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="item.title" v-for="(item, index) in openData.new_theme" :key="index">
|
||||
<div class="flex items-center">
|
||||
<el-color-picker v-model="item.value" show-alpha :predefine="diyStore.predefineColors" />
|
||||
<span class="text-primary cursor-pointer text-[14px] ml-[20px]" @click="editThemeFn(item)">编辑</span>
|
||||
<span class="text-primary cursor-pointer text-[14px] ml-[8px]" @click="deleteThemeFn(item)">删除</span>
|
||||
</div>
|
||||
<div class="form-tip">{{ item.tip }}</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<div class="flex items-center text-primary cursor-pointer text-[14px]" @click="addThemeFn">
|
||||
<span class="mr-[3px]">+</span>
|
||||
<span>新增颜色</span>
|
||||
</div>
|
||||
<div class="form-tip">新增颜色key值不能与当前的存在的key值重复</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<add-theme-component ref="addThemeRef" @confirm="addThemeConfirm" />
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="dialogThemeVisible = false">取消</el-button>
|
||||
<el-button type="primary" plain @click="resetConfirmFn()">重置</el-button>
|
||||
<el-button type="primary" @click="confirmFn(formRef)">保存</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import addThemeComponent from './add-theme.vue'
|
||||
import useDiyStore from '@/stores/modules/diy'
|
||||
import { addTheme, editTheme } from '@/app/api/diy'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
|
||||
const diyStore = useDiyStore()
|
||||
|
||||
const dialogThemeVisible = ref(false)
|
||||
const addThemeRef = ref()
|
||||
const openData: Record<string, any> = reactive({ // 用于接收弹窗打开时的参数
|
||||
title: '',
|
||||
id: '',
|
||||
theme: {},
|
||||
default_theme: {},
|
||||
new_theme: [],
|
||||
key: '',
|
||||
theme_field: [] // 展示数据源
|
||||
})
|
||||
|
||||
const emit = defineEmits(['confirm'])
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = computed(() => {
|
||||
return {
|
||||
title: [
|
||||
{ required: true, message: '请输入色调名称', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
const formData = ref()
|
||||
|
||||
const open = (res: any) => { // 参数: title=>色调名称,key=>区分系统还是应用的标识,default_theme=>色调颜色的默认值,用于重置,theme=>当前色调颜色值
|
||||
Object.keys(openData).forEach((key: string) => {
|
||||
openData[key] = res[key] != undefined ? cloneDeep(res[key]) : ''
|
||||
})
|
||||
|
||||
// 恢复默认值
|
||||
formData.value = [...cloneDeep(openData.theme_field)]
|
||||
|
||||
// 渲染值
|
||||
formData.value.forEach((item, index) => {
|
||||
item.value = res.theme[item.label] ? res.theme[item.label] : item.value
|
||||
})
|
||||
dialogThemeVisible.value = true
|
||||
}
|
||||
|
||||
// 新增颜色
|
||||
const addThemeFn = () => {
|
||||
// 传入keyArr, 避免添加重复key
|
||||
const keyArr: string[] = []
|
||||
formData.value.forEach((item, index) => {
|
||||
keyArr.push(item.label)
|
||||
})
|
||||
const obj = {
|
||||
key: keyArr
|
||||
}
|
||||
|
||||
addThemeRef.value.open(obj)
|
||||
}
|
||||
|
||||
// 编辑颜色
|
||||
const editThemeFn = (res: any) => {
|
||||
// 传入keyArr, 避免添加重复key
|
||||
const keyArr: string[] = []
|
||||
formData.value.forEach((item, index) => {
|
||||
keyArr.push(item.label)
|
||||
})
|
||||
|
||||
const obj = {
|
||||
key: keyArr,
|
||||
data: res
|
||||
}
|
||||
addThemeRef.value.open(obj)
|
||||
}
|
||||
// 删除颜色
|
||||
const deleteThemeFn = (res: any) => {
|
||||
let indent = -1
|
||||
for (let i = 0; i < openData.new_theme.length; i++) {
|
||||
if (openData.new_theme[i].label == res.label) {
|
||||
indent = i
|
||||
}
|
||||
}
|
||||
if (indent > -1) {
|
||||
openData.new_theme.splice(indent, 1)
|
||||
}
|
||||
}
|
||||
|
||||
// 添加颜色组件回调
|
||||
const addThemeConfirm = (res: any) => {
|
||||
for (let i = 0; i < openData.new_theme.length; i++) {
|
||||
if (openData.new_theme[i].label == res.label) {
|
||||
openData.new_theme[i] = res
|
||||
return
|
||||
}
|
||||
}
|
||||
openData.new_theme.push(res)
|
||||
}
|
||||
|
||||
// 重置当前配色
|
||||
const resetConfirmFn = () => {
|
||||
if (openData.default_theme && Object.keys(openData.default_theme).length) {
|
||||
formData.value.forEach((item, index) => {
|
||||
item.value = openData.default_theme[item.label]
|
||||
})
|
||||
} else {
|
||||
formData.value = cloneDeep(openData.theme_field)
|
||||
// 新增时,点击充值按钮,清空title
|
||||
if (!openData.id) {
|
||||
openData.title = ''
|
||||
}
|
||||
}
|
||||
|
||||
openData.new_theme = []
|
||||
|
||||
ElMessage({
|
||||
message: '重置成功',
|
||||
type: 'success'
|
||||
})
|
||||
}
|
||||
|
||||
let confirmRepeat = false
|
||||
const confirmFn = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
await formEl.validate(async (valid) => {
|
||||
if (valid) {
|
||||
const params = {
|
||||
title: '',
|
||||
id: '',
|
||||
theme: {},
|
||||
default_theme: {},
|
||||
new_theme: [],
|
||||
addon: ''
|
||||
}
|
||||
|
||||
params.title = openData.title
|
||||
|
||||
formData.value.forEach((item, index) => {
|
||||
params.theme[item.label] = item.value
|
||||
})
|
||||
openData.new_theme.forEach((item, index) => {
|
||||
params.theme[item.label] = item.value
|
||||
})
|
||||
|
||||
params.new_theme = openData.new_theme || []
|
||||
let api = null
|
||||
if (openData.id) {
|
||||
api = editTheme
|
||||
params.id = openData.id
|
||||
} else {
|
||||
api = addTheme
|
||||
}
|
||||
params.addon = openData.key
|
||||
|
||||
// 新增时,默认主题为当前主题
|
||||
if (openData.id == '') {
|
||||
const defaultTheme = {}
|
||||
openData.theme_field.forEach((item, index) => {
|
||||
defaultTheme[item.label] = item.value
|
||||
})
|
||||
params.default_theme = cloneDeep(defaultTheme)
|
||||
} else {
|
||||
params.default_theme = cloneDeep(openData.default_theme)
|
||||
}
|
||||
if (confirmRepeat) return false
|
||||
confirmRepeat = true
|
||||
|
||||
api(params).then((res: any) => {
|
||||
confirmRepeat = false
|
||||
dialogThemeVisible.value = false
|
||||
emit('confirm', params)
|
||||
}).catch(() => {
|
||||
confirmRepeat = false
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const applyOpacity = (color, opacity) => {
|
||||
// 解析十六进制或 RGBA 格式
|
||||
const hexRegex = /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/
|
||||
const rgbaRegex = /^rgba?\((\d+),\s*(\d+),\s*(\d+)(,\s*\d*\.?\d+)?\)$/
|
||||
|
||||
if (hexRegex.test(color)) {
|
||||
// 处理十六进制颜色(如 #ffffff)
|
||||
const hex = color.replace('#', '')
|
||||
const r = parseInt(hex.substring(0, 2), 16)
|
||||
const g = parseInt(hex.substring(2, 4), 16)
|
||||
const b = parseInt(hex.substring(4, 6), 16)
|
||||
return `rgba(${r},${g},${b},${opacity})`
|
||||
} else if (rgbaRegex.test(color)) {
|
||||
// 处理 RGBA 颜色(如 rgba(255,255,255,0.5))
|
||||
return color.replace(/[\d\.]+\)$/, `${opacity})`)
|
||||
}
|
||||
return color
|
||||
}
|
||||
|
||||
const colorPickerChange = (e: any, data: any) => {
|
||||
if (data.label == '--primary-color') {
|
||||
formData.value.forEach((item, index) => {
|
||||
if (item.label == '--primary-color-light') {
|
||||
item.value = applyOpacity(data.value, 0.1)
|
||||
}
|
||||
if (item.label == '--primary-color-light2') {
|
||||
item.value = applyOpacity(data.value, 0.8)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
defineExpose({
|
||||
dialogThemeVisible,
|
||||
open
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
194
admin-vben/src/app/views/diy/components/theme-list.vue
Normal file
194
admin-vben/src/app/views/diy/components/theme-list.vue
Normal file
@@ -0,0 +1,194 @@
|
||||
<template>
|
||||
<el-dialog v-model="dialogThemeVisible" :title="dialogTitle" width="535px" align-center class="custom-theme-dialog" @close="cancelFn">
|
||||
<div class="flex flex-col items-baseline">
|
||||
<div class="flex items-center flex-wrap max-h-[365px] overflow-auto [&>:nth-child(3n)]:mr-0">
|
||||
<div :key="tempIndex" v-for="(tempItem, tempIndex) in themeTemp"
|
||||
class="flex flex-col border-[1px] border-solid border-[#dcdee2] rounded-[4px] px-[10px] pt-[10px] pb-[15px] mr-[10px] cursor-pointer my-[5px]"
|
||||
:class="{ '!border-[var(--el-color-primary)]': currTheme.id == tempItem.id }"
|
||||
@click="themeTempChange(tempItem)">
|
||||
<div class="flex justify-between pb-[5px]">
|
||||
<div class="text-[14px] text-[#666] max-w-[85px] whitespace-nowrap overflow-hidden text-ellipsis" :class="{ '!text-[#333]': currTheme.id == tempItem.id }">{{ tempItem.title }}</div>
|
||||
<div>
|
||||
<span class="iconfont iconshanchu-fanggaiV6xx !text-[14px] text-[#999]" v-if="currTheme.id != tempItem.id && tempItem.theme_type != 'default' && currTableTheme != tempItem.id" @click.stop="deleteThemeFn(tempItem)"></span>
|
||||
<span class="nc-iconfont nc-icon-bianjiV6xx1 !text-[14px] text-[#999] ml-[5px]" @click.stop="editThemeFn('edit', tempItem)"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<div class="w-[70px] h-[54px] pl-[7px] pt-[9px] flex flex-col mr-[4px] rounded-[4px] text-[10px] leading-[1] text-[#fff]"
|
||||
:style="{ backgroundColor: tempItem.theme['--primary-color'] }">
|
||||
<span>主色调</span>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<div class="secod-color-item mb-[4px]" :style="{ backgroundColor: tempItem.theme['--primary-help-color2'] }">
|
||||
<span>辅色</span>
|
||||
</div>
|
||||
<div class="secod-color-item" :style="{ backgroundColor: tempItem.theme['--primary-color-dark'] }">
|
||||
<span>配色</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center border-[1px] border-solid border-[var(--el-color-primary)] rounded-[2px] h-[32px] px-[15px] cursor-pointer mt-[15px]" @click="editThemeFn()">
|
||||
<span class="text-[14px] text-[var(--el-color-primary)]">新增配色</span>
|
||||
</div>
|
||||
</div>
|
||||
<edit-theme ref="editThemeRef" @confirm="editThemeConfirm" />
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="cancelFn()">取消</el-button>
|
||||
<el-button type="primary" @click="confirmFn()">确定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import { setDiyTheme, getDefaultTheme, deleteTheme } from '@/app/api/diy'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import editTheme from './edit-theme.vue'
|
||||
|
||||
const editThemeRef = ref()
|
||||
const dialogThemeVisible = ref(false)
|
||||
let confirmRepeat = false
|
||||
const currTheme = reactive({
|
||||
title: '',
|
||||
id: '',
|
||||
theme: {},
|
||||
default_theme: {},
|
||||
new_theme: [],
|
||||
addon_title: '',
|
||||
key: ''
|
||||
})
|
||||
const themeTemp = ref([])
|
||||
const emit = defineEmits(['confirm'])
|
||||
|
||||
const initData = (params: any, callback: any = '') => {
|
||||
getDefaultTheme({ addon: params.key }).then((res) => {
|
||||
themeTemp.value = res.data || []
|
||||
if (callback) {
|
||||
callback(res.data[res.data.length - 1])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const currTableTheme = ref('')
|
||||
const open = (res: any) => {
|
||||
currTableTheme.value = res.id
|
||||
initData(res)
|
||||
confirmRepeat = false
|
||||
currTheme.title = res.title
|
||||
currTheme.id = res.id
|
||||
currTheme.theme = res.theme
|
||||
currTheme.addon_title = res.addon_title
|
||||
currTheme.key = res.key
|
||||
dialogThemeVisible.value = true
|
||||
}
|
||||
|
||||
const dialogTitle = computed(() => {
|
||||
const name = `选择${ currTheme.addon_title }配色`
|
||||
return name
|
||||
})
|
||||
|
||||
// 切换不同配色
|
||||
const themeTempChange = (item: any = {}) => {
|
||||
currTheme.title = item.title
|
||||
currTheme.id = item.id
|
||||
currTheme.theme = item.theme
|
||||
currTheme.default_theme = item.default_theme
|
||||
currTheme.new_theme = item.new_theme
|
||||
}
|
||||
|
||||
// 编辑色调
|
||||
const editThemeFn = (type = 'add', item = {}) => {
|
||||
const theme = {
|
||||
default_theme: {}, // 当前色调的默认值
|
||||
theme: {}, // 当前色调
|
||||
title: '',
|
||||
id: '', // 标识,区分是自定义还是模版色调,
|
||||
new_theme: [], // 新增颜色值
|
||||
key: '', // 表示是哪个插件
|
||||
theme_field: ''
|
||||
}
|
||||
if (type == 'edit') {
|
||||
theme.title = item.title
|
||||
theme.theme = cloneDeep(item.theme) || {}
|
||||
theme.id = item.id
|
||||
theme.default_theme = cloneDeep(item.default_theme) || ''
|
||||
theme.new_theme = cloneDeep(item.new_theme) || []
|
||||
theme.new_theme = cloneDeep(item.new_theme) || []
|
||||
}
|
||||
theme.key = currTheme.key
|
||||
// 颜色展示的默认数据
|
||||
themeTemp.value.forEach((item, index) => {
|
||||
if (item.id == currTheme.id) {
|
||||
theme.theme_field = item.theme_field
|
||||
}
|
||||
})
|
||||
editThemeRef.value.open(theme)
|
||||
}
|
||||
|
||||
// 编辑色调回调
|
||||
const editThemeConfirm = (res: any) => {
|
||||
initData(currTheme, (params: any) => {
|
||||
currTheme.new_theme = res.new_theme
|
||||
currTheme.theme = res.theme
|
||||
currTheme.title = res.title
|
||||
currTheme.id = res.id || params.id // 若是新增的色调,id为空, 需要把之前的的id赋值
|
||||
})
|
||||
}
|
||||
|
||||
// 删除色调
|
||||
let deleteRepeat = false
|
||||
const deleteThemeFn = (res: any) => {
|
||||
if (deleteRepeat) return false
|
||||
deleteRepeat = true
|
||||
const id = res.id
|
||||
deleteTheme(id).then((res) => {
|
||||
initData(currTheme)
|
||||
deleteRepeat = false
|
||||
}).catch(() => {
|
||||
deleteRepeat = false
|
||||
})
|
||||
}
|
||||
|
||||
// 点击保存
|
||||
const confirmFn = () => {
|
||||
if (confirmRepeat) return
|
||||
confirmRepeat = true
|
||||
|
||||
const params = {}
|
||||
params.addon = currTheme.key
|
||||
params.id = currTheme.id
|
||||
params.title = currTheme.title
|
||||
params.theme = currTheme.theme
|
||||
params.new_theme = currTheme.new_theme
|
||||
|
||||
setDiyTheme(params).then((res) => {
|
||||
confirmRepeat = false
|
||||
dialogThemeVisible.value = false
|
||||
emit('confirm')
|
||||
}).catch(() => {
|
||||
confirmRepeat = false
|
||||
})
|
||||
}
|
||||
|
||||
// 点击取消
|
||||
const cancelFn = () => {
|
||||
dialogThemeVisible.value = false
|
||||
emit('confirm')
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
dialogThemeVisible,
|
||||
open
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.secod-color-item {
|
||||
@apply w-[60px] h-[25px] flex flex-col rounded-[4px] text-[10px] text-[#fff] leading-[1] items-end pt-[8px] pr-[7px];
|
||||
}
|
||||
</style>
|
||||
842
admin-vben/src/app/views/diy/edit.vue
Normal file
842
admin-vben/src/app/views/diy/edit.vue
Normal file
@@ -0,0 +1,842 @@
|
||||
<template>
|
||||
<div class="main-container flex-1">
|
||||
<el-header class="flex items-center h-[60px] bg-primary px-[20px]">
|
||||
<div class="text-white cursor-pointer flex items-center" @click="goBack">
|
||||
<el-icon size="14">
|
||||
<ArrowLeft />
|
||||
</el-icon>
|
||||
<span class="pl-[5px] text-[14px]">{{ t('back') }}</span>
|
||||
</div>
|
||||
<div class="text-white ml-[10px] mr-[20px] flex items-center">
|
||||
<span class="mr-[5px] text-[rgba(255,255,255,.5)]">|</span>
|
||||
<span class="mr-[5px] text-[14px]">{{ t('decorating') }}:{{ diyStore.typeName }}</span>
|
||||
<!--<el-icon class="font-bold"><EditPen /></el-icon>-->
|
||||
</div>
|
||||
|
||||
<div v-if="diyStore.type && diyStore.type != 'DIY_PAGE'" class="flex items-center">
|
||||
<span class="text-white mr-[10px] text-base">{{ t('templatePagePlaceholder') }}</span>
|
||||
<div class="w-[180px]">
|
||||
<el-select size="small" v-model="template" :placeholder="t('templatePagePlaceholder')" @change="changeTemplatePage">
|
||||
<el-option :label="t('templatePageEmpty')" value="" />
|
||||
<el-option v-for="(item, key) in templatePages" :label="item.title" :value="key" :key="key"/>
|
||||
</el-select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1"></div>
|
||||
<el-button @click="preview()">{{ t('preview') }}</el-button>
|
||||
<el-button @click="save()">{{ t('save') }}</el-button>
|
||||
</el-header>
|
||||
|
||||
<div class="full-container flex flex-row flex-1 bg-page">
|
||||
|
||||
<div class="component-list w-[290px]">
|
||||
|
||||
<!-- 组件列表区域 -->
|
||||
<el-scrollbar class="px-[10px]">
|
||||
<el-collapse v-model="activeNames" @change="handleChange">
|
||||
<el-collapse-item v-for="(item, key) in component" :key="key" :title="item.title" :name="key">
|
||||
<ul class="flex flex-row flex-wrap">
|
||||
<li v-for="(compItem, compKey) in item.list" :key="compKey" class="w-2/6 text-center cursor-pointer h-[65px]" :title="compItem.title" @click="diyStore.addComponent(compKey, compItem)">
|
||||
<icon v-if="compItem.icon" :name="compItem.icon" size="20px" class="inline-block mt-[3px]" />
|
||||
<icon v-else name="iconfont iconkaifazujian" size="20px" class="inline-block mt-[3px]" />
|
||||
<span class="block text-[12px] truncate">{{ compItem.title }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</el-scrollbar>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="preview-wrap flex-1 relative mt-[20px]">
|
||||
|
||||
<el-scrollbar>
|
||||
<el-button class="page-btn absolute right-[20px]" @click="diyStore.changeCurrentIndex(-99)">{{ t('pageSet') }}</el-button>
|
||||
<div class="diy-view-wrap w-[375px] shadow-lg mx-auto">
|
||||
<div class="preview-head bg-no-repeat bg-center bg-cover cursor-pointer h-[64px]" :class="[diyStore.global.topStatusBar.style]" :style="{backgroundColor :diyStore.global.topStatusBar.bgColor}" @click="diyStore.changeCurrentIndex(-99)">
|
||||
<div v-if="diyStore.global.topStatusBar.style == 'style-1' && diyStore.global.topStatusBar.isShow" class="content-wrap">
|
||||
<div class="title-wrap" :style="{ fontSize: '14px', color: diyStore.global.topStatusBar.textColor, textAlign: diyStore.global.topStatusBar.textAlign }">
|
||||
{{ diyStore.global.title }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="diyStore.global.topStatusBar.style == 'style-2' && diyStore.global.topStatusBar.isShow" class="content-wrap">
|
||||
<div class="title-wrap" :style="{ color: diyStore.global.topStatusBar.textColor }">
|
||||
<div class="h-[28px] max-w-[150px] mr-[8px]" v-if="diyStore.global.topStatusBar.imgUrl">
|
||||
<img class="max-w-[100%] max-h-[100%]" :src="img(diyStore.global.topStatusBar.imgUrl)" mode="heightFix" />
|
||||
</div>
|
||||
<div :style="{ color: diyStore.global.topStatusBar.textColor }">{{ diyStore.global.title }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="diyStore.global.topStatusBar.style == 'style-3' && diyStore.global.topStatusBar.isShow" class="content-wrap">
|
||||
<div class="title-wrap" v-if="diyStore.global.topStatusBar.imgUrl">
|
||||
<img class="max-w-[100%] max-h-[100%]" :src="img(diyStore.global.topStatusBar.imgUrl)" />
|
||||
</div>
|
||||
<div class="search">
|
||||
<span class="nc-iconfont nc-icon-sousuo-duanV6xx1 !text-[12px] absolute left-[10px]"></span>
|
||||
<span class="text-[12px]">{{diyStore.global.topStatusBar.inputPlaceholder}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="diyStore.global.topStatusBar.style == 'style-4' && diyStore.global.topStatusBar.isShow" class="content-wrap">
|
||||
<span class="iconfont iconxiazai19 !text-[14px]" :style="{ color: diyStore.global.topStatusBar.textColor }"></span>
|
||||
<div class="title-wrap" :style="{ color: diyStore.global.topStatusBar.textColor }">我的位置</div>
|
||||
<span class="iconfont iconxiangyoujiantou !text-[12px]" :style="{ color: diyStore.global.topStatusBar.textColor }"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="preview-block relative">
|
||||
|
||||
<ul class="quick-action absolute text-center -right-[70px] top-[20px] w-[42px] rounded shadow-md">
|
||||
<el-tooltip effect="light" :content="t('moveUpComponent')" placement="right">
|
||||
<icon name="iconfont iconjiantoushang" size="20px" class="block cursor-pointer leading-[40px]" @click="diyStore.moveUpComponent" />
|
||||
</el-tooltip>
|
||||
<el-tooltip effect="light" :content="t('moveDownComponent')" placement="right">
|
||||
<icon name="iconfont iconjiantouxia" size="20px" class="block cursor-pointer leading-[40px]" @click="diyStore.moveDownComponent" />
|
||||
</el-tooltip>
|
||||
<el-tooltip effect="light" :content="t('copyComponent')" placement="right">
|
||||
<icon name="iconfont iconcopy-line" size="20px" class="block cursor-pointer leading-[40px]" @click="diyStore.copyComponent" />
|
||||
</el-tooltip>
|
||||
<el-tooltip effect="light" :content="t('delComponent')" placement="right">
|
||||
<icon name="iconfont icondelete-line" size="20px" class="block cursor-pointer leading-[40px]" @click="diyStore.delComponent" />
|
||||
</el-tooltip>
|
||||
<el-tooltip effect="light" :content="t('resetComponent')" placement="right">
|
||||
<icon name="iconfont iconloader-line" size="20px" class="block cursor-pointer leading-[40px]" @click="diyStore.resetComponent" />
|
||||
</el-tooltip>
|
||||
</ul>
|
||||
|
||||
<!-- 组件预览渲染区域 -->
|
||||
<iframe id="previewIframe" v-show="loadingIframe" :src="wapPreview" frameborder="0" class="preview-iframe w-[375px]"></iframe>
|
||||
|
||||
<div v-show="loadingDev" class="preview-iframe w-[375px] pt-[20px] px-[20px]">
|
||||
<div class="font-bold text-xl mb-[40px]">{{ t('developTitle') }}</div>
|
||||
<div class="mb-[20px] flex flex-col">
|
||||
<text class="mb-[10px]">{{ t('wapDomain') }}</text>
|
||||
<el-input v-model.trim="wapDomain" :placeholder="t('wapDomainPlaceholder')" clearable />
|
||||
</div>
|
||||
<el-button type="primary" @click="saveWapDomain">{{ t('confirm') }}</el-button>
|
||||
<el-button type="primary" @click="settingTips()" plain>{{ t('settingTips') }}</el-button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="edit-attribute-wrap w-[400px]">
|
||||
|
||||
<!-- 编辑组件属性区域 -->
|
||||
<el-scrollbar>
|
||||
<el-card class="box-card" shadow="never">
|
||||
<template #header>
|
||||
<div class="card-header flex justify-between items-center">
|
||||
<span class="title flex-1">{{ diyStore.currentIndex == -99 ? t('pageSet') : diyStore.editComponent.componentTitle }}</span>
|
||||
<div class="tab-wrap flex rounded-[50px] bg-gray-100 text-[14px]" v-if="diyStore.currentComponent">
|
||||
<span class="cursor-pointer rounded-[50px] py-[5px] px-[15px]" :class="{ 'bg-primary text-white': diyStore.editTab == 'content' }" @click="diyStore.editTab = 'content'">{{ t('tabEditContent') }}</span>
|
||||
<span class="cursor-pointer rounded-[50px] py-[5px] px-[15px]" :class="{ 'bg-primary text-white': diyStore.editTab == 'style' }" @click="diyStore.editTab = 'style'">{{ t('tabEditStyle') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="edit-component-wrap">
|
||||
|
||||
<component v-if="diyStore.currentComponent" :is="modules[diyStore.currentComponent]" :key="diyStore.currentIndex" :value="diyStore.value[diyStore.currentIndex]">
|
||||
<template #style>
|
||||
<div class="edit-attr-item-wrap">
|
||||
<h3 class="mb-[10px]">{{ t('componentStyleTitle') }}</h3>
|
||||
<el-form label-width="90px" class="px-[10px]">
|
||||
|
||||
<template v-if="diyStore.editComponent.ignore.indexOf('pageBgColor') == -1">
|
||||
<el-form-item :label="t('bottomBgColor')">
|
||||
<el-color-picker v-model="diyStore.editComponent.pageStartBgColor" show-alpha :predefine="diyStore.predefineColors" />
|
||||
<icon name="iconfont iconmap-connect" size="20px" class="block !text-gray-400 mx-[5px]"/>
|
||||
<el-color-picker v-model="diyStore.editComponent.pageEndBgColor" show-alpha :predefine="diyStore.predefineColors" />
|
||||
</el-form-item>
|
||||
<div class="text-sm text-gray-400 ml-[90px] mb-[10px]">{{ t('bottomBgTips') }}</div>
|
||||
</template>
|
||||
|
||||
<el-form-item :label="t('bgGradientAngle')" v-if="diyStore.editComponent.ignore.indexOf('pageBgColor') == -1">
|
||||
<el-radio-group v-model="diyStore.editComponent.pageGradientAngle">
|
||||
<el-radio label="to bottom">{{ t('topToBottom') }}</el-radio>
|
||||
<el-radio label="to right">{{ t('leftToRight') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('componentBgUrl')" v-if="diyStore.editComponent.ignore.indexOf('componentBgUrl') == -1">
|
||||
<upload-image v-model="diyStore.editComponent.componentBgUrl" :limit="1"/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('componentBgAlpha')" v-if="diyStore.editComponent.ignore.indexOf('componentBgUrl') == -1 && diyStore.editComponent.componentBgUrl">
|
||||
<el-slider v-model="diyStore.editComponent.componentBgAlpha" show-input size="small" :min="0" :max="10" class="ml-[10px] diy-nav-slider" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('componentBgColor')" v-if="diyStore.editComponent.ignore.indexOf('componentBgColor') == -1">
|
||||
<el-color-picker v-model="diyStore.editComponent.componentStartBgColor" show-alpha :predefine="diyStore.predefineColors" />
|
||||
<icon name="iconfont iconmap-connect" size="20px" class="block !text-gray-400 mx-[5px]"/>
|
||||
<el-color-picker v-model="diyStore.editComponent.componentEndBgColor" show-alpha :predefine="diyStore.predefineColors" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('bgGradientAngle')" v-if="diyStore.editComponent.ignore.indexOf('componentBgColor') == -1">
|
||||
<el-radio-group v-model="diyStore.editComponent.componentGradientAngle">
|
||||
<el-radio label="to bottom">{{ t('topToBottom') }}</el-radio>
|
||||
<el-radio label="to right">{{ t('leftToRight') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('marginTop')" v-if="diyStore.editComponent.ignore.indexOf('marginTop') == -1">
|
||||
<el-slider v-model="diyStore.editComponent.margin.top" show-input size="small" :min="-100" class="ml-[10px] diy-nav-slider" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('marginBottom')" v-if="diyStore.editComponent.ignore.indexOf('marginBottom') == -1">
|
||||
<el-slider v-model="diyStore.editComponent.margin.bottom" show-input size="small" class="ml-[10px] diy-nav-slider" :min="-100" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('marginBoth')" v-if="diyStore.editComponent.ignore.indexOf('marginBoth') == -1">
|
||||
<el-slider v-model="diyStore.editComponent.margin.both" show-input size="small" class="ml-[10px] diy-nav-slider" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('topRounded')" v-if="diyStore.editComponent.ignore.indexOf('topRounded') == -1">
|
||||
<el-slider v-model="diyStore.editComponent.topRounded" show-input size="small" class="ml-[10px] diy-nav-slider" :max="100" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('bottomRounded')" v-if="diyStore.editComponent.ignore.indexOf('bottomRounded') == -1">
|
||||
<el-slider v-model="diyStore.editComponent.bottomRounded" show-input size="small" class="ml-[10px] diy-nav-slider" :max="100" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
</component>
|
||||
|
||||
</div>
|
||||
|
||||
</el-card>
|
||||
</el-scrollbar>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, toRaw, watch, inject } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import { ArrowLeft } from "@element-plus/icons-vue"
|
||||
import { img } from '@/utils/common'
|
||||
import { getDiyTemplatePages, addDiyPage, editDiyPage, initPage } from '@/app/api/diy'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import useDiyStore from '@/stores/modules/diy'
|
||||
import storage from '@/utils/storage'
|
||||
|
||||
const setLayout = inject('setLayout')
|
||||
setLayout('decorate')
|
||||
|
||||
const diyStore = useDiyStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
route.query.id = route.query.id || 0
|
||||
route.query.name = route.query.name || ''
|
||||
route.query.url = route.query.url || '' // 页面路径
|
||||
route.query.type = route.query.type || '' // 页面模板,新页面传入
|
||||
route.query.title = route.query.title || ''
|
||||
route.query.back = route.query.back || '/site/diy/list'
|
||||
|
||||
const backPath = route.query.back
|
||||
const template = ref('')
|
||||
const oldTemplate = ref('')
|
||||
const wapUrl = ref('')
|
||||
const wapDomain = ref('')
|
||||
const wapPreview = ref('')
|
||||
|
||||
const loadingIframe = ref(false) // 加载iframe
|
||||
const loadingDev = ref(false) // 加载开发环境配置
|
||||
const timeIframe = ref(0) // iframe打开时间
|
||||
const difference = ref(0) // 检测页面加载差异,小于1000毫秒,则配置wap端域名
|
||||
|
||||
const component = ref([])
|
||||
const componentType: string[] = reactive([])
|
||||
const page = ref('')
|
||||
|
||||
const activeNames = ref(componentType)
|
||||
const handleChange = (val: string[]) => {
|
||||
}
|
||||
|
||||
// 初始化原数据
|
||||
const originData = reactive({
|
||||
id: diyStore.id,
|
||||
name: diyStore.name,
|
||||
pageTitle: diyStore.pageTitle,
|
||||
title: diyStore.global.title,
|
||||
value: JSON.stringify({
|
||||
global: toRaw(diyStore.global),
|
||||
value: toRaw(diyStore.value)
|
||||
})
|
||||
})
|
||||
|
||||
// 返回上一页
|
||||
const isChange = ref(true) // 数据是否发生变化,true:没变化,false:变化了
|
||||
const goBack = () => {
|
||||
if (isChange.value) {
|
||||
location.href = `${location.origin}${backPath}`
|
||||
router.push(backPath)
|
||||
} else {
|
||||
// 数据发生变化,弹框提示:确定离开此页面
|
||||
ElMessageBox.confirm(
|
||||
t('leavePageTitleTips'),
|
||||
t('leavePageContentTips'),
|
||||
{
|
||||
confirmButtonText: t('confirm'),
|
||||
cancelButtonText: t('cancel'),
|
||||
type: 'warning',
|
||||
autofocus: false
|
||||
}
|
||||
).then(() => {
|
||||
location.href = `${location.origin}${backPath}`
|
||||
}).catch(() => {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 动态加载后台自定义组件编辑
|
||||
const modulesFiles = import.meta.glob('./components/*.vue', { eager: true })
|
||||
const addonModulesFiles = import.meta.glob('@/addon/**/views/diy/components/*.vue', { eager: true })
|
||||
addonModulesFiles && Object.assign(modulesFiles, addonModulesFiles)
|
||||
|
||||
const modules = {}
|
||||
for (const [key, value] of Object.entries(modulesFiles)) {
|
||||
const moduleName = key.split('/').pop()
|
||||
const name = moduleName.split('.')[0]
|
||||
modules[name] = value.default
|
||||
}
|
||||
|
||||
// 获取模板页面列表
|
||||
const templatePages: any = reactive({})
|
||||
|
||||
const loadDiyTemplatePages = (type:any)=>{
|
||||
getDiyTemplatePages({
|
||||
type,
|
||||
mode: 'diy'
|
||||
}).then(res => {
|
||||
for (const key in res.data) {
|
||||
templatePages[key] = res.data[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 全局监听自定义数据变化
|
||||
watch(
|
||||
() => template.value,
|
||||
(newValue, oldValue) => {
|
||||
oldTemplate.value = oldValue;
|
||||
}
|
||||
)
|
||||
|
||||
// 切换模板页面
|
||||
const changeTemplatePage = (value: any) => {
|
||||
// 存在数据则弹框提示确认
|
||||
if (diyStore.value.length) {
|
||||
ElMessageBox.confirm(t('changeTemplatePageTips'), t('warning'), {
|
||||
confirmButtonText: t('confirm'),
|
||||
cancelButtonText: t('cancel'),
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
diyStore.changeCurrentIndex(-99)
|
||||
diyStore.init() // 清空
|
||||
if (value) {
|
||||
let data = cloneDeep(templatePages[value].data);
|
||||
diyStore.global = data.global;
|
||||
if (data.value.length) {
|
||||
diyStore.value = data.value
|
||||
}
|
||||
} else {
|
||||
if (route.query.title) diyStore.global.title = diyStore.typeName
|
||||
}
|
||||
}).catch(() => {
|
||||
// 还原
|
||||
template.value = oldTemplate.value
|
||||
});
|
||||
} else {
|
||||
diyStore.init() // 清空
|
||||
if (value) {
|
||||
let data = cloneDeep(templatePages[value].data)
|
||||
diyStore.global = data.global
|
||||
if (data.value.length) {
|
||||
diyStore.value = data.value
|
||||
}
|
||||
} else {
|
||||
if (route.query.title) diyStore.global.title = diyStore.typeName
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 全局监听自定义数据变化
|
||||
watch(
|
||||
() => diyStore,
|
||||
(newValue, oldValue) => {
|
||||
const data = {
|
||||
id: newValue.id,
|
||||
name: newValue.name,
|
||||
pageTitle: newValue.pageTitle,
|
||||
title: newValue.global.title,
|
||||
value: JSON.stringify({
|
||||
global: toRaw(newValue.global),
|
||||
value: toRaw(newValue.value)
|
||||
})
|
||||
}
|
||||
|
||||
diyStore.postMessage()
|
||||
isChange.value = JSON.stringify(data) == JSON.stringify(originData)
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
|
||||
// 根据当前页面路由查询页面初始化数据
|
||||
initPage({
|
||||
id: route.query.id,
|
||||
name: route.query.name,
|
||||
url: route.query.url,
|
||||
type: route.query.type,
|
||||
title: route.query.title
|
||||
}).then(async (res) => {
|
||||
const data = res.data
|
||||
diyStore.init() // 初始化清空数据
|
||||
diyStore.id = data.id || 0
|
||||
diyStore.name = data.name
|
||||
diyStore.pageTitle = data.page_title
|
||||
diyStore.type = data.type
|
||||
diyStore.typeName = data.type_name
|
||||
diyStore.templateName = data.template
|
||||
template.value = data.template;
|
||||
diyStore.isDefault = data.is_default
|
||||
diyStore.pageMode = data.mode
|
||||
if (data.global) {
|
||||
for (const key in data.global) {
|
||||
if(data.global[key]) {
|
||||
for (const childKey in data.global[key]) {
|
||||
diyStore.global[key][childKey] = data.global[key][childKey]
|
||||
}
|
||||
}else{
|
||||
diyStore.global[key] = data.global[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
if (data.value) {
|
||||
const sources = JSON.parse(data.value)
|
||||
diyStore.global = sources.global
|
||||
if (sources.value.length) {
|
||||
diyStore.value = sources.value
|
||||
// diyStore.changeCurrentIndex(0,diyStore.value[0]);
|
||||
}
|
||||
} else {
|
||||
diyStore.global.title = data.title
|
||||
}
|
||||
|
||||
// 初始化原数据
|
||||
originData.id = diyStore.id
|
||||
originData.name = diyStore.name
|
||||
originData.pageTitle = diyStore.pageTitle
|
||||
originData.title = diyStore.global.title
|
||||
originData.value = JSON.stringify({
|
||||
global: toRaw(diyStore.global),
|
||||
value: toRaw(diyStore.value)
|
||||
})
|
||||
|
||||
component.value = data.component
|
||||
for (const type in component.value) {
|
||||
componentType.push(type)
|
||||
for (const key in component.value[type].list) {
|
||||
const com = cloneDeep(component.value[type].list[key])
|
||||
com.id = diyStore.generateRandom()
|
||||
com.componentName = key
|
||||
com.componentTitle = com.title
|
||||
Object.assign(com, com.value)
|
||||
delete com.name
|
||||
delete com.title
|
||||
delete com.value
|
||||
delete com.type
|
||||
delete com.icon
|
||||
diyStore.components.push(com)
|
||||
}
|
||||
}
|
||||
|
||||
loadDiyTemplatePages(data.type)
|
||||
|
||||
// 加载预览
|
||||
wapDomain.value = data.domain_url.wap_domain
|
||||
wapUrl.value = data.domain_url.wap_url
|
||||
page.value = data.page
|
||||
|
||||
let repeat = true; // 防重复执行
|
||||
|
||||
// 开发模式下执行
|
||||
if (import.meta.env.MODE == 'development') {
|
||||
|
||||
// env文件配置过wap域名
|
||||
if (wapDomain.value) {
|
||||
wapUrl.value = wapDomain.value + '/wap'
|
||||
repeat = false
|
||||
setDomain()
|
||||
}
|
||||
|
||||
let wap_domain_storage = storage.get('wap_domain')
|
||||
if (wap_domain_storage) {
|
||||
wapUrl.value = wap_domain_storage
|
||||
repeat = false
|
||||
setDomain()
|
||||
}
|
||||
}
|
||||
|
||||
if (repeat) {
|
||||
setDomain()
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
const uniAppLoadStatus = ref(false) // uni-app 加载状态,true:加载完成,false:未完成
|
||||
|
||||
// 监听组件数据 uni-app端
|
||||
window.addEventListener('message', (event) => {
|
||||
try {
|
||||
let data = {
|
||||
type: ''
|
||||
};
|
||||
if(typeof event.data == 'string') {
|
||||
data = JSON.parse(event.data)
|
||||
}else if(typeof event.data == 'object') {
|
||||
data = event.data
|
||||
}
|
||||
if (!data.type) return
|
||||
|
||||
switch (data.type) {
|
||||
case 'appOnLaunch':
|
||||
case 'appOnReady':
|
||||
// uni-app 加载完成
|
||||
loadingDev.value = false
|
||||
loadingIframe.value = true
|
||||
let loadTime = new Date().getTime()
|
||||
difference.value = loadTime - timeIframe.value
|
||||
uniAppLoadStatus.value = true // 加载完成
|
||||
break
|
||||
case 'init':
|
||||
// 初始化,与uniapp建立连接传输数据
|
||||
diyStore.load = true
|
||||
diyStore.postMessage()
|
||||
break
|
||||
case 'change':
|
||||
// 切换
|
||||
diyStore.changeCurrentIndex(data.index, data.component)
|
||||
break
|
||||
case 'data':
|
||||
// 传数据
|
||||
diyStore.changeCurrentIndex(data.index, data.component)
|
||||
diyStore.global = data.global
|
||||
diyStore.value = data.value
|
||||
break
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('diy edit 后台接受数据错误', e)
|
||||
}
|
||||
}, false)
|
||||
|
||||
const saveWapDomain = () => {
|
||||
if (wapDomain.value.trim().length == 0) {
|
||||
ElMessage({
|
||||
type: 'warning',
|
||||
message: `${t('wapDomainPlaceholder')}`
|
||||
})
|
||||
return
|
||||
}
|
||||
wapUrl.value = wapDomain.value + '/wap'
|
||||
setDomain()
|
||||
storage.set({ key: 'wap_domain', data: wapUrl.value })
|
||||
loadingIframe.value = true
|
||||
loadingDev.value = false
|
||||
}
|
||||
|
||||
const setDomain = () => {
|
||||
wapPreview.value = `${wapUrl.value}${page.value}?mode=decorate` // 模式:decorate 装修 访问预览页面
|
||||
|
||||
const send = ()=>{
|
||||
timeIframe.value = new Date().getTime()
|
||||
postMessage()
|
||||
}
|
||||
|
||||
// 同步发送一次消息
|
||||
send()
|
||||
|
||||
// 如果同步发送消息的 uni-app没有接收到回应,则定时发送消息
|
||||
let sendCount = 0;
|
||||
let timeInterVal = setInterval(()=>{
|
||||
// 接收 uni-app 发送的消息 或者 发送50次后未响应,则停止发送
|
||||
if(uniAppLoadStatus.value || sendCount >= 50){
|
||||
clearInterval(timeInterVal)
|
||||
return
|
||||
}
|
||||
|
||||
send()
|
||||
sendCount++;
|
||||
},200)
|
||||
|
||||
// 如果10秒内加载不出来,则需要配置域名
|
||||
setTimeout(() => {
|
||||
if (difference.value == 0) initLoad()
|
||||
}, 1000 * 10)
|
||||
|
||||
}
|
||||
|
||||
// 将数据发送到uniapp
|
||||
const postMessage = () => {
|
||||
const data = JSON.stringify({
|
||||
type: 'appOnReady',
|
||||
message: '加载完成'
|
||||
})
|
||||
if (window.previewIframe) window.previewIframe.contentWindow.postMessage(data, '*')
|
||||
}
|
||||
|
||||
// 初始化加载状态
|
||||
const initLoad = () => {
|
||||
loadingDev.value = true
|
||||
loadingIframe.value = false
|
||||
wapPreview.value = ''
|
||||
}
|
||||
|
||||
const isRepeat = ref(false)
|
||||
const save = (callback: any) => {
|
||||
if (!diyStore.verify()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (isRepeat.value) return
|
||||
isRepeat.value = true
|
||||
|
||||
diyStore.templateName = template.value;
|
||||
|
||||
let data = {
|
||||
id: diyStore.id,
|
||||
name: diyStore.name,
|
||||
page_title: diyStore.pageTitle,
|
||||
title: diyStore.global.title,
|
||||
type: diyStore.type,
|
||||
template: diyStore.templateName,
|
||||
is_default: diyStore.isDefault,
|
||||
is_change: isChange.value ? 0 : 1,
|
||||
value: JSON.stringify({
|
||||
global: toRaw(diyStore.global),
|
||||
value: toRaw(diyStore.value)
|
||||
})
|
||||
}
|
||||
|
||||
const api = diyStore.id ? editDiyPage : addDiyPage
|
||||
api(data).then((res: any) => {
|
||||
isRepeat.value = false
|
||||
if (res.code == 1) {
|
||||
if (diyStore.id) {
|
||||
isRepeat.value = false // 不刷新
|
||||
} else {
|
||||
location.href = `${location.origin}${backPath}`;
|
||||
}
|
||||
if (callback) callback(res.data.id)
|
||||
}
|
||||
}).catch(() => {
|
||||
isRepeat.value = false
|
||||
})
|
||||
}
|
||||
|
||||
// 预览
|
||||
const preview = () => {
|
||||
save((id: number) => {
|
||||
id = diyStore.id || id
|
||||
const url = router.resolve({
|
||||
path: '/site/preview/wap',
|
||||
query: {
|
||||
page: page.value + '?id=' + id
|
||||
}
|
||||
})
|
||||
window.open(url.href)
|
||||
})
|
||||
}
|
||||
|
||||
const settingTips = () => {
|
||||
window.open('https://www.kancloud.cn/niucloud/niucloud-admin-develop/3213393')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.el-collapse-item__wrap {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.el-collapse-item__content {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.el-collapse-item__header {
|
||||
font-size: var(--el-font-size-base);
|
||||
}
|
||||
|
||||
.display-block {
|
||||
.el-form-item__content {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.edit-component-wrap {
|
||||
.content-wrap,
|
||||
.style-wrap {
|
||||
.edit-attr-item-wrap {
|
||||
border-top: 2px solid var(--el-color-info-light-8);
|
||||
padding-top: 20px;
|
||||
|
||||
&:first-of-type {
|
||||
border-top: none;
|
||||
padding-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.diy-nav-slider {
|
||||
.el-slider__input {
|
||||
width: 100px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style lang="scss" scoped>
|
||||
.full-container {
|
||||
height: calc(100vh - 60px);
|
||||
}
|
||||
|
||||
.preview-iframe {
|
||||
height: calc(100vh - 160px);
|
||||
}
|
||||
|
||||
.component-list {
|
||||
background: var(--el-bg-color);
|
||||
}
|
||||
|
||||
.component-list ul li {
|
||||
&:not(.disabled):hover {
|
||||
color: var(--el-color-primary);
|
||||
background: var(--el-color-primary-light-9);
|
||||
}
|
||||
}
|
||||
|
||||
.diy-view-wrap {
|
||||
background: var(--el-bg-color-page);
|
||||
}
|
||||
|
||||
.diy-view-wrap .preview-head {
|
||||
background-image: url(@/app/assets/images/diy_preview_head.png);
|
||||
background-color: var(--el-bg-color);
|
||||
}
|
||||
|
||||
.quick-action {
|
||||
background: var(--el-bg-color);
|
||||
}
|
||||
|
||||
.edit-attribute-wrap {
|
||||
background: var(--el-bg-color);
|
||||
}
|
||||
|
||||
.edit-attribute-wrap .box-card {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.diy-view-wrap .preview-head {
|
||||
padding: 28px 15px 0;
|
||||
|
||||
.content-wrap {
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
&.style-1 {
|
||||
.content-wrap {
|
||||
.title-wrap {
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.style-2 {
|
||||
.content-wrap {
|
||||
.title-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
> div {
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
max-width: 150px;
|
||||
font-size: 14px;
|
||||
|
||||
&:last-child {
|
||||
overflow: hidden; //超出的文本隐藏
|
||||
text-overflow: ellipsis; //用省略号显示
|
||||
white-space: nowrap; //不换行
|
||||
flex: 1;
|
||||
max-width: 200px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.style-3 {
|
||||
.content-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.title-wrap {
|
||||
height: 30px;
|
||||
max-width: 85px;
|
||||
margin-right: 5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.search {
|
||||
flex: 1;
|
||||
padding-right: 10px;
|
||||
padding-left: 31px;
|
||||
position: relative;
|
||||
background-color: #fff;
|
||||
text-align: left;
|
||||
border-radius: 30px;
|
||||
height: 30px;
|
||||
line-height: 30px;
|
||||
border: 1px solid #eeeeee;
|
||||
color: rgb(102, 102, 102);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 105px;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
|
||||
span {
|
||||
overflow: hidden; //超出的文本隐藏
|
||||
text-overflow: ellipsis; //用省略号显示
|
||||
white-space: nowrap; //不换行
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
color: #909399;
|
||||
font-size: 16px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.style-4 {
|
||||
.content-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.title-wrap {
|
||||
flex: none;
|
||||
margin: 0 5px;
|
||||
max-width: 180px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
345
admin-vben/src/app/views/diy/index.vue
Normal file
345
admin-vben/src/app/views/diy/index.vue
Normal file
@@ -0,0 +1,345 @@
|
||||
<template>
|
||||
<div class="main-container">
|
||||
<el-card class="box-card !border-none" shadow="never">
|
||||
|
||||
<div class="flex flex-wrap min-w-[1200px]" v-if="page.use_template">
|
||||
<div class="page-item relative w-[340px] mr-[40px] pt-[90px] pb-[20px] bg-[#f7f7f7] bg-no-repeat">
|
||||
<p class="absolute top-[54px] left-[50%] translate-x-[-50%] w-[130px] text-[14px] truncate text-center">{{ page.use_template.title }}</p>
|
||||
|
||||
<div v-show="page.use_template.url" class="w-[320px] h-[550px] mx-auto">
|
||||
<iframe :id="'previewIframe_' + type" v-show="page.loadingIframe" class="w-[320px] h-[550px] mx-auto" :src="page.use_template.wapPreview" frameborder="0"></iframe>
|
||||
<div v-show="page.loadingDev" class="w-[320px] h-[550px] mx-auto bg-body pt-[20px] px-[20px]">
|
||||
<div class="font-bold text-xl mb-[40px]">{{ t('developTitle') }}</div>
|
||||
<div class="mb-[20px] flex flex-col">
|
||||
<text class="mb-[10px]">{{ t('wapDomain') }}</text>
|
||||
<el-input v-model.trim="wapDomain" :placeholder="t('wapDomainPlaceholder')" clearable />
|
||||
</div>
|
||||
<div class="flex">
|
||||
<el-button type="primary" @click="saveDomain()">{{ t('confirm') }}</el-button>
|
||||
<el-button type="primary" @click="settingTips()" plain>{{ t('settingTips') }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-show="!page.use_template.wapPreview" class="overflow-hidden w-[320px] h-[550px] mx-auto">
|
||||
<img class="max-w-full" v-if="page.use_template.cover" :src="img(page.use_template.cover)" />
|
||||
</div>
|
||||
|
||||
<div class="popup-wrap absolute inset-x-0 inset-y-0 select-none" :class="{ 'disabled': page.isDisabledPop }"></div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="w-[700px]">
|
||||
<div class="flex flex-wrap">
|
||||
<diy-link v-model="link" :ignore="['OTHER_LINK']" @success="changePage">
|
||||
<el-button type="primary">{{ t('changePage') }}</el-button>
|
||||
</diy-link>
|
||||
<el-button type="primary" @click="toDecorate()" v-show="page.use_template.action == 'decorate'" class="ml-[12px]">{{ t('decorate') }}</el-button>
|
||||
</div>
|
||||
|
||||
<div class="info-wrap">
|
||||
<div class="mt-[20px] p-[20px] flex items-center justify-between bg">
|
||||
<div>
|
||||
<div class="font-bold">{{ t('H5') }}</div>
|
||||
<el-form label-width="40px" class="mt-[5px]">
|
||||
<el-form-item :label="t('link')" class="mb-[5px]">
|
||||
<el-input readonly :value="page.shareUrl" class="!w-[390px]">
|
||||
<template #append>
|
||||
<el-button @click="copyEvent(page.shareUrl)" class="bg-primary copy">{{ t('copy') }}</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="text-[#999] text-base">{{ t('scanQRCodeOnRight') }}</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<el-image class="w-[100px] h-[100px] mb-[5px]" :src="wapImage" />
|
||||
<div @click="toPreview()" class="text-primary text-base cursor-pointer">{{ t('preview') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref, watch } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import { img } from '@/utils/common'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getDecoratePage, changeTemplate } from '@/app/api/diy'
|
||||
import storage from '@/utils/storage'
|
||||
import QRCode from 'qrcode'
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
|
||||
const type: any = ref('DIY_INDEX');
|
||||
const page: any = reactive({})
|
||||
const router = useRouter()
|
||||
const wapDomain = ref('')
|
||||
const wapImage = ref('')
|
||||
const link: any = ref({
|
||||
name: ''
|
||||
})
|
||||
|
||||
// 切换页面
|
||||
const formData = reactive({
|
||||
type: '',
|
||||
name: '',
|
||||
parent:'',
|
||||
page: '',
|
||||
title: '',
|
||||
action: ''
|
||||
})
|
||||
|
||||
// 初始化数据
|
||||
const refreshData = () => {
|
||||
getDecoratePage({
|
||||
type : type.value
|
||||
}).then(res => {
|
||||
for (const key in res.data) {
|
||||
page[key] = res.data[key]
|
||||
}
|
||||
|
||||
link.value.name = page.use_template.name;
|
||||
link.value.title = page.use_template.title;
|
||||
link.value.url = page.use_template.page;
|
||||
link.value.action = page.use_template.action;
|
||||
link.value.parent = page.use_template.parent;
|
||||
|
||||
if (page.use_template.url) {
|
||||
page.loadingIframe = false // 加载iframe
|
||||
page.loadingDev = false // 加载开发环境配置
|
||||
page.isDisabledPop = false // 是否禁止打开遮罩层
|
||||
page.difference = 0 // 检测页面加载差异,小于1000毫秒,则配置wap端域名
|
||||
|
||||
wapDomain.value = page.domain_url.wap_domain
|
||||
page.wapUrl = page.domain_url.wap_url
|
||||
|
||||
let repeat = true; // 防重复执行
|
||||
|
||||
if (import.meta.env.MODE == 'development') {
|
||||
// 开发模式情况下,并且未配置wap域名,则获取缓存域名
|
||||
if (wapDomain.value && wapDomain.value.indexOf('localhost') != -1) {
|
||||
page.wapUrl = wapDomain.value + '/wap'
|
||||
repeat = false
|
||||
setDomain()
|
||||
}
|
||||
if (storage.get('wap_domain')) {
|
||||
page.wapUrl = storage.get('wap_domain')
|
||||
repeat = false
|
||||
setDomain()
|
||||
}
|
||||
}
|
||||
|
||||
if(repeat) {
|
||||
setDomain()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
refreshData()
|
||||
|
||||
const uniAppLoadStatus = ref(false) // uni-app 加载状态,true:加载完成,false:未完成
|
||||
|
||||
// 监听 uni-app 端 是否加载完成
|
||||
window.addEventListener('message', (event) => {
|
||||
try {
|
||||
let data = {
|
||||
type :''
|
||||
};
|
||||
if(typeof event.data == 'string') {
|
||||
data = JSON.parse(event.data)
|
||||
}else if(typeof event.data == 'object') {
|
||||
data = event.data
|
||||
}
|
||||
if (data.type && ['appOnLaunch', 'appOnReady'].indexOf(data.type) != -1) {
|
||||
page.loadingDev = false // 禁用开发环境配置
|
||||
page.loadingIframe = true // 加载iframe
|
||||
let loadTime = new Date().getTime()
|
||||
page.difference = loadTime - page.timeIframe
|
||||
page.isDisabledPop = false // 是否禁止打开遮罩层
|
||||
uniAppLoadStatus.value = true // 加载完成
|
||||
}
|
||||
} catch (e) {
|
||||
initLoad()
|
||||
console.log('diy index 后台接受数据错误', e)
|
||||
}
|
||||
}, false)
|
||||
|
||||
// 将数据发送到uniapp
|
||||
const postMessage = () => {
|
||||
const diyData = JSON.stringify({
|
||||
type: 'appOnReady',
|
||||
message: '加载完成'
|
||||
})
|
||||
if (window['previewIframe_' + type.value]) window['previewIframe_' + type.value].contentWindow.postMessage(diyData, '*')
|
||||
}
|
||||
|
||||
// 初始化加载状态
|
||||
const initLoad = () => {
|
||||
page.loadingDev = true
|
||||
page.isDisabledPop = true
|
||||
page.loadingIframe = false
|
||||
}
|
||||
|
||||
const saveDomain = () => {
|
||||
if (wapDomain.value.trim().length == 0) {
|
||||
ElMessage({
|
||||
type: 'warning',
|
||||
message: `${t('wapDomainPlaceholder')}`,
|
||||
})
|
||||
return
|
||||
}
|
||||
const wapUrl = wapDomain.value + '/wap'
|
||||
storage.set({key: 'wap_domain', data: wapUrl})
|
||||
|
||||
if (page.use_template.url) {
|
||||
page.wapUrl = wapUrl
|
||||
setDomain()
|
||||
}
|
||||
setTimeout(() => {
|
||||
if (page.use_template.url) {
|
||||
page.loadingIframe = true // 加载iframe
|
||||
page.loadingDev = false // 加载开发环境配置
|
||||
page.isDisabledPop = false // 是否禁止打开遮罩层
|
||||
}
|
||||
}, 100 * 3)
|
||||
}
|
||||
|
||||
const settingTips = () => {
|
||||
window.open('https://www.kancloud.cn/niucloud/niucloud-admin-develop/3213393')
|
||||
}
|
||||
|
||||
const setDomain = () => {
|
||||
page.use_template.wapPreview = page.wapUrl + page.use_template.url
|
||||
page.shareUrl = page.wapUrl + '/';
|
||||
QRCode.toDataURL(page.shareUrl, { errorCorrectionLevel: 'L', margin: 0, width: 100 }).then(url => {
|
||||
wapImage.value = url
|
||||
})
|
||||
|
||||
const send = ()=>{
|
||||
page.timeIframe = new Date().getTime()
|
||||
postMessage()
|
||||
}
|
||||
|
||||
// 同步发送一次消息
|
||||
send()
|
||||
|
||||
// 如果同步发送消息的 uni-app没有接收到回应,则定时发送消息
|
||||
let sendCount = 0;
|
||||
let timeInterVal = setInterval(()=>{
|
||||
// 接收 uni-app 发送的消息 或者 发送50次后未响应,则停止发送
|
||||
if(uniAppLoadStatus.value || sendCount >= 50){
|
||||
clearInterval(timeInterVal)
|
||||
return
|
||||
}
|
||||
|
||||
send()
|
||||
sendCount++;
|
||||
},200)
|
||||
|
||||
// 如果10秒内加载不出来,则需要配置域名
|
||||
setTimeout(() => {
|
||||
if (page.difference == 0) initLoad()
|
||||
}, 1000 * 10)
|
||||
}
|
||||
|
||||
// 跳转去装修
|
||||
const toDecorate = () => {
|
||||
const query: any = {
|
||||
back: '/site/diy/index'
|
||||
}
|
||||
if (page.use_template.id) {
|
||||
query.id = page.use_template.id
|
||||
} else if (page.use_template.type) {
|
||||
query.name = page.use_template.type
|
||||
} else if (page.use_template.url) {
|
||||
query.url = page.use_template.url
|
||||
}
|
||||
const url = router.resolve({
|
||||
path: '/decorate/edit',
|
||||
query
|
||||
})
|
||||
window.open(url.href)
|
||||
}
|
||||
|
||||
// 跳转去预览
|
||||
const toPreview = () => {
|
||||
let value = page.use_template.page
|
||||
if (page.use_template.url) {
|
||||
value = page.use_template.url
|
||||
} else if (page.use_template.id) {
|
||||
value += '?id=' + page.use_template.id
|
||||
}
|
||||
const url = router.resolve({
|
||||
path: '/preview/wap',
|
||||
query: {
|
||||
page:value
|
||||
}
|
||||
})
|
||||
window.open(url.href)
|
||||
}
|
||||
|
||||
const isRepeat = ref(false)
|
||||
const changePage = ()=>{
|
||||
formData.type = type.value;
|
||||
formData.name = link.value.name;
|
||||
formData.page = link.value.url;
|
||||
formData.title = link.value.title;
|
||||
formData.action = link.value.action;
|
||||
formData.parent = link.value.parent;
|
||||
|
||||
if (isRepeat.value) return
|
||||
isRepeat.value = true
|
||||
|
||||
changeTemplate({
|
||||
...formData
|
||||
}).then((res) => {
|
||||
isRepeat.value = false
|
||||
refreshData()
|
||||
})
|
||||
}
|
||||
|
||||
// 复制
|
||||
const { copy, isSupported, copied } = useClipboard()
|
||||
const copyEvent = (text: string) => {
|
||||
if (!isSupported.value) {
|
||||
ElMessage({
|
||||
message: t('notSupportCopy'),
|
||||
type: 'warning'
|
||||
})
|
||||
}
|
||||
copy(text)
|
||||
}
|
||||
|
||||
watch(copied, () => {
|
||||
if (copied.value) {
|
||||
ElMessage({
|
||||
message: t('copySuccess'),
|
||||
type: 'success'
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-item {
|
||||
background-image: url(@/app/assets/images/iphone_bg.png);
|
||||
background-color: var(--el-bg-color);
|
||||
background-size: 100%;
|
||||
|
||||
.popup-wrap {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.popup-wrap:not(.disabled) {
|
||||
display: block !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
377
admin-vben/src/app/views/diy/list.vue
Normal file
377
admin-vben/src/app/views/diy/list.vue
Normal file
@@ -0,0 +1,377 @@
|
||||
<template>
|
||||
<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>
|
||||
<el-button type="primary" class="w-[100px]" @click="dialogVisible = true">{{ t('addDiyPage') }}</el-button>
|
||||
</div>
|
||||
|
||||
<el-card class="box-card !border-none my-[10px] table-search-wrap" shadow="never">
|
||||
<el-form :inline="true" :model="diyPageTableData.searchParam" ref="searchFormDiyPageRef">
|
||||
<el-form-item :label="t('title')" prop="title">
|
||||
<el-input v-model.trim="diyPageTableData.searchParam.title" :placeholder="t('titlePlaceholder')" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('forAddon')" prop="addon_name">
|
||||
<el-select v-model="diyPageTableData.searchParam.addon_name" :placeholder="t('forAddonPlaceholder')" @change="handleSelectAddonChange">
|
||||
<el-option :label="t('all')" value="" />
|
||||
<el-option v-for="(item, key) in apps" :label="item.title" :value="key" :key="key"/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('typeName')" prop="type">
|
||||
<el-select v-model="diyPageTableData.searchParam.type" :placeholder="t('pageTypePlaceholder')">
|
||||
<el-option :label="t('all')" value="" />
|
||||
<el-option v-for="(item, key) in pageType" :label="item.title" :value="key" :key="key"/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="loadDiyPageList()">{{ t('search') }}</el-button>
|
||||
<el-button @click="resetForm(searchFormDiyPageRef)">{{ t('reset') }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<el-table :data="diyPageTableData.data" size="large" v-loading="diyPageTableData.loading">
|
||||
<template #empty>
|
||||
<span>{{ !diyPageTableData.loading ? t('emptyData') : '' }}</span>
|
||||
</template>
|
||||
|
||||
<el-table-column prop="page_title" :label="t('title')" min-width="120" />
|
||||
<el-table-column prop="addon_name" :label="t('forAddon')" min-width="80" />
|
||||
<el-table-column prop="type_name" :label="t('typeName')" min-width="80" />
|
||||
<el-table-column :label="t('status')" min-width="80">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.type == 'DIY_PAGE'">-</span>
|
||||
<template v-else>
|
||||
<span v-if="row.is_default == 1" class="text-primary">{{ t('isUse') }}</span>
|
||||
<span v-else>{{ t('unused') }}</span>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="update_time" :label="t('updateTime')" min-width="120" />
|
||||
|
||||
<el-table-column :label="t('operation')" fixed="right" align="right" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="toPreview(row)">{{ t('preview') }}</el-button>
|
||||
<el-button v-if="row.is_default == 0" type="primary" link @click="setUse(row.id)">{{ t('use') }}</el-button>
|
||||
<el-button v-if="row.type == 'DIY_PAGE'" type="primary" link @click="openShare(row)">{{ t('shareSet') }}</el-button>
|
||||
<el-button type="primary" link @click="editEvent(row)">{{ t('edit') }}</el-button>
|
||||
<el-button v-if="row.is_default == 0 || row.type == 'DIY_PAGE'" type="primary" link @click="deleteEvent(row.id)">{{ t('delete') }}</el-button>
|
||||
<el-button type="primary" link @click="copyEvent(row.id)">{{ t('copy') }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
</el-table>
|
||||
<div class="mt-[16px] flex justify-end">
|
||||
<el-pagination v-model:current-page="diyPageTableData.page" v-model:page-size="diyPageTableData.limit"
|
||||
layout="total, sizes, prev, pager, next, jumper" :total="diyPageTableData.total"
|
||||
@size-change="loadDiyPageList()" @current-change="loadDiyPageList" />
|
||||
</div>
|
||||
|
||||
</el-card>
|
||||
|
||||
<!--添加页面-->
|
||||
<el-dialog v-model="dialogVisible" :title="t('addPageTips')" width="350px">
|
||||
|
||||
<el-form :model="formData" label-width="90px" ref="formRef" :rules="formRules">
|
||||
<el-form-item :label="t('title')" prop="title">
|
||||
<el-input v-model.trim="formData.title" :placeholder="t('titlePlaceholder')" clearable maxlength="12" show-word-limit class="w-full" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('typeName')" prop="type">
|
||||
<el-select v-model="formData.type" :placeholder="t('pageTypePlaceholder')" class="!w-full">
|
||||
<el-option v-for="(item, key) in pageType" :label="item.title" :value="key" :key="key"/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="dialogVisible = false">{{ t('cancel') }}</el-button>
|
||||
<el-button type="primary" @click="addEvent(formRef)">{{ t('confirm') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 分享设置-->
|
||||
<el-dialog v-model="shareDialogVisible" :title="t('shareSet')" width="30%">
|
||||
<el-tabs v-model="tabShareType">
|
||||
<el-tab-pane :label="t('wechat')" name="wechat"></el-tab-pane>
|
||||
<el-tab-pane :label="t('weapp')" name="weapp"></el-tab-pane>
|
||||
</el-tabs>
|
||||
<el-form :model="shareFormData[tabShareType]" label-width="90px" ref="shareFormRef" :rules="shareFormRules">
|
||||
<el-form-item :label="t('sharePage')">
|
||||
<span>{{ sharePage }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('shareTitle')" prop="title">
|
||||
<el-input v-model.trim="shareFormData[tabShareType].title" :placeholder="t('shareTitlePlaceholder')" clearable maxlength="30" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('shareDesc')" prop="desc" v-if="tabShareType == 'wechat'">
|
||||
<el-input v-model.trim="shareFormData[tabShareType].desc" :placeholder="t('shareDescPlaceholder')" type="textarea" rows="4" clearable maxlength="100" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('shareImageUrl')" prop="url">
|
||||
<upload-image v-model="shareFormData[tabShareType].url" :limit="1" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="shareDialogVisible = false">{{ t('cancel') }}</el-button>
|
||||
<el-button type="primary" @click="shareEvent(shareFormRef)">{{ t('confirm') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref, computed } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import { getApps, getDiyPageList, deleteDiyPage, getDiyTemplate, editDiyPageShare, setUseDiyPage, copyDiy } from '@/app/api/diy'
|
||||
import { ElMessageBox, FormInstance } from 'element-plus'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { setTablePageStorage, getTablePageStorage } from '@/utils/common'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const pageName = route.meta.title
|
||||
|
||||
const pageType: any = reactive({}) // 页面类型
|
||||
|
||||
// 添加自定义页面
|
||||
const formData = reactive({
|
||||
title: '',
|
||||
type: ''
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = computed(() => {
|
||||
return {
|
||||
title: [
|
||||
{ required: true, message: t('titlePlaceholder'), trigger: 'blur' }
|
||||
],
|
||||
type: [
|
||||
{ required: true, message: t('pageTypePlaceholder'), trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
const dialogVisible = ref(false)
|
||||
const addEvent = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
|
||||
await formEl.validate(async (valid) => {
|
||||
if (valid) {
|
||||
const query = { type: formData.type, title: formData.title }
|
||||
const url = router.resolve({
|
||||
path: '/decorate/edit',
|
||||
query
|
||||
})
|
||||
window.open(url.href)
|
||||
dialogVisible.value = false
|
||||
formData.title = ''
|
||||
formData.type = ''
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 获取自定义页面类型
|
||||
const loadDiyTemplate = (addon = '') => {
|
||||
getDiyTemplate({ mode: '', addon }).then(res => {
|
||||
for (const key in pageType) {
|
||||
delete pageType[key]
|
||||
}
|
||||
|
||||
for (const key in res.data) {
|
||||
pageType[key] = res.data[key]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
loadDiyTemplate()
|
||||
|
||||
const apps: any = reactive({}) // 应用插件列表
|
||||
|
||||
getApps({}).then(res => {
|
||||
if (res.data) {
|
||||
for (const key in res.data) {
|
||||
apps[key] = res.data[key]
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// 根据所属插件,查询页面类型
|
||||
const handleSelectAddonChange = (value: any) => {
|
||||
diyPageTableData.searchParam.type = ''
|
||||
loadDiyTemplate(value)
|
||||
}
|
||||
|
||||
const diyPageTableData: any = reactive({
|
||||
page: 1,
|
||||
limit: 10,
|
||||
total: 0,
|
||||
loading: true,
|
||||
data: [],
|
||||
searchParam: {
|
||||
title: '',
|
||||
type: '',
|
||||
mode: '',
|
||||
addon_name: ''
|
||||
}
|
||||
})
|
||||
|
||||
const searchFormDiyPageRef = ref<FormInstance>()
|
||||
|
||||
// 获取自定义页面列表
|
||||
const loadDiyPageList = (page: number = 1) => {
|
||||
diyPageTableData.loading = true
|
||||
diyPageTableData.page = page
|
||||
|
||||
getDiyPageList({
|
||||
page: diyPageTableData.page,
|
||||
limit: diyPageTableData.limit,
|
||||
...diyPageTableData.searchParam
|
||||
}).then(res => {
|
||||
diyPageTableData.loading = false
|
||||
diyPageTableData.data = res.data.data
|
||||
diyPageTableData.total = res.data.total
|
||||
setTablePageStorage(diyPageTableData.page, diyPageTableData.limit, diyPageTableData.searchParam)
|
||||
}).catch(() => {
|
||||
diyPageTableData.loading = false
|
||||
})
|
||||
}
|
||||
|
||||
loadDiyPageList(getTablePageStorage(diyPageTableData.searchParam).page)
|
||||
|
||||
// 编辑自定义页面
|
||||
const editEvent = (data: any) => {
|
||||
const url = router.resolve({
|
||||
path: '/decorate/edit',
|
||||
query: { id: data.id }
|
||||
})
|
||||
window.open(url.href)
|
||||
}
|
||||
|
||||
// 设为使用
|
||||
const setUse = (id: any) => {
|
||||
setUseDiyPage({ id }).then(() => {
|
||||
loadDiyPageList(getTablePageStorage(diyPageTableData.searchParam).page)
|
||||
})
|
||||
}
|
||||
const repeat = ref(false)
|
||||
|
||||
// 复制页面
|
||||
const copyEvent = (id: any) => {
|
||||
ElMessageBox.confirm(t('diyPageCopyTips'), t('warning'),
|
||||
{
|
||||
confirmButtonText: t('confirm'),
|
||||
cancelButtonText: t('cancel'),
|
||||
type: 'warning'
|
||||
}
|
||||
).then(() => {
|
||||
if (repeat.value) return
|
||||
repeat.value = true
|
||||
|
||||
copyDiy({ id }).then((res: any) => {
|
||||
if (res.code == 1) {
|
||||
loadDiyPageList(getTablePageStorage(diyPageTableData.searchParam).page)
|
||||
}
|
||||
repeat.value = false
|
||||
}).catch(err => {
|
||||
repeat.value = false
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 删除自定义页面
|
||||
const deleteEvent = (id: number) => {
|
||||
ElMessageBox.confirm(t('diyPageDeleteTips'), t('warning'),
|
||||
{
|
||||
confirmButtonText: t('confirm'),
|
||||
cancelButtonText: t('cancel'),
|
||||
type: 'warning'
|
||||
}
|
||||
).then(() => {
|
||||
deleteDiyPage(id).then(() => {
|
||||
loadDiyPageList(getTablePageStorage(diyPageTableData.searchParam).page)
|
||||
}).catch(() => {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// 跳转去预览
|
||||
const toPreview = (data: any) => {
|
||||
const url = router.resolve({
|
||||
path: '/preview/wap',
|
||||
query: {
|
||||
page: data.type_page + '?id=' + data.id
|
||||
}
|
||||
})
|
||||
window.open(url.href)
|
||||
}
|
||||
|
||||
const tabShareType = ref('wechat')
|
||||
const sharePage = ref('')
|
||||
const shareFormId = ref(0)
|
||||
const shareFormData = reactive({
|
||||
wechat: {
|
||||
title: '',
|
||||
desc: '',
|
||||
url: ''
|
||||
},
|
||||
weapp: {
|
||||
title: '',
|
||||
url: ''
|
||||
}
|
||||
})
|
||||
|
||||
const shareDialogVisible = ref(false)
|
||||
const shareFormRules = computed(() => {
|
||||
return {}
|
||||
})
|
||||
|
||||
const shareFormRef = ref<FormInstance>()
|
||||
const openShare = async (row: any) => {
|
||||
shareFormId.value = row.id
|
||||
sharePage.value = row.title
|
||||
const share = row.share
|
||||
? JSON.parse(row.share)
|
||||
: {
|
||||
wechat: { title: '', desc: '', url: '' },
|
||||
weapp: { title: '', url: '' }
|
||||
}
|
||||
if (share) {
|
||||
shareFormData.wechat = share.wechat
|
||||
shareFormData.weapp = share.weapp
|
||||
}
|
||||
|
||||
shareDialogVisible.value = true
|
||||
}
|
||||
|
||||
const shareEvent = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
|
||||
await formEl.validate(async (valid) => {
|
||||
if (valid) {
|
||||
editDiyPageShare({
|
||||
id: shareFormId.value,
|
||||
share: JSON.stringify(shareFormData)
|
||||
}).then(() => {
|
||||
loadDiyPageList(getTablePageStorage(diyPageTableData.searchParam).page)
|
||||
shareDialogVisible.value = false
|
||||
}).catch(() => {
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const resetForm = (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
formEl.resetFields()
|
||||
loadDiyPageList()
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
388
admin-vben/src/app/views/diy/member.vue
Normal file
388
admin-vben/src/app/views/diy/member.vue
Normal file
@@ -0,0 +1,388 @@
|
||||
<template>
|
||||
<div class="main-container">
|
||||
<el-card class="box-card !border-none" shadow="never">
|
||||
<div class="flex flex-wrap min-w-[1200px]" v-if="page.use_template">
|
||||
<div class="page-item relative w-[340px] mr-[40px] pt-[90px] pb-[20px] bg-[#f7f7f7] bg-no-repeat">
|
||||
<p class="absolute top-[54px] left-[50%] translate-x-[-50%] w-[130px] text-[14px] truncate text-center">{{ page.use_template.title }}</p>
|
||||
|
||||
<div v-show="page.use_template.url" class="w-[320px] h-[550px] mx-auto">
|
||||
<iframe :id="'previewIframe_' + type" v-show="page.loadingIframe" class="w-[320px] h-[550px] mx-auto" :src="page.use_template.wapPreview" frameborder="0"></iframe>
|
||||
<div v-show="page.loadingDev" class="w-[320px] h-[550px] mx-auto bg-body pt-[20px] px-[20px]">
|
||||
<div class="font-bold text-xl mb-[40px]">{{ t('developTitle') }}</div>
|
||||
<div class="mb-[20px] flex flex-col">
|
||||
<text class="mb-[10px]">{{ t('wapDomain') }}</text>
|
||||
<el-input v-model.trim="wapDomain" :placeholder="t('wapDomainPlaceholder')" clearable />
|
||||
</div>
|
||||
<div class="flex">
|
||||
<el-button type="primary" @click="saveDomain()">{{ t('confirm') }}</el-button>
|
||||
<el-button type="primary" @click="settingTips()" plain>{{ t('settingTips') }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-show="!page.use_template.wapPreview" class="overflow-hidden w-[320px] h-[550px] mx-auto">
|
||||
<img class="max-w-full" v-if="page.use_template.cover" :src="img(page.use_template.cover)" />
|
||||
</div>
|
||||
|
||||
<div class="popup-wrap absolute inset-x-0 inset-y-0 select-none" :class="{ 'disabled': page.isDisabledPop }"></div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="w-[700px]">
|
||||
<div class="flex flex-wrap">
|
||||
<!-- 多应用切换启动页 -->
|
||||
<el-button type="primary" @click="showDialog = true" v-if="siteApps.length > 1">{{ t('changePage') }}</el-button>
|
||||
<el-button type="primary" @click="toDecorate()" v-show="page.use_template.action == 'decorate'" class="ml-[12px]">{{ t('decorate') }}</el-button>
|
||||
</div>
|
||||
|
||||
<div class="info-wrap">
|
||||
<div class="mt-[20px] p-[20px] flex items-center justify-between bg">
|
||||
<div>
|
||||
<div class="font-bold">{{ t('H5') }}</div>
|
||||
<el-form label-width="40px" class="mt-[5px]">
|
||||
<el-form-item :label="t('link')" class="mb-[5px]">
|
||||
<el-input readonly :value="page.shareUrl" class="!w-[400px]">
|
||||
<template #append>
|
||||
<el-button @click="copyEvent(page.shareUrl)" class="bg-primary copy">{{ t('copy') }}</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="text-[#999] text-base">{{ t('scanQRCodeOnRight') }}</div>
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<el-image class="w-[100px] h-[100px] mb-[5px]" :src="wapImage" />
|
||||
<div @click="toPreview()" class="text-primary text-base cursor-pointer">{{ t('preview') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-dialog v-model="showDialog" :title="t('pageSelectTips')" width="400px" :close-on-press-escape="true" :destroy-on-close="true" :close-on-click-modal="false">
|
||||
<div class="flex items-start">
|
||||
<el-scrollbar class="pl-4 h-[300px] flex-1">
|
||||
<div class="flex flex-wrap">
|
||||
<div v-for="(item, key) in pageType" :key="key"
|
||||
class="border border-br rounded-[3px] mr-[10px] mb-[10px] px-4 h-[32px] leading-[32px] cursor-pointer hover:bg-primary-light-9 px-[10px] hover:text-primary"
|
||||
:class="[key == link.name ? 'border-primary text-primary' : '']"
|
||||
@click="changeLink(key,item)">{{ item.title }}
|
||||
</div>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button>
|
||||
<el-button type="primary" @click="changePage()">{{ t('confirm') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref, watch, computed } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import { img } from '@/utils/common'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getDecoratePage, changeTemplate,getDiyTemplate } from '@/app/api/diy'
|
||||
import storage from '@/utils/storage'
|
||||
import QRCode from 'qrcode'
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
|
||||
const userStore = useUserStore()
|
||||
const siteApps = computed(()=>{
|
||||
return userStore.siteInfo.apps;
|
||||
})
|
||||
|
||||
const type: any = ref('DIY_MEMBER_INDEX');
|
||||
const page: any = reactive({})
|
||||
const router = useRouter()
|
||||
const wapDomain = ref('')
|
||||
const wapImage = ref('')
|
||||
const link: any = ref({
|
||||
name: ''
|
||||
})
|
||||
|
||||
// 切换页面
|
||||
const formData = reactive({
|
||||
type: '',
|
||||
name: '',
|
||||
page: '',
|
||||
title: '',
|
||||
action: ''
|
||||
})
|
||||
|
||||
const showDialog = ref(false)
|
||||
const pageType: any = reactive({}) // 页面类型
|
||||
|
||||
// 获取自定义页面类型
|
||||
getDiyTemplate({ type: 'member_index' }).then(res => {
|
||||
for (const key in res.data) {
|
||||
pageType[key] = res.data[key]
|
||||
}
|
||||
})
|
||||
|
||||
// 初始化数据
|
||||
const refreshData = () => {
|
||||
getDecoratePage({
|
||||
type : type.value
|
||||
}).then(res => {
|
||||
for (const key in res.data) {
|
||||
page[key] = res.data[key]
|
||||
}
|
||||
|
||||
link.value.name = page.use_template.name;
|
||||
link.value.title = page.use_template.title;
|
||||
link.value.page = page.use_template.page;
|
||||
link.value.action = page.use_template.action;
|
||||
|
||||
if (page.use_template.url) {
|
||||
page.loadingIframe = false // 加载iframe
|
||||
page.loadingDev = false // 加载开发环境配置
|
||||
page.isDisabledPop = false // 是否禁止打开遮罩层
|
||||
page.difference = 0 // 检测页面加载差异,小于1000毫秒,则配置wap端域名
|
||||
|
||||
wapDomain.value = page.domain_url.wap_domain
|
||||
page.wapUrl = page.domain_url.wap_url
|
||||
|
||||
let repeat = true; // 防重复执行
|
||||
|
||||
if (import.meta.env.MODE == 'development') {
|
||||
// 开发模式情况下,并且未配置wap域名,则获取缓存域名
|
||||
if (wapDomain.value && wapDomain.value.indexOf('localhost') != -1) {
|
||||
page.wapUrl = wapDomain.value + '/wap'
|
||||
repeat = false
|
||||
setDomain()
|
||||
}
|
||||
if (storage.get('wap_domain')) {
|
||||
page.wapUrl = storage.get('wap_domain')
|
||||
repeat = false
|
||||
setDomain()
|
||||
}
|
||||
}
|
||||
|
||||
if(repeat) {
|
||||
setDomain()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
refreshData()
|
||||
|
||||
const uniAppLoadStatus = ref(false) // uni-app 加载状态,true:加粗完成,false:未完成
|
||||
|
||||
// 监听 uni-app 端 是否加载完成
|
||||
window.addEventListener('message', (event) => {
|
||||
try {
|
||||
let data = {
|
||||
type: ''
|
||||
};
|
||||
if(typeof event.data == 'string') {
|
||||
data = JSON.parse(event.data)
|
||||
}else if(typeof event.data == 'object') {
|
||||
data = event.data
|
||||
}
|
||||
|
||||
if (data.type && ['appOnLaunch', 'appOnReady'].indexOf(data.type) != -1) {
|
||||
page.loadingDev = false // 禁用开发环境配置
|
||||
page.loadingIframe = true // 加载iframe
|
||||
let loadTime = new Date().getTime()
|
||||
page.difference = loadTime - page.timeIframe
|
||||
page.isDisabledPop = false // 是否禁止打开遮罩层
|
||||
uniAppLoadStatus.value = true // 加载完成
|
||||
}
|
||||
} catch (e) {
|
||||
initLoad()
|
||||
console.log('diy member 后台接受数据错误', e)
|
||||
}
|
||||
}, false)
|
||||
|
||||
// 将数据发送到uniapp
|
||||
const postMessage = () => {
|
||||
const diyData = JSON.stringify({
|
||||
type: 'appOnReady',
|
||||
message: '加载完成'
|
||||
})
|
||||
if (window['previewIframe_' + type.value]) window['previewIframe_' + type.value].contentWindow.postMessage(diyData, '*')
|
||||
}
|
||||
|
||||
// 初始化加载状态
|
||||
const initLoad = () => {
|
||||
page.loadingDev = true
|
||||
page.isDisabledPop = true
|
||||
page.loadingIframe = false
|
||||
}
|
||||
|
||||
const saveDomain = () => {
|
||||
if (wapDomain.value.trim().length == 0) {
|
||||
ElMessage({
|
||||
type: 'warning',
|
||||
message: `${t('wapDomainPlaceholder')}`,
|
||||
})
|
||||
return
|
||||
}
|
||||
const wapUrl = wapDomain.value + '/wap'
|
||||
storage.set({key: 'wap_domain', data: wapUrl})
|
||||
|
||||
if (page.use_template.url) {
|
||||
page.wapUrl = wapUrl
|
||||
setDomain()
|
||||
}
|
||||
setTimeout(() => {
|
||||
if (page.use_template.url) {
|
||||
page.loadingIframe = true // 加载iframe
|
||||
page.loadingDev = false // 加载开发环境配置
|
||||
page.isDisabledPop = false // 是否禁止打开遮罩层
|
||||
}
|
||||
}, 100 * 3)
|
||||
}
|
||||
|
||||
const settingTips = () => {
|
||||
window.open('https://www.kancloud.cn/niucloud/niucloud-admin-develop/3213393')
|
||||
}
|
||||
|
||||
const setDomain = () => {
|
||||
page.use_template.wapPreview = page.wapUrl + page.use_template.url
|
||||
page.shareUrl = page.wapUrl + page.page;
|
||||
QRCode.toDataURL(page.shareUrl, { errorCorrectionLevel: 'L', margin: 0, width: 100 }).then(url => {
|
||||
wapImage.value = url
|
||||
})
|
||||
|
||||
const send = ()=>{
|
||||
page.timeIframe = new Date().getTime()
|
||||
postMessage()
|
||||
}
|
||||
|
||||
// 同步发送一次消息
|
||||
send()
|
||||
|
||||
// 如果同步发送消息的 uni-app没有接收到回应,则定时发送消息
|
||||
let sendCount = 0;
|
||||
let timeInterVal = setInterval(()=>{
|
||||
// 接收 uni-app 发送的消息 或者 发送50次后未响应,则停止发送
|
||||
if(uniAppLoadStatus.value || sendCount >= 50){
|
||||
clearInterval(timeInterVal)
|
||||
return
|
||||
}
|
||||
|
||||
send()
|
||||
sendCount++;
|
||||
},200)
|
||||
|
||||
// 如果10秒内加载不出来,则需要配置域名
|
||||
setTimeout(() => {
|
||||
if (page.difference == 0) initLoad()
|
||||
}, 1000 * 10)
|
||||
}
|
||||
|
||||
// 跳转去装修
|
||||
const toDecorate = () => {
|
||||
const query: any = {
|
||||
back: '/site/diy/member'
|
||||
}
|
||||
if (page.use_template.id) {
|
||||
query.id = page.use_template.id
|
||||
} else if (page.use_template.type) {
|
||||
query.name = page.use_template.type
|
||||
} else if (page.use_template.url) {
|
||||
query.url = page.use_template.url
|
||||
}
|
||||
const url = router.resolve({
|
||||
path: '/decorate/edit',
|
||||
query
|
||||
})
|
||||
window.open(url.href)
|
||||
}
|
||||
|
||||
// 跳转去预览
|
||||
const toPreview = () => {
|
||||
let value = page.use_template.page
|
||||
if (page.use_template.url) {
|
||||
value = page.use_template.url
|
||||
} else if (page.use_template.id) {
|
||||
value += '?id=' + page.use_template.id
|
||||
}
|
||||
const url = router.resolve({
|
||||
path: '/preview/wap',
|
||||
query: {
|
||||
page:value
|
||||
}
|
||||
})
|
||||
window.open(url.href)
|
||||
}
|
||||
|
||||
// 切换页面链接
|
||||
const changeLink = (key:any,item: any) => {
|
||||
link.value.name = key;
|
||||
link.value.page = item.page;
|
||||
link.value.title = item.title;
|
||||
link.value.action = item.action;
|
||||
}
|
||||
|
||||
const isRepeat = ref(false)
|
||||
|
||||
const changePage = ()=>{
|
||||
formData.type = type.value;
|
||||
formData.name = link.value.name;
|
||||
formData.page = link.value.page;
|
||||
formData.title = link.value.title;
|
||||
formData.action = link.value.action;
|
||||
|
||||
if (isRepeat.value) return
|
||||
isRepeat.value = true
|
||||
|
||||
changeTemplate({
|
||||
...formData
|
||||
}).then((res) => {
|
||||
isRepeat.value = false
|
||||
showDialog.value = false;
|
||||
refreshData()
|
||||
})
|
||||
}
|
||||
|
||||
// 复制
|
||||
const { copy, isSupported, copied } = useClipboard()
|
||||
const copyEvent = (text: string) => {
|
||||
if (!isSupported.value) {
|
||||
ElMessage({
|
||||
message: t('notSupportCopy'),
|
||||
type: 'warning'
|
||||
})
|
||||
}
|
||||
copy(text)
|
||||
}
|
||||
|
||||
watch(copied, () => {
|
||||
if (copied.value) {
|
||||
ElMessage({
|
||||
message: t('copySuccess'),
|
||||
type: 'success'
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.page-item {
|
||||
background-image: url(@/app/assets/images/iphone_bg.png);
|
||||
background-color: var(--el-bg-color);
|
||||
background-size: 100%;
|
||||
|
||||
.popup-wrap {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.popup-wrap:not(.disabled) {
|
||||
display: block !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
314
admin-vben/src/app/views/diy/route.vue
Normal file
314
admin-vben/src/app/views/diy/route.vue
Normal file
@@ -0,0 +1,314 @@
|
||||
<template>
|
||||
<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>
|
||||
|
||||
<el-card class="box-card !border-none my-[10px] table-search-wrap" shadow="never">
|
||||
<el-form :inline="true" :model="diyRouteTableData.searchParam" ref="searchFormDiyRouteRef">
|
||||
<el-form-item :label="t('title')" prop="title">
|
||||
<el-input v-model.trim="diyRouteTableData.searchParam.title" :placeholder="t('titlePlaceholder')" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('forAddon')" prop="addon_name">
|
||||
<el-select v-model="diyRouteTableData.searchParam.addon_name" :placeholder="t('forAddonPlaceholder')">
|
||||
<el-option :label="t('all')" value="" />
|
||||
<el-option v-for="(item, key) in apps" :label="item.title" :value="key" :key="key"/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" @click="loadDiyRouteList()">{{ t('search') }}</el-button>
|
||||
<el-button @click="resetForm(searchFormDiyRouteRef)">{{ t('reset') }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<el-table :data="diyRouteTableData.data" size="large" v-loading="diyRouteTableData.loading">
|
||||
<template #empty>
|
||||
<span>{{ !diyRouteTableData.loading ? t('emptyData') : '' }}</span>
|
||||
</template>
|
||||
|
||||
<el-table-column prop="title" :label="t('title')" min-width="70" />
|
||||
<el-table-column prop="addon_title" :label="t('forAddon')" min-width="70">
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.addon_info.title }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="page" :label="t('wapUrl')" min-width="230">
|
||||
<template #default="{ row }">
|
||||
<span class="mr-[10px]">{{ wapDomain + row.page }}</span>
|
||||
<el-button type="primary" link @click="copyEvent(wapDomain + row.page)">{{ t('copy') }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="page" :label="t('weappUrl')" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<span class="mr-[10px]">{{ row.page }}</span>
|
||||
<el-button type="primary" link @click="copyEvent(row.page)">{{ t('copy') }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="t('share')" fixed="right" align="right" min-width="40">
|
||||
<template #default="{ row }">
|
||||
<el-button v-if="row.is_share == 1" type="primary" link @click="openShare(row)">{{ t('shareSet') }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<div class="mt-[16px] flex justify-end">
|
||||
<el-pagination v-model:current-page="diyRouteTableData.page" v-model:page-size="diyRouteTableData.limit" layout="total, sizes, prev, pager, next, jumper" :total="diyRouteTableData.total" @size-change="getDiyRouteListFn" @current-change="loadDiyRouteList" />
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<!-- 分享设置-->
|
||||
<el-dialog v-model="shareDialogVisible" :title="t('shareSet')" width="30%">
|
||||
|
||||
<el-tabs v-model="tabShareType">
|
||||
<el-tab-pane :label="t('wechat')" name="wechat"></el-tab-pane>
|
||||
<el-tab-pane :label="t('weapp')" name="weapp"></el-tab-pane>
|
||||
</el-tabs>
|
||||
<el-form :model="shareFormData[tabShareType]" label-width="90px" ref="shareFormRef" :rules="shareFormRules">
|
||||
<el-form-item :label="t('sharePage')">
|
||||
<span>{{ sharePage }}</span>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('shareTitle')" prop="title">
|
||||
<el-input v-model.trim="shareFormData[tabShareType].title" :placeholder="t('shareTitlePlaceholder')" clearable maxlength="30" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('shareDesc')" prop="desc" v-if="tabShareType == 'wechat'">
|
||||
<el-input v-model.trim="shareFormData[tabShareType].desc" :placeholder="t('shareDescPlaceholder')" type="textarea" rows="4" clearable maxlength="100" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('shareImageUrl')" prop="url">
|
||||
<upload-image v-model="shareFormData[tabShareType].url" :limit="1" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="shareDialogVisible = false">{{ t('cancel') }}</el-button>
|
||||
<el-button type="primary" @click="shareEvent(shareFormRef)">{{ t('confirm') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref, watch, computed } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import { getDiyRouteAppList, getDiyTemplate, getDiyRouteList, getDiyRouteInfo, editDiyRouteShare } from '@/app/api/diy'
|
||||
import { ElMessage, FormInstance } from 'element-plus'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
import { getUrl } from '@/app/api/sys'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const pageName = route.meta.title
|
||||
|
||||
const pageTemplate: any = reactive({})
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
const dialogVisible = ref(false)
|
||||
|
||||
const diyRouteTableData = reactive({
|
||||
page: 1,
|
||||
limit: 10,
|
||||
total: 0,
|
||||
loading: true,
|
||||
data: [],
|
||||
searchParam: {
|
||||
title: '',
|
||||
addon_name: ''
|
||||
}
|
||||
})
|
||||
|
||||
const diyRouteList: any = ref([])
|
||||
|
||||
const wapDomain = ref('')
|
||||
const getDomain = async () => {
|
||||
wapDomain.value = (await getUrl()).data.wap_url
|
||||
}
|
||||
|
||||
getDomain()
|
||||
|
||||
const apps: any = reactive({}) // 应用插件列表
|
||||
|
||||
getDiyRouteAppList().then(res => {
|
||||
if (res.data) {
|
||||
for (const key in res.data) {
|
||||
apps[key] = res.data[key]
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const getDiyRouteListFn = () => {
|
||||
getDiyRouteList({}).then(res => {
|
||||
diyRouteTableData.loading = false
|
||||
diyRouteList.value = cloneDeep(res.data)
|
||||
loadDiyRouteList(diyRouteTableData.page)
|
||||
}).catch(() => {
|
||||
diyRouteTableData.loading = false
|
||||
})
|
||||
}
|
||||
|
||||
getDiyRouteListFn()
|
||||
|
||||
/**
|
||||
* 获取自定义路由列表
|
||||
*/
|
||||
const loadDiyRouteList = (page: number = 1) => {
|
||||
diyRouteTableData.page = page
|
||||
|
||||
const tempData = cloneDeep(diyRouteList.value)
|
||||
const data: any = []
|
||||
|
||||
// 筛选条件
|
||||
for (let i = 0; i < tempData.length; i++) {
|
||||
let isAdd = true
|
||||
if (diyRouteTableData.searchParam.title && tempData[i].title.indexOf(diyRouteTableData.searchParam.title) == -1) {
|
||||
isAdd = false
|
||||
}
|
||||
|
||||
if (diyRouteTableData.searchParam.addon_name && tempData[i].addon_info && tempData[i].addon_info.key != diyRouteTableData.searchParam.addon_name) {
|
||||
isAdd = false
|
||||
}
|
||||
|
||||
if (isAdd) {
|
||||
data.push(tempData[i])
|
||||
}
|
||||
}
|
||||
|
||||
diyRouteTableData.total = data.length
|
||||
const len = Math.ceil(data.length / diyRouteTableData.limit)
|
||||
const dataGather = []
|
||||
|
||||
for (let i = 0; i < len; i++) {
|
||||
dataGather[i] = data.splice(0, diyRouteTableData.limit)
|
||||
}
|
||||
diyRouteTableData.data = dataGather[diyRouteTableData.page - 1]
|
||||
}
|
||||
|
||||
// 获取自定义页面模板
|
||||
getDiyTemplate({}).then(res => {
|
||||
for (const key in res.data) {
|
||||
pageTemplate[key] = res.data[key]
|
||||
}
|
||||
})
|
||||
|
||||
const searchFormDiyRouteRef = ref<FormInstance>()
|
||||
|
||||
/**
|
||||
* 复制
|
||||
*/
|
||||
const { copy, isSupported, copied } = useClipboard()
|
||||
const copyEvent = (text: string) => {
|
||||
if (!isSupported.value) {
|
||||
ElMessage({
|
||||
message: t('notSupportCopy'),
|
||||
type: 'warning'
|
||||
})
|
||||
}
|
||||
copy(text)
|
||||
}
|
||||
|
||||
watch(copied, () => {
|
||||
if (copied.value) {
|
||||
ElMessage({
|
||||
message: t('copySuccess'),
|
||||
type: 'success'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const tabShareType = ref('wechat')
|
||||
const sharePage = ref('')
|
||||
const shareFormId = ref(0)
|
||||
const diyRouteData = reactive({
|
||||
title: '',
|
||||
name: '',
|
||||
page: '',
|
||||
is_share: 0,
|
||||
sort: 0
|
||||
})
|
||||
const shareFormData = reactive({
|
||||
wechat: {
|
||||
title: '',
|
||||
desc: '',
|
||||
url: ''
|
||||
},
|
||||
weapp: {
|
||||
title: '',
|
||||
url: ''
|
||||
}
|
||||
})
|
||||
const shareDialogVisible = ref(false)
|
||||
const shareFormRules = computed(() => {
|
||||
return {}
|
||||
})
|
||||
|
||||
const shareFormRef = ref<FormInstance>()
|
||||
const openShare = async (row: any) => {
|
||||
// 基础页面
|
||||
const info = (await getDiyRouteInfo({
|
||||
name: row.name
|
||||
})).data
|
||||
|
||||
if (info.title) {
|
||||
row.id = info.id
|
||||
row.title = info.title
|
||||
row.name = info.name
|
||||
row.page = info.page
|
||||
row.is_share = info.is_share
|
||||
row.sort = info.sort
|
||||
row.share = info.share
|
||||
}
|
||||
|
||||
diyRouteData.title = row.title
|
||||
diyRouteData.name = row.name
|
||||
diyRouteData.page = row.page
|
||||
diyRouteData.is_share = row.is_share
|
||||
diyRouteData.sort = row.sort
|
||||
|
||||
shareFormId.value = row.id
|
||||
sharePage.value = row.title
|
||||
const share = row.share
|
||||
? JSON.parse(row.share)
|
||||
: {
|
||||
wechat: { title: '', desc: '', url: '' },
|
||||
weapp: { title: '', url: '' }
|
||||
}
|
||||
if (share) {
|
||||
shareFormData.wechat = share.wechat
|
||||
shareFormData.weapp = share.weapp
|
||||
}
|
||||
|
||||
shareDialogVisible.value = true
|
||||
}
|
||||
|
||||
const shareEvent = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
|
||||
await formEl.validate(async (valid) => {
|
||||
if (valid) {
|
||||
const save = editDiyRouteShare
|
||||
save({
|
||||
id: shareFormId.value,
|
||||
share: JSON.stringify(shareFormData),
|
||||
...diyRouteData
|
||||
}).then(() => {
|
||||
getDiyRouteListFn()
|
||||
shareDialogVisible.value = false
|
||||
}).catch(() => {
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const resetForm = (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
formEl.resetFields()
|
||||
getDiyRouteListFn()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
96
admin-vben/src/app/views/diy/tabbar.vue
Normal file
96
admin-vben/src/app/views/diy/tabbar.vue
Normal file
@@ -0,0 +1,96 @@
|
||||
<template>
|
||||
<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>
|
||||
|
||||
<el-table class="mt-[20px]" :data="bottomNavTableData.data" size="large" v-loading="bottomNavTableData.loading">
|
||||
<template #empty>
|
||||
<span>{{ !bottomNavTableData.loading ? t('emptyData') : '' }}</span>
|
||||
</template>
|
||||
|
||||
<el-table-column prop="title" :label="t('title')" min-width="120" >
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.info.title }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="key" :label="t('key')" min-width="120"/>
|
||||
|
||||
<el-table-column :label="t('type')" min-width="120">
|
||||
<template #default="{ row }">
|
||||
<span>{{ row.info.type === 'app' ? t('app') : t('addon') }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="t('operation')" fixed="right" align="right" min-width="160">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="editEvent(row)">{{ t('edit') }}</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<div class="mt-[16px] flex justify-end">
|
||||
<el-pagination v-model:current-page="bottomNavTableData.page" v-model:page-size="bottomNavTableData.limit"
|
||||
layout="total, sizes, prev, pager, next, jumper" :total="bottomNavTableData.total"
|
||||
@size-change="loadbottomNavList()" @current-change="loadbottomNavList" />
|
||||
</div>
|
||||
|
||||
</el-card>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import { getDiyBottomList } from '@/app/api/diy'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const pageName = route.meta.title
|
||||
|
||||
const bottomNavTableData: any = reactive({
|
||||
page: 1,
|
||||
limit: 10,
|
||||
total: 0,
|
||||
loading: true,
|
||||
data: [],
|
||||
})
|
||||
|
||||
// 获取自定义页面列表
|
||||
const loadBottomNavList = (page: number = 1) => {
|
||||
bottomNavTableData.loading = true
|
||||
bottomNavTableData.page = page
|
||||
|
||||
getDiyBottomList({}).then(res => {
|
||||
bottomNavTableData.loading = false
|
||||
|
||||
const len = Math.ceil(res.data.length / bottomNavTableData.limit)
|
||||
const data = cloneDeep(res.data)
|
||||
const dataGather = []
|
||||
for (let i = 0; i < len; i++) {
|
||||
dataGather[i] = data.splice(0, bottomNavTableData.limit)
|
||||
}
|
||||
bottomNavTableData.data = dataGather[bottomNavTableData.page - 1]
|
||||
|
||||
bottomNavTableData.total = res.data.length
|
||||
}).catch(() => {
|
||||
bottomNavTableData.loading = false
|
||||
})
|
||||
}
|
||||
|
||||
loadBottomNavList()
|
||||
|
||||
// 编辑底部导航
|
||||
const editEvent = (data: any) => {
|
||||
router.push('/diy/tabbar_edit?key=' + data.key)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
273
admin-vben/src/app/views/diy/tabbar_edit.vue
Normal file
273
admin-vben/src/app/views/diy/tabbar_edit.vue
Normal file
@@ -0,0 +1,273 @@
|
||||
<template>
|
||||
<div class="main-container">
|
||||
<el-card class="card !border-none" shadow="never">
|
||||
<el-page-header :content="pageName" :icon="ArrowLeft" @back="back()" />
|
||||
</el-card>
|
||||
|
||||
<el-card class="box-card mt-[15px] !border-none" shadow="never" v-loading="loading">
|
||||
<div class="flex">
|
||||
<div class="w-[360px] h-[400px] absolute mr-[30px] border-[1px] border-gray-300">
|
||||
<div class="flex items-center justify-between absolute h-[60px] left-[0px] right-[0px] bottom-[0px] border-[1px] border-primary" :style="{ 'backgroundColor': diyBottomData.value.backgroundColor }">
|
||||
<div class="flex flex-1 flex-col items-center justify-center" v-for="(item, index) in diyBottomData.value.list" :key="'b' + index">
|
||||
<el-image class="w-[22px] h-[22px] mb-[5px] leading-1" :src="img(item.iconPath)" :fit="contain" v-if="['1', '2'].includes(diyBottomData.value.type.toString())">
|
||||
<template #error>
|
||||
<div class="image-slot flex justify-center items-center mt-1">
|
||||
<el-icon><Picture class="text-3xl text-gray-500" /></el-icon>
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
<span class="text-[12px]" v-if="['1', '3'].includes(diyBottomData.value.type.toString())" :style="{ 'color': diyBottomData.value.textColor }">{{ item.text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 ml-[430px]">
|
||||
<div class="flex items-center border-l-[3px] border-primary pl-[5px] leading-[1.1] mt-[10px]">
|
||||
<span class="text-[14px]">{{ t('editing') }}</span>
|
||||
<span class="text-[14px] text-primary mx-[3px]">{{ diyBottomData.info.title }}</span>
|
||||
<span class="text-[14px]">{{ t('bottomNav') }}</span>
|
||||
<span class="text-[12px] ml-[8px] text-gray-500">{{ t('bottomNavHint') }}</span>
|
||||
</div>
|
||||
<el-form :model="diyBottomData.value" label-width="100px" ref="formRef">
|
||||
<el-tabs v-model="activeName" class="demo-tabs mt-[15px]">
|
||||
<el-tab-pane :label="t('navImage')" name="navPicture">
|
||||
<div ref="navItemRef">
|
||||
<div v-for="(item,index) in diyBottomData.value.list" :key="'a'+index" :data-id="index" class="item-wrap border-2 border-dashed pt-[18px] m-[10px] mb-[15px] relative list-item" :class="{ 'not-sort': useDrag }">
|
||||
<el-form-item :label="t('navIconOne')">
|
||||
<div class="flex align-center">
|
||||
<div class="flex flex-col justify-center items-center">
|
||||
<upload-image v-model="item.iconPath" width="60px" height="60px" :limit="1" />
|
||||
<span class="mr-[10px] text-sm">{{t('uploadImgUnselected')}}</span>
|
||||
</div>
|
||||
<div class="flex flex-col justify-center items-center">
|
||||
<upload-image v-model="item.iconSelectPath" width="60px" height="60px" :limit="1" />
|
||||
<span class="mr-[10px] text-sm">{{t('uploadImgSelected')}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('navTitleOne')">
|
||||
<el-input class="!w-[215px]" v-model.trim="item.text" :placeholder="t('titleContent')" maxlength="5" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('navLinkOne')">
|
||||
<diy-link v-model="item.link" :ignore="['DIY_JUMP_OTHER_APPLET']" @confirm="diyLinkFn" />
|
||||
</el-form-item>
|
||||
<el-icon class="close-icon cursor-pointer -top-[11px] -right-[8px]" @click="deleteNav(index)">
|
||||
<CircleCloseFilled />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-button type="primary" class="mt-[15px]" v-show="diyBottomData.value.list.length < 5" @click="addNav">{{ t('addnav') }}</el-button>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="t('styleSet')" name="setStyle">
|
||||
<el-form-item :label="t('navType')">
|
||||
<el-radio-group v-model="diyBottomData.value.type" class="ml-4">
|
||||
<el-radio label="1" size="large">{{ t('imageText') }}</el-radio>
|
||||
<el-radio label="2" size="large">{{ t('image') }}</el-radio>
|
||||
<el-radio label="3" size="large">{{ t('text') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('textColor')">
|
||||
<div class="flex align-center">
|
||||
<el-color-picker v-model="diyBottomData.value.textColor" />
|
||||
<el-input class="ml-[10px]" v-model.trim="diyBottomData.value.textColor" disabled />
|
||||
<el-button class="ml-[10px]" type="primary" @click="diyBottomData.value.textColor = '#333333'">{{ t('reset') }}</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('textSelectColor')">
|
||||
<div class="flex align-center">
|
||||
<el-color-picker v-model="diyBottomData.value.textHoverColor" />
|
||||
<el-input class="ml-[10px]" v-model.trim="diyBottomData.value.textHoverColor" disabled />
|
||||
<el-button class="ml-[10px]" type="primary" @click="diyBottomData.value.textHoverColor = '#333333'">{{ t('reset') }}</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('backgroundColor')">
|
||||
<div class="flex align-center">
|
||||
<el-color-picker v-model="diyBottomData.value.backgroundColor" />
|
||||
<el-input class="ml-[10px]" v-model.trim="diyBottomData.value.backgroundColor" disabled />
|
||||
<el-button class="ml-[10px]" type="primary" @click="diyBottomData.value.backgroundColor = '#FFFFFF'">{{ t('reset') }}</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
<div class="fixed-footer-wrap">
|
||||
<div class="fixed-footer">
|
||||
<el-button type="primary" @click="onSave(formRef)">{{ t('save') }}</el-button>
|
||||
<el-button @click="back()">{{ t('back') }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, onMounted, nextTick } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import { img } from '@/utils/common'
|
||||
import type { FormInstance, ElNotification } from 'element-plus'
|
||||
import { ArrowLeft } from '@element-plus/icons-vue'
|
||||
import { getDiyBottomConfig, setDiyBottomConfig } from '@/app/api/diy'
|
||||
import Sortable from 'sortablejs'
|
||||
import { range } from 'lodash-es'
|
||||
import { useRoute,useRouter } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const pageName = route.meta.title
|
||||
|
||||
const activeName = ref<string>('navPicture')
|
||||
const loading = ref<boolean>(false)
|
||||
route.query.key = route.query.key || '';
|
||||
|
||||
// 底部导航数据
|
||||
const diyBottomData = reactive({
|
||||
key:'',
|
||||
info:{},
|
||||
value:{
|
||||
backgroundColor: '#FFFFFF',
|
||||
textColor: '#333333',
|
||||
textHoverColor: '#333333',
|
||||
type: '1',
|
||||
list: []
|
||||
}
|
||||
})
|
||||
|
||||
// 底部导航项数据
|
||||
const diyBottomItemData = reactive({
|
||||
text: '',
|
||||
link: {
|
||||
name: '',
|
||||
title: '',
|
||||
parent: '',
|
||||
url: ''
|
||||
},
|
||||
iconSelectPath: '',
|
||||
iconPath: ''
|
||||
})
|
||||
|
||||
// 添加导航
|
||||
const addNav = (): void => {
|
||||
if (diyBottomData.value.list.length >= 5) return
|
||||
diyBottomData.value.list.push({ ...diyBottomItemData })
|
||||
}
|
||||
|
||||
addNav()
|
||||
|
||||
// 删除导航
|
||||
const deleteNav = (index:any): void => {
|
||||
const data = diyBottomData.value.list
|
||||
data.splice(index, 1)
|
||||
}
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
/**
|
||||
* 获取导航数据
|
||||
*/
|
||||
const getDiyBottomFn = () => {
|
||||
loading.value = true
|
||||
|
||||
getDiyBottomConfig({
|
||||
key:route.query.key
|
||||
}).then(res => {
|
||||
loading.value = false
|
||||
Object.keys(diyBottomData).forEach((item, index) => {
|
||||
diyBottomData[item] = res.data[item]
|
||||
})
|
||||
}).catch(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
getDiyBottomFn()
|
||||
|
||||
// 保存导航数据
|
||||
const onSave = async (formEl: FormInstance | undefined) => {
|
||||
if (verifyFn()) return false
|
||||
|
||||
if (loading.value || !formEl) return
|
||||
await formEl.validate(async (valid) => {
|
||||
if (valid) {
|
||||
loading.value = true
|
||||
setDiyBottomConfig({key: diyBottomData.key, value: diyBottomData.value}).then(res => {
|
||||
loading.value = false
|
||||
}).catch(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const back = () => {
|
||||
router.push('/diy/tabbar')
|
||||
}
|
||||
|
||||
// 验证
|
||||
const verifyFn = (): boolean => {
|
||||
if (diyBottomData.value.list.length < 2) {
|
||||
ElNotification({
|
||||
type: 'error',
|
||||
message: t('leastTwoNav')
|
||||
})
|
||||
return true
|
||||
}
|
||||
try {
|
||||
const msg = ref<string>('')
|
||||
diyBottomData.value.list.forEach((item: any, index) => {
|
||||
if (!item.iconPath) msg.value = `${t('pleaseUpload')}${index + 1}${t('navIcon')}`
|
||||
if (!item.iconSelectPath) msg.value = `${t('pleaseUpload')}${index + 1}${t('navSelectIcon')}`
|
||||
if (!item.text) msg.value = `${t('pleaseEnter')}[${index + 1}${t('navTitle')}`
|
||||
if (!item.link.url) msg.value = `${t('pleaseChoose')}${index + 1}${t('navLink')}`
|
||||
if (msg.value) {
|
||||
ElNotification({
|
||||
type: 'error',
|
||||
message: msg.value
|
||||
})
|
||||
throw Error()
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const navItemRef = ref()
|
||||
|
||||
onMounted(() => {
|
||||
const sortable = Sortable.create(navItemRef.value, {
|
||||
group: 'item-wrap',
|
||||
animation: 200,
|
||||
filter: '.not-sort', // 过滤.not-sort的元素
|
||||
onEnd: event => {
|
||||
const temp = diyBottomData.value.list[event.oldIndex!]
|
||||
diyBottomData.value.list.splice(event.oldIndex!, 1)
|
||||
diyBottomData.value.list.splice(event.newIndex!, 0, temp)
|
||||
nextTick(() => {
|
||||
sortable.sort(
|
||||
range(diyBottomData.value.list.length).map(value => {
|
||||
return value.toString()
|
||||
})
|
||||
)
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const useDrag = ref(false)
|
||||
const diyLinkFn = (val) => {
|
||||
useDrag.value = val
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.close-icon {
|
||||
display: none;
|
||||
position: absolute !important;
|
||||
font-size: 20px !important;
|
||||
color: #7d7b7b !important;
|
||||
}
|
||||
|
||||
.list-item:hover .close-icon {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
97
admin-vben/src/app/views/diy/theme_style.vue
Normal file
97
admin-vben/src/app/views/diy/theme_style.vue
Normal file
@@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<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>
|
||||
|
||||
<el-table :data="data" size="large" class="mt-[20px]" v-loading="loading">
|
||||
<template #empty>
|
||||
<span>{{ !loading ? t('emptyData') : '' }}</span>
|
||||
</template>
|
||||
|
||||
<el-table-column label="应用" min-width="120" >
|
||||
<template #default="{ row }">
|
||||
<div class="flex items-center">
|
||||
<el-image class="w-[40px] h-[40px] rounded-md overflow-hidden" :src="img(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.png" alt="">
|
||||
</div>
|
||||
</template>
|
||||
</el-image>
|
||||
<div class="flex-1 ml-2 truncate">{{ row.addon_title }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="配色名称" min-width="120" >
|
||||
<template #default="{ row }">
|
||||
<div>{{ row.title }}</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column label="配色方案" min-width="120" >
|
||||
<template #default="{ row }">
|
||||
<div class="rounded-[3px] inline-flex items-center justify-center border-[1px] border-solid border-[#f2f2f2] overflow-hidden" v-if="row.theme">
|
||||
<span class="w-[18px] h-[18px]" :style="{backgroundColor: row.theme['--primary-color']}"></span>
|
||||
<span class="w-[18px] h-[18px]" :style="{backgroundColor: row.theme['--primary-help-color2']}"></span>
|
||||
<span class="w-[18px] h-[18px]" :style="{backgroundColor: row.theme['--primary-color-dark']}"></span>
|
||||
</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="editEvent(row)">编辑</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-card>
|
||||
|
||||
<theme-list ref="themeListRef" @confirm="initData()" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref, watch, computed } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import { img } from '@/utils/common'
|
||||
import { getDiyTheme } from '@/app/api/diy'
|
||||
import { useRoute } from 'vue-router'
|
||||
import themeList from './components/theme-list.vue'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
|
||||
const route = useRoute()
|
||||
const pageName = route.meta.title
|
||||
const loading = ref(true)
|
||||
const themeListRef = ref(null)
|
||||
const data = ref([])
|
||||
|
||||
const initData = () => {
|
||||
loading.value = true;
|
||||
getDiyTheme({}).then((res) => {
|
||||
let obj = cloneDeep(res.data);
|
||||
for(let key in obj){
|
||||
obj[key].key = key;
|
||||
}
|
||||
data.value = Object.values(obj);
|
||||
loading.value = false;
|
||||
})
|
||||
}
|
||||
initData()
|
||||
|
||||
// 编辑
|
||||
const editEvent = (data)=> {
|
||||
themeListRef.value.open(data)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
<!-- 设置弹窗标题 -->
|
||||
<style scoped>
|
||||
/* 使用深度选择器 */
|
||||
::v-deep .custom-theme-dialog .el-dialog__title {
|
||||
font-size: 16px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-image v-for="(item,index) in props.data.handle_field_value" :src="img(item)" class="w-[70px] h-[70px]" :class="{ 'mr-[5px]' : (index + 1) < props.data.handle_field_value.length }" fit="contain" :preview-src-list="imgList" :zoom-rate="1.2" :max-scale="7"
|
||||
:min-scale="0.2" :initial-index="index" :hide-on-click-modal="true" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { img } from '@/utils/common'
|
||||
import { t } from "@/lang";
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
})
|
||||
const imgList = computed(() => {
|
||||
return props.data.handle_field_value.map((item: any) => {
|
||||
return img(item)
|
||||
})
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<div class="form-render">{{ props.data.render_value }}</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { t } from "@/lang";
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.form-render {
|
||||
word-wrap: break-word; /* 长单词或长字符串自动换行 */
|
||||
word-break: break-word; /* 强制断词换行 */
|
||||
white-space: pre-wrap; /* 保持空格和换行 */
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<!-- 内容 -->
|
||||
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
|
||||
|
||||
<!-- 表单组件 字段内容设置 -->
|
||||
<slot name="field"></slot>
|
||||
<el-form label-width="100px" class="px-[10px]">
|
||||
<el-form-item :label="t('地址格式')">
|
||||
<el-radio-group v-model="diyStore.editComponent.addressFormat" class="!block">
|
||||
<el-radio class="!block" label="province/city/district/address">{{ t('省/市/区/街道/详细地址') }}</el-radio>
|
||||
<el-radio class="!block" label="province/city/district/street">{{ t('省/市/区/街道(镇)') }}</el-radio>
|
||||
<el-radio class="!block" label="province/city/district">{{ t('省/市/区(县)') }}</el-radio>
|
||||
<el-radio class="!block" label="province/city">{{ t('省/市') }}</el-radio>
|
||||
<el-radio class="!block" label="province">{{ t('省') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 表单组件 其他设置 -->
|
||||
<slot name="other"></slot>
|
||||
<el-form label-width="100px" class="px-[10px]">
|
||||
<el-form-item class="display-block">
|
||||
<template #label>
|
||||
<div class="flex items-center">
|
||||
<span class="mr-[3px]">{{ t('隐私保护') }}</span>
|
||||
<el-tooltip effect="light" placement="top">
|
||||
<template #content>
|
||||
<p>会自动将提交的个人信息做加密展示。</p>
|
||||
<p>适用于公开展示收集的数据且不暴露用户隐私。</p>
|
||||
</template>
|
||||
<el-icon>
|
||||
<QuestionFilled color="#999999" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<el-switch v-model="diyStore.editComponent.field.privacyProtection" :disabled ="diyStore.editComponent.addressFormat != 'province/city/district/address'" />
|
||||
<div class="text-sm text-gray-400">{{ t('提交后自动隐藏地址,仅管理员可查看') }}</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 样式 -->
|
||||
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
|
||||
|
||||
<!-- 表单组件 字段样式 -->
|
||||
<slot name="style-field"></slot>
|
||||
|
||||
<!-- 组件样式 -->
|
||||
<slot name="style"></slot>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { t } from '@/lang'
|
||||
import { ref,watch } from 'vue'
|
||||
import useDiyStore from '@/stores/modules/diy'
|
||||
|
||||
const diyStore = useDiyStore()
|
||||
diyStore.editComponent.ignore = ['componentBgUrl'] // 忽略公共属性
|
||||
|
||||
// 组件验证
|
||||
diyStore.editComponent.verify = (index: number) => {
|
||||
const res = { code: true, message: '' }
|
||||
return res
|
||||
}
|
||||
watch(
|
||||
() => diyStore.editComponent.addressFormat,
|
||||
(newVal) => {
|
||||
if (newVal !== 'province/city/district/address') {
|
||||
diyStore.editComponent.field.privacyProtection = false
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
defineExpose({})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,194 @@
|
||||
<template>
|
||||
<!-- 内容 -->
|
||||
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
|
||||
|
||||
<!-- 表单组件 字段内容设置 -->
|
||||
<slot name="field"></slot>
|
||||
<el-form label-width="100px" class="px-[10px]">
|
||||
<el-form-item :label="t('style')">
|
||||
<el-radio-group v-model="diyStore.editComponent.style">
|
||||
<el-radio label="style-1">{{ t('defaultSources') }}</el-radio>
|
||||
<el-radio label="style-2">{{ t('listStyle') }}</el-radio>
|
||||
<el-radio label="style-3">{{ t('dropDownStyle') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('option')">
|
||||
<div ref="formCheckboxRef">
|
||||
<div v-for="(option, index) in diyStore.editComponent.options" :key="option.id" class="option-item flex items-center mb-[15px]">
|
||||
<el-input v-model="diyStore.editComponent.options[index].text" class="!w-[215px]" :placeholder="t('optionPlaceholder')" maxlength="30" clearable />
|
||||
<span v-if="diyStore.editComponent.options.length > 1" @click="removeOption(index)" class="cursor-pointer ml-[5px] nc-iconfont nc-icon-shanchu-yuangaizhiV6xx"></span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-primary cursor-pointer mr-[10px]" @click="addOption">{{ t('addSingleOption') }}</span>
|
||||
<el-popover :visible="visible" placement="bottom" :width="300">
|
||||
<p class="mb-[5px]">{{ t('addMultipleOption') }}</p>
|
||||
<p class="text-[#888] text-[12px] mb-[5px]">{{ t('addOptionTips') }}</p>
|
||||
<el-input v-model.trim="optionsValue" type="textarea" clearable maxlength="200" show-word-limit />
|
||||
<div class="mt-[10px] text-right">
|
||||
<el-button size="small" text @click="visible = false">{{ t('cancel') }}</el-button>
|
||||
<el-button size="small" type="primary" @click="batchAddOptions">{{ t('confirm') }}</el-button>
|
||||
</div>
|
||||
<template #reference>
|
||||
<span class="text-primary cursor-pointer" @click="visible = true">{{ t('addMultipleOption') }}</span>
|
||||
</template>
|
||||
</el-popover>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 表单组件 其他设置 -->
|
||||
<slot name="other"></slot>
|
||||
<!-- <el-form label-width="100px" class="px-[10px]">-->
|
||||
<!-- <el-form-item class="display-block">-->
|
||||
<!-- <template #label>-->
|
||||
<!-- <div class="flex items-center">-->
|
||||
<!-- <span class="mr-[3px]">{{ t('隐私保护') }}</span>-->
|
||||
<!-- <el-tooltip effect="light" placement="top">-->
|
||||
<!-- <template #content>-->
|
||||
<!-- <p>会自动将提交的个人信息做加密展示。</p>-->
|
||||
<!-- <p>适用于公开展示收集的数据且不暴露用户隐私。</p>-->
|
||||
<!-- </template>-->
|
||||
<!-- <el-icon>-->
|
||||
<!-- <QuestionFilled color="#999999" />-->
|
||||
<!-- </el-icon>-->
|
||||
<!-- </el-tooltip>-->
|
||||
<!-- </div>-->
|
||||
<!-- </template>-->
|
||||
<!-- <el-switch v-model="diyStore.editComponent.field.privacyProtection" />-->
|
||||
<!-- <div class="text-sm text-gray-400">{{ t('提交后自动隐藏内容,仅管理员可查看') }}</div>-->
|
||||
<!-- </el-form-item>-->
|
||||
|
||||
<!-- </el-form>-->
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 样式 -->
|
||||
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
|
||||
|
||||
<!-- 表单组件 字段样式 -->
|
||||
<slot name="style-field"></slot>
|
||||
|
||||
<!-- 组件样式 -->
|
||||
<slot name="style"></slot>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { t } from '@/lang'
|
||||
import { ref, onMounted, nextTick } from 'vue'
|
||||
import useDiyStore from '@/stores/modules/diy'
|
||||
import Sortable from 'sortablejs'
|
||||
import { range } from 'lodash-es'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const diyStore = useDiyStore()
|
||||
diyStore.editComponent.ignore = ['componentBgUrl'] // 忽略公共属性
|
||||
|
||||
// 组件验证
|
||||
diyStore.editComponent.verify = (index: number) => {
|
||||
const res = { code: true, message: '' }
|
||||
let pass = true
|
||||
for (let i = 0; i < diyStore.value[index].options.length; i++) {
|
||||
if (!diyStore.value[index].options[i].text) {
|
||||
res.code = false
|
||||
res.message = t('optionPlaceholder')
|
||||
pass = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!pass) return res
|
||||
|
||||
const uniqueOptions = uniqueByKey(diyStore.value[index].options, 'text')
|
||||
if (uniqueOptions.length != diyStore.value[index].options.length) {
|
||||
res.code = false
|
||||
res.message = t('errorTipsOne')
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
diyStore.editComponent.options.forEach((item: any) => {
|
||||
if (!item.id) item.id = diyStore.generateRandom()
|
||||
})
|
||||
|
||||
const visible = ref(false)
|
||||
const optionsValue = ref()
|
||||
const addOption = () => {
|
||||
diyStore.editComponent.options.push({
|
||||
id: diyStore.generateRandom(),
|
||||
text: '选项' + (diyStore.editComponent.options.length + 1)
|
||||
})
|
||||
}
|
||||
|
||||
const removeOption = (index: any) => {
|
||||
diyStore.editComponent.options.splice(index, 1)
|
||||
}
|
||||
|
||||
// 批量添加选项
|
||||
const batchAddOptions = () => {
|
||||
if (optionsValue.value.trim()) {
|
||||
const newOptions = optionsValue.value.split(',').map((option: any) => {
|
||||
return {
|
||||
id: diyStore.generateRandom(),
|
||||
text: option.trim()
|
||||
}
|
||||
}).filter((option: any) => option.text !== '')
|
||||
|
||||
// 去除重复的选项
|
||||
const uniqueNewOptions = uniqueByKey(newOptions, 'text')
|
||||
|
||||
// 过滤掉已存在的选项
|
||||
const filteredNewOptions = uniqueNewOptions.filter((newOption: any) =>
|
||||
!diyStore.editComponent.options.some((existingOption: any) => existingOption.text === newOption.text)
|
||||
)
|
||||
|
||||
// 如果有新的选项,添加到选项列表中
|
||||
if (filteredNewOptions.length > 0) {
|
||||
diyStore.editComponent.options.push(...filteredNewOptions)
|
||||
} else {
|
||||
ElMessage({
|
||||
message: t('errorTipsTwo'),
|
||||
type: 'error'
|
||||
})
|
||||
}
|
||||
|
||||
optionsValue.value = ''
|
||||
visible.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 数组去重
|
||||
const uniqueByKey = (arr: any, key: any) => {
|
||||
const seen = new Set()
|
||||
return arr.filter((item: any) => {
|
||||
const serializedKey = JSON.stringify(item[key])
|
||||
return seen.has(serializedKey) ? false : seen.add(serializedKey)
|
||||
})
|
||||
}
|
||||
|
||||
const formCheckboxRef = ref()
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
const sortable = Sortable.create(formCheckboxRef.value, {
|
||||
group: 'option-item',
|
||||
animation: 200,
|
||||
onEnd: event => {
|
||||
const temp = diyStore.editComponent.options[event.oldIndex!]
|
||||
diyStore.editComponent.options.splice(event.oldIndex!, 1)
|
||||
diyStore.editComponent.options.splice(event.newIndex!, 0, temp)
|
||||
sortable.sort(
|
||||
range(diyStore.editComponent.options.length).map(value => {
|
||||
return value.toString()
|
||||
})
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
defineExpose({})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,210 @@
|
||||
<template>
|
||||
<!-- 内容 -->
|
||||
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
|
||||
|
||||
<!-- 表单组件 字段内容设置 -->
|
||||
<slot name="field"></slot>
|
||||
<el-form label-width="100px" class="px-[10px]">
|
||||
<el-form-item :label="t('dataFormat')">
|
||||
<el-radio-group v-model="diyStore.editComponent.dateFormat">
|
||||
<div class="flex flex-col">
|
||||
<el-radio label="YYYY年M月D日">{{ dateFormat.format1 }}</el-radio>
|
||||
<el-radio label="YYYY-MM-DD">{{ dateFormat.format2 }}</el-radio>
|
||||
<el-radio label="YYYY/MM/DD">{{ dateFormat.format3 }}</el-radio>
|
||||
</div>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<div class="edit-attr-item-wrap">
|
||||
<h3 class="mb-[10px]">{{ t('startDate') }}</h3>
|
||||
<el-form label-width="100px" class="px-[10px]" @submit.prevent>
|
||||
<el-form-item :label="t('formPlaceholder')">
|
||||
<el-input v-model.trim="diyStore.editComponent.start.placeholder" :placeholder="t('formPlaceholderTips')" clearable maxlength="15" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('defaultValue')">
|
||||
<el-switch v-model="diyStore.editComponent.start.defaultControl" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="diyStore.editComponent.start.defaultControl">
|
||||
<el-radio-group v-model="diyStore.editComponent.start.dateWay">
|
||||
<el-radio label="current">{{ t('currentDate') }}</el-radio>
|
||||
<el-radio label="diy">{{ t('diyDate') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="diyStore.editComponent.start.defaultControl && diyStore.editComponent.start.dateWay == 'diy'">
|
||||
<el-date-picker v-model="diyStore.editComponent.field.default.start.date" format="YYYY/MM/DD" value-format="YYYY-MM-DD" type="date" :placeholder="t('startDataPlaceholder')" @change="startDateChange" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="edit-attr-item-wrap">
|
||||
<h3 class="mb-[10px]">{{ t('endDate') }}</h3>
|
||||
<el-form label-width="100px" class="px-[10px]" @submit.prevent>
|
||||
<el-form-item :label="t('formPlaceholder')">
|
||||
<el-input v-model.trim="diyStore.editComponent.end.placeholder" :placeholder="t('formPlaceholderTips')" clearable maxlength="15" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('defaultValue')">
|
||||
<el-switch v-model="diyStore.editComponent.end.defaultControl" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="diyStore.editComponent.end.defaultControl">
|
||||
<el-radio-group v-model="diyStore.editComponent.end.dateWay">
|
||||
<el-radio label="current">{{ t('currentDate') }}</el-radio>
|
||||
<el-radio label="diy">{{ t('diyDate') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="diyStore.editComponent.end.defaultControl && diyStore.editComponent.end.dateWay == 'diy'">
|
||||
<el-date-picker :disabled-date="disabledEndDate"
|
||||
v-model="diyStore.editComponent.field.default.end.date" format="YYYY/MM/DD"
|
||||
value-format="YYYY-MM-DD" type="date" :placeholder="t('endDataPlaceholder')"
|
||||
@change="endDateChange" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 表单组件 其他设置 -->
|
||||
<slot name="other"></slot>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 样式 -->
|
||||
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
|
||||
|
||||
<div class="edit-attr-item-wrap">
|
||||
<h3 class="mb-[10px]">{{ t('textStyle') }}</h3>
|
||||
<el-form label-width="80px" class="px-[10px]">
|
||||
<el-form-item :label="t('textFontSize')">
|
||||
<el-slider v-model="diyStore.editComponent.fontSize" show-input size="small" class="ml-[10px] diy-nav-slider" :min="12" :max="18" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('textFontWeight')">
|
||||
<el-radio-group v-model="diyStore.editComponent.fontWeight">
|
||||
<el-radio :label="'normal'">{{ t('fontWeightNormal') }}</el-radio>
|
||||
<el-radio :label="'bold'">{{ t('fontWeightBold') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('textColor')">
|
||||
<el-color-picker v-model="diyStore.editComponent.textColor" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 组件样式 -->
|
||||
<slot name="style"></slot>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { t } from '@/lang'
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { timeTurnTimeStamp } from '@/utils/common'
|
||||
import useDiyStore from '@/stores/modules/diy'
|
||||
|
||||
const diyStore = useDiyStore()
|
||||
diyStore.editComponent.ignore = ['componentBgUrl'] // 忽略公共属性
|
||||
|
||||
// 组件验证
|
||||
diyStore.editComponent.verify = (index: number) => {
|
||||
const res = { code: true, message: '' }
|
||||
let starTime = diyStore.value[index].field.default.start.date
|
||||
let endTime = diyStore.value[index].field.default.end.date
|
||||
|
||||
const today = new Date()
|
||||
const hours = String(today.getHours()).padStart(2, '0')
|
||||
const minutes = String(today.getMinutes()).padStart(2, '0')
|
||||
|
||||
if (diyStore.editComponent.start.dateWay == 'current') {
|
||||
starTime = today.toISOString().split('T')[0]
|
||||
}
|
||||
if (diyStore.editComponent.end.dateWay == 'current') {
|
||||
endTime = today.toISOString().split('T')[0]
|
||||
}
|
||||
|
||||
if (diyStore.editComponent.start.defaultControl && starTime == '' && diyStore.editComponent.end.dateWay == 'diy') {
|
||||
res.code = false
|
||||
res.message = t('startDataTips')
|
||||
return res
|
||||
}
|
||||
if (diyStore.editComponent.end.defaultControl && endTime == '' && diyStore.editComponent.end.dateWay == 'diy') {
|
||||
res.code = false
|
||||
res.message = t('endDataTips')
|
||||
return res
|
||||
}
|
||||
|
||||
if (diyStore.editComponent.start.defaultControl && diyStore.editComponent.end.defaultControl && timeTurnTimeStamp(starTime) > timeTurnTimeStamp(endTime)) {
|
||||
res.code = false
|
||||
res.message = t('startEndDataTips')
|
||||
return res
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
const dateFormat: any = reactive({
|
||||
format1: '',
|
||||
format2: '',
|
||||
format3: ''
|
||||
})
|
||||
|
||||
// 结束日期-禁止的日期
|
||||
const disabledEndDate = (time: Date) => {
|
||||
let cutoffDate = null
|
||||
let bool = false
|
||||
if (diyStore.editComponent.start && diyStore.editComponent.start.defaultControl) {
|
||||
if (diyStore.editComponent.start.dateWay == 'diy') {
|
||||
cutoffDate = new Date(diyStore.editComponent.field.default.start.date)
|
||||
} else {
|
||||
cutoffDate = new Date()
|
||||
}
|
||||
bool = time.getTime() < cutoffDate.getTime()
|
||||
}
|
||||
return bool
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const today = new Date()
|
||||
const endDate = new Date()
|
||||
endDate.setDate(endDate.getDate() + 7) // 设置日期为7天后的日期
|
||||
|
||||
if (diyStore.editComponent.field.default.start.timestamp) {
|
||||
diyStore.editComponent.field.default.start.date = today.toISOString().split('T')[0]
|
||||
diyStore.editComponent.field.default.start.timestamp = parseInt(today.getTime() / 1000)
|
||||
}
|
||||
|
||||
if (diyStore.editComponent.field.default.end.timestamp) {
|
||||
diyStore.editComponent.field.default.end.date = endDate.toISOString().split('T')[0]
|
||||
diyStore.editComponent.field.default.end.timestamp = parseInt(endDate.getTime() / 1000)
|
||||
}
|
||||
|
||||
const year = today.getFullYear()
|
||||
const month = String(today.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(today.getDate()).padStart(2, '0')
|
||||
|
||||
const hours = String(today.getHours()).padStart(2, '0')
|
||||
const minutes = String(today.getMinutes()).padStart(2, '0')
|
||||
dateFormat.format1 = `${year}年${month}月${day}日`
|
||||
dateFormat.format2 = `${year}-${month}-${day}`
|
||||
dateFormat.format3 = `${year}/${month}/${day}`
|
||||
dateFormat.format4 = `${year}-${month}-${day} ${hours}:${minutes}`
|
||||
})
|
||||
|
||||
// 开始日期选择器
|
||||
const startDateChange = (date) => {
|
||||
diyStore.editComponent.field.default.start.date = date
|
||||
diyStore.editComponent.field.default.start.timestamp = timeTurnTimeStamp(date)
|
||||
|
||||
const endDate = new Date(date)
|
||||
endDate.setDate(endDate.getDate() + 7)
|
||||
diyStore.editComponent.field.default.end.date = endDate.toISOString().split('T')[0]
|
||||
diyStore.editComponent.field.default.end.timestamp = parseInt(endDate.getTime() / 1000)
|
||||
}
|
||||
// 结束日期选择器
|
||||
const endDateChange = (date) => {
|
||||
diyStore.editComponent.field.default.end.date = date
|
||||
diyStore.editComponent.field.default.end.timestamp = timeTurnTimeStamp(date)
|
||||
}
|
||||
|
||||
defineExpose({})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
101
admin-vben/src/app/views/diy_form/components/edit-form-date.vue
Normal file
101
admin-vben/src/app/views/diy_form/components/edit-form-date.vue
Normal file
@@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<!-- 内容 -->
|
||||
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
|
||||
|
||||
<!-- 表单组件 字段内容设置 -->
|
||||
<slot name="field"></slot>
|
||||
<el-form label-width="100px" class="px-[10px]" @submit.prevent>
|
||||
<el-form-item :label="t('dataFormat')">
|
||||
<el-radio-group v-model="diyStore.editComponent.dateFormat" class="!block">
|
||||
<el-radio class="!block" label="YYYY年M月D日">{{ dateFormat.format1 }}</el-radio>
|
||||
<el-radio class="!block" label="YYYY-MM-DD">{{ dateFormat.format2 }}</el-radio>
|
||||
<el-radio class="!block" label="YYYY/MM/DD">{{ dateFormat.format3 }}</el-radio>
|
||||
<el-radio class="!block" label="YYYY-MM-DD HH:mm">{{ dateFormat.format4 }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('formPlaceholder')">
|
||||
<el-input v-model.trim="diyStore.editComponent.placeholder" :placeholder="t('formPlaceholderTips')" clearable maxlength="15" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('defaultValue')">
|
||||
<el-switch v-model="diyStore.editComponent.defaultControl" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="diyStore.editComponent.defaultControl">
|
||||
<el-radio-group v-model="diyStore.editComponent.dateWay">
|
||||
<el-radio label="current">{{ t('currentDate') }}</el-radio>
|
||||
<el-radio label="diy">{{ t('diyDate') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="diyStore.editComponent.defaultControl && diyStore.editComponent.dateWay == 'diy'">
|
||||
<el-date-picker v-if="diyStore.editComponent.dateFormat != 'YYYY-MM-DD HH:mm'" v-model="diyStore.editComponent.field.default.date" format="YYYY/MM/DD" value-format="YYYY-MM-DD" type="date" :placeholder="t('dataPlaceholder')" @change="dateChange" />
|
||||
<el-date-picker v-else v-model="diyStore.editComponent.field.default.date" format="YYYY/MM/DD HH:mm" value-format="YYYY-MM-DD HH:mm" type="datetime" :placeholder="t('dataPlaceholder')" @change="dateChange" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 表单组件 其他设置 -->
|
||||
<slot name="other"></slot>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 样式 -->
|
||||
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
|
||||
|
||||
<!-- 表单组件 字段样式 -->
|
||||
<slot name="style-field"></slot>
|
||||
|
||||
<!-- 组件样式 -->
|
||||
<slot name="style"></slot>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { t } from '@/lang'
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { timeTurnTimeStamp } from '@/utils/common'
|
||||
import useDiyStore from '@/stores/modules/diy'
|
||||
|
||||
const diyStore = useDiyStore()
|
||||
diyStore.editComponent.ignore = ['componentBgUrl'] // 忽略公共属性
|
||||
|
||||
// 组件验证
|
||||
diyStore.editComponent.verify = (index: number) => {
|
||||
const res = { code: true, message: '' }
|
||||
return res
|
||||
}
|
||||
|
||||
const dateFormat: any = reactive({
|
||||
format1: '',
|
||||
format2: '',
|
||||
format3: '',
|
||||
format4: ''
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
// 初始赋值当天日期
|
||||
const today = new Date()
|
||||
if (!diyStore.editComponent.field.default.date) {
|
||||
diyStore.editComponent.field.default.date = today.toISOString().split('T')[0]
|
||||
diyStore.editComponent.field.default.timestamp = today.getTime() / 1000
|
||||
}
|
||||
const year = today.getFullYear()
|
||||
const month = String(today.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(today.getDate()).padStart(2, '0')
|
||||
|
||||
const hours = String(today.getHours()).padStart(2, '0')
|
||||
const minutes = String(today.getMinutes()).padStart(2, '0')
|
||||
dateFormat.format1 = `${year}年${month}月${day}日`
|
||||
dateFormat.format2 = `${year}-${month}-${day}`
|
||||
dateFormat.format3 = `${year}/${month}/${day}`
|
||||
dateFormat.format4 = `${year}-${month}-${day} ${hours}:${minutes}`
|
||||
})
|
||||
|
||||
const dateChange = (date: any) => {
|
||||
diyStore.editComponent.field.default.date = date
|
||||
diyStore.editComponent.field.default.timestamp = timeTurnTimeStamp(date)
|
||||
}
|
||||
|
||||
defineExpose({})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<!-- 内容 -->
|
||||
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
|
||||
|
||||
<!-- 表单组件 字段内容设置 -->
|
||||
<slot name="field"></slot>
|
||||
<el-form label-width="100px" class="px-[10px]" @submit.prevent>
|
||||
<el-form-item :label="t('formPlaceholder')">
|
||||
<el-input v-model.trim="diyStore.editComponent.placeholder" :placeholder="t('formPlaceholderTips')" clearable maxlength="15" show-word-limit />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 表单组件 其他设置 -->
|
||||
<slot name="other"></slot>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 样式 -->
|
||||
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
|
||||
|
||||
<!-- 表单组件 字段样式 -->
|
||||
<slot name="style-field"></slot>
|
||||
|
||||
<!-- 组件样式 -->
|
||||
<slot name="style"></slot>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { t } from '@/lang'
|
||||
import { ref } from 'vue'
|
||||
import useDiyStore from '@/stores/modules/diy'
|
||||
|
||||
const diyStore = useDiyStore()
|
||||
diyStore.editComponent.ignore = ['componentBgUrl'] // 忽略公共属性
|
||||
|
||||
// 组件验证
|
||||
diyStore.editComponent.verify = (index: number) => {
|
||||
const res = { code: true, message: '' }
|
||||
// todo 只需要考虑该组件自身的验证
|
||||
return res
|
||||
}
|
||||
defineExpose({})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<!-- 内容 -->
|
||||
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
|
||||
|
||||
<!-- 表单组件 字段内容设置 -->
|
||||
<slot name="field"></slot>
|
||||
<el-form label-width="100px" class="px-[10px]" @submit.prevent>
|
||||
<el-form-item :label="t('限制上传大小')">
|
||||
<el-input v-model.trim="diyStore.editComponent.limitUploadSize" clearable maxlength="15" />
|
||||
/单位MB,目前是Bit,要转换,*1024
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 表单组件 其他设置 -->
|
||||
<slot name="other"></slot>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 样式 -->
|
||||
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
|
||||
|
||||
<!-- 表单组件 字段样式 -->
|
||||
<slot name="style-field"></slot>
|
||||
|
||||
<!-- 组件样式 -->
|
||||
<slot name="style"></slot>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { t } from '@/lang'
|
||||
import { ref } from 'vue'
|
||||
import useDiyStore from '@/stores/modules/diy'
|
||||
|
||||
const diyStore = useDiyStore()
|
||||
diyStore.editComponent.ignore = ['componentBgUrl'] // 忽略公共属性
|
||||
|
||||
defineExpose({})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<!-- 内容 -->
|
||||
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
|
||||
|
||||
<!-- 表单组件 字段内容设置 -->
|
||||
<slot name="field"></slot>
|
||||
<el-form label-width="100px" class="px-[10px]" @submit.prevent>
|
||||
<el-form-item :label="t('formPlaceholder')">
|
||||
<el-input v-model.trim="diyStore.editComponent.placeholder" :placeholder="t('formPlaceholderTips')" clearable maxlength="15" show-word-limit />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 表单组件 其他设置 -->
|
||||
<slot name="other"></slot>
|
||||
<el-form label-width="100px" class="px-[10px]">
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<div class="flex items-center">
|
||||
<span class="mr-[3px]">{{ t('preventDuplication') }}</span>
|
||||
<el-tooltip effect="light" placement="top">
|
||||
<template #content>
|
||||
<p>{{ t('preventDuplicationTipsOne') }}</p>
|
||||
<p>{{ t('preventDuplicationTipsTwo') }}</p>
|
||||
</template>
|
||||
<el-icon>
|
||||
<QuestionFilled color="#999999" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<el-switch v-model="diyStore.editComponent.field.unique" />
|
||||
</el-form-item>
|
||||
<el-form-item class="display-block">
|
||||
<template #label>
|
||||
<div class="flex items-center">
|
||||
<span class="mr-[3px]">{{ t('privacyProtection') }}</span>
|
||||
<el-tooltip effect="light" placement="top">
|
||||
<template #content>
|
||||
<p>{{ t('privacyProtectionTipsOne') }}</p>
|
||||
<p>{{ t('privacyProtectionTipsTwo') }}</p>
|
||||
</template>
|
||||
<el-icon>
|
||||
<QuestionFilled color="#999999" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<el-switch v-model="diyStore.editComponent.field.privacyProtection" />
|
||||
<div class="text-sm text-gray-400">{{ t('privacyProtectionTipsThree') }}</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 样式 -->
|
||||
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
|
||||
|
||||
<!-- 表单组件 字段样式 -->
|
||||
<slot name="style-field"></slot>
|
||||
|
||||
<!-- 组件样式 -->
|
||||
<slot name="style"></slot>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { t } from '@/lang'
|
||||
import { ref } from 'vue'
|
||||
import useDiyStore from '@/stores/modules/diy'
|
||||
|
||||
const diyStore = useDiyStore()
|
||||
diyStore.editComponent.ignore = ['componentBgUrl'] // 忽略公共属性
|
||||
|
||||
// 组件验证
|
||||
diyStore.editComponent.verify = (index: number) => {
|
||||
const res = { code: true, message: '' }
|
||||
// todo 只需要考虑该组件自身的验证
|
||||
return res
|
||||
}
|
||||
defineExpose({})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<!-- 内容 -->
|
||||
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
|
||||
|
||||
<!-- 表单组件 字段内容设置 -->
|
||||
<slot name="field"></slot>
|
||||
<el-form label-width="100px" class="px-[10px]" @submit.prevent>
|
||||
<el-form-item :label="t('imageLimit')">
|
||||
<el-input v-model.trim="diyStore.editComponent.limit" :placeholder="t('imageLimitPlaceholder')" clearable maxlength="2" />
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('上传方式')">
|
||||
<el-checkbox-group v-model="diyStore.editComponent.uploadMode" :min="1">
|
||||
<el-checkbox label="拍照上传" value="take_pictures" />
|
||||
<el-checkbox label="从相册选择" value="select_from_album" />
|
||||
</el-checkbox-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 表单组件 其他设置 -->
|
||||
<slot name="other"></slot>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 样式 -->
|
||||
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
|
||||
|
||||
<!-- 表单组件 字段样式 -->
|
||||
<slot name="style-field"></slot>
|
||||
|
||||
<!-- 组件样式 -->
|
||||
<slot name="style"></slot>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { t } from '@/lang'
|
||||
import { ref } from 'vue'
|
||||
import useDiyStore from '@/stores/modules/diy'
|
||||
|
||||
const diyStore = useDiyStore()
|
||||
diyStore.editComponent.ignore = ['componentBgUrl'] // 忽略公共属性
|
||||
|
||||
// 组件验证
|
||||
diyStore.editComponent.verify = (index: number) => {
|
||||
const res = { code: true, message: '' }
|
||||
if (diyStore.value[index].limit == '') {
|
||||
res.code = false
|
||||
res.message = t('imageLimitPlaceholder')
|
||||
return res
|
||||
}
|
||||
if (isNaN(diyStore.value[index].limit) || !regExp.number.test(diyStore.value[index].limit)) {
|
||||
res.code = false
|
||||
res.message = t('imageLimitErrorTips')
|
||||
return res
|
||||
}
|
||||
if (diyStore.value[index].limit < 0) {
|
||||
res.code = false
|
||||
res.message = t('imageLimitErrorTipsTwo')
|
||||
return res
|
||||
}
|
||||
if (diyStore.value[index].limit == 0) {
|
||||
res.code = false
|
||||
res.message = t('imageLimitErrorTipsThree')
|
||||
return res
|
||||
}
|
||||
if (diyStore.value[index].limit > 9) {
|
||||
res.code = false
|
||||
res.message = t('imageLimitErrorTipsFour')
|
||||
return res
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// 正则表达式
|
||||
const regExp: any = {
|
||||
required: /[\S]+/,
|
||||
number: /^\d{0,10}$/,
|
||||
digit: /^\d{0,10}(.?\d{0,2})$/,
|
||||
special: /^\d{0,10}(.?\d{0,3})$/
|
||||
}
|
||||
|
||||
defineExpose({})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,99 @@
|
||||
<template>
|
||||
<!-- 内容 -->
|
||||
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
|
||||
|
||||
<!-- 表单组件 字段内容设置 -->
|
||||
<slot name="field"></slot>
|
||||
<el-form label-width="100px" class="px-[10px]" @submit.prevent>
|
||||
<el-form-item :label="t('formPlaceholder')">
|
||||
<el-input v-model.trim="diyStore.editComponent.placeholder" :placeholder="t('formPlaceholderTips')" clearable maxlength="15" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<div class="flex items-center">
|
||||
<span class="mr-[3px]">{{ t('defaultValue') }}</span>
|
||||
<el-tooltip effect="light" :content="t('defaultValueTips')" placement="top">
|
||||
<el-icon>
|
||||
<QuestionFilled color="#999999" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<el-input v-model.trim="diyStore.editComponent.field.default" :placeholder="t('defaultValuePlaceholder')" clearable maxlength="18" show-word-limit />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 表单组件 其他设置 -->
|
||||
<slot name="other"></slot>
|
||||
<el-form label-width="100px" class="px-[10px]">
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<div class="flex items-center">
|
||||
<span class="mr-[3px]">{{ t('preventDuplication') }}</span>
|
||||
<el-tooltip effect="light" placement="top">
|
||||
<template #content>
|
||||
<p>{{ t('preventDuplicationTipsOne') }}</p>
|
||||
<p>{{ t('preventDuplicationTipsTwo') }}</p>
|
||||
</template>
|
||||
<el-icon>
|
||||
<QuestionFilled color="#999999" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<el-switch v-model="diyStore.editComponent.field.unique" />
|
||||
</el-form-item>
|
||||
<!-- <el-form-item class="display-block">-->
|
||||
<!-- <template #label>-->
|
||||
<!-- <div class="flex items-center">-->
|
||||
<!-- <span class="mr-[3px]">{{ t('隐私保护') }}</span>-->
|
||||
<!-- <el-tooltip effect="light" placement="top">-->
|
||||
<!-- <template #content>-->
|
||||
<!-- <p>会自动将提交的个人信息做加密展示。</p>-->
|
||||
<!-- <p>适用于公开展示收集的数据且不暴露用户隐私。</p>-->
|
||||
<!-- </template>-->
|
||||
<!-- <el-icon>-->
|
||||
<!-- <QuestionFilled color="#999999" />-->
|
||||
<!-- </el-icon>-->
|
||||
<!-- </el-tooltip>-->
|
||||
<!-- </div>-->
|
||||
<!-- </template>-->
|
||||
<!-- <el-switch v-model="diyStore.editComponent.field.privacyProtection" />-->
|
||||
<!-- <div class="text-sm text-gray-400">{{ t('提交后自动隐藏文本,仅管理员可查看') }}</div>-->
|
||||
<!-- </el-form-item>-->
|
||||
</el-form>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 样式 -->
|
||||
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
|
||||
|
||||
<!-- 表单组件 字段样式 -->
|
||||
<slot name="style-field"></slot>
|
||||
|
||||
<!-- 组件样式 -->
|
||||
<slot name="style"></slot>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { t } from '@/lang'
|
||||
import { ref } from 'vue'
|
||||
import useDiyStore from '@/stores/modules/diy'
|
||||
|
||||
const diyStore = useDiyStore()
|
||||
diyStore.editComponent.ignore = ['componentBgUrl'] // 忽略公共属性
|
||||
|
||||
// 组件验证
|
||||
diyStore.editComponent.verify = (index: number) => {
|
||||
const res = { code: true, message: '' }
|
||||
// todo 只需要考虑该组件自身的验证
|
||||
return res
|
||||
}
|
||||
|
||||
defineExpose({})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<!-- 内容 -->
|
||||
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
|
||||
|
||||
<!-- 表单组件 字段内容设置 -->
|
||||
<slot name="field"></slot>
|
||||
<el-form label-width="100px" class="px-[10px]">
|
||||
<el-form-item :label="t('access')">
|
||||
<el-radio-group v-model="diyStore.editComponent.mode">
|
||||
<el-radio class="!mr-[20px]" label="authorized_wechat_location">{{ t('authorizeWeChatLocation') }}</el-radio>
|
||||
<el-radio label="open_choose_location">{{ t('manuallySelectPositioning') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 表单组件 其他设置 -->
|
||||
<slot name="other"></slot>
|
||||
<el-form label-width="100px" class="px-[10px]">
|
||||
<el-form-item class="display-block">
|
||||
<template #label>
|
||||
<div class="flex items-center">
|
||||
<span class="mr-[3px]">{{ t('privacyProtection') }}</span>
|
||||
<el-tooltip effect="light" placement="top">
|
||||
<template #content>
|
||||
<p>{{ t('privacyProtectionTipsOne') }}</p>
|
||||
<p>{{ t('privacyProtectionTipsTwo') }}</p>
|
||||
</template>
|
||||
<el-icon>
|
||||
<QuestionFilled color="#999999" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<el-switch v-model="diyStore.editComponent.field.privacyProtection" />
|
||||
<div class="text-sm text-gray-400">{{ t('privacyProtectionTipsFour') }}</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 样式 -->
|
||||
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
|
||||
|
||||
<!-- 表单组件 字段样式 -->
|
||||
<slot name="style-field"></slot>
|
||||
|
||||
<!-- 组件样式 -->
|
||||
<slot name="style"></slot>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { t } from '@/lang'
|
||||
import useDiyStore from '@/stores/modules/diy'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const diyStore = useDiyStore()
|
||||
diyStore.editComponent.ignore = ['componentBgUrl'] // 忽略公共属性
|
||||
|
||||
// 组件验证
|
||||
diyStore.editComponent.verify = (index: number) => {
|
||||
const res = { code: true, message: '' }
|
||||
// todo 只需要考虑该组件自身的验证
|
||||
return res
|
||||
}
|
||||
defineExpose({})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<!-- 内容 -->
|
||||
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
|
||||
|
||||
<!-- 表单组件 字段内容设置 -->
|
||||
<slot name="field"></slot>
|
||||
<el-form label-width="100px" class="px-[10px]" @submit.prevent>
|
||||
<el-form-item :label="t('formPlaceholder')">
|
||||
<el-input v-model.trim="diyStore.editComponent.placeholder" :placeholder="t('formPlaceholderTips')" clearable maxlength="15" show-word-limit />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 表单组件 其他设置 -->
|
||||
<slot name="other"></slot>
|
||||
<el-form label-width="100px" class="px-[10px]">
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<div class="flex items-center">
|
||||
<span class="mr-[3px]">{{ t('preventDuplication') }}</span>
|
||||
<el-tooltip effect="light" placement="top">
|
||||
<template #content>
|
||||
<p>{{ t('preventDuplicationTipsOne') }}</p>
|
||||
<p>{{ t('preventDuplicationTipsTwo') }}</p>
|
||||
</template>
|
||||
<el-icon>
|
||||
<QuestionFilled color="#999999" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<el-switch v-model="diyStore.editComponent.field.unique" />
|
||||
</el-form-item>
|
||||
<el-form-item class="display-block">
|
||||
<template #label>
|
||||
<div class="flex items-center">
|
||||
<span class="mr-[3px]">{{ t('privacyProtection') }}</span>
|
||||
<el-tooltip effect="light" placement="top">
|
||||
<template #content>
|
||||
<p>{{ t('privacyProtectionTipsOne') }}</p>
|
||||
<p>{{ t('privacyProtectionTipsTwo') }}</p>
|
||||
</template>
|
||||
<el-icon>
|
||||
<QuestionFilled color="#999999" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<el-switch v-model="diyStore.editComponent.field.privacyProtection" />
|
||||
<div class="text-sm text-gray-400">{{ t('privacyProtectionTipsFive') }}</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 样式 -->
|
||||
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
|
||||
|
||||
<!-- 表单组件 字段样式 -->
|
||||
<slot name="style-field"></slot>
|
||||
|
||||
<!-- 组件样式 -->
|
||||
<slot name="style"></slot>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { t } from '@/lang'
|
||||
import useDiyStore from '@/stores/modules/diy'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const diyStore = useDiyStore()
|
||||
diyStore.editComponent.ignore = ['componentBgUrl'] // 忽略公共属性
|
||||
|
||||
// 组件验证
|
||||
diyStore.editComponent.verify = (index: number) => {
|
||||
const res = { code: true, message: '' }
|
||||
// todo 只需要考虑该组件自身的验证
|
||||
return res
|
||||
}
|
||||
defineExpose({})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,119 @@
|
||||
<template>
|
||||
<!-- 内容 -->
|
||||
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
|
||||
|
||||
<!-- 表单组件 字段内容设置 -->
|
||||
<slot name="field"></slot>
|
||||
<el-form label-width="100px" class="px-[10px]" @submit.prevent>
|
||||
<el-form-item :label="t('formPlaceholder')">
|
||||
<el-input v-model.trim="diyStore.editComponent.placeholder" :placeholder="t('formPlaceholderTips')" clearable maxlength="15" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('unit')">
|
||||
<el-input v-model.trim="diyStore.editComponent.unit" :placeholder="t('unitPlaceholder')" clearable maxlength="5" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<div class="flex items-center">
|
||||
<span class="mr-[3px]">{{ t('defaultValue') }}</span>
|
||||
<el-tooltip effect="light" :content="t('defaultValueTips')" placement="top">
|
||||
<el-icon>
|
||||
<QuestionFilled color="#999999" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<el-input v-model.trim="diyStore.editComponent.field.default" :placeholder="t('defaultValuePlaceholder')" @keyup="filterDigit($event)" clearable maxlength="18" show-word-limit />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 表单组件 其他设置 -->
|
||||
<slot name="other"></slot>
|
||||
<el-form label-width="100px" class="px-[10px]">
|
||||
<!-- <el-form-item>-->
|
||||
<!-- <template #label>-->
|
||||
<!-- <div class="flex items-center">-->
|
||||
<!-- <span class="mr-[3px]">{{ t('内容防重复') }}</span>-->
|
||||
<!-- <el-tooltip effect="light" placement="top">-->
|
||||
<!-- <template #content>-->
|
||||
<!-- <p>该组件填写的内容不能与已提交的数据重复。</p>-->
|
||||
<!-- <p>极端情况下可能存在延时导致限制失效。</p>-->
|
||||
<!-- </template>-->
|
||||
<!-- <el-icon>-->
|
||||
<!-- <QuestionFilled color="#999999" />-->
|
||||
<!-- </el-icon>-->
|
||||
<!-- </el-tooltip>-->
|
||||
<!-- </div>-->
|
||||
<!-- </template>-->
|
||||
<!-- <el-switch v-model="diyStore.editComponent.field.unique" />-->
|
||||
<!-- </el-form-item>-->
|
||||
<!-- <el-form-item class="display-block">-->
|
||||
<!-- <template #label>-->
|
||||
<!-- <div class="flex items-center">-->
|
||||
<!-- <span class="mr-[3px]">{{ t('隐私保护') }}</span>-->
|
||||
<!-- <el-tooltip effect="light" placement="top">-->
|
||||
<!-- <template #content>-->
|
||||
<!-- <p>会自动将提交的个人信息做加密展示。</p>-->
|
||||
<!-- <p>适用于公开展示收集的数据且不暴露用户隐私。</p>-->
|
||||
<!-- </template>-->
|
||||
<!-- <el-icon>-->
|
||||
<!-- <QuestionFilled color="#999999" />-->
|
||||
<!-- </el-icon>-->
|
||||
<!-- </el-tooltip>-->
|
||||
<!-- </div>-->
|
||||
<!-- </template>-->
|
||||
<!-- <el-switch v-model="diyStore.editComponent.field.privacyProtection" />-->
|
||||
<!-- <div class="text-sm text-gray-400">{{ t('提交后自动隐藏数字,仅管理员可查看') }}</div>-->
|
||||
<!-- </el-form-item>-->
|
||||
</el-form>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 样式 -->
|
||||
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
|
||||
|
||||
<!-- 表单组件 字段样式 -->
|
||||
<slot name="style-field"></slot>
|
||||
|
||||
<!-- 组件样式 -->
|
||||
<slot name="style"></slot>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { t } from '@/lang'
|
||||
import { ref } from 'vue'
|
||||
import useDiyStore from '@/stores/modules/diy'
|
||||
import { filterDigit } from '@/utils/common'
|
||||
|
||||
const diyStore = useDiyStore()
|
||||
diyStore.editComponent.ignore = ['componentBgUrl'] // 忽略公共属性
|
||||
|
||||
// 组件验证
|
||||
diyStore.editComponent.verify = (index: number) => {
|
||||
const res = { code: true, message: '' }
|
||||
if (diyStore.value[index].field.default) {
|
||||
if (isNaN(diyStore.value[index].field.default) || !regExp.digit.test(diyStore.value[index].field.default)) {
|
||||
res.code = false
|
||||
res.message = t('defaultErrorTips')
|
||||
} else if (diyStore.value[index].field.default < 0) {
|
||||
res.code = false
|
||||
res.message = t('defaultMustZeroTips')
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// 正则表达式
|
||||
const regExp: any = {
|
||||
required: /[\S]+/,
|
||||
number: /^\d{0,10}$/,
|
||||
digit: /^\d{0,10}(.?\d{0,2})$/,
|
||||
special: /^\d{0,10}(.?\d{0,3})$/
|
||||
}
|
||||
|
||||
defineExpose({})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
214
admin-vben/src/app/views/diy_form/components/edit-form-radio.vue
Normal file
214
admin-vben/src/app/views/diy_form/components/edit-form-radio.vue
Normal file
@@ -0,0 +1,214 @@
|
||||
<template>
|
||||
<!-- 内容 -->
|
||||
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
|
||||
|
||||
<!-- 表单组件 字段内容设置 -->
|
||||
<slot name="field"></slot>
|
||||
<el-form label-width="100px" class="px-[10px]">
|
||||
<el-form-item :label="t('style')">
|
||||
<el-radio-group v-model="diyStore.editComponent.style">
|
||||
<el-radio label="style-1">{{ t('defaultSources') }}</el-radio>
|
||||
<el-radio label="style-2">{{ t('listStyle') }}</el-radio>
|
||||
<el-radio label="style-3">{{ t('dropDownStyle') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('option')">
|
||||
<div ref="formRadioRef">
|
||||
<div v-for="(option, index) in diyStore.editComponent.options" :key="option.id" class="option-item flex items-center mb-[15px]">
|
||||
<el-input v-model="diyStore.editComponent.options[index].text" class="!w-[215px]" :placeholder="t('optionPlaceholder')" clearable maxlength="30" />
|
||||
<span v-if="diyStore.editComponent.options.length > 1" @click="removeOption(index)" class="cursor-pointer ml-[5px] nc-iconfont nc-icon-shanchu-yuangaizhiV6xx"></span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-primary cursor-pointer mr-[10px]" @click="addOption">{{ t('addSingleOption') }}</span>
|
||||
<el-popover :visible="visible" placement="bottom" :width="300">
|
||||
<p class="mb-[5px]">{{ t('addMultipleOption') }}</p>
|
||||
<p class="text-[#888] text-[12px] mb-[5px]">{{ t('addOptionTips') }}</p>
|
||||
<el-input v-model.trim="optionsValue" type="textarea" clearable maxlength="200" show-word-limit />
|
||||
<div class="mt-[10px] text-right">
|
||||
<el-button size="small" text @click="visible = false">{{ t('cancel') }}</el-button>
|
||||
<el-button size="small" type="primary" @click="batchAddOptions">{{ t('confirm') }}</el-button>
|
||||
</div>
|
||||
<template #reference>
|
||||
<span class="text-primary cursor-pointer" @click="visible = true">{{ t('addMultipleOption') }}</span>
|
||||
</template>
|
||||
</el-popover>
|
||||
</el-form-item>
|
||||
<!-- <el-form-item class="display-block">
|
||||
<template #label>
|
||||
<div class="flex items-center">
|
||||
<span class="mr-[3px]">{{ t('逻辑规则') }}</span>
|
||||
<el-tooltip effect="light" placement="top">
|
||||
<template #content>
|
||||
<p>支持选择某个选项后,显示特定的组件。</p>
|
||||
</template>
|
||||
<el-icon>
|
||||
<QuestionFilled color="#999999" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<div>
|
||||
<el-button plain>{{ t('添加字段显示规则') }}</el-button>
|
||||
<span class="mr-[3px]">1条字段显示规则</span>
|
||||
<span class="text-primary cursor-pointer" @click="">设置</span>
|
||||
</div>
|
||||
</el-form-item> -->
|
||||
</el-form>
|
||||
|
||||
<!-- 表单组件 其他设置 -->
|
||||
<slot name="other"></slot>
|
||||
<!-- <el-form label-width="100px" class="px-[10px]">-->
|
||||
<!-- <el-form-item class="display-block">-->
|
||||
<!-- <template #label>-->
|
||||
<!-- <div class="flex items-center">-->
|
||||
<!-- <span class="mr-[3px]">{{ t('隐私保护') }}</span>-->
|
||||
<!-- <el-tooltip effect="light" placement="top">-->
|
||||
<!-- <template #content>-->
|
||||
<!-- <p>会自动将提交的个人信息做加密展示。</p>-->
|
||||
<!-- <p>适用于公开展示收集的数据且不暴露用户隐私。</p>-->
|
||||
<!-- </template>-->
|
||||
<!-- <el-icon>-->
|
||||
<!-- <QuestionFilled color="#999999" />-->
|
||||
<!-- </el-icon>-->
|
||||
<!-- </el-tooltip>-->
|
||||
<!-- </div>-->
|
||||
<!-- </template>-->
|
||||
<!-- <el-switch v-model="diyStore.editComponent.field.privacyProtection" />-->
|
||||
<!-- <div class="text-sm text-gray-400">{{ t('提交后自动隐藏内容,仅管理员可查看') }}</div>-->
|
||||
<!-- </el-form-item>-->
|
||||
|
||||
<!-- </el-form>-->
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 样式 -->
|
||||
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
|
||||
|
||||
<!-- 表单组件 字段样式 -->
|
||||
<slot name="style-field"></slot>
|
||||
|
||||
<!-- 组件样式 -->
|
||||
<slot name="style"></slot>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { t } from '@/lang'
|
||||
import { ref, onMounted, nextTick } from 'vue'
|
||||
import useDiyStore from '@/stores/modules/diy'
|
||||
import Sortable from 'sortablejs'
|
||||
import { range } from 'lodash-es'
|
||||
import { ElMessage } from 'element-plus'
|
||||
|
||||
const diyStore = useDiyStore()
|
||||
diyStore.editComponent.ignore = ['componentBgUrl'] // 忽略公共属性
|
||||
|
||||
// 组件验证
|
||||
diyStore.editComponent.verify = (index: number) => {
|
||||
const res = { code: true, message: '' }
|
||||
let pass = true
|
||||
for (let i = 0; i < diyStore.value[index].options.length; i++) {
|
||||
if (!diyStore.value[index].options[i].text) {
|
||||
res.code = false
|
||||
res.message = t('optionPlaceholder')
|
||||
pass = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!pass) return res
|
||||
|
||||
const uniqueOptions = uniqueByKey(diyStore.value[index].options, 'text')
|
||||
|
||||
if (uniqueOptions.length != diyStore.value[index].options.length) {
|
||||
res.code = false
|
||||
res.message = t('errorTipsOne')
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
diyStore.editComponent.options.forEach((item: any) => {
|
||||
if (!item.id) item.id = diyStore.generateRandom()
|
||||
})
|
||||
|
||||
const visible = ref(false)
|
||||
const optionsValue = ref()
|
||||
const addOption = () => {
|
||||
diyStore.editComponent.options.push({
|
||||
id: diyStore.generateRandom(),
|
||||
text: '选项' + (diyStore.editComponent.options.length + 1)
|
||||
})
|
||||
}
|
||||
|
||||
const removeOption = (index: any) => {
|
||||
diyStore.editComponent.options.splice(index, 1)
|
||||
}
|
||||
|
||||
const batchAddOptions = () => {
|
||||
if (optionsValue.value.trim()) {
|
||||
const newOptions = optionsValue.value.split(',').map((option: any) => {
|
||||
return {
|
||||
id: diyStore.generateRandom(),
|
||||
text: option.trim()
|
||||
}
|
||||
}).filter((option: any) => option.text !== '')
|
||||
|
||||
// 去除重复的选项
|
||||
const uniqueNewOptions = uniqueByKey(newOptions, 'text')
|
||||
|
||||
// 过滤掉已存在的选项
|
||||
const filteredNewOptions = uniqueNewOptions.filter(newOption =>
|
||||
!diyStore.editComponent.options.some(existingOption => existingOption.text === newOption.text)
|
||||
)
|
||||
|
||||
// 如果有新的选项,添加到选项列表中
|
||||
if (filteredNewOptions.length > 0) {
|
||||
diyStore.editComponent.options.push(...filteredNewOptions)
|
||||
} else {
|
||||
ElMessage({
|
||||
message: t('errorTipsTwo'),
|
||||
type: 'warning'
|
||||
})
|
||||
}
|
||||
|
||||
optionsValue.value = ''
|
||||
visible.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 数组去重
|
||||
const uniqueByKey = (arr: any, key: any) => {
|
||||
const seen = new Set()
|
||||
return arr.filter((item: any) => {
|
||||
const serializedKey = JSON.stringify(item[key])
|
||||
return seen.has(serializedKey) ? false : seen.add(serializedKey)
|
||||
})
|
||||
}
|
||||
|
||||
const formRadioRef = ref()
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
const sortable = Sortable.create(formRadioRef.value, {
|
||||
group: 'option-item',
|
||||
animation: 200,
|
||||
onEnd: event => {
|
||||
const temp = diyStore.editComponent.options[event.oldIndex!]
|
||||
diyStore.editComponent.options.splice(event.oldIndex!, 1)
|
||||
diyStore.editComponent.options.splice(event.newIndex!, 0, temp)
|
||||
sortable.sort(
|
||||
range(diyStore.editComponent.options.length).map(value => {
|
||||
return value.toString()
|
||||
})
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
defineExpose({})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,117 @@
|
||||
<template>
|
||||
<!-- 内容 -->
|
||||
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
|
||||
<div class="edit-attr-item-wrap">
|
||||
<el-form label-width="80px" class="px-[10px]">
|
||||
<el-form-item :label="t('floatBtnButton')" class="display-block">
|
||||
<el-radio-group v-model="diyStore.editComponent.btnPosition" @change="btnPositionChangeFn">
|
||||
<el-radio label="follow_content">{{ t('followContent') }}</el-radio>
|
||||
<el-radio label="hover_screen_bottom">{{ t('hoverScreenBottom') }}</el-radio>
|
||||
</el-radio-group>
|
||||
<div class="text-sm text-gray-400 mb-[5px] leading-[1.4]"
|
||||
v-show="diyStore.editComponent.btnPosition == 'follow_content'">{{ t('btnTips') }}
|
||||
</div>
|
||||
<div class="text-sm text-gray-400 mb-[5px]"
|
||||
v-show="diyStore.editComponent.btnPosition == 'hover_screen_bottom'">{{ t('btnTipsTwo') }}
|
||||
</div>
|
||||
<div class="text-sm text-gray-400 mb-[10px] leading-[1.4]">{{ t('btnTipsThree') }}</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="edit-attr-item-wrap">
|
||||
<h3 class="mb-[10px]">{{ t('submitBtn') }}</h3>
|
||||
<el-form label-width="80px" class="px-[10px]" @submit.prevent>
|
||||
<el-form-item :label="t('submitBtnName')">
|
||||
<el-input v-model.trim="diyStore.editComponent.submitBtn.text" :placeholder="t('btnNamePlaceholder')" clearable maxlength="10" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('textColor')">
|
||||
<el-color-picker v-model="diyStore.editComponent.submitBtn.color" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('subTextBgColor')">
|
||||
<el-color-picker v-model="diyStore.editComponent.submitBtn.bgColor" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="edit-attr-item-wrap">
|
||||
<h3 class="mb-[10px]">{{ t('resetBtn') }}</h3>
|
||||
<el-form label-width="80px" class="px-[10px]" @submit.prevent>
|
||||
<el-form-item :label="t('carouselSearchTabControl')">
|
||||
<el-switch v-model="diyStore.editComponent.resetBtn.control" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('submitBtnName')">
|
||||
<el-input v-model.trim="diyStore.editComponent.resetBtn.text" :placeholder="t('btnNamePlaceholder')" clearable maxlength="10" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('textColor')">
|
||||
<el-color-picker v-model="diyStore.editComponent.resetBtn.color" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('subTextBgColor')">
|
||||
<el-color-picker v-model="diyStore.editComponent.resetBtn.bgColor" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 样式 -->
|
||||
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
|
||||
|
||||
<div class="edit-attr-item-wrap">
|
||||
<h3 class="mb-[10px]">{{ t('btnStyle') }}</h3>
|
||||
<el-form label-width="100px" class="px-[10px]">
|
||||
<el-form-item :label="t('topRounded')">
|
||||
<el-slider v-model="diyStore.editComponent.topElementRounded" show-input size="small" class="ml-[10px] diy-nav-slider" :max="50" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('bottomRounded')">
|
||||
<el-slider v-model="diyStore.editComponent.bottomElementRounded" show-input size="small" class="ml-[10px] diy-nav-slider" :max="50" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 组件样式 -->
|
||||
<slot name="style"></slot>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { t } from '@/lang'
|
||||
import { ref } from 'vue'
|
||||
import useDiyStore from '@/stores/modules/diy'
|
||||
|
||||
const diyStore = useDiyStore()
|
||||
diyStore.editComponent.ignore = ['componentBgUrl'] // 忽略公共属性
|
||||
|
||||
// 单选
|
||||
const btnPositionChangeFn = (e) => {
|
||||
if (e == 'hover_screen_bottom') {
|
||||
diyStore.editComponent.margin.bottom = 0
|
||||
diyStore.editComponent.margin.both = 0
|
||||
diyStore.editComponent.margin.top = 0
|
||||
}
|
||||
}
|
||||
|
||||
// 组件验证
|
||||
diyStore.editComponent.verify = (index: number) => {
|
||||
const res = { code: true, message: '' }
|
||||
if (diyStore.value[index].submitBtn.text == '') {
|
||||
res.code = false
|
||||
res.message = t('submitBtnNamePlaceholder')
|
||||
return res
|
||||
}
|
||||
if (diyStore.value[index].resetBtn.text == '') {
|
||||
res.code = false
|
||||
res.message = t('resetBtnNamePlaceholder')
|
||||
return res
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
defineExpose({})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
405
admin-vben/src/app/views/diy_form/components/edit-form-table.vue
Normal file
405
admin-vben/src/app/views/diy_form/components/edit-form-table.vue
Normal file
@@ -0,0 +1,405 @@
|
||||
<template>
|
||||
<!-- 内容 -->
|
||||
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
|
||||
|
||||
<!-- 表单组件 字段内容设置 -->
|
||||
<slot name="field"></slot>
|
||||
<el-form label-width="100px" class="px-[10px]" @submit.prevent>
|
||||
<el-form-item :label="t('列设置')">
|
||||
<div ref="imageBoxRef">
|
||||
<div v-for="(item, index) in diyStore.editComponent.columnList" :key="item.id"
|
||||
class="border-b-[1px] border-[#e0e0e0] py-1">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex">
|
||||
<span :class="['iconfont', 'ml-[5px]', 'cursor-pointer', getIconClass(item.type)]"></span>
|
||||
<el-input v-model="item.name" class="input-style" :input-style="{ boxShadow: 'none' }"
|
||||
:placeholder="t('请输入列名')" />
|
||||
</div>
|
||||
<div class="flex">
|
||||
<span v-if="diyStore.editComponent.columnList.length > 1" @click="removeOption(index)"
|
||||
class="cursor-pointer ml-[5px] nc-iconfont nc-icon-shanchu-yuangaizhiV6xx"></span>
|
||||
<span class="cursor-pointer ml-[5px] nc-iconfont nc-icon-xiaV6xx"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="item.type == 'radio'" class="flex">
|
||||
<div class="text-[#999] mr-3" >{{ item.options?.length || 0 }}个选项</div>
|
||||
<span class="text-primary cursor-pointer mr-[10px]" @click="openRadioDialog(item, index)">{{ t('编辑') }}</span>
|
||||
</div>
|
||||
<div v-if="item.type == 'date'" class="flex">
|
||||
<span class="text-primary cursor-pointer mr-[10px]" @click="openRadioDialog(item, index)">{{ t('设置日期格式') }}</span>
|
||||
</div>
|
||||
<div v-if="item.type == 'address'" class="flex">
|
||||
<div class="text-[#999] mr-3">精确到详细地址</div>
|
||||
<span class="text-primary cursor-pointer mr-[10px]" @click="openRadioDialog(item, index)">{{ t('设置') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<el-popover placement="bottom" :width="50" trigger="hover">
|
||||
<template #reference>
|
||||
<span class="text-primary cursor-pointer mr-[10px]">{{ t('添加') }}</span>
|
||||
</template>
|
||||
<div v-for="(item, index) in columnTypeOptions" :key="index" @click="addOption(item)"
|
||||
class="cursor-pointer hover:bg-[#d1e1ff] rounded text-center">
|
||||
<div class="py-1 text-[var(--el-text-color-primary]">{{ item.label }}</div>
|
||||
</div>
|
||||
</el-popover>
|
||||
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('是否自增')">
|
||||
<el-switch v-model="diyStore.editComponent.autoIncrementControl" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('填写限制')" v-if="diyStore.editComponent.autoIncrementControl">
|
||||
<div class="flex items-center">
|
||||
<span>默认显示</span>
|
||||
<el-input v-model="diyStore.editComponent.writeLimit.default" class="input-short" :placeholder="t('')" />
|
||||
<span>项</span>
|
||||
</div>
|
||||
<div class="flex items-center my-1">
|
||||
<span>最少填写</span>
|
||||
<el-input v-model="diyStore.editComponent.writeLimit.min" class="input-short" :placeholder="t('')" />
|
||||
<span>项</span>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<span>最多填写</span>
|
||||
<el-input v-model="diyStore.editComponent.writeLimit.max" class="input-short" :placeholder="t('')" />
|
||||
<span>项</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('按钮名称')" v-if="diyStore.editComponent.autoIncrementControl">
|
||||
<el-input v-model="diyStore.editComponent.btnText" :placeholder="t('请输入按钮名称')" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<!-- 单选项 -->
|
||||
<!-- <el-dialog v-model="radioDialogVisible" :title="t('设置单选项')" width="500">
|
||||
<div v-if="activeColumnTemp.type == 'radio'">
|
||||
<el-form label-width="80px" class="px-[10px]">
|
||||
<el-form-item :label="t('选项名称')">
|
||||
<el-input v-model="activeColumnTemp.name" :input-style="{ boxShadow: 'none' }" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('设置选项')">
|
||||
<div ref="radioBoxRef">
|
||||
<div v-for="(opt, idx) in activeColumnTemp.options" :key="opt.id">
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="flex-1">
|
||||
<el-input v-model="opt.label" :input-style="{ boxShadow: 'none' }"
|
||||
:placeholder="t('请输入')" />
|
||||
</div>
|
||||
<span v-if="activeColumnTemp.options.length > 1" @click="removeOptionItem(idx)"
|
||||
class="cursor-pointer ml-[5px] nc-iconfont nc-icon-shanchu-yuangaizhiV6xx"></span>
|
||||
<span class="cursor-pointer ml-[5px] nc-iconfont nc-icon-iconpaixu1"></span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-primary cursor-pointer mr-[10px]" @click="addOptionItem">{{ t('添加选项') }}</span>
|
||||
<span class="text-primary cursor-pointer mr-[10px]" @click="addOtherOption">{{ t('添加其它项') }}</span>
|
||||
<el-popover :visible="visible" placement="bottom" :width="300">
|
||||
<p class="mb-[5px]">{{ t('addMultipleOption') }}</p>
|
||||
<p class="text-[#888] text-[12px] mb-[5px]">{{ t('addOptionTips') }}</p>
|
||||
<el-input v-model.trim="optionsValue" type="textarea" clearable maxlength="200"
|
||||
show-word-limit />
|
||||
<div class="mt-[10px] text-right">
|
||||
<el-button size="small" text @click="visible = false">{{ t('cancel') }}</el-button>
|
||||
<el-button size="small" type="primary" @click="batchAddOptions">{{t('confirm')}}</el-button>
|
||||
</div>
|
||||
<template #reference>
|
||||
<span class="text-primary cursor-pointer"
|
||||
@click="visible = true">{{ t('addMultipleOption') }}</span>
|
||||
</template>
|
||||
</el-popover>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<div v-else-if="activeColumnTemp.type == 'date'">
|
||||
<el-form>
|
||||
<el-form-item :label="t('dataFormat')">
|
||||
<el-radio-group v-model="activeColumnTemp.dateFormat" class="!block">
|
||||
<el-radio class="!block" label="YYYY年M月D日">{{ dateFormat.format1 }}</el-radio>
|
||||
<el-radio class="!block" label="YYYY-MM-DD">{{ dateFormat.format2 }}</el-radio>
|
||||
<el-radio class="!block" label="YYYY/MM/DD">{{ dateFormat.format3 }}</el-radio>
|
||||
<el-radio class="!block" label="YYYY-MM-DD HH:mm">{{ dateFormat.format4 }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<div v-else-if="activeColumnTemp.type == 'address'">
|
||||
<el-form>
|
||||
<el-form-item :label="t('地址格式')">
|
||||
<el-radio-group v-model="activeColumnTemp.addressFormat" class="!block">
|
||||
<el-radio class="!block" label="province/city/district/address">{{ t('省/市/区/街道/详细地址') }}</el-radio>
|
||||
<el-radio class="!block" label="province/city/district/street">{{ t('省/市/区/街道(镇)') }}</el-radio>
|
||||
<el-radio class="!block" label="province/city/district">{{ t('省/市/区(县)') }}</el-radio>
|
||||
<el-radio class="!block" label="province/city">{{ t('省/市') }}</el-radio>
|
||||
<el-radio class="!block" label="province">{{ t('省') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="radioDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" @click="handleDialogConfirm">确定</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog> -->
|
||||
|
||||
<div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 表单组件 其他设置 -->
|
||||
<slot name="other"></slot>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 样式 -->
|
||||
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
|
||||
|
||||
<!-- 表单组件 字段样式 -->
|
||||
<slot name="style-field"></slot>
|
||||
|
||||
<!-- 组件样式 -->
|
||||
<slot name="style"></slot>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { t } from '@/lang'
|
||||
import Sortable from 'sortablejs'
|
||||
import { ref, watch, onMounted, nextTick, reactive, computed } from 'vue'
|
||||
import useDiyStore from '@/stores/modules/diy'
|
||||
import { range } from 'lodash-es'
|
||||
const diyStore = useDiyStore()
|
||||
diyStore.editComponent.ignore = ['componentBgUrl'] // 忽略公共属性
|
||||
// 组件验证
|
||||
diyStore.editComponent.verify = (index: number) => {
|
||||
const res = { code: true, message: '' }
|
||||
// todo 只需要考虑该组件自身的验证
|
||||
return res
|
||||
}
|
||||
|
||||
// 类型选项数组
|
||||
const columnTypeOptions = ref([
|
||||
{ label: '单选项', value: 'radio' },
|
||||
{ label: '文本', value: 'text' },
|
||||
{ label: '数字', value: 'number' },
|
||||
{ label: '手机号', value: 'mobile' },
|
||||
{ label: '地址', value: 'address' },
|
||||
{ label: '身份证', value: 'idcard' },
|
||||
{ label: '性别', value: 'gender' },
|
||||
{ label: '日期', value: 'date' }
|
||||
])
|
||||
|
||||
const getIconClass = (type:any) => {
|
||||
switch (type) {
|
||||
case 'radio':
|
||||
return 'icona-duihaopc30'
|
||||
case 'text':
|
||||
return 'icona-danhangwenben-1pc30'
|
||||
case 'number':
|
||||
return 'icona-shuzipc30-1'
|
||||
case 'mobile':
|
||||
return 'icona-shoujipc30'
|
||||
case 'address':
|
||||
return 'iconbiaotipc'
|
||||
case 'idcard':
|
||||
return 'icona-shenfenzhengpc30'
|
||||
case 'gender':
|
||||
return 'el-icon-s-opportunity'
|
||||
case 'date':
|
||||
return 'icona-riqipc30'
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
const imageBoxRef = ref()
|
||||
const generateId = () => Date.now().toString(36) + Math.random().toString(36).substr(2, 5)
|
||||
// 添加列方法
|
||||
const addOption = (item) => {
|
||||
const newColumn: any = {
|
||||
id: generateId(),
|
||||
name: item.label,
|
||||
type: item.value, // 列类型
|
||||
value: '' // 默认值(可选)
|
||||
}
|
||||
|
||||
// 如果是单选项,初始化 options
|
||||
if (item.value === 'radio') {
|
||||
newColumn.options = [
|
||||
{ id: generateId(), label: '选项1' },
|
||||
{ id: generateId(), label: '选项2' }
|
||||
]
|
||||
}
|
||||
// 如果是日期,初始化 dateFormat
|
||||
if (item.value === 'date') {
|
||||
newColumn.dateFormat = 'YYYY年M月D日' // 默认日期格式
|
||||
}
|
||||
// 如果是地址,初始化 addressFormat
|
||||
if (item.value === 'address') {
|
||||
newColumn.addressFormat = 'province/city/district/address' // 默认日期格式
|
||||
}
|
||||
|
||||
diyStore.editComponent.columnList.push(newColumn)
|
||||
}
|
||||
|
||||
const removeOption = (index: number) => {
|
||||
diyStore.editComponent.columnList.splice(index, 1)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// nextTick(() => {
|
||||
// if (diyStore.editComponent.columnList.length < 2) return;
|
||||
// const sortable = Sortable.create(imageBoxRef.value, {
|
||||
// group: 'item-wrap',
|
||||
// animation: 200,
|
||||
// onEnd: event => {
|
||||
// const temp = diyStore.editComponent.columnList[event.oldIndex!]
|
||||
// diyStore.editComponent.columnList.splice(event.oldIndex!, 1)
|
||||
// diyStore.editComponent.columnList.splice(event.newIndex!, 0, temp)
|
||||
// sortable.sort(
|
||||
// range(diyStore.editComponent.columnList.length).map(value => {
|
||||
// return value.toString()
|
||||
// })
|
||||
// )
|
||||
// }
|
||||
// })
|
||||
// })
|
||||
console.log(diyStore.editComponent.columnList)
|
||||
})
|
||||
|
||||
const activeColumn = ref<any>({}) // 真正数据(原始数据,不动它)
|
||||
const activeColumnTemp = ref<any>({}) // 弹窗编辑临时副本
|
||||
const activeRadioIndex = ref(0) // 当前编辑列的下标
|
||||
|
||||
const radioDialogVisible = ref(false)
|
||||
const radioBoxRef = ref()
|
||||
|
||||
const optionsValue = ref('')
|
||||
const visible = ref(false)
|
||||
const dateFormat: any = reactive({
|
||||
format1: '',
|
||||
format2: '',
|
||||
format3: '',
|
||||
format4: ''
|
||||
})
|
||||
|
||||
const openRadioDialog = (item, index) => {
|
||||
activeRadioIndex.value = index // 记录当前列的下标,方便确定时更新
|
||||
activeColumn.value = item
|
||||
activeColumnTemp.value = JSON.parse(JSON.stringify(item)) // 深拷贝,避免联动
|
||||
if (item.type == 'radio') {
|
||||
if (!activeColumnTemp.value.options) activeColumnTemp.value.options = []
|
||||
radioDialogVisible.value = true
|
||||
// nextTick(() => initRadioSortable()) // 拖拽初始化
|
||||
} else if (item.type == 'date') {
|
||||
// 初始赋值当天日期
|
||||
const today = new Date()
|
||||
const year = today.getFullYear()
|
||||
const month = String(today.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(today.getDate()).padStart(2, '0')
|
||||
|
||||
const hours = String(today.getHours()).padStart(2, '0')
|
||||
const minutes = String(today.getMinutes()).padStart(2, '0')
|
||||
dateFormat.format1 = `${year}年${month}月${day}日`
|
||||
dateFormat.format2 = `${year}-${month}-${day}`
|
||||
dateFormat.format3 = `${year}/${month}/${day}`
|
||||
dateFormat.format4 = `${year}-${month}-${day} ${hours}:${minutes}`
|
||||
radioDialogVisible.value = true
|
||||
} else if (item.type == 'address') {
|
||||
radioDialogVisible.value = true
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化拖拽
|
||||
// const initRadioSortable = () => {
|
||||
// Sortable.create(radioBoxRef.value, {
|
||||
// group: 'radio-option-wrap',
|
||||
// animation: 200,
|
||||
// draggable: '.drag-radio-item',
|
||||
// onEnd: event => {
|
||||
// const options = activeColumnTemp.value.options // 注意!这里用 temp 的
|
||||
// const temp = options[event.oldIndex!]
|
||||
// options.splice(event.oldIndex!, 1)
|
||||
// options.splice(event.newIndex!, 0, temp)
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
|
||||
const handleDialogConfirm = () => {
|
||||
console.log(activeColumnTemp.value)
|
||||
|
||||
diyStore.editComponent.columnList[activeRadioIndex.value] = JSON.parse(JSON.stringify(activeColumnTemp.value)) // 同步副本到原数据
|
||||
radioDialogVisible.value = false // 关闭弹窗
|
||||
}
|
||||
|
||||
const addOptionItem = () => {
|
||||
const newOption = { id: generateId(), label: '选项' + (activeColumnTemp.value.options.length + 1) }
|
||||
activeColumnTemp.value.options.push(newOption)
|
||||
}
|
||||
|
||||
const addOtherOption = () => {
|
||||
const newOption = { id: generateId(), label: '其他' }
|
||||
activeColumnTemp.value.options.push(newOption)
|
||||
}
|
||||
|
||||
const removeOptionItem = (index: number) => {
|
||||
activeColumnTemp.value.options.splice(index, 1)
|
||||
}
|
||||
|
||||
// 数组去重
|
||||
const uniqueByKey = (arr: any, key: any) => {
|
||||
const seen = new Set()
|
||||
return arr.filter((item: any) => {
|
||||
const serializedKey = JSON.stringify(item[key])
|
||||
return seen.has(serializedKey) ? false : seen.add(serializedKey)
|
||||
})
|
||||
}
|
||||
// 批量添加
|
||||
const batchAddOptions = () => {
|
||||
if (optionsValue.value.trim()) {
|
||||
const newOptions = optionsValue.value.split(',').map((option: any) => {
|
||||
return {
|
||||
id: diyStore.generateRandom(),
|
||||
label: option.trim()
|
||||
}
|
||||
}).filter((option: any) => option.label !== '')
|
||||
|
||||
// 去除重复的选项
|
||||
const uniqueNewOptions = uniqueByKey(newOptions, 'label')
|
||||
|
||||
// 过滤掉已存在的选项
|
||||
const filteredNewOptions = uniqueNewOptions.filter(newOption =>
|
||||
!activeColumnTemp.value.options.some(existingOption => existingOption.label === newOption.label)
|
||||
)
|
||||
|
||||
// 如果有新的选项,添加到选项列表中
|
||||
if (filteredNewOptions.length > 0) {
|
||||
activeColumnTemp.value.options.push(...filteredNewOptions)
|
||||
} else {
|
||||
ElMessage({
|
||||
message: t('errorTipsTwo'),
|
||||
type: 'warning'
|
||||
})
|
||||
}
|
||||
|
||||
optionsValue.value = ''
|
||||
visible.value = false
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.input-style .el-input__wrapper) {
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.input-short{
|
||||
width: 80px;
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,85 @@
|
||||
<template>
|
||||
<!-- 内容 -->
|
||||
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
|
||||
|
||||
<!-- 表单组件 字段内容设置 -->
|
||||
<slot name="field"></slot>
|
||||
<el-form label-width="100px" class="px-[10px]" @submit.prevent>
|
||||
<el-form-item :label="t('formPlaceholder')">
|
||||
<el-input v-model.trim="diyStore.editComponent.placeholder" :placeholder="t('formPlaceholderTips')" clearable maxlength="15" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<div class="flex items-center">
|
||||
<span class="mr-[3px]">{{ t('defaultValue') }}</span>
|
||||
<el-tooltip effect="light" :content="t('defaultValueTips')" placement="top">
|
||||
<el-icon>
|
||||
<QuestionFilled color="#999999" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<el-input v-model.trim="diyStore.editComponent.field.default"
|
||||
:placeholder="t('defaultValuePlaceholder')" clearable maxlength="18" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('rowCount')">
|
||||
<el-input v-model.trim="diyStore.editComponent.rowCount" :placeholder="t('rowCountPlaceholder')" clearable maxlength="2" show-word-limit />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 表单组件 其他设置 -->
|
||||
<slot name="other"></slot>
|
||||
<!--<el-form label-width="100px" class="px-[10px]">
|
||||
<el-form-item class="display-block">
|
||||
<template #label>
|
||||
<div class="flex items-center">
|
||||
<span class="mr-[3px]">{{ t('隐私保护') }}</span>
|
||||
<el-tooltip effect="light" placement="top">
|
||||
<template #content>
|
||||
<p>会自动将提交的个人信息做加密展示。</p>
|
||||
<p>适用于公开展示收集的数据且不暴露用户隐私。</p>
|
||||
</template>
|
||||
<el-icon>
|
||||
<QuestionFilled color="#999999" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<el-switch v-model="diyStore.editComponent.field.privacyProtection" />
|
||||
<div class="text-sm text-gray-400">{{ t('提交后自动隐藏文本,仅管理员可查看') }}</div>
|
||||
</el-form-item>
|
||||
</el-form>-->
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 样式 -->
|
||||
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
|
||||
|
||||
<!-- 表单组件 字段样式 -->
|
||||
<slot name="style-field"></slot>
|
||||
|
||||
<!-- 组件样式 -->
|
||||
<slot name="style"></slot>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { t } from '@/lang'
|
||||
import { ref } from 'vue'
|
||||
import useDiyStore from '@/stores/modules/diy'
|
||||
|
||||
const diyStore = useDiyStore()
|
||||
diyStore.editComponent.ignore = ['componentBgUrl'] // 忽略公共属性
|
||||
|
||||
// 组件验证
|
||||
diyStore.editComponent.verify = (index: number) => {
|
||||
const res = { code: true, message: '' }
|
||||
// todo 只需要考虑该组件自身的验证
|
||||
return res
|
||||
}
|
||||
defineExpose({})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,201 @@
|
||||
<template>
|
||||
<!-- 内容 -->
|
||||
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
|
||||
|
||||
<!-- 表单组件 字段内容设置 -->
|
||||
<slot name="field"></slot>
|
||||
|
||||
<div class="edit-attr-item-wrap">
|
||||
<h3 class="mb-[10px]">{{ t('startTime') }}</h3>
|
||||
<el-form label-width="100px" class="px-[10px]" @submit.prevent>
|
||||
<el-form-item :label="t('formPlaceholder')">
|
||||
<el-input v-model.trim="diyStore.editComponent.start.placeholder" :placeholder="t('formPlaceholderTips')" clearable maxlength="15" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('defaultValue')">
|
||||
<el-switch v-model="diyStore.editComponent.start.defaultControl" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="diyStore.editComponent.start.defaultControl">
|
||||
<el-radio-group v-model="diyStore.editComponent.start.timeWay">
|
||||
<el-radio label="current">{{ t('currentTime') }}</el-radio>
|
||||
<el-radio label="diy">{{ t('diyTime') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="diyStore.editComponent.start.defaultControl && diyStore.editComponent.start.timeWay == 'diy'">
|
||||
<el-time-picker v-model="diyStore.editComponent.field.default.start.date" :placeholder="t('startTimePlaceholder')" format="HH:mm" value-format="HH:mm" @change="startTimePickerChange" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<div class="edit-attr-item-wrap">
|
||||
<h3 class="mb-[10px]">{{ t('endTime') }}</h3>
|
||||
<el-form label-width="100px" class="px-[10px]" @submit.prevent>
|
||||
<el-form-item :label="t('formPlaceholder')">
|
||||
<el-input v-model.trim="diyStore.editComponent.end.placeholder" :placeholder="t('formPlaceholderTips')" clearable maxlength="15" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('defaultValue')">
|
||||
<el-switch v-model="diyStore.editComponent.end.defaultControl" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="diyStore.editComponent.end.defaultControl">
|
||||
<el-radio-group v-model="diyStore.editComponent.end.timeWay">
|
||||
<el-radio label="current">{{ t('currentTime') }}</el-radio>
|
||||
<el-radio label="diy">{{ t('diyTime') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="diyStore.editComponent.end.defaultControl && diyStore.editComponent.end.timeWay == 'diy'">
|
||||
<el-time-picker :disabled-hours="disabledHours" :disabled-minutes="disabledMinutes" v-model="diyStore.editComponent.field.default.end.date" :placeholder="t('endTimePlaceholder')" format="HH:mm" value-format="HH:mm" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 表单组件 其他设置 -->
|
||||
<slot name="other"></slot>
|
||||
</div>
|
||||
|
||||
<!-- 样式 -->
|
||||
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
|
||||
|
||||
<div class="edit-attr-item-wrap">
|
||||
<h3 class="mb-[10px]">{{ t('textStyle') }}</h3>
|
||||
<el-form label-width="80px" class="px-[10px]">
|
||||
<el-form-item :label="t('textFontSize')">
|
||||
<el-slider v-model="diyStore.editComponent.fontSize" show-input size="small" class="ml-[10px] diy-nav-slider" :min="12" :max="18" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('textFontWeight')">
|
||||
<el-radio-group v-model="diyStore.editComponent.fontWeight">
|
||||
<el-radio :label="'normal'">{{ t('fontWeightNormal') }}</el-radio>
|
||||
<el-radio :label="'bold'">{{ t('fontWeightBold') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('textColor')">
|
||||
<el-color-picker v-model="diyStore.editComponent.textColor" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 组件样式 -->
|
||||
<slot name="style"></slot>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { t } from '@/lang'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import useDiyStore from '@/stores/modules/diy'
|
||||
|
||||
const diyStore = useDiyStore()
|
||||
diyStore.editComponent.ignore = ['componentBgUrl'] // 忽略公共属性
|
||||
|
||||
// 组件验证
|
||||
diyStore.editComponent.verify = (index: number) => {
|
||||
const res = { code: true, message: '' }
|
||||
let starTime = diyStore.value[index].field.default.start.date
|
||||
let endTime = diyStore.value[index].field.default.end.date
|
||||
|
||||
const today = new Date()
|
||||
const hours = String(today.getHours()).padStart(2, '0')
|
||||
const minutes = String(today.getMinutes()).padStart(2, '0')
|
||||
|
||||
if (diyStore.editComponent.start.timeWay == 'current') {
|
||||
starTime = `${hours}:${minutes}`
|
||||
}
|
||||
if (diyStore.editComponent.end.timeWay == 'current') {
|
||||
endTime = `${hours}:${minutes}`
|
||||
}
|
||||
|
||||
if (diyStore.editComponent.start.defaultControl && starTime == '') {
|
||||
res.code = false
|
||||
res.message = t('startTimeTips')
|
||||
return res
|
||||
}
|
||||
if (diyStore.editComponent.end.defaultControl && endTime == '') {
|
||||
res.code = false
|
||||
res.message = t('endTimeTips')
|
||||
return res
|
||||
}
|
||||
|
||||
if (diyStore.editComponent.start.defaultControl && diyStore.editComponent.end.defaultControl && timeInvertSecond(starTime) > timeInvertSecond(endTime)) {
|
||||
res.code = false
|
||||
res.message = t('startEndTimeTips')
|
||||
return res
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const today = new Date()
|
||||
const hours = String(today.getHours()).padStart(2, '0')
|
||||
const minutes = String(today.getMinutes()).padStart(2, '0')
|
||||
|
||||
if (!diyStore.editComponent.field.default.start.date) {
|
||||
diyStore.editComponent.field.default.start.date = `${hours}:${minutes}`
|
||||
diyStore.editComponent.field.default.start.timestamp = timeInvertSecond(`${hours}:${minutes}`)
|
||||
}
|
||||
if (!diyStore.editComponent.field.default.end.date) {
|
||||
const endDate = new Date()
|
||||
endDate.setHours(today.getHours(), today.getMinutes() + 10, 0, 0) // 在当前时间基础上加 10 分钟
|
||||
const endHours = String(endDate.getHours()).padStart(2, '0')
|
||||
const endMinutes = String(endDate.getMinutes()).padStart(2, '0')
|
||||
|
||||
diyStore.editComponent.field.default.end.date = `${endHours}:${endMinutes}`
|
||||
diyStore.editComponent.field.default.end.timestamp = timeInvertSecond(`${endHours}:${endMinutes}`)
|
||||
}
|
||||
})
|
||||
|
||||
// 开始时间选择器
|
||||
const startTimePickerChange = (e) => {
|
||||
diyStore.editComponent.field.default.start.timestamp = timeInvertSecond(e)
|
||||
|
||||
const startTimeArr = e.split(':')
|
||||
const date = new Date()
|
||||
date.setHours(parseInt(startTimeArr[0]), parseInt(startTimeArr[1]), 0, 0)
|
||||
date.setMinutes(date.getMinutes() + 10)
|
||||
const updatedEndTime = `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`
|
||||
diyStore.editComponent.field.default.end.date = updatedEndTime
|
||||
diyStore.editComponent.field.default.end.timestamp = timeInvertSecond(updatedEndTime)
|
||||
}
|
||||
|
||||
// 结束时间选择器
|
||||
const endTimePickerChange = (e) => {
|
||||
diyStore.editComponent.field.default.end.timestamp = timeInvertSecond(e)
|
||||
}
|
||||
|
||||
const disabledHours = () => {
|
||||
const timeArr = diyStore.editComponent.field.default.start.date.split(':')
|
||||
return makeRange(0, timeArr[0])
|
||||
}
|
||||
|
||||
const disabledMinutes = (hour: number) => {
|
||||
const timeArr = diyStore.editComponent.field.default.start.date.split(':')
|
||||
return makeRange(0, timeArr[1])
|
||||
}
|
||||
|
||||
const makeRange = (start: number, end: number) => {
|
||||
const result: number[] = []
|
||||
for (let i = start; i < end; i++) {
|
||||
result.push(i)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
const timeInvertSecond = (time: any) => {
|
||||
const arr = time.split(':')
|
||||
let num = 0
|
||||
if (arr[0]) {
|
||||
num += arr[0] * 60 * 60
|
||||
}
|
||||
if (arr[1]) {
|
||||
num += arr[1] * 60
|
||||
}
|
||||
if (arr[2]) {
|
||||
num += arr[2]
|
||||
}
|
||||
return num
|
||||
}
|
||||
|
||||
defineExpose({})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<!-- 内容 -->
|
||||
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
|
||||
|
||||
<!-- 表单组件 字段内容设置 -->
|
||||
<slot name="field"></slot>
|
||||
<el-form label-width="100px" class="px-[10px]" @submit.prevent>
|
||||
<el-form-item :label="t('formPlaceholder')">
|
||||
<el-input v-model.trim="diyStore.editComponent.placeholder" :placeholder="t('formPlaceholderTips')" clearable maxlength="15" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('defaultValue')">
|
||||
<el-switch v-model="diyStore.editComponent.defaultControl" @change="changeDateDefaultControl" />
|
||||
</el-form-item>
|
||||
<el-form-item v-if="diyStore.editComponent.defaultControl">
|
||||
<el-radio-group v-model="diyStore.editComponent.timeWay">
|
||||
<el-radio label="current">{{ t('currentTime') }}</el-radio>
|
||||
<el-radio label="diy">{{ t('diyTime') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="diyStore.editComponent.defaultControl && diyStore.editComponent.timeWay == 'diy'">
|
||||
<el-time-picker v-model="diyStore.editComponent.field.default" :placeholder="t('timePlaceholder')" format="HH:mm" value-format="HH:mm" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 表单组件 其他设置 -->
|
||||
<slot name="other"></slot>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 样式 -->
|
||||
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
|
||||
|
||||
<!-- 表单组件 字段样式 -->
|
||||
<slot name="style-field"></slot>
|
||||
|
||||
<!-- 组件样式 -->
|
||||
<slot name="style"></slot>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { t } from '@/lang'
|
||||
import { ref, watch, onMounted } from 'vue'
|
||||
import useDiyStore from '@/stores/modules/diy'
|
||||
|
||||
const diyStore = useDiyStore()
|
||||
diyStore.editComponent.ignore = ['componentBgUrl'] // 忽略公共属性
|
||||
|
||||
// 组件验证
|
||||
diyStore.editComponent.verify = (index: number) => {
|
||||
const res = { code: true, message: '' }
|
||||
return res
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 初始赋值当天时间
|
||||
if (!diyStore.editComponent.field.default) {
|
||||
const today = new Date()
|
||||
const hours = String(today.getHours()).padStart(2, '0')
|
||||
const minutes = String(today.getMinutes()).padStart(2, '0')
|
||||
diyStore.editComponent.field.default = `${hours}:${minutes}`
|
||||
}
|
||||
})
|
||||
|
||||
const changeDateDefaultControl = (val: any) => {
|
||||
if (val) {
|
||||
const today = new Date()
|
||||
const hours = String(today.getHours()).padStart(2, '0')
|
||||
const minutes = String(today.getMinutes()).padStart(2, '0')
|
||||
diyStore.editComponent.field.default = `${hours}:${minutes}`
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => diyStore.editComponent.timeWay,
|
||||
(newVal) => {
|
||||
const today = new Date()
|
||||
const hours = String(today.getHours()).padStart(2, '0')
|
||||
const minutes = String(today.getMinutes()).padStart(2, '0')
|
||||
diyStore.editComponent.field.default = `${hours}:${minutes}`
|
||||
}
|
||||
)
|
||||
|
||||
defineExpose({})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<!-- 内容 -->
|
||||
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
|
||||
|
||||
<!-- 表单组件 字段内容设置 -->
|
||||
<slot name="field"></slot>
|
||||
<el-form label-width="100px" class="px-[10px]">
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<div class="flex items-center">
|
||||
<span class="mr-[3px]">{{ t('上传方式') }}</span>
|
||||
<el-tooltip effect="light" :content="t('拍摄时长限制1分钟,从相册上传不限制时长。')" placement="top">
|
||||
<el-icon>
|
||||
<QuestionFilled color="#999999" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<el-radio-group v-model="diyStore.editComponent.uploadMode">
|
||||
<el-radio label="shoot_and_album">{{ t('拍摄和相册') }}</el-radio>
|
||||
<el-radio label="shoot_only">{{ t('只允许拍摄') }}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<!-- 表单组件 其他设置 -->
|
||||
<slot name="other"></slot>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 样式 -->
|
||||
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
|
||||
|
||||
<!-- 表单组件 字段样式 -->
|
||||
<slot name="style-field"></slot>
|
||||
|
||||
<!-- 组件样式 -->
|
||||
<slot name="style"></slot>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { t } from '@/lang'
|
||||
import { ref } from 'vue'
|
||||
import useDiyStore from '@/stores/modules/diy'
|
||||
|
||||
const diyStore = useDiyStore()
|
||||
diyStore.editComponent.ignore = ['componentBgUrl'] // 忽略公共属性
|
||||
|
||||
defineExpose({})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<!-- 内容 -->
|
||||
<div class="content-wrap" v-show="diyStore.editTab == 'content'">
|
||||
|
||||
<!-- 表单组件 字段内容设置 -->
|
||||
<slot name="field"></slot>
|
||||
<el-form label-width="100px" class="px-[10px]" @submit.prevent>
|
||||
<el-form-item :label="t('formPlaceholder')">
|
||||
<el-input v-model.trim="diyStore.editComponent.placeholder" :placeholder="t('formPlaceholderTips')" clearable maxlength="15" show-word-limit />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<!-- 表单组件 其他设置 -->
|
||||
<slot name="other"></slot>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 样式 -->
|
||||
<div class="style-wrap" v-show="diyStore.editTab == 'style'">
|
||||
|
||||
<!-- 表单组件 字段样式 -->
|
||||
<slot name="style-field"></slot>
|
||||
|
||||
<!-- 组件样式 -->
|
||||
<slot name="style"></slot>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { t } from '@/lang'
|
||||
import { ref } from 'vue'
|
||||
import useDiyStore from '@/stores/modules/diy'
|
||||
|
||||
const diyStore = useDiyStore()
|
||||
diyStore.editComponent.ignore = ['componentBgUrl'] // 忽略公共属性
|
||||
|
||||
// 组件验证
|
||||
diyStore.editComponent.verify = (index: number) => {
|
||||
const res = { code: true, message: '' }
|
||||
// todo 只需要考虑该组件自身的验证
|
||||
return res
|
||||
}
|
||||
defineExpose({})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
@@ -0,0 +1,177 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-form :inline="true" :model="tableData.searchParam" ref="searchFormRef">
|
||||
<el-form-item :label="t('formSelectContentTitle')" prop="title" class="form-item-wrap">
|
||||
<el-input v-model.trim="tableData.searchParam.title" :placeholder="t('formSelectContentTitlePlaceholder')" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('formSelectContentTypeName')" prop="type" class="form-item-wrap">
|
||||
<el-select v-model="tableData.searchParam.type" :placeholder="t('formSelectContentTypeNamePlaceholder')">
|
||||
<el-option :label="t('formSelectContentTypeAll')" value="" />
|
||||
<el-option v-for="(item, key) in formType" :label="item.title" :value="key" :key="key" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item class="form-item-wrap">
|
||||
<el-button type="primary" @click="loadList()">{{ t('search') }}</el-button>
|
||||
<el-button @click="resetForm(searchFormRef)">{{ t('reset') }}</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-table :data="tableData.data" size="large" ref="tableRef" v-loading="tableData.loading">
|
||||
<template #empty>
|
||||
<span>{{ !tableData.loading ? t('emptyData') : '' }}</span>
|
||||
</template>
|
||||
<el-table-column min-width="7%">
|
||||
<template #default="{ row }">
|
||||
<el-checkbox v-model="row.checked" @change="handleCheckChange($event,row)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="page_title" :label="t('formSelectContentTitle')" min-width="65%" />
|
||||
<el-table-column prop="type_name" :label="t('formSelectContentTypeName')" min-width="25%" />
|
||||
</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="loadList()" @current-change="loadList" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref, nextTick } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import { getFormType, getDiyFormSelectPageList } from '@/app/api/diy_form'
|
||||
import { FormInstance, ElMessage } from 'element-plus'
|
||||
|
||||
const prop = defineProps({
|
||||
formId: {
|
||||
type: [Number, String],
|
||||
default: 0
|
||||
}
|
||||
})
|
||||
|
||||
const formType: any = reactive({}) // 表单类型
|
||||
|
||||
const searchFormRef = ref<FormInstance>()
|
||||
|
||||
const tableRef = ref()
|
||||
|
||||
const tableData: any = reactive({
|
||||
page: 1,
|
||||
limit: 10,
|
||||
total: 0,
|
||||
loading: true,
|
||||
data: [],
|
||||
searchParam: {
|
||||
title: '',
|
||||
type: '',
|
||||
verify_form_ids: []
|
||||
}
|
||||
})
|
||||
|
||||
// 已选万能表单
|
||||
const selectData: any = reactive({
|
||||
form_id: prop.formId
|
||||
})
|
||||
|
||||
// 获取自定义表单列表
|
||||
const loadList = (page: number = 1) => {
|
||||
tableData.loading = true
|
||||
tableData.page = page
|
||||
|
||||
if (selectData.form_id) {
|
||||
tableData.searchParam.verify_form_ids = [selectData.form_id]
|
||||
}
|
||||
|
||||
getDiyFormSelectPageList({
|
||||
page: tableData.page,
|
||||
limit: tableData.limit,
|
||||
...tableData.searchParam
|
||||
}).then(res => {
|
||||
tableData.loading = false
|
||||
tableData.data = res.data.data
|
||||
|
||||
tableData.data.forEach((item: any) => {
|
||||
item.checked = item.form_id == selectData.form_id
|
||||
})
|
||||
tableData.total = res.data.total
|
||||
setGoodsSelected()
|
||||
}).catch(() => {
|
||||
tableData.loading = false
|
||||
})
|
||||
}
|
||||
|
||||
// 获取万能表单类型
|
||||
const loadFormType = (addon = '') => {
|
||||
getFormType({}).then(res => {
|
||||
for (const key in formType) {
|
||||
delete formType[key]
|
||||
}
|
||||
|
||||
for (const key in res.data) {
|
||||
formType[key] = res.data[key]
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
loadFormType()
|
||||
loadList()
|
||||
|
||||
const handleCheckChange = (isSelect: any, row: any) => {
|
||||
if (isSelect) {
|
||||
selectData.form_id = row.form_id
|
||||
} else {
|
||||
selectData.form_id = 0 // 未选中,移除当前
|
||||
}
|
||||
setGoodsSelected()
|
||||
}
|
||||
|
||||
// 表格设置选中状态
|
||||
const setGoodsSelected = () => {
|
||||
nextTick(() => {
|
||||
for (let i = 0; i < tableData.data.length; i++) {
|
||||
tableData.data[i].checked = false
|
||||
if (selectData.form_id == tableData.data[i].form_id) {
|
||||
tableData.data[i].checked = true
|
||||
Object.assign(selectData, tableData.data[i])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const resetForm = (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
formEl.resetFields()
|
||||
loadList()
|
||||
}
|
||||
|
||||
const getData = () => {
|
||||
if (selectData.form_id == 0) {
|
||||
ElMessage({
|
||||
type: 'warning',
|
||||
message: `${t('formSelectContentTips')}`
|
||||
})
|
||||
return
|
||||
}
|
||||
return {
|
||||
name: 'DIY_FORM',
|
||||
title: selectData.page_title,
|
||||
url: `/app/pages/index/diy_form?form_id=${selectData.form_id}`,
|
||||
action: '',
|
||||
formId: selectData.form_id
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
getData
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.form-item-wrap {
|
||||
margin-right: 10px !important;
|
||||
margin-bottom: 10px !important;
|
||||
|
||||
&.last-child {
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,435 @@
|
||||
<template>
|
||||
<div>
|
||||
<el-dialog v-model="showDialog" :title="t('submitSuccess')" width="850px" :close-on-press-escape="true" :destroy-on-close="true" :close-on-click-modal="false">
|
||||
|
||||
<div class="flex flex-1 mt-[24px] mx-[24px] mb-0">
|
||||
<div class="preview-wrap">
|
||||
<div class="absolute z-1 left-0 top-0">
|
||||
<img src="@/app/assets/images/diy_form/mobile_tabbar.png" class="w-[324px]" />
|
||||
</div>
|
||||
<div class="absolute z-1 left-0 bottom-0">
|
||||
<img src="@/app/assets/images/diy_form/mobile_bottom.png" class="w-[324px]" />
|
||||
</div>
|
||||
|
||||
<div class="page-wrap">
|
||||
<div class="px-[13px] flex flex-col items-center flex-1">
|
||||
<div class="flex items-center justify-center w-[48px] h-[48px] text-[40px] p-[4px] mt-[32px] mb-[16px] mx-auto rounded-[50%]">
|
||||
<el-icon><SuccessFilled color="#20bf64" /></el-icon>
|
||||
</div>
|
||||
<div class="record-name">
|
||||
<span class="text-[#1E1E1E] font-bold text-[24px]" v-if="formData.tips_type == 'default'">{{ t('writeSuccess') }}</span>
|
||||
<span class="text-[#1E1E1E] font-bold text-[16px]" v-else-if="formData.tips_type == 'diy'">{{ formData.tips_text ? formData.tips_text : '填写成功' }}</span>
|
||||
</div>
|
||||
<div class="to-detail">
|
||||
<div class="text-[14px] mt-[16px] py-[4px] px[8px] text-[#576b95]">{{ t('viewFillingDetails') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative pt-[8px] pb-[48px] h-[112px]">
|
||||
<div v-if="formData.success_after_action.finish" class="!mt-[16px] rounded-[3px] mx-auto text-[15px] w-[100px] min-w-[160px] h-[32px] leading-[32px] text-center max-w-[274px] truncate bg-[#20bf64] text-[#ffffff]">
|
||||
<div class="text-[15px]">{{ t('finish') }}</div>
|
||||
</div>
|
||||
<div v-if="formData.success_after_action.goback" class="!mt-[16px] rounded-[3px] mx-auto text-[15px] w-[100px] min-w-[160px] h-[32px] leading-[32px] text-center max-w-[274px] truncate bg-[#f2f2f2] text-[#353535]">
|
||||
<div class="text-[14px]">{{ t('back') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 核销凭证 todo 后续完善 -->
|
||||
<!-- <div class="page-wrap verify-voucher-wrap" style="display:none;">-->
|
||||
|
||||
<!-- <div class="tips-wrap">感谢你的填写,以下是你的核销凭证</div>-->
|
||||
<!-- <div class="qrcode-wrap">-->
|
||||
<!-- <div class="text-[14px] text-[#333]">请妥善保存你的核销凭证</div>-->
|
||||
<!-- <div class="text-[20px] font-bold text-[#333] my-[10px]">现场出示凭证</div>-->
|
||||
<!-- <el-image class="w-[180px]" :src="wapImage" />-->
|
||||
<!-- <div class="text-primary mt-[10px]">保存凭证</div>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="relative pt-[8px] pb-[48px] h-[112px]">-->
|
||||
<!-- <div class="!mt-[16px] rounded-[3px] mx-auto text-[15px] w-[100px] min-w-[160px] h-[32px] leading-[32px] text-center max-w-[274px] truncate bg-[#20bf64] text-[#ffffff]">-->
|
||||
<!-- <div class="text-[15px]">返回二维码</div>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="!mt-[16px] rounded-[3px] mx-auto text-[15px] w-[100px] min-w-[160px] h-[32px] leading-[32px] text-center max-w-[274px] truncate bg-[#fff] text-[#353535]">-->
|
||||
<!-- <div class="text-[14px]">完成</div>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<!-- <div class="text-[14px] mt-[16px] py-[4px] px[8px] text-[#576b95]">查看填写详情</div>-->
|
||||
|
||||
<!-- </div>-->
|
||||
|
||||
</div>
|
||||
|
||||
<div class="flex-1">
|
||||
<div class="item-wrap">
|
||||
<div class="text-[16px] h-[24px] font-bold text-[#262626] mb-[16px] w-[140px] mr-[32px] flex-shrink-0">{{ t('afterSubmission') }}</div>
|
||||
<el-radio-group v-model="formData.submit_after_action" class="!block">
|
||||
<el-radio label="text" class="!flex">
|
||||
<span class="mr-[3px]">{{ t('displayTextMessages') }}</span>
|
||||
<el-tooltip effect="light" placement="top">
|
||||
<template #content>
|
||||
<p>{{ t('displayTextMessagesTips') }}</p>
|
||||
</template>
|
||||
<el-icon>
|
||||
<QuestionFilled color="#999999" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</el-radio>
|
||||
<!-- todo 后续完善 -->
|
||||
<!-- <el-radio label="voucher" class="!flex">-->
|
||||
<!-- <span class="mr-[3px]">{{ t('获取核销凭证') }}</span>-->
|
||||
<!-- <el-tooltip effect="light" placement="top">-->
|
||||
<!-- <template #content>-->
|
||||
<!-- <p>{{ t('提交后页面会将提交的表单记录内容生成二维码并展示,可选择设置两种不同的二维码内容。适合核销、数据录入等场景。') }}</p>-->
|
||||
<!-- </template>-->
|
||||
<!-- <el-icon>-->
|
||||
<!-- <QuestionFilled color="#999999" />-->
|
||||
<!-- </el-icon>-->
|
||||
<!-- </el-tooltip>-->
|
||||
<!-- </el-radio>-->
|
||||
</el-radio-group>
|
||||
</div>
|
||||
|
||||
<div class="item-wrap" v-if="formData.submit_after_action == 'text'">
|
||||
<div class="text-[16px] h-[24px] font-bold text-[#262626] mb-[16px] w-[140px] mr-[32px] flex-shrink-0">{{ t('promptText') }}</div>
|
||||
<div>
|
||||
<el-radio-group v-model="formData.tips_type" class="!block">
|
||||
<el-radio label="default" class="!block">
|
||||
<span class="mr-[3px]">{{ t('defaultPrompt') }}</span>
|
||||
<span class="!text-[#999] text-[12px] ml-[8px]">{{ t('defaultPromptTips') }}</span>
|
||||
</el-radio>
|
||||
<el-radio label="diy" class="!block">{{ t('diyPrompt') }}</el-radio>
|
||||
</el-radio-group>
|
||||
<el-input v-if="formData.tips_type == 'diy'" v-model.trim="formData.tips_text" :placeholder="t('tipsTextPlaceholder')" class="w-[350px]" maxlength="30" clearable show-word-limit />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 核销凭证 todo 后续完善 -->
|
||||
<template v-else-if="formData.submit_after_action == 'voucher'">
|
||||
|
||||
<div class="item-wrap">
|
||||
<div class="text-[16px] h-[24px] font-bold text-[#262626] mb-[16px] w-[140px] mr-[32px] flex-shrink-0">{{ t('validityPeriodOfVoucher') }}</div>
|
||||
<div>
|
||||
<el-radio-group v-model="formData.time_limit_type" class="!block">
|
||||
<el-radio label="no_limit" class="!block">{{ t('noLimit') }}</el-radio>
|
||||
<el-radio label="specify_time" class="!block">
|
||||
<span class="mr-[3px]">{{ t('specifyTime') }}</span>
|
||||
<el-tooltip effect="light" placement="top">
|
||||
<template #content>
|
||||
<p>{{ t('specifyTimeTips') }}</p>
|
||||
</template>
|
||||
<el-icon>
|
||||
<QuestionFilled color="#999999" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</el-radio>
|
||||
<el-radio label="submission_time" class="!block">
|
||||
<span class="mr-[3px]">{{ t('submissionTime') }}</span>
|
||||
<el-tooltip effect="light" placement="top">
|
||||
<template #content>
|
||||
<p class="w-[250px]">{{ t('submissionTimeTips') }}</p>
|
||||
</template>
|
||||
<el-icon>
|
||||
<QuestionFilled color="#999999" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
<el-date-picker v-if="formData.time_limit_type == 'specify_time'" v-model="formData.validity_time" type="datetimerange" range-separator="至" start-placeholder="开始时间" end-placeholder="结束时间" />
|
||||
<div class="flex items-center mt-[5px]" v-if="formData.time_limit_type == 'submission_time'">
|
||||
<span>{{ t('afterSubmissionRecords') }}</span>
|
||||
<!-- <div class="flex items-center px-[5px]">-->
|
||||
<!-- v-model.trim="formData.length"-->
|
||||
<el-input v-model.trim="formData.submission_time_value" @keyup="filterNumber($event)" size="small" clearable class="!w-[100px] px-[5px]" maxlength="3" />
|
||||
<el-select v-model="formData.timeUnit" clearable class="!w-[100px] pr-[5px]" size="small">
|
||||
<el-option v-for="(item, index) in validityOptions" :key="item.value" :label="item.text" :value="item.value"></el-option>
|
||||
</el-select>
|
||||
<span>{{ t('effective') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="item-wrap">
|
||||
<div class="text-[16px] h-[24px] font-bold text-[#262626] mb-[16px] w-[140px] mr-[32px] flex-shrink-0">{{ t('voucherStyle') }}</div>
|
||||
<div>
|
||||
<el-form-item :label="t('titleAboveTheCode')">
|
||||
<!-- v-model.trim="formData.active_name"-->
|
||||
<el-input clearable :placeholder="t('titleAboveTheCodePlaceholder')" class="input-width" :maxlength="20" />
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('contentAboveTheCode')">
|
||||
<el-input clearable :placeholder="t('contentAboveTheCodePlaceholder')" class="input-width" :maxlength="20" />
|
||||
<div>
|
||||
<span class="text-primary cursor-pointer mr-[10px]">{{ t('addLinefeeds') }}</span>
|
||||
<span class="text-primary cursor-pointer">{{ t('addFields') }}</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item :label="t('contentBelowTheCode')">
|
||||
<div class="block">
|
||||
<el-checkbox class="!block" :label="t('submissionRecordTime')" value="" />
|
||||
|
||||
<el-checkbox class="!block !h-[20px]" :label="t('currentTime')" value="" />
|
||||
<div class="text-[#999] ml-[22px]">{{ t('currentTimeTips') }}</div>
|
||||
|
||||
<el-checkbox class="!block" :label="t('dispalyPromptText')" value="" />
|
||||
<el-input class="ml-[22px]" :rows="4" type="textarea" :placeholder="t('tipsTextPlaceholder')" maxlength="100" />
|
||||
|
||||
<el-checkbox class="!block" :label="t('voucherDeadline')" value="" />
|
||||
<el-checkbox class="!block" :label="t('saveVoucher')" value="" />
|
||||
</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- todo 后续完善 -->
|
||||
<div class="item-wrap">
|
||||
<div class="text-[16px] h-[24px] font-bold text-[#262626] mb-[16px] w-[140px] mr-[32px] flex-shrink-0">
|
||||
<span>{{ t('subsequentPperationButtons') }}</span>
|
||||
<!-- <p class="text-[12px] text-[#999] mt-[4px] font-normal">最多选择2个</p>-->
|
||||
</div>
|
||||
<div class="content-list-wrap">
|
||||
<!-- <el-checkbox-group :min="1" :max="2">-->
|
||||
<!-- <el-checkbox v-model="formData.success_after_action.share" label="转发填写内容" value="share">-->
|
||||
<!-- <div class="text-[#333]">转发填写内容</div>-->
|
||||
<!-- </el-checkbox>-->
|
||||
<!-- <p class="text-[#999] text-[12px] pl-[24px] mt-[4px]">提交表单后,可转发给微信好友查看。支持按钮文案自定义,提醒填表人转发给特定人员查看</p>-->
|
||||
|
||||
<el-checkbox v-model="formData.success_after_action.finish" :label="t('finish')" value="finish">
|
||||
<div class="text-[#333]">{{ t('finish') }}</div>
|
||||
</el-checkbox>
|
||||
<p class="text-[#999] text-[12px] pl-[24px] mt-[4px]">{{ t('finishTips') }}</p>
|
||||
|
||||
<el-checkbox v-model="formData.success_after_action.goback" :label="t('back')" value="goback">
|
||||
<div class="text-[#333]">{{ t('back') }}</div>
|
||||
</el-checkbox>
|
||||
<p class="text-[#999] text-[12px] pl-[24px] mt-[4px]">{{ t('backTips') }}</p>
|
||||
<!-- </el-checkbox-group>-->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-footer">
|
||||
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button>
|
||||
<el-button type="primary" @click="confirm">{{ t('save') }}</el-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { t } from '@/lang'
|
||||
import { ref, reactive } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import QRCode from 'qrcode'
|
||||
import storage from '@/utils/storage'
|
||||
import { filterNumber } from '@/utils/common'
|
||||
import { getUrl } from '@/app/api/sys'
|
||||
import { getFormSubmitConfig,editDiyFormSubmitConfig } from '@/app/api/diy_form'
|
||||
|
||||
const showDialog = ref(false)
|
||||
const repeat = ref(false)
|
||||
|
||||
/**
|
||||
* 表单数据
|
||||
*/
|
||||
const initialFormData = {
|
||||
id: 0,
|
||||
form_id: 0,
|
||||
submit_after_action: 'text', // 填表人提交后操作,text:文字信息,voucher:核销凭证
|
||||
tips_type: 'default', // 提示内容类型,default:默认提示,diy:自定义提示
|
||||
tips_text: '', // 自定义提示内容
|
||||
time_limit_type: 'no_limit', // 核销凭证有效期限制类型,no_limit:不限制,specify_time:指定固定开始结束时间,submission_time:按提交时间设置有效期
|
||||
// 核销凭证时间限制规则,json格式 todo 结构待定,后续完善
|
||||
time_limit_rule: {
|
||||
validity_time: [], // 指定固定开始结束时间
|
||||
submission_time_value: '', // 按提交时间设置有效期
|
||||
timeUnit: 'day', // 提交时间单位
|
||||
},
|
||||
// 核销凭证内容,json格式 todo 结构待定,后续完善
|
||||
voucher_content_rule: {},
|
||||
// 填写成功后续操作
|
||||
success_after_action: {
|
||||
share: false, // 转发填写内容
|
||||
finish: true, // 完成
|
||||
goback: true, // 返回
|
||||
}
|
||||
}
|
||||
|
||||
const formData: Record<string, any> = reactive({ ...initialFormData })
|
||||
|
||||
const wapUrl = ref('')
|
||||
const wapDomain = ref('')
|
||||
const wapImage = ref('')
|
||||
const wapPreview = ref('')
|
||||
const page = ref('')
|
||||
|
||||
// 核销凭证有效期
|
||||
const validityOptions = reactive([
|
||||
{
|
||||
text:'天',
|
||||
value:'day'
|
||||
},
|
||||
{
|
||||
text:'周',
|
||||
value:'week'
|
||||
},
|
||||
{
|
||||
text:'月',
|
||||
value:'month'
|
||||
},
|
||||
{
|
||||
text:'年',
|
||||
value:'year'
|
||||
},
|
||||
{
|
||||
text:'分钟',
|
||||
value:'minutes'
|
||||
}
|
||||
])
|
||||
|
||||
// getUrl().then((res: any) => {
|
||||
// wapUrl.value = res.data.wap_url
|
||||
//
|
||||
// // 生产模式禁止
|
||||
// if (import.meta.env.MODE == 'production') return
|
||||
//
|
||||
// wapDomain.value = res.data.wap_domain
|
||||
//
|
||||
// // env文件配置过wap域名
|
||||
// if (wapDomain.value) {
|
||||
// wapUrl.value = wapDomain.value + '/wap'
|
||||
// }
|
||||
//
|
||||
// const wapDomainStorage = storage.get('wap_domain')
|
||||
// if (wapDomainStorage) {
|
||||
// wapUrl.value = wapDomainStorage
|
||||
// }
|
||||
// })
|
||||
|
||||
const loadQrcode = () => {
|
||||
wapPreview.value = `${wapUrl.value}${page.value}`
|
||||
// errorCorrectionLevel:密度容错率L(低)H(高)
|
||||
QRCode.toDataURL(wapPreview.value, { errorCorrectionLevel: 'L', margin: 0, width: 120 }).then(url => {
|
||||
wapImage.value = url
|
||||
})
|
||||
}
|
||||
|
||||
const emit = defineEmits(['complete'])
|
||||
|
||||
const setFormData = async (row: any = null) => {
|
||||
Object.assign(formData, initialFormData)
|
||||
if (row) {
|
||||
const data = await (await getFormSubmitConfig(row.form_id)).data
|
||||
if (data) {
|
||||
Object.keys(formData).forEach((key: string) => {
|
||||
if (data[key] != undefined) formData[key] = data[key]
|
||||
})
|
||||
}
|
||||
// todo 靠后完善
|
||||
// page.value = `/app/pages/index/diy_form?form_id=${formData.form_id}`
|
||||
// loadQrcode()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认
|
||||
*/
|
||||
const confirm = () => {
|
||||
if(formData.tips_type == 'diy' && !formData.tips_text){
|
||||
ElMessage.error('提示不能为空')
|
||||
return
|
||||
}
|
||||
|
||||
if (repeat.value) return;
|
||||
repeat.value = true
|
||||
|
||||
const data = formData
|
||||
|
||||
editDiyFormSubmitConfig(data).then(res => {
|
||||
repeat.value = false
|
||||
showDialog.value = false
|
||||
emit('complete')
|
||||
}).catch(err => {
|
||||
repeat.value = false
|
||||
})
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
showDialog,
|
||||
setFormData
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.preview-wrap {
|
||||
position: relative;
|
||||
width: 324px;
|
||||
min-height: 555px;
|
||||
padding: 82px 12px 20px;
|
||||
background-size: 100%;
|
||||
background-repeat: repeat-y;
|
||||
background-image: url(../../../../app/assets/images/diy_form/mobile_line.png);
|
||||
border-radius: 38px;
|
||||
overflow: hidden;
|
||||
box-shadow: none;
|
||||
background-color: #fff !important;
|
||||
margin-right: 24px;
|
||||
overflow-y: auto;
|
||||
|
||||
.page-wrap {
|
||||
position: relative;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-ms-flex-direction: column;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
text-align: center;
|
||||
min-height: 548px;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.verify-voucher-wrap {
|
||||
background-color: #f4f4f4;
|
||||
.tips-wrap{
|
||||
font-size: 15px;
|
||||
font-weight: 400;
|
||||
line-height: 21px;
|
||||
color: rgba(0,0,0,.65);
|
||||
margin: 20px 10px;
|
||||
}
|
||||
.qrcode-wrap{
|
||||
border-radius: 12px;
|
||||
margin: 0 20px;
|
||||
background: #fff;
|
||||
padding: 20px 10px 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.item-wrap {
|
||||
padding: 20px 24px 24px;
|
||||
background-color: #fff;
|
||||
border-radius: 2px;
|
||||
display: flex;
|
||||
position: relative;
|
||||
|
||||
&:after {
|
||||
content: "";
|
||||
display: block;
|
||||
height: 1px;
|
||||
width: calc(100% - 48px);
|
||||
background-color: hsla(210, 8%, 51%, .13);
|
||||
position: absolute;
|
||||
left: 24px;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
&:last-child:after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,341 @@
|
||||
<template>
|
||||
<el-dialog v-model="showDialog" :title="t('writeSet')" width="600px" class="diy-dialog-wrap" :close-on-press-escape="true" :destroy-on-close="true" :close-on-click-modal="false">
|
||||
|
||||
<el-form :model="formData" label-width="120px" ref="formRef" :rules="formRules" class="page-form" v-loading="loading">
|
||||
|
||||
<!-- <el-form-item :label="t('填写方式')">-->
|
||||
<!-- <el-radio-group v-model="formData.write_way">-->
|
||||
<!-- <el-radio label="no_limit">{{t('不限制')}}</el-radio>-->
|
||||
<!-- <el-radio label="scan">{{t('仅限扫一扫')}}</el-radio>-->
|
||||
<!-- <el-radio label="url">{{t('仅限链接进入')}}</el-radio>-->
|
||||
<!-- </el-radio-group>-->
|
||||
<!-- </el-form-item>-->
|
||||
|
||||
<el-form-item :label="t('joinMemberType')">
|
||||
<el-radio-group v-model="formData.join_member_type">
|
||||
<el-radio label="all_member">{{t('allMember')}}</el-radio>
|
||||
<el-radio label="selected_member_level">{{t('selectedMemberLevel')}}</el-radio>
|
||||
<el-radio label="selected_member_label">{{t('selectedMemberLabel')}}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 会员标签 -->
|
||||
<el-form-item :label="t('memberLabel')" prop="label_ids" v-if="formData.join_member_type=='selected_member_label'">
|
||||
<el-select v-model="formData.label_ids" clearable multiple :placeholder="t('memberLabelPlaceholder')" class="input-width">
|
||||
<el-option :label="item['label_name']" :value="item['label_id']" v-for="(item, index) in labelSelectData" :key="index" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<!-- 会员等级 -->
|
||||
<el-form-item :label="t('memberLevel')" prop="level_ids" v-if="formData.join_member_type=='selected_member_level'">
|
||||
<el-select v-model="formData.level_ids" clearable multiple :placeholder="t('memberLevelPlaceholder')" class="input-width">
|
||||
<el-option :label="item['level_name']" :value="item['level_id']" v-for="(item, index) in levelSelectData" :key="index" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('apieceFillQuantity')" :class="{ '!mb-[5px]' : formData.member_write_type == 'diy' }">
|
||||
<el-radio-group v-model="formData.member_write_type">
|
||||
<el-radio label="no_limit">{{t('noLimit')}}</el-radio>
|
||||
<el-radio label="diy">{{t('diy')}}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label=" " v-if="formData.member_write_type == 'diy'" prop="member_write_rule">
|
||||
<div class="flex items-center">
|
||||
<span>每</span>
|
||||
<el-input v-model.trim="formData.member_write_rule.time_value" @keyup="filterNumber($event)" size="small" class="!w-[50px] px-[5px]" maxlength="3" />
|
||||
<el-select v-model="formData.member_write_rule.time_unit" class="!w-[60px] pr-[5px]" size="small">
|
||||
<el-option v-for="(item, index) in validityOptions" :key="item.value" :label="item.text" :value="item.value"></el-option>
|
||||
</el-select>
|
||||
<span>可填写</span>
|
||||
<el-input v-model.trim="formData.member_write_rule.num" @keyup="filterNumber($event)" size="small" class="!w-[50px] px-[5px]" maxlength="3" />
|
||||
<span>次</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('fillQuantityTotal')" :class="{ '!mb-[5px]' : formData.form_write_type == 'diy' }">
|
||||
<el-radio-group v-model="formData.form_write_type">
|
||||
<el-radio label="no_limit">{{t('noLimit')}}</el-radio>
|
||||
<el-radio label="diy">{{t('diy')}}</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label=" " v-if="formData.form_write_type == 'diy'" prop="form_write_rule">
|
||||
<div class="flex items-center">
|
||||
<span>每</span>
|
||||
<el-input v-model.trim="formData.form_write_rule.time_value" @keyup="filterNumber($event)" size="small" class="!w-[50px] px-[5px]" maxlength="3" />
|
||||
<el-select v-model="formData.form_write_rule.time_unit" class="!w-[60px] pr-[5px]" size="small">
|
||||
<el-option v-for="(item, index) in validityOptions" :key="item.value" :label="item.text" :value="item.value"></el-option>
|
||||
</el-select>
|
||||
<span>可填写</span>
|
||||
<el-input v-model.trim="formData.form_write_rule.num" @keyup="filterNumber($event)" size="small" class="!w-[50px] px-[5px]" maxlength="3" />
|
||||
<span class="mr-[5px]">次</span>
|
||||
<el-tooltip effect="light" placement="top">
|
||||
<template #content>
|
||||
<p class="w-[250px]">{{ t('writeTips') }}</p>
|
||||
</template>
|
||||
<el-icon>
|
||||
<QuestionFilled color="#999999" />
|
||||
</el-icon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="t('fillInTheTimePeriod')" prop="time_limit_rule">
|
||||
<el-radio-group v-model="formData.time_limit_type">
|
||||
<el-radio label="no_limit">{{t('noLimit')}}</el-radio>
|
||||
<el-radio label="specify_time">{{t('setSpecifyTime')}}</el-radio>
|
||||
<el-radio label="open_day_time">{{t('openDayTime')}}</el-radio>
|
||||
</el-radio-group>
|
||||
<el-date-picker v-if="formData.time_limit_type == 'specify_time'" v-model="formData.time_limit_rule.specify_time" type="datetimerange" range-separator="至" start-placeholder="开始时间" end-placeholder="结束时间" />
|
||||
<div class="flex items-center mt-[5px]" v-if="formData.time_limit_type == 'open_day_time'">
|
||||
<span class="mr-[5px]">每天</span>
|
||||
<el-time-picker class="!w-[180px]" v-model="formData.time_limit_rule.open_day_time" format="HH:mm" value-format="HH:mm" is-range range-separator="-" start-placeholder="开始时间" end-placeholder="结束时间" />
|
||||
<span class="ml-[5px]">可填写</span>
|
||||
</div>
|
||||
</el-form-item>
|
||||
|
||||
<!-- <el-form-item :label="t('允许修改内容')" class="display-block">-->
|
||||
<!-- <el-switch v-model="formData.is_allow_update_content" :active-value="1" :inactive-value="0" />-->
|
||||
<!-- <div class="text-sm text-gray-400">{{ t('开启后,填表人可以修改自己填写的内容。') }}</div>-->
|
||||
<!-- </el-form-item>-->
|
||||
|
||||
<!-- <el-form-item :label="t('填写须知')">-->
|
||||
<!-- <el-input v-model.trim="formData.write_instruction" :placeholder="t('请输入填写须知')" type="textarea" maxlength="500" show-word-limit rows="5" class="w-[400px]" clearable />-->
|
||||
<!-- </el-form-item>-->
|
||||
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click="showDialog = false">{{ t('cancel') }}</el-button>
|
||||
<el-button type="primary" :loading="loading" @click="confirm(formRef)">{{ t('confirm') }}</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import { t } from '@/lang'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import { filterNumber } from '@/utils/common'
|
||||
import {getMemberLabelAll,getMemberLevelAll } from '@/app/api/member'
|
||||
import { getFormWriteConfig,editDiyFormWriteConfig } from '@/app/api/diy_form'
|
||||
|
||||
const showDialog = ref(false)
|
||||
const loading = ref(false)
|
||||
|
||||
/**
|
||||
* 表单数据
|
||||
*/
|
||||
const initialFormData = {
|
||||
id: 0,
|
||||
form_id: 0, // 万能表单id
|
||||
write_way: 'no_limit', // 填写方式,no_limit:不限制,scan:仅限微信扫一扫,url:仅限链接进入
|
||||
join_member_type: 'all_member', // 参与会员,all_member:所有会员参与,selected_member_level:指定会员等级,selected_member_label:指定会员标签
|
||||
level_ids: [], // 会员等级id集合
|
||||
label_ids: [], // 会员标签id集合
|
||||
member_write_type: 'no_limit', // 每人可填写次数,no_limit:不限制,diy:自定义
|
||||
// 每人可填写次数自定义规则
|
||||
member_write_rule: {
|
||||
time_value: 1, // 时间
|
||||
time_unit: 'day', // 时间单位
|
||||
num: 1 // 可填写次数
|
||||
},
|
||||
form_write_type: 'no_limit', // 表单可填写数量,no_limit:不限制,diy:自定义
|
||||
// 表单可填写总数自定义规则
|
||||
form_write_rule: {
|
||||
time_value: 1, // 时间
|
||||
time_unit: 'day', // 时间单位
|
||||
num: 1 // 可填写次数
|
||||
},
|
||||
time_limit_type: 'no_limit', // 填写时间限制类型,no_limit:不限制,specify_time:指定开始结束时间,open_day_time:设置每日开启时间
|
||||
// 填写时间限制规则
|
||||
time_limit_rule: {
|
||||
specify_time: [], // 指定开始结束时间
|
||||
open_day_time: [], // 设置每日开启时间
|
||||
},
|
||||
is_allow_update_content: 0, // 是否允许修改自己填写的内容,0:否,1:是
|
||||
write_instruction: '', // 表单填写须知
|
||||
}
|
||||
|
||||
const formData: Record<string, any> = reactive({ ...initialFormData })
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = computed(() => {
|
||||
return {
|
||||
label_ids: [
|
||||
{ required: true, message: t('labelTips'), trigger: 'blur' }
|
||||
],
|
||||
level_ids: [
|
||||
{ required: true, message: t('levelTips'), trigger: 'blur' }
|
||||
],
|
||||
member_write_rule: [
|
||||
{
|
||||
validator: (rule: any, value: string, callback: any) => {
|
||||
let unit = ''
|
||||
validityOptions.forEach((item,index)=>{
|
||||
if(item.value == value.time_unit){
|
||||
unit = item.text;
|
||||
}
|
||||
})
|
||||
if(formData.member_write_type == 'diy'){
|
||||
if(!value.time_value){
|
||||
callback(new Error(`${unit}数不能为空`))
|
||||
}else if(!value.num){
|
||||
callback(new Error(t('numCannotNull')))
|
||||
}else{
|
||||
callback()
|
||||
}
|
||||
}else{
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: ['blur', 'change']
|
||||
}
|
||||
],
|
||||
form_write_rule: [
|
||||
{
|
||||
validator: (rule: any, value: string, callback: any) => {
|
||||
let unit = ''
|
||||
validityOptions.forEach((item,index)=>{
|
||||
if(item.value == value.time_unit){
|
||||
unit = item.text;
|
||||
}
|
||||
})
|
||||
if(formData.member_write_type == 'diy'){
|
||||
if(!value.time_value){
|
||||
callback(new Error(`${unit}数不能为空`))
|
||||
}else if(!value.num){
|
||||
callback(new Error(t('numCannotNull')))
|
||||
}else{
|
||||
callback()
|
||||
}
|
||||
}else{
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: ['blur', 'change']
|
||||
}
|
||||
],
|
||||
time_limit_rule: [
|
||||
{
|
||||
validator: (rule: any, value: string, callback: any) => {
|
||||
if (formData.time_limit_type == 'specify_time' && (!value.specify_time || !value.specify_time.length)) {
|
||||
callback(new Error(t('timeLimitRuleOne')))
|
||||
} else if (formData.time_limit_type == 'open_day_time' && (!value.open_day_time || !value.open_day_time.length)) {
|
||||
callback(new Error(t('timeLimitRuleTwo')))
|
||||
} else if (formData.time_limit_type == 'open_day_time' && value.open_day_time && value.open_day_time.length) {
|
||||
if (value.open_day_time[0] == value.open_day_time[1]) {
|
||||
callback(new Error(t('timeLimitRuleThree')))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
trigger: ['blur', 'change']
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
const levelSelectData = ref([])
|
||||
const labelSelectData = ref([])
|
||||
|
||||
// 获取全部标签
|
||||
getMemberLabelAll().then(({ data }) => {
|
||||
labelSelectData.value = data
|
||||
})
|
||||
|
||||
getMemberLevelAll().then(({ data }) => {
|
||||
levelSelectData.value = data
|
||||
})
|
||||
|
||||
const validityOptions = reactive([
|
||||
{
|
||||
text:'天',
|
||||
value:'day'
|
||||
},
|
||||
{
|
||||
text:'周',
|
||||
value:'week'
|
||||
},
|
||||
{
|
||||
text:'月',
|
||||
value:'month'
|
||||
},
|
||||
{
|
||||
text:'年',
|
||||
value:'year'
|
||||
}
|
||||
])
|
||||
|
||||
const emit = defineEmits(['complete'])
|
||||
|
||||
const setFormData = async (row: any = null) => {
|
||||
Object.assign(formData, initialFormData)
|
||||
loading.value = true
|
||||
if (row) {
|
||||
const data = await (await getFormWriteConfig(row.form_id)).data
|
||||
if (data && Object.keys(data).length) {
|
||||
Object.keys(formData).forEach((key: string) => {
|
||||
if (data[key] != undefined) formData[key] = data[key]
|
||||
})
|
||||
}else{
|
||||
formData.form_id = row.form_id;
|
||||
}
|
||||
}
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认
|
||||
* @param formEl
|
||||
*/
|
||||
const confirm = async (formEl: FormInstance | undefined) => {
|
||||
if (loading.value || !formEl) return
|
||||
await formEl.validate(async (valid) => {
|
||||
if (valid) {
|
||||
loading.value = true
|
||||
|
||||
const data = formData
|
||||
|
||||
editDiyFormWriteConfig(data).then(res => {
|
||||
loading.value = false
|
||||
showDialog.value = false
|
||||
emit('complete')
|
||||
}).catch(err => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const filterSpecial = (event:any) => {
|
||||
event.target.value = event.target.value.replace(/[^\u4e00-\u9fa5a-zA-Z0-9]/g, '')
|
||||
event.target.value = event.target.value.replace(/[`~!@#$%^&*()_\-+=<>?:"{}|,.\/;'\\[\]·~!@#¥%……&*()——\-+={}|《》?:“”【】、;‘’,。、]/g, '')
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
showDialog,
|
||||
setFormData
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
<style lang="scss">
|
||||
.diy-dialog-wrap .el-form-item__label{
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
.display-block {
|
||||
.el-form-item__content {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
1022
admin-vben/src/app/views/diy_form/edit.vue
Normal file
1022
admin-vben/src/app/views/diy_form/edit.vue
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user