🧹 清理重复配置文件

- 删除根目录中重复的 NestJS 配置文件
- 删除 tsconfig.json, tsconfig.build.json, eslint.config.mjs, .prettierrc
- 保留 wwjcloud-nest/ 目录中的完整配置
- 避免配置冲突,确保项目结构清晰
This commit is contained in:
wanwu
2025-10-14 23:56:20 +08:00
parent 7a160dd04b
commit e7a1d6b4d6
3263 changed files with 356 additions and 112679 deletions

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

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

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

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

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

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

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

View File

@@ -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 ? '&emsp;|--' : '--'
}
return t
})
</script>
<style lang="scss" scoped>
</style>

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

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

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

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

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

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

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

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

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

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

View File

@@ -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.pngpush.pngsplash.png放置到drawabledrawable-ldpidrawable-mdpidrawable-hdpidrawable-xhdpidrawable-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>

View File

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

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

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

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

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

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

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

View File

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

View File

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

View File

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

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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密度容错率LH(高)
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>

View File

@@ -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, // 是否允许修改自己填写的内容01
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>

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