This commit is contained in:
Your Name
2026-03-01 14:17:59 +08:00
parent 65aa12f146
commit a07e844c47
6071 changed files with 777651 additions and 0 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

+132
View File
@@ -0,0 +1,132 @@
<template>
<div class="login flex flex-col">
<div class="flex-1 flex items-center justify-center">
<div class="login-card flex rounded-md overflow-hidden">
<div class="flex-1 h-full hidden md:inline-block">
<image-contain :src="config.login_image" :width="400" height="100%" />
</div>
<div
class="login-form bg-body flex flex-col justify-center px-10 py-10 md:w-[400px] w-[375px] flex-none mx-auto"
>
<div class="text-center text-3xl font-medium mb-8">{{ config.web_name }}</div>
<el-form ref="formRef" :model="formData" size="large" :rules="rules">
<el-form-item prop="account">
<el-input
v-model="formData.account"
placeholder="请输入账号"
@keyup.enter="handleEnter"
>
<template #prepend>
<icon name="el-icon-User" size="16" />
</template>
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input
ref="passwordRef"
v-model="formData.password"
show-password
placeholder="请输入密码"
@keyup.enter="handleLogin"
>
<template #prepend>
<icon name="el-icon-Lock" size="16" />
</template>
</el-input>
</el-form-item>
</el-form>
<div class="mb-5">
<el-checkbox v-model="remAccount" label="记住账号"></el-checkbox>
</div>
<el-button type="primary" size="large" :loading="isLock" @click="lockLogin">
登录
</el-button>
</div>
</div>
</div>
<layout-footer />
</div>
</template>
<script lang="ts" setup>
import type { FormInstance, InputInstance } from 'element-plus'
import { computed, onMounted, reactive, ref, shallowRef } from 'vue'
import { ACCOUNT_KEY } from '@/enums/cacheEnums'
import { PageEnum } from '@/enums/pageEnum'
import { useLockFn } from '@/hooks/useLockFn'
import LayoutFooter from '@/layout/components/footer.vue'
import useAppStore from '@/stores/modules/app'
import useUserStore from '@/stores/modules/user'
import cache from '@/utils/cache'
const passwordRef = shallowRef<InputInstance>()
const formRef = shallowRef<FormInstance>()
const appStore = useAppStore()
const userStore = useUserStore()
const route = useRoute()
const router = useRouter()
const remAccount = ref(false)
const config = computed(() => appStore.config)
const formData = reactive({
account: '',
password: ''
})
const rules = {
account: [
{
required: true,
message: '请输入账号',
trigger: ['blur']
}
],
password: [
{
required: true,
message: '请输入密码',
trigger: ['blur']
}
]
}
// 回车按键监听
const handleEnter = () => {
if (!formData.password) {
return passwordRef.value?.focus()
}
handleLogin()
}
// 登录处理
const handleLogin = async () => {
await formRef.value?.validate()
// 记住账号,缓存
cache.set(ACCOUNT_KEY, {
remember: remAccount.value,
account: remAccount.value ? formData.account : ''
})
await userStore.login(formData)
const {
query: { redirect }
} = route
const path = typeof redirect === 'string' ? redirect : PageEnum.INDEX
router.push(path)
}
const { isLock, lockFn: lockLogin } = useLockFn(handleLogin)
onMounted(() => {
const value = cache.get(ACCOUNT_KEY)
if (value?.remember) {
remAccount.value = value.remember
formData.account = value.account
}
})
</script>
<style lang="scss" scoped>
.login {
background-image: url('./images/login_bg.png');
@apply min-h-screen bg-no-repeat bg-center bg-cover;
.login-card {
height: 400px;
}
}
</style>
+53
View File
@@ -0,0 +1,53 @@
<template>
<div>
<el-card shadow="never" class="!border-none">
<template #header>
<span class="font-extrabold text-lg">充值设置</span>
</template>
<el-form :model="formData" label-width="120px">
<el-form-item label="状态">
<div>
<el-radio-group v-model="formData.status" class="ml-4">
<el-radio :value="1">开启</el-radio>
<el-radio :value="0">关闭</el-radio>
</el-radio-group>
<div class="form-tips">关闭或开启充值功能关闭后将不显示充值入口</div>
</div>
</el-form-item>
<el-form-item label="最低充值金额">
<div>
<el-input
v-model="formData.min_amount"
placeholder="请输入最低充值金额"
clearable
/>
<div class="form-tips">
最低充值金额要求不填或填0表示不限制最低充值金额
</div>
</div>
</el-form-item>
</el-form>
</el-card>
<footer-btns v-perms="['recharge.recharge/setConfig']">
<el-button type="primary" @click="handleSubmit">保存</el-button>
</footer-btns>
</div>
</template>
<script lang="ts" setup name="rechargeConfig">
import { getRechargeConfig, setRechargeConfig } from '@/api/app/recharge'
const formData = reactive({
status: 1, //功能状态 1-开启 0-关闭
min_amount: '' //最低充值金额
})
const getConfig = async () => {
const data = await getRechargeConfig()
Object.assign(formData, data)
}
const handleSubmit = async () => {
await setRechargeConfig(formData)
getConfig()
}
getConfig()
</script>
+95
View File
@@ -0,0 +1,95 @@
<template>
<div class="edit-popup">
<popup
ref="popupRef"
:title="popupTitle"
:async="true"
width="550px"
@confirm="handleSubmit"
@close="handleClose"
>
<el-form ref="formRef" :model="formData" label-width="84px" :rules="formRules">
<el-form-item label="栏目名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入栏目名称" clearable />
</el-form-item>
<el-form-item label="排序" prop="sort">
<div>
<el-input-number v-model="formData.sort" :min="0" :max="9999" />
<div class="form-tips">默认为0 数值越大越排前</div>
</div>
</el-form-item>
<el-form-item label="状态" prop="is_show">
<el-switch v-model="formData.is_show" :active-value="1" :inactive-value="0" />
</el-form-item>
</el-form>
</popup>
</div>
</template>
<script lang="ts" setup name="articleColumnEdit">
import type { FormInstance } from 'element-plus'
import { articleCateAdd, articleCateDetail, articleCateEdit } from '@/api/article'
import Popup from '@/components/popup/index.vue'
const emit = defineEmits(['success', 'close'])
const formRef = shallowRef<FormInstance>()
const popupRef = shallowRef<InstanceType<typeof Popup>>()
const mode = ref('add')
const popupTitle = computed(() => {
return mode.value == 'edit' ? '编辑栏目' : '新增栏目'
})
const formData = reactive({
id: '',
name: '',
sort: 0,
is_show: 1
})
const formRules = {
name: [
{
required: true,
message: '请输入栏目名称',
trigger: ['blur']
}
]
}
const handleSubmit = async () => {
await formRef.value?.validate()
mode.value == 'edit' ? await articleCateEdit(formData) : await articleCateAdd(formData)
popupRef.value?.close()
emit('success')
}
const open = (type = 'add') => {
mode.value = type
popupRef.value?.open()
}
const setFormData = (data: Record<any, any>) => {
for (const key in formData) {
if (data[key] != null && data[key] != undefined) {
//@ts-ignore
formData[key] = data[key]
}
}
}
const getDetail = async (row: Record<string, any>) => {
const data = await articleCateDetail({
id: row.id
})
setFormData(data)
}
const handleClose = () => {
emit('close')
}
defineExpose({
open,
setFormData,
getDetail
})
</script>
+110
View File
@@ -0,0 +1,110 @@
<template>
<div>
<el-card class="!border-none" shadow="never">
<el-alert
type="warning"
title="温馨提示:用于管理网站的分类,只可添加到一级"
:closable="false"
show-icon
/>
</el-card>
<el-card class="!border-none mt-4" shadow="never" v-loading="pager.loading">
<div>
<el-button
class="mb-4"
v-perms="['article.articleCate/add']"
type="primary"
@click="handleAdd()"
>
<template #icon>
<icon name="el-icon-Plus" />
</template>
新增
</el-button>
</div>
<el-table size="large" :data="pager.lists">
<el-table-column label="栏目名称" prop="name" min-width="120" />
<el-table-column label="文章数" prop="article_count" min-width="120" />
<el-table-column label="状态" min-width="120">
<template #default="{ row }">
<el-switch
v-perms="['article.articleCate/updateStatus']"
v-model="row.is_show"
:active-value="1"
:inactive-value="0"
@change="changeStatus($event, row.id)"
/>
</template>
</el-table-column>
<el-table-column label="排序" prop="sort" min-width="120" />
<el-table-column label="操作" width="120" fixed="right">
<template #default="{ row }">
<el-button
v-perms="['article.articleCate/edit']"
type="primary"
link
@click="handleEdit(row)"
>
编辑
</el-button>
<el-button
v-perms="['article.articleCate/delete']"
type="danger"
link
@click="handleDelete(row.id)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<div class="flex justify-end mt-4">
<pagination v-model="pager" @change="getLists" />
</div>
</el-card>
<edit-popup v-if="showEdit" ref="editRef" @success="getLists" @close="showEdit = false" />
</div>
</template>
<script lang="ts" setup name="articleColumn">
import { articleCateDelete, articleCateLists, articleCateStatus } from '@/api/article'
import { usePaging } from '@/hooks/usePaging'
import feedback from '@/utils/feedback'
import EditPopup from './edit.vue'
const editRef = shallowRef<InstanceType<typeof EditPopup>>()
const showEdit = ref(false)
const { pager, getLists } = usePaging({
fetchFun: articleCateLists
})
const handleAdd = async () => {
showEdit.value = true
await nextTick()
editRef.value?.open('add')
}
const handleEdit = async (data: any) => {
showEdit.value = true
await nextTick()
editRef.value?.open('edit')
editRef.value?.getDetail(data)
}
const handleDelete = async (id: number) => {
await feedback.confirm('确定要删除?')
await articleCateDelete({ id })
getLists()
}
const changeStatus = async (is_show: any, id: number) => {
try {
await articleCateStatus({ id, is_show })
getLists()
} catch (error) {
getLists()
}
}
getLists()
</script>
+174
View File
@@ -0,0 +1,174 @@
<template>
<div class="article-edit">
<el-card class="!border-none" shadow="never">
<el-page-header :content="$route.meta.title" @back="$router.back()" />
</el-card>
<el-card class="mt-4 !border-none" shadow="never">
<el-form
ref="formRef"
class="ls-form"
:model="formData"
label-width="85px"
:rules="rules"
>
<div class="xl:flex">
<div>
<el-form-item label="文章标题" prop="title">
<div class="w-80">
<el-input
v-model="formData.title"
placeholder="请输入文章标题"
type="textarea"
:autosize="{ minRows: 3, maxRows: 3 }"
maxlength="64"
show-word-limit
clearable
/>
</div>
</el-form-item>
<el-form-item label="文章栏目" prop="cid">
<el-select
class="w-80"
v-model="formData.cid"
placeholder="请选择文章栏目"
clearable
>
<el-option
v-for="item in optionsData.article_cate"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="文章简介" prop="desc">
<div class="w-80">
<el-input
v-model="formData.desc"
placeholder="请输入文章简介"
type="textarea"
:autosize="{ minRows: 3, maxRows: 6 }"
:maxlength="200"
show-word-limit
clearable
/>
</div>
</el-form-item>
<el-form-item label="摘要" prop="abstract">
<div class="w-80">
<el-input
type="textarea"
:autosize="{ minRows: 6, maxRows: 6 }"
v-model="formData.abstract"
maxlength="200"
show-word-limit
clearable
/>
</div>
</el-form-item>
<el-form-item label="文章封面" prop="image">
<div>
<div>
<material-picker v-model="formData.image" :limit="1" />
</div>
<div class="form-tips">建议尺寸240*180px</div>
</div>
</el-form-item>
<el-form-item label="作者" prop="author">
<div class="w-80">
<el-input v-model="formData.author" placeholder="请输入作者名称" />
</div>
</el-form-item>
<el-form-item label="排序" prop="sort">
<div>
<el-input-number v-model="formData.sort" :min="0" :max="9999" />
<div class="form-tips">默认为0 数值越大越排前</div>
</div>
</el-form-item>
<el-form-item label="初始浏览量" prop="click_virtual">
<div>
<el-input-number v-model="formData.click_virtual" :min="0" />
</div>
</el-form-item>
<el-form-item label="文章状态" required prop="is_show">
<el-radio-group v-model="formData.is_show">
<el-radio :value="1">显示</el-radio>
<el-radio :value="0">隐藏</el-radio>
</el-radio-group>
</el-form-item>
</div>
<div class="xl:ml-20">
<el-form-item label="文章内容" prop="content">
<editor v-model="formData.content" :height="667" :width="375" />
</el-form-item>
</div>
</div>
</el-form>
</el-card>
<footer-btns>
<el-button type="primary" @click="handleSave">保存</el-button>
</footer-btns>
</div>
</template>
<script lang="ts" setup name="articleListsEdit">
import type { FormInstance } from 'element-plus'
import { articleAdd, articleCateAll, articleDetail, articleEdit } from '@/api/article'
import { useDictOptions } from '@/hooks/useDictOptions'
import useMultipleTabs from '@/hooks/useMultipleTabs'
const route = useRoute()
const router = useRouter()
const formData = reactive({
id: '',
title: '',
image: '',
cid: '',
desc: '',
author: '',
content: '',
click_virtual: 0,
sort: 0,
is_show: 1,
abstract: ''
})
const { removeTab } = useMultipleTabs()
const formRef = shallowRef<FormInstance>()
const rules = reactive({
title: [{ required: true, message: '请输入文章标题', trigger: 'blur' }],
cid: [{ required: true, message: '请选择文章栏目', trigger: 'blur' }]
})
const getDetails = async () => {
const data = await articleDetail({
id: route.query.id
})
Object.keys(formData).forEach((key) => {
//@ts-ignore
formData[key] = data[key]
})
}
const { optionsData } = useDictOptions<{
article_cate: any[]
}>({
article_cate: {
api: articleCateAll
}
})
const handleSave = async () => {
await formRef.value?.validate()
if (route.query.id) {
await articleEdit(formData)
} else {
await articleAdd(formData)
}
removeTab()
router.back()
}
route.query.id && getDetails()
</script>
+171
View File
@@ -0,0 +1,171 @@
<template>
<div class="article-lists">
<el-card class="!border-none" shadow="never">
<el-form ref="formRef" class="mb-[-16px]" :model="queryParams" :inline="true">
<el-form-item class="w-[280px]" label="文章标题">
<el-input
v-model="queryParams.title"
placeholder="输入文章标题"
clearable
@keyup.enter="resetPage"
/>
</el-form-item>
<el-form-item class="w-[280px]" label="栏目名称">
<el-select v-model="queryParams.cid">
<el-option label="全部" value />
<el-option
v-for="item in optionsData.article_cate"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item class="w-[280px]" label="文章状态">
<el-select v-model="queryParams.is_show">
<el-option label="全部" value />
<el-option label="显示" :value="1" />
<el-option label="隐藏" :value="0" />
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="resetPage">查询</el-button>
<el-button @click="resetParams">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card class="!border-none mt-4" shadow="never">
<div>
<router-link
v-perms="['article.article/add', 'article.article/add:edit']"
:to="{
path: getRoutePath('article.article/add:edit')
}"
>
<el-button type="primary" class="mb-4">
<template #icon>
<icon name="el-icon-Plus" />
</template>
发布文章
</el-button>
</router-link>
</div>
<el-table size="large" v-loading="pager.loading" :data="pager.lists">
<el-table-column label="ID" prop="id" min-width="80" />
<el-table-column label="封面" min-width="100">
<template #default="{ row }">
<image-contain
v-if="row.image"
:src="row.image"
:width="60"
:height="45"
:preview-src-list="[row.image]"
preview-teleported
fit="contain"
/>
</template>
</el-table-column>
<el-table-column
label="标题"
prop="title"
min-width="160"
show-tooltip-when-overflow
/>
<el-table-column label="栏目" prop="cate_name" min-width="100" />
<el-table-column label="作者" prop="author" min-width="120" />
<el-table-column label="浏览量" prop="click" min-width="100" />
<el-table-column label="状态" min-width="100">
<template #default="{ row }">
<el-switch
v-perms="['article.article/updateStatus']"
v-model="row.is_show"
:active-value="1"
:inactive-value="0"
@change="changeStatus($event, row.id)"
/>
</template>
</el-table-column>
<el-table-column label="排序" prop="sort" min-width="100" />
<el-table-column label="发布时间" prop="create_time" min-width="120" />
<el-table-column label="操作" width="120" fixed="right">
<template #default="{ row }">
<el-button
v-perms="['article.article/edit', 'article.article/add:edit']"
type="primary"
link
>
<router-link
:to="{
path: getRoutePath('article.article/add:edit'),
query: {
id: row.id
}
}"
>
编辑
</router-link>
</el-button>
<el-button
v-perms="['article.article/delete']"
type="danger"
link
@click="handleDelete(row.id)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<div class="flex justify-end mt-4">
<pagination v-model="pager" @change="getLists" />
</div>
</el-card>
</div>
</template>
<script lang="ts" setup name="articleLists">
import { articleCateAll, articleDelete, articleLists, articleStatus } from '@/api/article'
import { useDictOptions } from '@/hooks/useDictOptions'
import { usePaging } from '@/hooks/usePaging'
import { getRoutePath } from '@/router'
import feedback from '@/utils/feedback'
const queryParams = reactive({
title: '',
cid: '',
is_show: ''
})
const { pager, getLists, resetPage, resetParams } = usePaging({
fetchFun: articleLists,
params: queryParams
})
const { optionsData } = useDictOptions<{
article_cate: any[]
}>({
article_cate: {
api: articleCateAll
}
})
const changeStatus = async (is_show: any, id: number) => {
try {
await articleStatus({ id, is_show })
getLists()
} catch (error) {
getLists()
}
}
const handleDelete = async (id: number) => {
await feedback.confirm('确定要删除?')
await articleDelete({ id })
getLists()
}
onActivated(() => {
getLists()
})
getLists()
</script>
+65
View File
@@ -0,0 +1,65 @@
<template>
<div>
<el-card class="!border-none" shadow="never">
<el-alert type="warning" title="温馨提示:H5设置" :closable="false" show-icon />
</el-card>
<el-card class="!border-none mt-4" shadow="never">
<el-form ref="formRef" :model="formData" label-width="120px">
<el-form-item label="渠道状态" required prop="status">
<div>
<el-radio-group v-model="formData.status">
<el-radio :value="1">开启</el-radio>
<el-radio :value="0">关闭</el-radio>
</el-radio-group>
<div class="form-tips">状态为关闭时将不对外提供服务请谨慎操作</div>
</div>
</el-form-item>
<el-form-item label="关闭后访问页面" prop="page_status">
<el-radio-group v-model="formData.page_status">
<el-radio :value="0">空页面</el-radio>
<el-radio :value="1">自定义链接</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="" prop="page_url" v-if="formData.page_status == 1">
<div class="w-80">
<el-input v-model="formData.page_url" placeholder="请输入完整的url" />
</div>
</el-form-item>
<el-form-item label="访问链接">
<div class="flex-1 min-w-0 break-words">
{{ formData.url }}
<el-button v-copy="formData.url">复制</el-button>
</div>
</el-form-item>
</el-form>
</el-card>
<footer-btns v-perms="['channel.web_page_setting/setConfig']">
<el-button type="primary" @click="handelSave">保存</el-button>
</footer-btns>
</div>
</template>
<script lang="ts" setup name="h5Config">
import { getH5Config, setH5Config } from '@/api/channel/h5'
const formData = reactive({
status: 0,
page_status: 0,
page_url: '',
url: ''
})
const getDetail = async () => {
const data = await getH5Config()
for (const key in formData) {
//@ts-ignore
formData[key] = data[key]
}
}
const handelSave = async () => {
await setH5Config(formData)
getDetail()
}
getDetail()
</script>
+76
View File
@@ -0,0 +1,76 @@
<template>
<div>
<el-card class="!border-none" shadow="never">
<el-alert
type="warning"
title="温馨提示:填写微信开放平台开发配置,请前往微信开放平台创建应用并完成认证;网站应用配置主要用于网站微信登录和微信支付"
:closable="false"
show-icon
/>
</el-card>
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="160px">
<el-card class="!border-none mt-4" shadow="never">
<div class="font-medium mb-7">网站应用</div>
<el-form-item label="AppID" prop="app_id">
<div class="w-80">
<el-input v-model="formData.app_id" placeholder="请输入AppID" />
</div>
</el-form-item>
<el-form-item label="AppSecret" prop="app_secret">
<div>
<div class="w-80">
<el-input v-model="formData.app_secret" placeholder="请输入AppSecret" />
</div>
</div>
</el-form-item>
</el-card>
</el-form>
<footer-btns v-perms="['channel.open_setting/setConfig']">
<el-button type="primary" @click="handelSave">保存</el-button>
</footer-btns>
</div>
</template>
<script lang="ts" setup name="wxDevConfig">
import type { FormInstance } from 'element-plus'
import { getOpenSettingConfig, setOpenSettingConfig } from '@/api/channel/open_setting'
const formData = reactive({
app_id: '',
app_secret: ''
})
const formRef = shallowRef<FormInstance>()
const formRules = {
app_id: [
{
required: true,
message: '请输入AppID',
trigger: ['blur', 'change']
}
],
app_secret: [
{
required: true,
message: '请输入AppSecret',
trigger: ['blur', 'change']
}
]
}
const getDetail = async () => {
const data = await getOpenSettingConfig()
for (const key in formData) {
//@ts-ignore
formData[key] = data[key]
}
}
const handelSave = async () => {
await formRef.value?.validate()
await setOpenSettingConfig(formData)
getDetail()
}
getDetail()
</script>
+199
View File
@@ -0,0 +1,199 @@
<template>
<div>
<el-card class="!border-none" shadow="never">
<el-alert
type="warning"
title="温馨提示:填写微信小程序开发配置,请前往微信公众平台申请小程序并完成认证"
:closable="false"
show-icon
/>
</el-card>
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
:label-width="appStore.isMobile ? '80px' : '160px'"
>
<el-card class="!border-none mt-4" shadow="never">
<div class="font-medium mb-7">微信小程序</div>
<el-form-item label="小程序名称" prop="name">
<div class="w-80">
<el-input v-model="formData.name" placeholder="请输入小程序名称" />
</div>
</el-form-item>
<el-form-item label="原始ID" prop="original_id">
<div class="w-80">
<el-input v-model="formData.original_id" placeholder="请输入原始ID" />
</div>
</el-form-item>
<el-form-item label="小程序码" prop="qr_code">
<div class="flex-1">
<div>
<material-picker v-model="formData.qr_code" :limit="1" />
</div>
<div class="form-tips">建议尺寸宽400px*高400pxjpgjpegpng格式</div>
</div>
</el-form-item>
</el-card>
<el-card class="!border-none mt-4" shadow="never">
<div class="font-medium mb-7">开发者ID</div>
<el-form-item label="AppID" prop="app_id">
<div class="w-80">
<el-input v-model="formData.app_id" placeholder="请输入AppID" />
</div>
</el-form-item>
<el-form-item label="AppSecret" prop="app_secret">
<div class="w-80">
<el-input v-model="formData.app_secret" placeholder="请输入AppSecret" />
</div>
</el-form-item>
<el-form-item>
<div class="form-tips">
小程序账号登录微信公众平台点击开发>开发设置->开发者ID设置AppID和AppSecret
</div>
</el-form-item>
</el-card>
<el-card class="!border-none mt-4" shadow="never">
<div class="font-medium mb-7">服务器域名</div>
<el-form-item label="request合法域名" prop="appId">
<div class="flex-1 min-w-0">
<div class="sm:flex">
<div class="mr-4 sm:w-80 flex">
<el-input v-model="formData.request_domain" disabled />
</div>
<el-button v-copy="formData.request_domain">复制</el-button>
</div>
<div class="form-tips">
小程序账号登录微信公众平台点击开发>开发设置->服务器域名填写https协议域名
</div>
</div>
</el-form-item>
<el-form-item label="socket合法域名">
<div class="flex-1 min-w-0">
<div class="sm:flex">
<div class="mr-4 sm:w-80 flex">
<el-input v-model="formData.socket_domain" disabled />
</div>
<el-button v-copy="formData.socket_domain">复制</el-button>
</div>
<div class="form-tips">
小程序账号登录微信公众平台点击开发>开发设置->服务器域名填写wss协议域名
</div>
</div>
</el-form-item>
<el-form-item label="uploadFile合法域名">
<div class="flex-1 min-w-0">
<div class="sm:flex">
<div class="mr-4 sm:w-80 flex">
<el-input v-model="formData.upload_file_domain" disabled />
</div>
<el-button v-copy="formData.upload_file_domain">复制</el-button>
</div>
<div class="form-tips">
小程序账号登录微信公众平台点击开发>开发设置->服务器域名填写https协议域名
</div>
</div>
</el-form-item>
<el-form-item label="downloadFile合法域名">
<div class="flex-1 min-w-0">
<div class="sm:flex">
<div class="mr-4 sm:w-80 flex">
<el-input v-model="formData.download_file_domain" disabled />
</div>
<el-button v-copy="formData.download_file_domain">复制</el-button>
</div>
<div class="form-tips">
小程序账号登录微信公众平台点击开发>开发设置->服务器域名填写https协议域名
</div>
</div>
</el-form-item>
<el-form-item label="udp合法域名">
<div class="flex-1 min-w-0">
<div class="sm:flex">
<div class="mr-4 sm:w-80 flex">
<el-input v-model="formData.udp_domain" disabled />
</div>
<el-button v-copy="formData.udp_domain">复制</el-button>
</div>
<div class="form-tips">
小程序账号登录微信公众平台点击开发>开发设置->服务器域名填写udp协议域名
</div>
</div>
</el-form-item>
</el-card>
<el-card class="!border-none mt-4" shadow="never">
<div class="font-medium mb-7">业务域名</div>
<el-form-item label="业务域名">
<div class="flex-1 min-w-0">
<div class="sm:flex">
<div class="mr-4 sm:w-80 flex">
<el-input v-model="formData.business_domain" disabled />
</div>
<el-button v-copy="formData.business_domain">复制</el-button>
</div>
<div class="form-tips">
小程序账号登录微信公众平台点击开发>开发设置->业务域名填写业务域名
</div>
</div>
</el-form-item>
</el-card>
</el-form>
<footer-btns v-perms="['channel.mnp_settings/setConfig']">
<el-button type="primary" @click="handelSave">保存</el-button>
</footer-btns>
</div>
</template>
<script lang="ts" setup name="weappConfig">
import type { FormInstance } from 'element-plus'
import { getWeappConfig, setWeappConfig } from '@/api/channel/weapp'
import useAppStore from '@/stores/modules/app'
const appStore = useAppStore()
const formData = reactive({
name: '',
original_id: '',
qr_code: '',
app_id: '',
app_secret: '',
business_domain: '',
download_file_domain: '',
request_domain: '',
socket_domain: '',
tcpDomain: '',
udp_domain: '',
upload_file_domain: ''
})
const formRef = shallowRef<FormInstance>()
const formRules = {
app_id: [
{
required: true,
message: '请输入AppID',
trigger: ['blur', 'change']
}
],
app_secret: [
{
required: true,
message: '请输入AppSecret',
trigger: ['blur', 'change']
}
]
}
const getDetail = async () => {
const data = await getWeappConfig()
for (const key in formData) {
//@ts-ignore
formData[key] = data[key]
}
}
const handelSave = async () => {
await formRef.value?.validate()
await setWeappConfig(formData)
getDetail()
}
getDetail()
</script>
+217
View File
@@ -0,0 +1,217 @@
<template>
<div>
<el-card class="!border-none" shadow="never">
<el-alert
type="warning"
title="温馨提示:填写微信公众号开发配置,请前往微信公众平台申请服务号并完成认证"
:closable="false"
show-icon
/>
</el-card>
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
:label-width="appStore.isMobile ? '80px' : '160px'"
>
<el-card class="!border-none mt-4" shadow="never">
<div class="font-medium mb-7">微信公众号</div>
<el-form-item label="公众号名称" prop="name">
<div class="w-80">
<el-input v-model="formData.name" placeholder="请输入公众号名称" />
</div>
</el-form-item>
<el-form-item label="原始ID" prop="original_id">
<div class="w-80">
<el-input v-model="formData.original_id" placeholder="请输入原始ID" />
</div>
</el-form-item>
<el-form-item label="公众号二维码" prop="qr_code">
<div>
<div>
<material-picker v-model="formData.qr_code" :limit="1" />
</div>
<div class="form-tips">建议尺寸宽400px*高400pxjpgjpegpng格式</div>
</div>
</el-form-item>
</el-card>
<el-card class="!border-none mt-4" shadow="never">
<div class="font-medium mb-7">公众号开发者信息</div>
<el-form-item label="AppID" prop="app_id">
<div class="w-80">
<el-input v-model="formData.app_id" placeholder="请输入AppID" />
</div>
</el-form-item>
<el-form-item label="AppSecret" prop="app_secret">
<div class="w-80">
<el-input v-model="formData.app_secret" placeholder="请输入AppSecret" />
</div>
</el-form-item>
<el-form-item>
<div class="form-tips">
小程序账号登录微信公众平台点击开发>开发设置->开发者ID设置AppID和AppSecret
</div>
</el-form-item>
</el-card>
<el-card class="!border-none mt-4" shadow="never">
<div class="font-medium mb-7">服务器配置</div>
<el-form-item label="URL">
<div class="flex-1 min-w-0">
<div class="sm:flex">
<div class="mr-4 sm:w-80 flex">
<el-input v-model="formData.url" disabled />
</div>
<el-button v-copy="formData.url">复制</el-button>
</div>
<div class="form-tips">
登录微信公众平台点击开发>基本配置>服务器配置填写服务器地址URL
</div>
</div>
</el-form-item>
<el-form-item label="Token" prop="Token">
<div class="flex-1 min-w-0">
<div class="w-80">
<el-input v-model="formData.token" placeholder="请输入Token" />
</div>
<div class="form-tips">
登录微信公众平台点击开发>基本配置>服务器配置设置令牌Token不填默认为likeshop
</div>
</div>
</el-form-item>
<el-form-item label="EncodingAESKey" prop="encoding_aes_key">
<div class="flex-1 min-w-0">
<div class="w-80">
<el-input
v-model="formData.encoding_aes_key"
placeholder="请输入EncodingAESKey"
/>
</div>
<div class="form-tips">
消息加密密钥由43位字符组成字符范围为A-Z,a-z,0-9
</div>
</div>
</el-form-item>
<el-form-item label="消息加密方式" required prop="encryption_type">
<div class="flex-1 min-w-0">
<el-radio-group
class="flex-col !items-start min-w-0"
v-model="formData.encryption_type"
>
<el-radio :value="1">
明文模式 (不使用消息体加解密功能安全系数较低)
</el-radio>
<el-radio :value="2">
兼容模式 (明文密文将共存方便开发者调试和维护)
</el-radio>
<el-radio :value="3">
安全模式推荐 (消息包为纯密文需要开发者加密和解密安全系数高)
</el-radio>
</el-radio-group>
</div>
</el-form-item>
</el-card>
<el-card class="!border-none mt-4" shadow="never">
<div class="font-medium mb-7">功能设置</div>
<el-form-item label="业务域名">
<div class="flex-1 min-w-0">
<div class="sm:flex">
<div class="mr-4 sm:w-80 flex">
<el-input v-model="formData.business_domain" disabled />
</div>
<el-button v-copy="formData.business_domain">复制</el-button>
</div>
<div class="form-tips">
登录微信公众平台点击设置>公众号设置>功能设置填写业务域名
</div>
</div>
</el-form-item>
<el-form-item label="JS接口安全域名">
<div class="flex-1 min-w-0">
<div class="sm:flex">
<div class="mr-4 sm:w-80 flex">
<el-input v-model="formData.js_secure_domain" disabled />
</div>
<el-button v-copy="formData.js_secure_domain">复制</el-button>
</div>
<div class="form-tips">
登录微信公众平台点击设置>公众号设置>功能设置填写JS接口安全域名
</div>
</div>
</el-form-item>
<el-form-item label="网页授权域名">
<div class="flex-1 min-w-0">
<div class="sm:flex">
<div class="mr-4 sm:w-80 flex">
<el-input v-model="formData.web_auth_domain" disabled />
</div>
<el-button v-copy="formData.web_auth_domain">复制</el-button>
</div>
<div class="form-tips">
登录微信公众平台点击设置>公众号设置>功能设置填写网页授权域名
</div>
</div>
</el-form-item>
</el-card>
</el-form>
<footer-btns v-perms="['channel.official_account_setting/setConfig']">
<el-button type="primary" @click="handelSave">保存</el-button>
</footer-btns>
</div>
</template>
<script lang="ts" setup name="wxOaConfig">
import type { FormInstance } from 'element-plus'
import { getOaConfig, setOaConfig } from '@/api/channel/wx_oa'
import useAppStore from '@/stores/modules/app'
const appStore = useAppStore()
const formData = reactive({
name: '',
original_id: ' ',
qr_code: '',
app_id: '',
app_secret: '',
url: '',
token: '',
encoding_aes_key: '',
encryption_type: 1,
business_domain: '',
js_secure_domain: '',
web_auth_domain: ''
})
const formRef = shallowRef<FormInstance>()
const formRules = {
app_id: [
{
required: true,
message: '请输入AppID',
trigger: ['blur', 'change']
}
],
app_secret: [
{
required: true,
message: '请输入AppSecret',
trigger: ['blur', 'change']
}
]
}
const getDetail = async () => {
const data = await getOaConfig()
for (const key in formData) {
//@ts-ignore
formData[key] = data[key]
}
}
const handelSave = async () => {
await formRef.value?.validate()
await setOaConfig(formData)
getDetail()
}
getDetail()
</script>
+47
View File
@@ -0,0 +1,47 @@
<script setup lang="ts" name="wxOaMenu">
import OaAttr from './menu_com/oa-attr.vue'
import OaPhone from './menu_com/oa-phone.vue'
import { useMenuOa } from './menu_com/useMenuOa'
const { getOaMenuFunc, handleSave, handlePublish } = useMenuOa(undefined)
getOaMenuFunc()
</script>
<template>
<div class="menu-oa">
<el-card class="!border-none" shadow="never">
<el-alert
type="warning"
title="配置微信公众号菜单,点击确认,保存菜单并发布至微信公众号"
:closable="false"
show-icon
/>
</el-card>
<el-card class="!border-none mt-4" shadow="never">
<div class="lg:flex flex-1">
<!-- Phone -->
<oa-phone></oa-phone>
<!-- Attr -->
<div class="mt-4 lg:mt-0 max-w-[400px]">
<oa-attr></oa-attr>
</div>
</div>
</el-card>
<footer-btns>
<el-button type="primary" @click="handleSave" v-perms="['channel:oaMenu:save']">
保存
</el-button>
<el-button type="primary" @click="handlePublish" v-perms="['channel:oaMenu:publish']">
发布
</el-button>
</footer-btns>
</div>
</template>
<style lang="scss" scoped>
.menu-oa {
}
</style>
@@ -0,0 +1,90 @@
<script lang="ts" setup>
import oaMenuForm from './oa-menu-form.vue'
import oaMenuFormEdit from './oa-menu-form-edit.vue'
import { useMenuOa } from './useMenuOa'
const menuRef = shallowRef()
const {
menuList,
menuIndex,
handleAddSubMenu,
handleEditSubMenu,
handleDelMenu,
handleDelSubMenu
} = useMenuOa(menuRef)
</script>
<template>
<!-- Attr -->
<template v-for="(attrItem, attrIndex) in menuList" :key="attrIndex">
<div class="flex-1 oa-attr min-w-0" v-show="attrIndex === menuIndex">
<div class="text-base oa-attr-title">菜单配置</div>
<del-wrap @close="handleDelMenu(menuIndex)">
<div class="flex items-center w-full p-4 mt-4 rounded bg-fill-light">
<oa-menu-form
ref="menuRef"
modular="master"
v-model:name="attrItem.name"
v-model:menuType="attrItem.has_menu"
v-model:visitType="attrItem.type"
v-model:url="attrItem.url"
v-model:appId="attrItem.appid"
v-model:pagePath="attrItem.pagepath"
>
<div class="flex-1">
<!-- 编辑子菜单 -->
<ul>
<li
class="flex"
v-for="(subItem, subIndex) in attrItem.sub_button"
:key="subIndex"
style="padding: 8px"
>
<span class="mr-auto">{{ subItem.name }}</span>
<!-- 编辑子菜单 -->
<oa-menu-form-edit
modular="edit"
:subItem="subItem"
@edit="handleEditSubMenu($event, subIndex)"
>
<el-button link>
<el-icon><EditPen /></el-icon>
</el-button>
</oa-menu-form-edit>
<!-- 删除子菜单 -->
<popup @confirm="handleDelSubMenu(menuIndex, subIndex)">
是否删除当前子菜单
<template #trigger>
<el-button link>
<el-icon class="ml-5"><Delete /></el-icon>
</el-button>
</template>
</popup>
</li>
</ul>
<!-- 新增子菜单 -->
<oa-menu-form-edit modular="add" @add="handleAddSubMenu">
<el-button
type="primary"
link
:disabled="attrItem.sub_button.length >= 5"
>
添加子菜单({{ attrItem.sub_button.length }}/5)
</el-button>
</oa-menu-form-edit>
</div>
</oa-menu-form>
</div>
</del-wrap>
</div>
</template>
</template>
<style lang="scss" scoped>
.oa-attr {
}
</style>
@@ -0,0 +1,73 @@
<script lang="ts" setup>
import oaMenuForm from './oa-menu-form.vue'
const emit = defineEmits<{
(event: 'add', value: any): void
(event: 'edit', value: any): void
}>()
const props = withDefaults(
defineProps<{
modular: string
subItem?: any
}>(),
{
modular: 'edit',
subItem: {}
}
)
const menuFormEditRef = shallowRef()
const menuFromPopupRef = shallowRef()
const form = {
name: '',
type: 'view',
url: '',
appid: '',
pagepath: ''
}
watchEffect(() => {
if (Object.keys(props.subItem).length != 0) {
for (const key in form) {
//@ts-ignore
form[key] = props.subItem[key]
}
}
})
const handleRules = async () => {
await menuFormEditRef.value.menuFormRef.validate()
if (props.modular === 'edit') {
emit('edit', { ...form })
} else {
emit('add', { ...form })
}
menuFromPopupRef.value.close()
menuFormEditRef.value.menuFormRef.resetFields()
}
</script>
<template>
<popup
ref="menuFromPopupRef"
async
:clickModalClose="false"
:title="`${modular === 'add' ? '新增' : '编辑'}子菜单`"
@confirm="handleRules"
>
<oa-menu-form
ref="menuFormEditRef"
modular="secondary"
v-model:name="form.name"
v-model:visitType="form.type"
v-model:url="form.url"
v-model:appId="form.appid"
v-model:pagePath="form.pagepath"
></oa-menu-form>
<template #trigger>
<slot></slot>
</template>
</popup>
</template>
@@ -0,0 +1,109 @@
<template>
<el-form ref="menuFormRef" :rules="rules" :model="menuForm" label-width="100px">
<!-- 菜单名称 -->
<el-form-item :label="modular === 'master' ? '主菜单名称' : '子菜单名称'" prop="name">
<el-input v-model="menuForm.name" />
</el-form-item>
<!-- 菜单类型 -->
<el-form-item label="主菜单类型" prop="menuType" v-if="modular === 'master'">
<el-radio-group v-model="menuForm.menuType">
<el-radio :value="false">不配置子菜单</el-radio>
<el-radio :value="true">配置子菜单</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="" v-if="menuForm.menuType && modular === 'master'">
<slot></slot>
</el-form-item>
<template v-if="!menuForm.menuType">
<!-- 跳转链接 -->
<el-form-item label="跳转链接" prop="visitType">
<el-radio-group v-model="menuForm.visitType">
<el-radio value="view">网页</el-radio>
<el-radio value="miniprogram">小程序</el-radio>
</el-radio-group>
</el-form-item>
<!-- 网址 -->
<el-form-item label="网址" prop="url">
<el-input v-model="menuForm.url" />
</el-form-item>
<template v-if="menuForm.visitType == 'miniprogram'">
<!-- AppId -->
<el-form-item label="AppId" prop="appId">
<el-input v-model="menuForm.appId" />
</el-form-item>
<!-- 路径 -->
<el-form-item label="路径" prop="pagePath">
<el-input v-model="menuForm.pagePath" />
</el-form-item>
</template>
</template>
</el-form>
</template>
<script lang="ts" setup>
import type { FormInstance } from 'element-plus'
import { rules } from './useMenuOa'
const emit = defineEmits([
'update:name',
'update:menuType',
'update:visitType',
'update:url',
'update:appId',
'update:pagePath'
])
const props = withDefaults(
defineProps<{
modular?: string
name?: string
menuType?: boolean
visitType?: string
url?: string
appId?: string
pagePath?: string
}>(),
{
modular: 'master',
name: '',
menuType: false,
visitType: 'view',
url: '',
appId: '',
pagePath: ''
}
)
const menuFormRef = shallowRef<FormInstance>()
// 表单数据
const menuForm = ref({ ...props })
watch(
() => props,
(value) => {
menuForm.value = value
},
{ immediate: true }
)
watchEffect(() => {
if (props.modular === 'master') {
emit('update:menuType', menuForm.value.menuType)
}
emit('update:name', menuForm.value.name)
emit('update:visitType', menuForm.value.visitType)
emit('update:url', menuForm.value.url)
emit('update:appId', menuForm.value.appId)
emit('update:pagePath', menuForm.value.pagePath)
})
defineExpose({
menuFormRef
})
</script>
@@ -0,0 +1,122 @@
<script lang="ts" setup>
import useSettingStore from '@/stores/modules/setting'
import { useMenuOa } from './useMenuOa'
// 菜单颜色(这里采用全局颜色)
const settingStore = useSettingStore()
const themeColor = computed(() => settingStore.theme || '#4A5DFF')
const { menuList, menuIndex, handleAddMenu } = useMenuOa(useMenuOa)
</script>
<template>
<!-- Phone -->
<div class="oa-phone mr-[35px]">
<div class="oa-phone-content"></div>
<div class="flex oa-phone-menu">
<div class="flex items-center justify-center oa-phone-menu-switch">
<el-icon>
<Grid />
</el-icon>
</div>
<template v-for="(menuItem, i) in menuList" :key="i">
<div class="relative flex-1" @click="menuIndex = i">
<!-- 一级菜单 -->
<div
class="flex items-center justify-center flex-1 text-sm oa-phone-menu-item"
:class="{ 'active-menu': menuIndex === i }"
>
{{ menuItem.name }}
</div>
<!-- 二级菜单 -->
<div
class="oa-phone-menu-subitem"
v-show="menuItem.sub_button.length && menuItem.has_menu"
>
<template v-for="(subItem, index2) in menuItem.sub_button" :key="index2">
<div class="oa-phone-menu-subitem-title">
{{ subItem.name }}
</div>
</template>
</div>
</div>
</template>
<!-- 新增菜单 -->
<template v-if="menuList.length <= 2">
<div class="flex items-center justify-center flex-1 h-full" @click="handleAddMenu">
<el-icon>
<Plus />
</el-icon>
</div>
</template>
</div>
</div>
</template>
<style lang="scss" scoped>
.oa-phone {
width: 260px;
height: 461px;
border: 1px solid #e5e5ea;
flex: none;
&-content {
height: 420px;
border-bottom: 1px solid #e5e5ea;
}
&-menu {
height: 40px;
cursor: pointer;
&-switch {
width: 40px;
height: 100%;
border-right: 1px solid #e5e5ea;
}
// 一级菜单
&-item {
height: 100%;
border-right: 1px solid #e5e5ea;
}
&-item:nth-child(4) {
border-right: none;
}
.active-menu {
position: relative;
}
.active-menu::after {
content: '';
width: 100%;
height: 41px;
top: -1px;
position: absolute;
border: 1px solid v-bind(themeColor);
}
// 二级菜单
&-subitem {
width: 98%;
left: 2px;
bottom: calc(100% + 10px);
position: absolute;
border: 1px solid #e5e5ea;
border-top: none;
&-title {
height: 40px;
line-height: 40px;
text-align: center;
border-top: 1px solid #e5e5ea;
}
}
}
}
</style>
@@ -0,0 +1,167 @@
import type { FormRules } from 'element-plus'
import { ref } from 'vue'
import type { Menu } from '@/api/channel/wx_oa'
import { getOaMenu, setOaMenuPublish, setOaMenuSave } from '@/api/channel/wx_oa'
import feedback from '@/utils/feedback'
// 菜单实例
export const menuRef = shallowRef()
// 菜单数据
const menuList = ref<Menu[]>([])
const menuIndex = ref<number>(0)
// 校验
export const rules = reactive<FormRules>({
name: [
{
required: true,
message: '必填项不能为空',
trigger: ['blur', 'change']
},
{
min: 1,
max: 12,
message: '长度限制12个字符',
trigger: ['blur', 'change']
}
],
menuType: [
{
required: true,
message: '必填项不能为空',
trigger: ['blur', 'change']
}
],
visitType: [
{
required: true,
message: '必填项不能为空',
trigger: ['blur', 'change']
}
],
url: [
{
required: true,
message: '必填项不能为空',
trigger: ['blur', 'change']
},
{
pattern:
// eslint-disable-next-line no-useless-escape
/(http|https):\/\/[\w\-_]+(\.[\w\-_]+)+([\w\-\.,@?^=%&:/~\+#]*[\w\-\@?^=%&/~\+#])?/,
message: '请输入合法的网址链接',
trigger: ['blur', 'change']
}
],
appId: [
{
required: true,
message: '必填项不能为空',
trigger: ['blur', 'change']
}
],
pagePath: [
{
required: true,
message: '必填项不能为空',
trigger: ['blur', 'change']
}
]
})
export const useMenuOa = (ref: any) => {
if (ref) menuRef.value = ref
// 添加主菜单
const handleAddMenu = () => {
menuList.value.push({
name: '菜单名称',
has_menu: false,
type: 'view',
url: '',
appid: '',
pagepath: '',
sub_button: []
})
}
// 添加子菜单
const handleAddSubMenu = (event?: Menu) => {
const index = menuIndex.value
if (menuList.value[index].sub_button.length >= 5) {
feedback.msgError('已添加上限~')
return
}
menuList.value[index].sub_button.push(event)
}
// 编辑子菜单
const handleEditSubMenu = (event: Menu, subIndex: number) => {
const index = menuIndex.value
menuList.value[index].sub_button[subIndex] = event
}
// 删除主菜单
const handleDelMenu = (index: number) => {
if (index != 0) {
menuIndex.value--
}
menuList.value.splice(index, 1)
}
// 删除子菜单
const handleDelSubMenu = (index: number, subIndex: number) => {
menuList.value[index].sub_button.splice(subIndex, 1)
}
// 获取菜单
const getOaMenuFunc = async () => {
try {
menuList.value = await getOaMenu()
} catch (error) {
console.log('获取菜单=>', error)
}
}
// 保存菜单
const handleSave = async () => {
const refs = menuRef.value.value
for (let i = 0; i < refs.length; i++) {
try {
await refs[i].menuFormRef.validate()
} catch (error) {
menuIndex.value = i
return
}
}
await setOaMenuSave(menuList.value)
}
// 保存菜单
const handlePublish = async () => {
const refs = menuRef.value.value
for (let i = 0; i < refs.length; i++) {
try {
await refs[i].menuFormRef.validate()
} catch (error) {
menuIndex.value = i
return
}
}
await setOaMenuPublish(menuList.value)
}
return {
menuList,
menuIndex,
handleAddMenu,
handleAddSubMenu,
handleEditSubMenu,
handleDelMenu,
handleDelSubMenu,
getOaMenuFunc,
handleSave,
handlePublish
}
}
@@ -0,0 +1,111 @@
<template>
<div>
<el-card class="!border-none" shadow="never">
<el-alert
type="warning"
title="温馨提示:1.粉丝在公众号发送内容时,系统无法匹配情况下发送启用的默认文本回复;2.同时只能启用一个默认回复。"
:closable="false"
show-icon
/>
</el-card>
<el-card class="!border-none mt-4" shadow="never">
<div>
<el-button class="mb-4" type="primary" @click="handleAdd()">
<template #icon>
<icon name="el-icon-Plus" />
</template>
新增
</el-button>
</div>
<el-table size="large" :data="pager.lists" v-loading="pager.loading">
<el-table-column label="规则名称" prop="name" min-width="120" />
<el-table-column label="回复类型" min-width="120">
<template #default="{ row }">
{{ getContentType(row.content_type) }}
</template>
</el-table-column>
<el-table-column label="回复内容" prop="content" min-width="120" />
<el-table-column label="状态" min-width="120">
<template #default="{ row }">
<el-switch
v-model="row.status"
:active-value="1"
:inactive-value="0"
@change="changeStatus(row.id)"
/>
</template>
</el-table-column>
<el-table-column label="排序" prop="sort" min-width="120" />
<el-table-column label="操作" width="120" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="handleEdit(row)"> 编辑 </el-button>
<el-button type="danger" link @click="handleDelete(row.id)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<div class="flex justify-end mt-4">
<pagination v-model="pager" @change="getLists" />
</div>
</el-card>
<edit-popup v-if="showEdit" ref="editRef" @success="getLists" @close="showEdit = false" />
</div>
</template>
<script lang="ts" setup>
import { changeOaReplyStatus, getOaReplyList, oaReplyDel } from '@/api/channel/wx_oa'
import { usePaging } from '@/hooks/usePaging'
import feedback from '@/utils/feedback'
import EditPopup from './edit.vue'
const editRef = shallowRef<InstanceType<typeof EditPopup>>()
const showEdit = ref(false)
const getContentType = computed(() => {
return (val: number) => {
switch (val) {
case 1:
return '文本'
}
}
})
const { pager, getLists } = usePaging({
fetchFun: getOaReplyList,
params: {
reply_type: 3
}
})
const handleAdd = async () => {
showEdit.value = true
await nextTick()
editRef.value?.open('add', 3)
}
const handleEdit = async (data: any) => {
showEdit.value = true
await nextTick()
editRef.value?.open('edit', 3)
editRef.value?.getDetail(data)
}
const handleDelete = async (id: number) => {
await feedback.confirm('确定要删除?')
await oaReplyDel({ id })
feedback.msgSuccess('删除成功')
getLists()
}
const changeStatus = async (id: number) => {
try {
await changeOaReplyStatus({ id })
getLists()
} catch (error) {
getLists()
}
}
getLists()
</script>
@@ -0,0 +1,190 @@
<template>
<div class="edit-popup">
<popup
ref="popupRef"
:title="popupTitle"
:async="true"
width="500px"
@confirm="handleSubmit"
@close="handleClose"
>
<el-form
ref="formRef"
:model="formData"
label-width="84px"
:rules="formRules"
class="pr-10"
>
<el-form-item label="规则名称" prop="name">
<div class="flex-1">
<el-input v-model="formData.name" placeholder="请输入规则名称" />
<div class="form-tips">方便通过名称管理关注回复内容</div>
</div>
</el-form-item>
<el-form-item label="关键词" prop="keyword" v-if="formData.reply_type == 2">
<div class="flex-1">
<el-input v-model="formData.keyword" placeholder="请输入关键词" />
<div class="form-tips">方便通过名称管理关注回复内容</div>
</div>
</el-form-item>
<el-form-item label="匹配方式" prop="matching_type" v-if="formData.reply_type == 2">
<div class="flex-1">
<el-radio-group v-model="formData.matching_type">
<el-radio :value="1">全匹配</el-radio>
<el-radio :value="2">模糊匹配</el-radio>
</el-radio-group>
<div class="form-tips">模糊匹配时关键词部分匹配用户输入的内容即可</div>
</div>
</el-form-item>
<el-form-item label="回复类型" prop="content_type" :min="0">
<div class="flex-1">
<el-radio-group v-model="formData.content_type">
<el-radio :value="1">文本</el-radio>
</el-radio-group>
<div class="form-tips">暂时只支持文本类型</div>
</div>
</el-form-item>
<el-form-item label="回复内容" prop="content">
<div class="flex-1">
<el-input
v-model="formData.content"
:autosize="{ minRows: 4, maxRows: 4 }"
type="textarea"
maxlength="200"
show-word-limit
placeholder="请输入回复内容"
/>
</div>
</el-form-item>
<el-form-item label="排序">
<div class="flex-1">
<el-input-number v-model="formData.sort" :min="0" :max="9999" />
</div>
</el-form-item>
<el-form-item
label="回复数量"
prop="reply_num"
required
v-if="formData.reply_type == 2"
>
<div class="flex-1">
<el-radio-group v-model="formData.reply_num">
<el-radio :value="1">回复匹配首词条</el-radio>
</el-radio-group>
<div class="form-tips">
设置关键词匹配多条时回复的数量暂时支持回复一条内容
</div>
</div>
</el-form-item>
<el-form-item label="启用状态">
<el-switch v-model="formData.status" :active-value="1" :inactive-value="0" />
</el-form-item>
</el-form>
</popup>
</div>
</template>
<script lang="ts" setup>
import type { FormInstance } from 'element-plus'
import type { FormRules } from 'element-plus'
import { getOaReplyDetail, oaReplyAdd, oaReplyEdit } from '@/api/channel/wx_oa'
import Popup from '@/components/popup/index.vue'
const emit = defineEmits(['success', 'close'])
const formRef = shallowRef<FormInstance>()
const popupRef = shallowRef<InstanceType<typeof Popup>>()
const mode = ref('add')
const popupTitle = computed(() => {
return mode.value == 'edit' ? '编辑' : '新增'
})
const formData = reactive({
id: '',
name: '',
reply_type: 0,
content_type: 1,
keyword: '',
content: '',
matching_type: 1,
status: 1,
sort: 0,
reply_num: 1
})
const formRules: FormRules = {
name: [
{
required: true,
message: '请输入规则名称',
trigger: ['blur']
}
],
keyword: [
{
required: true,
message: '请输入关键词',
trigger: ['blur']
}
],
matching_type: [
{
required: true,
message: '请选择匹配方式',
trigger: ['blur']
}
],
content_type: [
{
required: true,
message: '请选择回复类型',
trigger: ['blur']
}
],
content: [
{
required: true,
message: '请输入回复内容',
trigger: ['blur']
}
]
}
const handleSubmit = async () => {
await formRef.value?.validate()
mode.value == 'edit' ? await oaReplyEdit(formData) : await oaReplyAdd(formData)
popupRef.value?.close()
emit('success')
}
const open = (modes = 'add', type = 0) => {
mode.value = modes
formData.reply_type = type
popupRef.value?.open()
}
const setFormData = (data: Record<any, any>) => {
for (const key in formData) {
if (data[key] != null && data[key] != undefined) {
//@ts-ignore
formData[key] = data[key]
}
}
}
const getDetail = async (row: Record<string, any>) => {
const data = await getOaReplyDetail({
id: row.id
})
setFormData(data)
}
const handleClose = () => {
emit('close')
}
defineExpose({
open,
setFormData,
getDetail
})
</script>
@@ -0,0 +1,110 @@
<template>
<div>
<el-card class="!border-none" shadow="never">
<el-alert
type="warning"
title="温馨提示:1.粉丝关注公众号时,会自动发送启用的关注回复;2.同时只能启用一个关注回复。"
:closable="false"
show-icon
/>
</el-card>
<el-card class="!border-none mt-4" shadow="never">
<div>
<el-button class="mb-4" type="primary" @click="handleAdd()">
<template #icon>
<icon name="el-icon-Plus" />
</template>
新增
</el-button>
</div>
<el-table size="large" :data="pager.lists" v-loading="pager.loading">
<el-table-column label="规则名称" prop="name" min-width="120" />
<el-table-column label="回复类型" min-width="120">
<template #default="{ row }">
{{ getContentType(row.content_type) }}
</template>
</el-table-column>
<el-table-column label="回复内容" prop="content" min-width="120" />
<el-table-column label="状态" min-width="120">
<template #default="{ row }">
<el-switch
v-model="row.status"
:active-value="1"
:inactive-value="0"
@change="changeStatus(row.id)"
/>
</template>
</el-table-column>
<el-table-column label="排序" prop="sort" min-width="120" />
<el-table-column label="操作" width="120" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="handleEdit(row)"> 编辑 </el-button>
<el-button type="danger" link @click="handleDelete(row.id)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<div class="flex justify-end mt-4">
<pagination v-model="pager" @change="getLists" />
</div>
</el-card>
<edit-popup v-if="showEdit" ref="editRef" @success="getLists" @close="showEdit = false" />
</div>
</template>
<script lang="ts" setup>
import { changeOaReplyStatus, getOaReplyList, oaReplyDel } from '@/api/channel/wx_oa'
import { usePaging } from '@/hooks/usePaging'
import feedback from '@/utils/feedback'
import EditPopup from './edit.vue'
const editRef = shallowRef<InstanceType<typeof EditPopup>>()
const showEdit = ref(false)
const getContentType = computed(() => {
return (val: number) => {
switch (val) {
case 1:
return '文本'
}
}
})
const { pager, getLists } = usePaging({
fetchFun: getOaReplyList,
params: {
reply_type: 1
}
})
const handleAdd = async () => {
showEdit.value = true
await nextTick()
editRef.value?.open('add', 1)
}
const handleEdit = async (data: any) => {
showEdit.value = true
await nextTick()
editRef.value?.open('edit', 1)
editRef.value?.getDetail(data)
}
const handleDelete = async (id: number) => {
await feedback.confirm('确定要删除?')
await oaReplyDel({ id })
getLists()
}
const changeStatus = async (id: number) => {
try {
await changeOaReplyStatus({ id })
getLists()
} catch (error) {
getLists()
}
}
getLists()
</script>
@@ -0,0 +1,126 @@
<template>
<div>
<el-card class="!border-none" shadow="never">
<el-alert
type="warning"
title="温馨提示:1.粉丝在公众号发送内容时,通过关键词可触发关键词回复;2.同时可启用多个关键词回复,有多条关键词匹配时优选选择排序靠前的一条"
:closable="false"
show-icon
/>
</el-card>
<el-card class="!border-none mt-4" shadow="never">
<div>
<el-button class="mb-4" type="primary" @click="handleAdd()">
<template #icon>
<icon name="el-icon-Plus" />
</template>
新增
</el-button>
</div>
<el-table size="large" :data="pager.lists" v-loading="pager.loading">
<el-table-column label="规则名称" prop="name" min-width="120" />
<el-table-column label="关键词" prop="keyword" min-width="120" />
<el-table-column label="匹配方式" min-width="120">
<template #default="{ row }">
{{ getMatchingType(row.matching_type) }}
</template>
</el-table-column>
<el-table-column label="回复类型" min-width="120">
<template #default="{ row }">
{{ getContentType(row.content_type) }}
</template>
</el-table-column>
<el-table-column label="状态" min-width="120">
<template #default="{ row }">
<el-switch
v-model="row.status"
:active-value="1"
:inactive-value="0"
@change="changeStatus(row.id)"
/>
</template>
</el-table-column>
<el-table-column label="排序" prop="sort" min-width="120" />
<el-table-column label="操作" width="120" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="handleEdit(row)"> 编辑 </el-button>
<el-button type="danger" link @click="handleDelete(row.id)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<div class="flex justify-end mt-4">
<pagination v-model="pager" @change="getLists" />
</div>
</el-card>
<edit-popup v-if="showEdit" ref="editRef" @success="getLists" @close="showEdit = false" />
</div>
</template>
<script lang="ts" setup>
import { changeOaReplyStatus, getOaReplyList, oaReplyDel } from '@/api/channel/wx_oa'
import { usePaging } from '@/hooks/usePaging'
import feedback from '@/utils/feedback'
import EditPopup from './edit.vue'
const editRef = shallowRef<InstanceType<typeof EditPopup>>()
const showEdit = ref(false)
const getMatchingType = computed(() => {
return (val: number) => {
switch (val) {
case 1:
return '全匹配'
case 2:
return '模糊匹配'
}
}
})
const getContentType = computed(() => {
return (val: number) => {
switch (val) {
case 1:
return '文本'
}
}
})
const { pager, getLists } = usePaging({
fetchFun: getOaReplyList,
params: {
reply_type: 2
}
})
const handleAdd = async () => {
showEdit.value = true
await nextTick()
editRef.value?.open('add', 2)
}
const handleEdit = async (data: any) => {
showEdit.value = true
await nextTick()
editRef.value?.open('edit', 2)
editRef.value?.getDetail(data)
}
const handleDelete = async (id: number) => {
await feedback.confirm('确定要删除?')
await oaReplyDel({ id })
getLists()
}
const changeStatus = async (id: number) => {
try {
await changeOaReplyStatus({ id })
getLists()
} catch (error) {
getLists()
}
}
getLists()
</script>
@@ -0,0 +1,106 @@
<template>
<popup
ref="popupRef"
title="余额调整"
width="500px"
@confirm="handleConfirm"
:async="true"
@close="popupClose"
>
<div class="pr-8">
<el-form ref="formRef" :model="formData" label-width="120px" :rules="formRules">
<el-form-item label="当前余额">¥ {{ value }} </el-form-item>
<el-form-item label="余额增减" required prop="action">
<el-radio-group v-model="formData.action">
<el-radio :value="1">增加余额</el-radio>
<el-radio :value="2">扣减余额</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="调整余额" prop="num">
<el-input
:model-value="formData.num"
placeholder="请输入调整的金额"
type="number"
@input="numberValidate"
/>
</el-form-item>
<el-form-item label="调整后余额"> ¥ {{ adjustmentMoney }} </el-form-item>
<el-form-item label="备注" prop="remark">
<el-input v-model="formData.remark" type="textarea" :rows="4" />
</el-form-item>
</el-form>
</div>
</popup>
</template>
<script lang="ts" setup>
import type { FormInstance, FormRules } from 'element-plus'
import Popup from '@/components/popup/index.vue'
import feedback from '@/utils/feedback'
const formRef = shallowRef<FormInstance>()
const props = defineProps({
show: {
type: Boolean,
required: true
},
value: {
type: [Number, String],
required: true
}
})
const emit = defineEmits<{
(event: 'update:show', value: boolean): void
(event: 'confirm', value: any): void
}>()
const formData = reactive({
action: 1, //变动类型 1-增加 2-减少
num: '',
remark: ''
})
const popupRef = shallowRef<InstanceType<typeof Popup>>()
const adjustmentMoney = computed(() => {
return Number(props.value) + Number(formData.num) * (formData.action == 1 ? 1 : -1)
})
const formRules: FormRules = {
num: [
{
required: true,
message: '请输入调整的金额'
}
]
}
const numberValidate = (value: string) => {
if (value.includes('-')) {
return feedback.msgError('请输入正整数')
}
formData.num = value
}
const handleConfirm = async () => {
await formRef.value?.validate()
emit('confirm', formData)
}
const popupClose = () => {
emit('update:show', false)
formRef.value?.resetFields()
}
watch(
() => props.show,
(val) => {
if (val) {
popupRef.value?.open()
} else {
popupRef.value?.close()
}
}
)
watch(adjustmentMoney, (val) => {
if (val < 0) {
feedback.msgError('调整后余额需大于0')
formData.num = ''
}
})
</script>
+170
View File
@@ -0,0 +1,170 @@
<template>
<div>
<el-card class="!border-none" shadow="never">
<el-page-header content="用户详情" @back="$router.back()" />
</el-card>
<el-card class="mt-4 !border-none" header="基本资料" shadow="never">
<el-form ref="formRef" class="ls-form" :model="formData" label-width="120px">
<div class="bg-page flex py-5 mb-10 items-center">
<div class="basis-40 flex flex-col justify-center items-center">
<div class="mb-2 text-tx-regular">用户头像</div>
<el-avatar :src="formData.avatar" :size="58" />
</div>
<div class="basis-40 flex flex-col justify-center items-center">
<div class="text-tx-regular">账户余额</div>
<div class="mt-2 flex items-center">
¥{{ formData.user_money }}
<el-button
v-perms="['user.user/adjustMoney']"
type="primary"
link
@click="handleAdjust(formData.user_money)"
>
调整
</el-button>
</div>
</div>
</div>
<el-form-item label="用户昵称:">
{{ formData.nickname }}
</el-form-item>
<el-form-item label="账号:">
{{ formData.account }}
<popover-input
class="ml-[10px]"
@confirm="handleEdit($event, 'account')"
:limit="32"
v-perms="['user.user/edit']"
>
<el-button type="primary" link>
<icon name="el-icon-EditPen" />
</el-button>
</popover-input>
</el-form-item>
<el-form-item label="真实姓名:">
{{ formData.real_name || '-' }}
<popover-input
class="ml-[10px]"
@confirm="handleEdit($event, 'real_name')"
:limit="32"
v-perms="['user.user/edit']"
>
<el-button type="primary" link>
<icon name="el-icon-EditPen" />
</el-button>
</popover-input>
</el-form-item>
<el-form-item label="性别:">
{{ formData.sex }}
<popover-input
class="ml-[10px]"
type="select"
:options="[
{
label: '未知',
value: 0
},
{
label: '男',
value: 1
},
{
label: '女',
value: 2
}
]"
@confirm="handleEdit($event, 'sex')"
v-perms="['user.user/edit']"
>
<el-button type="primary" link>
<icon name="el-icon-EditPen" />
</el-button>
</popover-input>
</el-form-item>
<el-form-item label="联系电话:">
{{ formData.mobile || '-' }}
<popover-input
class="ml-[10px]"
type="number"
@confirm="handleEdit($event, 'mobile')"
v-perms="['user.user/edit']"
>
<el-button type="primary" link>
<icon name="el-icon-EditPen" />
</el-button>
</popover-input>
</el-form-item>
<el-form-item label="注册来源:"> {{ formData.channel }} </el-form-item>
<el-form-item label="注册时间:"> {{ formData.create_time }} </el-form-item>
<el-form-item label="最近登录时间:"> {{ formData.login_time }} </el-form-item>
</el-form>
</el-card>
<account-adjust
v-model:show="adjustState.show"
:value="adjustState.value"
@confirm="handleConfirmAdjust"
/>
</div>
</template>
<script lang="ts" setup name="consumerDetail">
import type { FormInstance } from 'element-plus'
import { adjustMoney, getUserDetail, userEdit } from '@/api/consumer'
import { isEmpty } from '@/utils/util'
import AccountAdjust from '../components/account-adjust.vue'
const route = useRoute()
const formData = reactive({
avatar: '',
channel: '',
create_time: '',
login_time: '',
mobile: '',
nickname: '',
real_name: 0,
sex: 0,
sn: '',
account: '',
user_money: ''
})
const adjustState = reactive({
show: false,
value: ''
})
const formRef = shallowRef<FormInstance>()
const getDetails = async () => {
const data = await getUserDetail({
id: route.query.id
})
Object.keys(formData).forEach((key) => {
//@ts-ignore
formData[key] = data[key]
})
}
const handleEdit = async (value: string, field: string) => {
if (isEmpty(value)) return
await userEdit({
id: route.query.id,
field,
value
})
getDetails()
}
const handleAdjust = (value: string) => {
adjustState.show = true
adjustState.value = value
}
const handleConfirmAdjust = async (value: any) => {
await adjustMoney({ user_id: route.query.id, ...value })
adjustState.show = false
getDetails()
}
getDetails()
</script>
+98
View File
@@ -0,0 +1,98 @@
<template>
<div>
<el-card class="!border-none" shadow="never">
<el-form ref="formRef" class="mb-[-16px]" :model="queryParams" :inline="true">
<el-form-item class="w-[280px]" label="用户信息">
<el-input
v-model="queryParams.keyword"
placeholder="账号/昵称/手机号码"
clearable
@keyup.enter="resetPage"
/>
</el-form-item>
<el-form-item label="注册时间">
<daterange-picker
v-model:startTime="queryParams.create_time_start"
v-model:endTime="queryParams.create_time_end"
/>
</el-form-item>
<el-form-item class="w-[280px]" label="注册来源">
<el-select v-model="queryParams.channel">
<el-option
v-for="(item, key) in ClientMap"
:key="key"
:label="item"
:value="key"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="resetPage">查询</el-button>
<el-button @click="resetParams">重置</el-button>
<export-data
class="ml-2.5"
:fetch-fun="getUserList"
:params="queryParams"
:page-size="pager.size"
/>
</el-form-item>
</el-form>
</el-card>
<el-card class="!border-none mt-4" shadow="never">
<el-table size="large" v-loading="pager.loading" :data="pager.lists">
<el-table-column label="头像" min-width="100">
<template #default="{ row }">
<el-avatar :src="row.avatar" :size="50" />
</template>
</el-table-column>
<el-table-column label="昵称" prop="nickname" min-width="100" />
<el-table-column label="账号" prop="account" min-width="120" />
<el-table-column label="手机号码" prop="mobile" min-width="100" />
<el-table-column label="注册来源" prop="channel" min-width="100" />
<el-table-column label="注册时间" prop="create_time" min-width="120" />
<el-table-column label="操作" width="120" fixed="right">
<template #default="{ row }">
<el-button v-perms="['user.user/detail']" type="primary" link>
<router-link
:to="{
path: getRoutePath('user.user/detail'),
query: {
id: row.id
}
}"
>
详情
</router-link>
</el-button>
</template>
</el-table-column>
</el-table>
<div class="flex justify-end mt-4">
<pagination v-model="pager" @change="getLists" />
</div>
</el-card>
</div>
</template>
<script lang="ts" setup name="consumerLists">
import { getUserList } from '@/api/consumer'
import { ClientMap } from '@/enums/appEnums'
import { usePaging } from '@/hooks/usePaging'
import { getRoutePath } from '@/router'
const queryParams = reactive({
keyword: '',
channel: '',
create_time_start: '',
create_time_end: ''
})
const { pager, getLists, resetPage, resetParams } = usePaging({
fetchFun: getUserList,
params: queryParams
})
onActivated(() => {
getLists()
})
getLists()
</script>
@@ -0,0 +1,109 @@
<template>
<div>
<div>
<draggable
class="draggable"
v-model="navLists"
animation="300"
handle=".drag-move"
item-key="index"
>
<template v-slot:item="{ element: item, index }">
<del-wrap class="w-[467px]" :key="index" @close="handleDelete(index)">
<div class="bg-fill-light flex items-center w-full p-4 mb-4">
<material-picker
v-model="item.image"
upload-class="bg-body"
size="60px"
exclude-domain
>
<template #upload>
<div class="upload-btn w-[60px] h-[60px]">
<icon name="el-icon-Plus" :size="20" />
</div>
</template>
</material-picker>
<div class="ml-3 flex-1">
<div class="flex items-center">
<span class="text-tx-regular flex-none mr-3">名称</span>
<el-input v-model="item.name" placeholder="请输入名称" />
</div>
<div class="flex items-center mt-[18px]">
<span class="text-tx-regular flex-none mr-3">链接</span>
<link-picker v-model="item.link" />
</div>
<el-form-item label="是否显示" class="mt-[18px]">
<div class="flex-1 flex items-center">
<el-switch
v-model="item.is_show"
active-value="1"
inactive-value="0"
/>
<div class="drag-move cursor-move ml-auto">
<icon name="el-icon-Rank" size="18" />
</div>
</div>
</el-form-item>
</div>
</div>
</del-wrap>
</template>
</draggable>
</div>
<div>
<el-button type="primary" @click="handleAdd">添加</el-button>
</div>
</div>
</template>
<script lang="ts" setup>
import type { PropType } from 'vue'
import Draggable from 'vuedraggable'
import feedback from '@/utils/feedback'
const props = defineProps({
modelValue: {
type: Array as PropType<any[]>,
default: () => []
},
max: {
type: Number,
default: 100
},
min: {
type: Number,
default: 1
}
})
const emit = defineEmits(['update:modelValue'])
const navLists = computed({
get() {
return props.modelValue
},
set(value) {
emit('update:modelValue', value)
}
})
const handleAdd = () => {
if (props.modelValue?.length < props.max) {
navLists.value.push({
image: '',
name: '导航名称',
link: {},
is_show: '1'
})
} else {
feedback.msgError(`最多添加${props.max}`)
}
}
const handleDelete = (index: number) => {
if (props.modelValue?.length <= props.min) {
return feedback.msgError(`最少保留${props.min}`)
}
navLists.value.splice(index, 1)
}
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,61 @@
<template>
<el-image :style="styles" v-bind="props" :src="getImageUrl(src)">
<template #placeholder>
<div class="image-slot"></div>
</template>
<template #error>
<div class="image-slot">
<icon name="el-icon-Picture" :size="30" />
</div>
</template>
</el-image>
</template>
<script lang="ts" setup>
import { imageProps } from 'element-plus'
import type { CSSProperties } from 'vue'
import { computed } from 'vue'
import useAppStore from '@/stores/modules/app'
import { addUnit } from '@/utils/util'
const props = defineProps({
width: {
type: [String, Number],
default: 'auto'
},
height: {
type: [String, Number],
default: 'auto'
},
radius: {
type: [String, Number],
default: 0
},
...imageProps
})
const { getImageUrl } = useAppStore()
const styles = computed<CSSProperties>(() => {
return {
width: addUnit(props.width),
height: addUnit(props.height),
borderRadius: addUnit(props.radius)
}
})
</script>
<style lang="scss" scoped>
.el-image {
display: block;
.image-slot {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
background: #fafafa;
color: #909399;
}
}
</style>
@@ -0,0 +1,43 @@
<template>
<div class="pages-setting">
<el-card shadow="never" class="!border-none flex">
<div
class="title flex items-center before:w-[3px] before:h-[14px] before:block before:bg-primary before:mr-2 text-xl font-medium"
>
{{ widget?.title }}
</div>
</el-card>
<el-scrollbar class="w-full" style="height: calc(100% - 60px)">
<keep-alive>
<component
:is="widgets[widget?.name]?.attr"
:content="widget?.content"
:styles="widget?.styles"
:type="type"
@update:content="handleUpdateContent"
/>
</keep-alive>
</el-scrollbar>
</div>
</template>
<script lang="ts" setup>
import type { PropType } from 'vue'
import widgets from '../widgets'
const emits = defineEmits(['update:content'])
const handleUpdateContent = (data: any) => {
emits('update:content', data)
}
defineProps({
widget: {
type: Object as PropType<Record<string, any>>,
default: () => ({})
},
type: {
type: String as PropType<'mobile' | 'pc'>,
default: 'mobile'
}
})
</script>
@@ -0,0 +1,49 @@
<template>
<div class="pages-menu">
<el-menu
:default-active="modelValue"
class="w-[160px] min-h-[668px]"
@select="handleSelect"
>
<el-menu-item v-for="(item, key) in menus" :index="key" :key="item.id">
<span>{{ item.name }}</span>
</el-menu-item>
</el-menu>
</div>
</template>
<script lang="ts" setup>
import type { PropType } from 'vue'
defineProps({
menus: {
type: Object as PropType<Record<string, any>>,
default: () => ({})
},
modelValue: {
type: String,
default: '1'
}
})
const emit = defineEmits<{
(event: 'update:modelValue', value: string): void
}>()
const handleSelect = (index: string) => {
emit('update:modelValue', index)
}
</script>
<style lang="scss" scoped>
.pages-menu {
:deep(.el-menu) {
border-right: none;
}
:deep(.el-menu-item) {
border-color: transparent;
&.is-active {
border-right-width: 2px;
border-color: var(--el-color-primary);
background-color: var(--el-color-primary-light-9);
}
}
}
</style>
@@ -0,0 +1,167 @@
<template>
<div class="pages-preview">
<div class="relative flex justify-center h-full mt-5 mx-10">
<!-- iframe预览 -->
<iframe
v-if="$route.query.url"
ref="previewIframeRef"
class="flex-1 h-full"
width="100%"
height="100%"
scrolling="no"
:src="$route.query.url as string"
></iframe>
<div class="max-w-[1200px] w-full absolute">
<div
v-for="(widget, index) in pageData"
:key="widget.id"
class="absolute left-0 top-0"
:class="{
'cursor-pointer': !widget?.disabled
}"
@click="handleClick(widget, index)"
>
<div
class="absolute w-full h-full z-[100] border-dashed"
:class="{
select: index == modelValue,
'border-[#dcdfe6] border-2': !widget?.disabled,
hide: canShowCom(widget.content)
}"
:style="widget.styles"
></div>
<slot>
<component
:is="widgets[widget?.name]?.content"
:content="widget.content"
:styles="widget.styles"
:key="widget.id"
ref="commonComponentRef"
/>
</slot>
<!-- 部件操作按钮组 -->
<div
class="widget-btns py-[5px]"
v-if="index == modelValue"
:style="{
top: widget.styles.top,
left: widget.styles.width
}"
>
<div>
<el-tooltip effect="dark" content="编辑组件内容" placement="right">
<el-button
class="py-[5px]"
type="primary"
:icon="Setting"
@click="handleClickSetting(index)"
/>
</el-tooltip>
</div>
<div>
<el-tooltip
effect="dark"
:content="canShowCom(widget.content) ? '显示' : '隐藏'"
placement="right"
>
<el-button
class="py-[5px]"
type="primary"
:icon="canShowCom(widget.content) ? View : Hide"
@click="changeShowCom(widget.content)"
/>
</el-tooltip>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { Hide, Setting, View } from '@element-plus/icons-vue'
import type { PropType } from 'vue'
import widgets from '../widgets'
const commonComponentRef = shallowRef<any>()
defineProps({
pageData: {
type: Array as PropType<any[]>,
default: () => []
},
modelValue: {
type: Number,
default: 0
}
})
const emit = defineEmits<{
(event: 'update:modelValue', value: number): void
}>()
// 是否显示组件
const canShowCom = computed(() => {
return (data: any) => {
return data?.enabled == 0
}
})
// 点击了组件设置
const handleClickSetting = (index: number) => {
commonComponentRef.value[index]?.open()
}
// 修改组件显示/隐藏
const changeShowCom = (data: any) => {
if (data.enabled === undefined) return
data.enabled = data.enabled ? 0 : 1
}
const handleClick = (widget: any, index: number) => {
if (widget.disabled) return
emit('update:modelValue', index)
}
</script>
<style lang="scss" scoped>
.pages-preview {
@apply w-full h-full relative;
height: 85vh;
.select {
@apply border-primary border-solid;
}
.hide::before {
content: '已隐藏';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
justify-content: center;
align-items: center;
color: white;
font-size: 14px;
background-color: rgba(0, 0, 0, 0.4);
}
.widget-btns {
position: absolute;
overflow: hidden;
width: 46px;
border-radius: 8px;
@apply bg-primary;
margin-left: 10px;
:deep(.el-button) {
width: 46px;
border-radius: 0;
}
}
}
</style>
@@ -0,0 +1,220 @@
<template>
<el-scrollbar class="pages-preview-container">
<div v-if="pageMeta !== null" class="absolute right-4 top-4" @click="handleClickPageMeta">
<el-button>页面设置</el-button>
</div>
<div class="shadow mx-[30px] pages-preview">
<div
v-for="(widget, index) in pageData"
:key="widget.id"
class="relative"
:class="{
'cursor-pointer': !widget?.disabled
}"
@click="handleClick(widget, index)"
>
<!-- 选中的边框 -->
<div
class="absolute w-full h-full z-[100] border-dashed"
:class="{
select: index == modelValue,
hide: canShowCom(widget.content),
'border-[#dcdfe6] border-2': !widget?.disabled
}"
></div>
<!-- 选中的组件 -->
<slot>
<component
:is="widgets[widget?.name]?.content"
:content="widget.content"
:styles="widget.styles"
:key="widget.id"
/>
</slot>
<!-- 部件操作按钮组 -->
<div class="widget-btns py-[5px]" v-if="index == modelValue">
<div>
<el-tooltip
effect="dark"
:content="canShowCom(widget.content) ? '显示' : '隐藏'"
placement="right"
>
<el-button
class="py-[5px]"
type="primary"
:icon="canShowCom(widget.content) ? View : Hide"
@click="changeShowCom(widget.content)"
/>
</el-tooltip>
</div>
<div>
<el-tooltip effect="dark" content="上移" placement="right">
<el-button
class="py-[5px]"
type="primary"
:icon="ArrowUpBold"
:disabled="canMoveUpCom(index)"
@click.stop="rearrangeArray(index, index - 1)"
/>
</el-tooltip>
</div>
<div>
<el-tooltip effect="dark" content="下移" placement="right">
<el-button
class="py-[5px]"
type="primary"
:icon="ArrowDownBold"
:disabled="canMoveDownCom(index)"
@click.stop="rearrangeArray(index, index + 1)"
/>
</el-tooltip>
</div>
</div>
</div>
</div>
</el-scrollbar>
</template>
<script lang="ts" setup>
import { ArrowDownBold, ArrowUpBold, Hide, View } from '@element-plus/icons-vue'
import { cloneDeep } from 'lodash-es'
import type { PropType } from 'vue'
import { computed } from 'vue'
import widgets from '../widgets'
const props = defineProps({
pageMeta: {
type: Object as any,
default: () => null
},
pageData: {
type: Array as PropType<any[]>,
default: () => []
},
modelValue: {
type: Number,
default: 0
}
})
const emit = defineEmits<{
(event: 'update:modelValue', value: number): void
(event: 'updatePageData', value: any[]): void
}>()
const oldModelValue = ref<number>(-1)
const handleClickPageMeta = () => {
if (props.modelValue === -1) {
emit('update:modelValue', oldModelValue.value)
} else {
oldModelValue.value = props.modelValue
emit('update:modelValue', -1)
}
}
const handleClick = (widget: any, index: number) => {
if (widget.disabled) return
emit('update:modelValue', index)
}
// 是否可以移动组件
const canMoveUpCom = computed(() => {
return (index: number) => {
return index === 0
}
})
// 是否可以移动组件
const canMoveDownCom = computed(() => {
return (index: number) => {
return props.pageData?.length === index + 1
}
})
// 是否显示组件
const canShowCom = computed(() => {
return (data: any) => {
return data?.enabled == 0
}
})
// 修改组件显示/隐藏
const changeShowCom = (data: any) => {
if (data.enabled === undefined) return
data.enabled = data.enabled ? 0 : 1
}
const rearrangeArray = (currentIdx: number, targetIdx: number) => {
if (
currentIdx < 0 ||
currentIdx >= props.pageData.length ||
targetIdx < 0 ||
targetIdx >= props.pageData.length
) {
return
}
// const element = props.pageData.splice(currentIdx, 1)[0]
// props.pageData.splice(targetIdx, 0, element)
const newPageData = cloneDeep(props.pageData)
const element = newPageData.splice(currentIdx, 1)[0]
newPageData.splice(targetIdx, 0, element)
emit('updatePageData', newPageData)
emit('update:modelValue', targetIdx)
}
</script>
<style lang="scss" scoped>
.pages-preview-container {
position: relative;
:deep(.el-scrollbar__wrap) {
width: 100%;
display: flex;
justify-content: center;
}
.pages-preview {
background-color: #f8f8f8;
width: 360px;
min-height: 615px;
color: #333;
.select {
@apply border-primary border-solid;
}
.hide::before {
content: '已隐藏';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
justify-content: center;
align-items: center;
color: white;
font-size: 14px;
background-color: rgba(0, 0, 0, 0.4);
}
.widget-btns {
position: absolute;
top: 10px;
right: -60px;
overflow: hidden;
width: 46px;
border-radius: 8px;
@apply bg-primary;
:deep(.el-button) {
width: 46px;
border-radius: 0;
}
}
}
}
</style>
@@ -0,0 +1,183 @@
<template>
<el-card shadow="never" class="!border-none flex">
<div
class="title flex items-center before:w-[3px] before:h-[14px] before:block before:bg-primary before:mr-2 text-xl font-medium"
>
底部导航设置
<span class="form-tips ml-[10px] !mt-0"> 至少添加2个导航最多添加5个导航 </span>
</div>
</el-card>
<el-form label-width="70px">
<el-card shadow="never" class="!border-none flex mt-2">
<div class="flex items-end mb-4">
<div class="text-base text-[#101010] font-medium">展示样式</div>
</div>
<el-form-item label="默认颜色">
<color-picker
class="max-w-[400px]"
v-model="data.style.default_color"
default-color="#999999"
/>
</el-form-item>
<el-form-item label="选中颜色" style="margin-bottom: 0">
<color-picker
class="max-w-[400px]"
v-model="data.style.selected_color"
default-color="#4173ff"
/>
</el-form-item>
</el-card>
<el-card shadow="never" class="!border-none flex mt-2">
<div class="flex items-end mb-4">
<div class="text-base text-[#101010] font-medium">菜单设置</div>
<div class="text-xs text-tx-secondary ml-2">建议图片尺寸100px*100px</div>
</div>
<div class="mb-[18px] max-w-[400px]">
<Draggable
class="draggable"
v-model="data.list"
animation="300"
draggable=".draggable"
handle=".drag-move"
:move="onMove"
item-key="index"
>
<template v-slot:item="{ element, index }">
<del-wrap
@close="handleDelete(index)"
class="max-w-[400px]"
:show-close="index !== 0"
:class="{ draggable: index != 0 }"
>
<div class="bg-fill-light w-full p-4 mt-4">
<el-form-item label="导航图标">
<material-picker
v-model="element.unselected"
upload-class="bg-body"
exclude-domain
size="60px"
>
<template #upload>
<div class="upload-btn w-[60px] h-[60px]">
<icon name="el-icon-Plus" :size="16" />
<span class="text-xs leading-5"> 未选中 </span>
</div>
</template>
</material-picker>
<material-picker
v-model="element.selected"
exclude-domain
upload-class="bg-body"
size="60px"
>
<template #upload>
<div class="upload-btn w-[60px] h-[60px]">
<icon name="el-icon-Plus" :size="16" />
<span class="text-xs leading-5"> 选中 </span>
</div>
</template>
</material-picker>
</el-form-item>
<el-form-item label="导航名称">
<el-input v-model="element.name" placeholder="请输入名称" />
</el-form-item>
<el-form-item label="链接地址">
<link-picker
:is-tab="true"
:disabled="index === 0"
v-model="element.link"
/>
</el-form-item>
<el-form-item label="是否显示">
<div class="flex-1 flex items-center">
<el-switch
:disabled="index == 0"
v-model="element.is_show"
:active-value="1"
:inactive-value="0"
@change="handleShowChange(element)"
/>
<div class="drag-move cursor-move ml-auto">
<icon name="el-icon-Rank" size="18" />
</div>
</div>
</el-form-item>
</div>
</del-wrap>
</template>
</Draggable>
</div>
<div v-if="data.list?.length < max" class="mt-4">
<el-button class="w-full" type="primary" @click="handleAdd">
添加导航 {{ data.list?.length }} / {{ max }}
</el-button>
</div>
</el-card>
</el-form>
</template>
<script lang="ts" setup>
import Draggable from 'vuedraggable'
import feedback from '@/utils/feedback'
const props = defineProps({
modelValue: {
type: Object,
default: () => ({
list: [],
style: {}
})
}
})
const emit = defineEmits<{
(event: 'update:modelValue', value: any): void
}>()
const data = computed({
get() {
return props.modelValue
},
set(value) {
emit('update:modelValue', value)
}
})
const max = 5
const min = 2
const showTabbarList = computed(() => {
return data.value.list?.filter((tab: any) => tab.is_show == '1') || []
})
const handleAdd = () => {
if (data.value.list?.length < max) {
data.value.list.push({
name: '',
selected: '',
unselected: '',
is_show: 1,
link: {}
})
} else {
feedback.msgError(`最多添加${max}`)
}
}
const handleDelete = (index: number) => {
if (data.value.list?.length <= min) {
return feedback.msgError(`最少保留${min}`)
}
data.value.list.splice(index, 1)
}
const onMove = (e: any) => {
if (e.relatedContext.index == 0) {
return false
}
return true
}
const handleShowChange = (item: any) => {
if (showTabbarList.value.length < min) {
item.is_show = 1
return feedback.msgError(`最少显示${min}`)
}
}
</script>
@@ -0,0 +1,45 @@
<template>
<div class="tabbar flex">
<template v-for="(item, index) in showTabbarList" :key="index">
<div
class="tabbar-item flex flex-col justify-center items-center flex-1"
:style="{ color: style.default_color }"
>
<decoration-img width="22px" height="22px" :src="item.unselected" fit="cover" />
<div class="leading-3 text-[12px] mt-[4px]">
{{ item.name }}
</div>
</div>
</template>
</div>
</template>
<script lang="ts" setup>
import type { PropType } from 'vue'
import DecorationImg from '../../decoration-img.vue'
const props = defineProps({
style: {
type: Object,
default: () => ({})
},
list: {
type: Array as PropType<any[]>,
default: () => []
}
})
const showTabbarList = computed(() => {
return props.list?.filter((tab: any) => tab.is_show == 1) || []
})
</script>
<style lang="scss" scoped>
.tabbar {
position: absolute;
height: 50px;
background-color: #fff;
bottom: 0;
width: 100%;
border: 2px solid var(--el-color-primary);
}
</style>
@@ -0,0 +1,7 @@
import attr from './attr.vue'
import content from './content.vue'
export default {
attr,
content
}
@@ -0,0 +1,41 @@
<template>
<div
class="title flex items-center before:w-[3px] before:h-[14px] before:block before:bg-primary before:mr-2"
>
pc导航设置
</div>
<el-form class="mt-4" label-width="70px">
<el-tabs model-value="nav">
<el-tab-pane label="主导航设置" name="nav">
<menu-set v-model="data.nav" />
</el-tab-pane>
<el-tab-pane label="菜单设置" name="menu">
<menu-set v-model="data.menu" />
</el-tab-pane>
</el-tabs>
</el-form>
</template>
<script lang="ts" setup>
import MenuSet from './menu-set.vue'
const props = defineProps({
modelValue: {
type: Object,
default: () => ({
nav: [],
menu: {}
})
}
})
const emit = defineEmits<{
(event: 'update:modelValue', value: any): void
}>()
const data = computed({
get() {
return props.modelValue
},
set(value) {
emit('update:modelValue', value)
}
})
</script>
@@ -0,0 +1,103 @@
<template>
<div class="pc-aside absolute top-0 bottom-0 left-0 w-[150px]">
<ElScrollbar>
<div class="h-full px-[10px] flex flex-col">
<div class="mb-auto">
<div class="nav">
<template v-for="(item, index) in nav" :key="index">
<div
v-if="item.is_show == '1'"
:to="item.link.path"
class="nav-item"
:class="{
active: index == 0
}"
>
<decoration-img
width="18px"
height="18px"
:src="index == 0 ? item.selected : item.unselected"
fit="cover"
>
<template #error>
<span></span>
</template>
</decoration-img>
<div class="ml-[10px]">{{ item.name }}</div>
</div>
</template>
</div>
</div>
<div>
<div class="menu">
<div class="menu-item" v-for="(item, index) in menu" :key="index">
<decoration-img
width="16px"
height="16px"
:src="item.unselected"
fit="cover"
>
<template #error>
<span></span>
</template>
</decoration-img>
<span class="ml-[6px] line-clamp-1">{{ item.name }}</span>
</div>
</div>
</div>
</div>
</ElScrollbar>
</div>
</template>
<script lang="ts" setup>
import type { PropType } from 'vue'
import DecorationImg from '../../decoration-img.vue'
defineProps({
nav: {
type: Array as PropType<any[]>,
default: () => []
},
menu: {
type: Array as PropType<any[]>,
default: () => []
}
})
</script>
<style lang="scss" scoped>
.pc-aside {
border: 2px solid var(--el-color-primary);
:deep(.el-scrollbar__view) {
height: 100%;
}
.nav {
.nav-item {
border-radius: 8px;
display: flex;
align-items: center;
height: 40px;
margin: 10px 0;
padding: 0 12px;
&.active {
color: #fff;
background: linear-gradient(90deg, #19e8b7 0%, #00abff 100%);
}
}
}
.menu-item {
display: flex;
align-items: center;
height: 38px;
line-height: 38px;
padding: 0 12px;
border-radius: 8px;
cursor: pointer;
margin: 3px 0;
font-size: 13px;
}
}
</style>
@@ -0,0 +1,7 @@
import attr from './attr.vue'
import content from './content.vue'
export default {
attr,
content
}
@@ -0,0 +1,127 @@
<template>
<div class="mb-[18px] max-w-[400px]">
<Draggable
class="draggable"
v-model="menu"
animation="300"
handle=".drag-move"
item-key="index"
>
<template v-slot:item="{ element, index }">
<del-wrap @close="handleDelete(index)" class="max-w-[400px]">
<div class="bg-fill-light w-full p-4 mt-4">
<el-form-item
label="导航图标"
v-if="
element.unselected !== undefined || element.selected !== undefined
"
>
<material-picker
v-if="element.unselected !== undefined"
v-model="element.unselected"
upload-class="bg-body"
exclude-domain
size="60px"
>
<template #upload>
<div class="upload-btn w-[60px] h-[60px]">
<icon name="el-icon-Plus" :size="16" />
<span class="text-xs leading-5"> 未选中 </span>
</div>
</template>
</material-picker>
<material-picker
v-model="element.selected"
v-if="element.selected !== undefined"
upload-class="bg-body"
exclude-domain
size="60px"
>
<template #upload>
<div class="upload-btn w-[60px] h-[60px]">
<icon name="el-icon-Plus" :size="16" />
<span class="text-xs leading-5"> 选中 </span>
</div>
</template>
</material-picker>
</el-form-item>
<el-form-item label="导航名称" v-if="element.name !== undefined">
<el-input v-model="element.name" placeholder="请输入名称" />
</el-form-item>
<el-form-item label="链接地址" v-if="element.link !== undefined">
<link-picker v-model="element.link" type="pc" />
</el-form-item>
<el-form-item label="是否显示" v-if="element.is_show !== undefined">
<div class="flex-1 flex items-center">
<el-switch
v-model="element.is_show"
active-value="1"
inactive-value="0"
/>
<div class="drag-move cursor-move ml-auto">
<icon name="el-icon-Rank" size="18" />
</div>
</div>
</el-form-item>
</div>
</del-wrap>
</template>
</Draggable>
</div>
<el-form-item v-if="menu.length < max" label-width="0">
<el-button type="primary" @click="handleAdd"> 添加导航 </el-button>
</el-form-item>
</template>
<script setup lang="ts">
import { cloneDeep } from 'lodash-es'
import Draggable from 'vuedraggable'
import feedback from '@/utils/feedback'
const props = withDefaults(
defineProps<{
modelValue: any[]
max?: number
min?: number
itemData: any
}>(),
{
max: 9999,
min: -1,
itemData: () => ({
name: '',
selected: '',
unselected: '',
is_show: '1',
link: {}
})
}
)
const emit = defineEmits<{
(event: 'update:modelValue', value: any): void
}>()
const menu = computed({
get() {
return props.modelValue
},
set(value) {
emit('update:modelValue', value)
}
})
const handleAdd = () => {
if (menu.value?.length < props.max) {
menu.value.push(cloneDeep(props.itemData))
} else {
feedback.msgError(`最多添加${props.max}`)
}
}
const handleDelete = (index: number) => {
if (menu.value.length <= props.min && props.min > -1) {
return feedback.msgError(`最少保留${props.min}`)
}
menu.value.splice(index, 1)
}
</script>
@@ -0,0 +1,180 @@
<template>
<el-form label-width="70px">
<el-card shadow="never" class="!border-none flex mt-2">
<div class="flex items-end mb-4">
<div class="text-base dark:text-[#ffffff] text-[#101010] font-medium">展示样式</div>
</div>
<el-radio-group v-model="contentData.style">
<el-radio :value="1">常规</el-radio>
<el-radio :value="2">大屏</el-radio>
</el-radio-group>
</el-card>
<el-card shadow="never" class="!border-none flex mt-2" v-if="content.style == 1">
<div class="flex items-end mb-4">
<div class="text-base text-[#101010] dark:text-[#ffffff] font-medium">背景联动</div>
</div>
<el-radio-group v-model="contentData.bg_style">
<el-radio :value="1">开启</el-radio>
<el-radio :value="0">关闭</el-radio>
</el-radio-group>
<div class="p-[15px] rounded-[8px] bg-[#f3f8ff] text-[#136bdf] mt-2">
开启背景联动后需为轮播图设置背景图轮播图切换时背景图也跟随切换此时该页面自身的页面背景设置将失效
</div>
</el-card>
<el-card shadow="never" class="!border-none flex-1 mt-2">
<div class="flex items-end">
<div class="text-base text-[#101010] dark:text-[#ffffff] font-medium">轮播图片</div>
<div v-if="content.style == 1" class="text-xs text-tx-secondary ml-2">
最多添加5张建议图片尺寸750px*340px
</div>
<div v-else class="text-xs text-tx-secondary ml-2">
最多添加5张建议图片尺寸750px*1100px
</div>
</div>
<div class="flex-1">
<draggable
class="draggable"
v-model="contentData.data"
animation="300"
handle=".drag-move"
item-key="index"
>
<template v-slot:item="{ element: item, index }">
<del-wrap :key="index" @close="handleDelete(index)" class="w-full">
<div class="bg-fill-light w-full p-4 mt-4">
<div class="flex justify-center w-[467px]">
<template v-if="content.style == 1">
<material-picker
size="122px"
v-model="item.image"
upload-class="bg-body"
exclude-domain
>
<template #upload>
<div class="w-[122px] h-[122px] banner-upload-btn">
轮播图
</div>
</template>
</material-picker>
<material-picker
v-if="content.style == 1 || content.bg_style == 1"
class="ml-[40px]"
size="122px"
v-model="item.bg"
upload-class="bg-body"
exclude-domain
>
<template #upload>
<div class="w-[122px] h-[122px] banner-upload-btn">
背景图
</div>
</template>
</material-picker>
</template>
<template v-else>
<material-picker
width="396px"
height="196px"
v-model="item.image"
upload-class="bg-body"
exclude-domain
/>
</template>
</div>
<div class="flex-1">
<el-form-item class="mt-[18px]" label="图片链接">
<link-picker v-if="type == 'mobile'" v-model="item.link" />
<el-input
v-if="type == 'pc'"
placeholder="请输入链接"
v-model="item.link.path"
/>
</el-form-item>
<el-form-item label="是否显示" class="mt-[18px] !mb-0">
<div class="flex-1 flex items-center">
<el-switch
v-model="item.is_show"
active-value="1"
inactive-value="0"
/>
<div class="drag-move cursor-move ml-auto">
<icon name="el-icon-Rank" size="18" />
</div>
</div>
</el-form-item>
</div>
</div>
</del-wrap>
</template>
</draggable>
</div>
<div class="mt-4" v-if="content.data?.length < limit">
<el-button class="w-full" type="primary" @click="handleAdd">添加图片</el-button>
</div>
</el-card>
</el-form>
</template>
<script lang="ts" setup>
import { cloneDeep } from 'lodash-es'
import type { PropType } from 'vue'
import Draggable from 'vuedraggable'
import feedback from '@/utils/feedback'
import type options from './options'
type OptionsType = ReturnType<typeof options>
const emits = defineEmits<(event: 'update:content', data: OptionsType['content']) => void>()
const limit = 5
const props = defineProps({
content: {
type: Object as PropType<OptionsType['content']>,
default: () => ({})
},
styles: {
type: Object as PropType<OptionsType['styles']>,
default: () => ({})
},
type: {
type: String as PropType<'mobile' | 'pc'>,
default: 'mobile'
}
})
const contentData = computed({
get: () => props.content,
set: (newValue) => {
emits('update:content', newValue)
}
})
const handleAdd = () => {
if (props.content.data?.length < limit) {
const content = cloneDeep(props.content)
content.data.push({
is_show: '1',
image: '',
bg: '',
name: '',
link: {}
})
emits('update:content', content)
} else {
feedback.msgError(`最多添加${limit}张图片`)
}
}
const handleDelete = (index: number) => {
if (props.content.data?.length <= 1) {
return feedback.msgError('最少保留一张图片')
}
const content = cloneDeep(props.content)
content.data.splice(index, 1)
emits('update:content', content)
}
</script>
<style lang="scss" scoped>
.banner-upload-btn {
@apply text-tx-secondary box-border rounded border-br border-dashed border flex flex-col justify-center items-center;
}
</style>
@@ -0,0 +1,47 @@
<template>
<div class="banner" :style="styles">
<div class="banner-image w-full h-full">
<decoration-img
width="100%"
:height="content.style == 1 ? height : '550px'"
:src="getImage"
fit="contain"
/>
</div>
</div>
</template>
<script lang="ts" setup>
import type { PropType } from 'vue'
import DecorationImg from '../../decoration-img.vue'
import type options from './options'
type OptionsType = ReturnType<typeof options>
const props = defineProps({
content: {
type: Object as PropType<OptionsType['content']>,
default: () => ({})
},
styles: {
type: Object as PropType<OptionsType['styles']>,
default: () => ({})
},
height: {
type: String,
default: '170px'
}
})
const showList = computed(() => {
return props.content.data?.filter((item: any) => item.is_show == '1') || []
})
const getImage = computed(() => {
if (Array.isArray(showList.value)) {
return showList.value[0] ? showList.value[0].image : ''
}
return ''
})
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,9 @@
import attr from './attr.vue'
import content from './content.vue'
import options from './options'
export default {
attr,
content,
options
}
@@ -0,0 +1,19 @@
export default () => ({
title: '首页轮播图',
name: 'banner',
content: {
enabled: 1,
style: 1, // 展示样式---1=常规,2=大屏
bg_style: 0, // 背景联动---0=关闭,1=开启
data: [
{
is_show: '1',
image: '',
bg: '',
name: '',
link: {}
}
]
},
styles: {}
})
@@ -0,0 +1,68 @@
<template>
<div>
<el-form label-width="90px" size="large" label-position="top">
<el-card shadow="never" class="!border-none flex mt-2">
<el-form-item label="平台名称">
<el-input
class="w-[400px]"
show-word-limit
maxlength="20"
v-model="contentData.title"
/>
</el-form-item>
<el-form-item label="客服二维码">
<div>
<material-picker v-model="contentData.qrcode" exclude-domain />
</div>
</el-form-item>
<el-form-item label="备注">
<el-input
class="w-[400px]"
show-word-limit
maxlength="20"
v-model="contentData.remark"
/>
</el-form-item>
<el-form-item label="联系电话">
<el-input class="w-[400px]" v-model="contentData.mobile" />
</el-form-item>
<el-form-item label="服务时间">
<el-input
class="w-[400px]"
show-word-limit
maxlength="20"
v-model="contentData.time"
/>
</el-form-item>
</el-card>
</el-form>
</div>
</template>
<script lang="ts" setup>
import type { PropType } from 'vue'
import type options from './options'
type OptionsType = ReturnType<typeof options>
const emits = defineEmits<(event: 'update:content', data: OptionsType['content']) => void>()
const props = defineProps({
content: {
type: Object as PropType<OptionsType['content']>,
default: () => ({})
},
styles: {
type: Object as PropType<OptionsType['styles']>,
default: () => ({})
}
})
const contentData = computed({
get: () => props.content,
set: (newValue) => {
emits('update:content', newValue)
}
})
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,53 @@
<template>
<view class="bg-white p-[15px] flex text-[#101010] font-medium text-lg"> 联系我们 </view>
<view
class="customer-service bg-white flex flex-col justify-center items-center mx-[18px] mt-[15px] rounded-[10px] px-[10px] pb-[50px]"
>
<view
class="w-full border-solid border-0 border-b border-[#f5f5f5] p-[15px] text-center text-[#101010] text-base font-medium"
>
{{ content.title }}
</view>
<view class="mt-[30px]">
<decoration-img width="100px" height="100px" :src="content.qrcode" alt="" />
</view>
<view v-if="content.remark" class="text-sm mt-[20px] font-medium">{{
content.remark
}}</view>
<view v-if="content.mobile" class="text-sm mt-[12px] flex flex-wrap">
<a class="ml-[5px] phone text-primary underline" :href="'tel:' + content.mobile">
{{ content.mobile }}
</a>
</view>
<view v-if="content.time" class="text-muted text-sm mt-[15px]">
服务时间{{ content.time }}
</view>
</view>
Î
</template>
<script lang="ts" setup>
import type { PropType } from 'vue'
import DecorationImg from '../../decoration-img.vue'
import type options from './options'
type OptionsType = ReturnType<typeof options>
defineProps({
content: {
type: Object as PropType<OptionsType['content']>,
default: () => ({})
},
styles: {
type: Object as PropType<OptionsType['styles']>,
default: () => ({})
}
})
</script>
<style lang="scss" scoped>
.customer-service {
background: #fff;
@apply flex flex-col justify-center items-center;
}
</style>
@@ -0,0 +1,9 @@
import attr from './attr.vue'
import content from './content.vue'
import options from './options'
export default {
attr,
content,
options
}
@@ -0,0 +1,12 @@
export default () => ({
title: '客服设置',
name: 'customer-service',
content: {
title: '添加客服二维码',
time: '',
mobile: '',
qrcode: '',
remark: ''
},
styles: {}
})
@@ -0,0 +1,14 @@
const widgets: Record<string, any> = import.meta.glob('./**/index.ts', { eager: true })
interface Widget {
attr?: any
content: any
options: any
}
const exportWidgets: Record<string, Widget> = {}
Object.keys(widgets).forEach((key) => {
const widgetName = key.replace(/^\.\/([\w-]+).*/gi, '$1')
exportWidgets[widgetName] = widgets[key]?.default
})
export default exportWidgets
@@ -0,0 +1,123 @@
<template>
<div>
<el-form label-width="70px">
<el-card shadow="never" class="!border-none flex mt-2">
<div class="flex items-end">
<div class="text-base text-[#101010] font-medium">图片设置</div>
<div class="text-xs text-tx-secondary ml-2">
最多添加5张建议图片尺寸750px*200px
</div>
</div>
<div class="flex-1">
<draggable
class="draggable"
v-model="contentData.data"
animation="300"
handle=".drag-move"
>
<template v-slot:item="{ element: item, index }">
<del-wrap :key="index" @close="handleDelete(index)" class="w-full">
<div class="bg-fill-light w-full p-4 mt-4">
<material-picker
width="396px"
height="196px"
v-model="item.image"
upload-class="bg-body"
exclude-domain
/>
<div class="flex-1">
<el-form-item class="mt-[18px]" label="图片链接">
<link-picker
v-if="type == 'mobile'"
v-model="item.link"
/>
<el-input
v-if="type == 'pc'"
placeholder="请输入链接"
v-model="item.link.path"
/>
</el-form-item>
<el-form-item label="是否显示" class="mt-[18px] !mb-0">
<div class="flex-1 flex items-center">
<el-switch
v-model="item.is_show"
active-value="1"
inactive-value="0"
/>
<div class="drag-move cursor-move ml-auto">
<icon name="el-icon-Rank" size="18" />
</div>
</div>
</el-form-item>
</div>
</div>
</del-wrap>
</template>
</draggable>
</div>
<div class="mt-4" v-if="content.data?.length < limit">
<el-button class="w-full" type="primary" @click="handleAdd">添加图片</el-button>
</div>
</el-card>
</el-form>
</div>
</template>
<script lang="ts" setup>
import { cloneDeep } from 'lodash-es'
import type { PropType } from 'vue'
import Draggable from 'vuedraggable'
import feedback from '@/utils/feedback'
import type options from './options'
const limit = 5
type OptionsType = ReturnType<typeof options>
const emits = defineEmits<(event: 'update:content', data: OptionsType['content']) => void>()
const props = defineProps({
content: {
type: Object as PropType<OptionsType['content']>,
default: () => ({})
},
styles: {
type: Object as PropType<OptionsType['styles']>,
default: () => ({})
},
type: {
type: String as PropType<'mobile' | 'pc'>,
default: 'mobile'
}
})
const contentData = computed({
get: () => props.content,
set: (newValue) => {
emits('update:content', newValue)
}
})
const handleAdd = () => {
if (props.content.data?.length < limit) {
const content = cloneDeep(props.content)
content.data.push({
is_show: '1',
image: '',
name: '',
link: {}
})
emits('update:content', content)
} else {
feedback.msgError(`最多添加${limit}张图片`)
}
}
const handleDelete = (index: number) => {
if (props.content.data?.length <= 1) {
return feedback.msgError('最少保留一张图片')
}
const content = cloneDeep(props.content)
content.data.splice(index, 1)
emits('update:content', content)
}
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,47 @@
<template>
<div class="banner" :style="styles">
<div class="banner-image w-full h-full">
<decoration-img
width="100%"
:height="styles.height || height"
:src="getImage"
fit="contain"
/>
</div>
</div>
</template>
<script lang="ts" setup>
import type { PropType } from 'vue'
import DecorationImg from '../../decoration-img.vue'
import type options from './options'
type OptionsType = ReturnType<typeof options>
const props = defineProps({
content: {
type: Object as PropType<OptionsType['content']>,
default: () => ({})
},
styles: {
type: Object as PropType<OptionsType['styles']>,
default: () => ({})
},
height: {
type: String,
default: '96px'
}
})
const showList = computed(() => {
return props.content.data?.filter((item: any) => item.is_show == '1') || []
})
const getImage = computed(() => {
if (Array.isArray(showList.value)) {
return showList.value[0] ? showList.value[0].image : ''
}
return ''
})
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,9 @@
import attr from './attr.vue'
import content from './content.vue'
import options from './options'
export default {
attr,
content,
options
}
@@ -0,0 +1,16 @@
export default () => ({
title: '首页中部轮播图',
name: 'middle-banner',
content: {
enabled: 1,
data: [
{
is_show: '1',
image: '',
name: '',
link: {}
}
]
},
styles: {}
})
@@ -0,0 +1,57 @@
<template>
<div>
<el-form label-width="70px">
<el-card shadow="never" class="!border-none flex mt-2">
<el-form-item label="标题">
<el-input class="w-[396px]" v-model="contentData.title" />
</el-form-item>
</el-card>
<el-card shadow="never" class="!border-none flex mt-2">
<div class="flex items-end mb-4">
<div class="text-base text-[#101010] font-medium">展示样式</div>
</div>
<el-radio-group v-model="contentData.style">
<el-radio :value="1">横排</el-radio>
<el-radio :value="2">竖排</el-radio>
</el-radio-group>
</el-card>
<el-card shadow="never" class="!border-none flex mt-2">
<div class="flex items-end mb-4">
<div class="text-base text-[#101010] font-medium">菜单</div>
<div class="text-xs text-tx-secondary ml-2">建议图片尺寸100px*100px</div>
</div>
<div class="flex-1">
<AddNav v-model="contentData.data" />
</div>
</el-card>
</el-form>
</div>
</template>
<script lang="ts" setup>
import type { PropType } from 'vue'
import AddNav from '../../add-nav.vue'
import type options from './options'
type OptionsType = ReturnType<typeof options>
const emits = defineEmits<(event: 'update:content', data: OptionsType['content']) => void>()
const props = defineProps({
content: {
type: Object as PropType<OptionsType['content']>,
default: () => ({})
},
styles: {
type: Object as PropType<OptionsType['styles']>,
default: () => ({})
}
})
const contentData = computed({
get: () => props.content,
set: (newValue) => {
emits('update:content', newValue)
}
})
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,64 @@
<template>
<div class="my-service">
<div v-if="content.title" class="title px-[15px] py-[10px]">
<div>{{ content.title }}</div>
</div>
<div v-if="content.style == 1" class="flex flex-wrap pt-[20px] pb-[10px]">
<div
v-for="(item, index) in showList"
:key="index"
class="flex flex-col items-center w-1/4 mb-[15px]"
>
<decoration-img width="26px" height="26px" :src="item.image" alt="" />
<div class="mt-[7px]">{{ item.name }}</div>
</div>
</div>
<div v-if="content.style == 2">
<div
v-for="(item, index) in showList"
:key="index"
class="flex items-center border-b border-[#e5e5e5] h-[50px] px-[12px]"
>
<decoration-img width="24px" height="24px" :src="item.image" alt="" />
<div class="ml-[10px] flex-1">{{ item.name }}</div>
<div>
<icon name="el-icon-ArrowRight" />
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import type { PropType } from 'vue'
import DecorationImg from '../../decoration-img.vue'
import type options from './options'
type OptionsType = ReturnType<typeof options>
const props = defineProps({
content: {
type: Object as PropType<OptionsType['content']>,
default: () => ({})
},
styles: {
type: Object as PropType<OptionsType['styles']>,
default: () => ({})
}
})
const showList = computed(() => {
return props.content.data?.filter((item: any) => item.is_show == '1') || []
})
</script>
<style lang="scss" scoped>
.my-service {
margin: 10px 10px 0;
background-color: #fff;
border-radius: 7px;
.title {
border-bottom: 1px solid #e5e5e5;
font-size: 16px;
font-weight: 500;
}
}
</style>
@@ -0,0 +1,9 @@
import attr from './attr.vue'
import content from './content.vue'
import options from './options'
export default {
attr,
content,
options
}
@@ -0,0 +1,16 @@
export default () => ({
title: '我的服务',
name: 'my-service',
content: {
style: 1,
title: '我的服务',
data: [
{
image: '',
name: '导航名称',
link: {}
}
]
},
styles: {}
})
@@ -0,0 +1,72 @@
<template>
<div>
<el-form label-width="70px">
<el-card shadow="never" class="!border-none flex mt-2">
<div class="flex items-end mb-4">
<div class="text-base text-[#101010] font-medium">展示样式</div>
</div>
<el-radio-group v-model="contentData.style">
<el-radio :value="1">固定显示</el-radio>
<el-radio :value="2">分页滑动</el-radio>
</el-radio-group>
<el-form-item label="每行数量" class="mt-4">
<el-select v-model="contentData.per_line" style="width: 300px">
<el-option
v-for="item in 5"
:key="item"
:label="item + '个'"
:value="item"
/>
</el-select>
</el-form-item>
<el-form-item label="显示行数">
<el-select v-model="contentData.show_line" style="width: 300px">
<el-option
v-for="item in 2"
:key="item"
:label="item + '行'"
:value="item"
/>
</el-select>
</el-form-item>
</el-card>
<el-card shadow="never" class="!border-none flex mt-2">
<div class="flex items-end">
<div class="text-base text-[#101010] font-medium">菜单设置</div>
<div class="text-xs text-tx-secondary ml-2">建议图片尺寸100px*100px</div>
</div>
<div class="flex-1 mt-4">
<AddNav v-model="contentData.data" />
</div>
</el-card>
</el-form>
</div>
</template>
<script lang="ts" setup>
import type { PropType } from 'vue'
import AddNav from '../../add-nav.vue'
import type options from './options'
type OptionsType = ReturnType<typeof options>
const emits = defineEmits<(event: 'update:content', data: OptionsType['content']) => void>()
const props = defineProps({
content: {
type: Object as PropType<OptionsType['content']>,
default: () => ({})
},
styles: {
type: Object as PropType<OptionsType['styles']>,
default: () => ({})
}
})
const contentData = computed({
get: () => props.content,
set: (newValue) => {
emits('update:content', newValue)
}
})
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,38 @@
<template>
<div class="nav bg-white pt-[15px] pb-[8px]">
<div
class="grid grid-rows-auto gap-y-3 w-full"
:style="{ 'grid-template-columns': `repeat(${content.per_line}, 1fr)` }"
>
<div v-for="(item, index) in showList" :key="index" class="flex flex-col items-center">
<decoration-img width="41px" height="41px" :src="item.image" alt="" />
<div class="mt-[7px]">{{ item.name }}</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import type { PropType } from 'vue'
import DecorationImg from '../../decoration-img.vue'
import type options from './options'
type OptionsType = ReturnType<typeof options>
const props = defineProps({
content: {
type: Object as PropType<OptionsType['content']>,
default: () => ({})
},
styles: {
type: Object as PropType<OptionsType['styles']>,
default: () => ({})
}
})
const showList = computed(() => {
const data = props.content.data?.filter((item: any) => item.is_show == '1') || []
return data.slice(0, props.content.show_line * props.content.per_line)
})
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,9 @@
import attr from './attr.vue'
import content from './content.vue'
import options from './options'
export default {
attr,
content,
options
}
@@ -0,0 +1,18 @@
export default () => ({
title: '导航菜单',
name: 'nav',
content: {
enabled: 1,
style: 1, // 展示样式1=固定显示,2=分页显示
per_line: 5, // 每行显示数量
show_line: 2, // 显示行数
data: [
{
image: '',
name: '导航名称',
link: {}
}
]
},
styles: {}
})
@@ -0,0 +1,22 @@
<template>
<div></div>
</template>
<script lang="ts" setup>
import type { PropType } from 'vue'
import type options from './options'
type OptionsType = ReturnType<typeof options>
defineProps({
content: {
type: Object as PropType<OptionsType['content']>,
default: () => ({})
},
styles: {
type: Object as PropType<OptionsType['styles']>,
default: () => ({})
}
})
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,73 @@
<template>
<div class="news">
<div class="flex items-center news-title mx-[10px] my-[15px] text-[17px] font-medium">
最新资讯
</div>
<div
v-for="item in newsList"
:key="item.id"
class="news-card flex bg-white px-[10px] py-[16px] text-[#333] border-[#f2f2f2] border-b"
>
<div class="mr-[10px]" v-if="item.image">
<img :src="item.image" class="w-[120px] h-[90px] object-contain" />
</div>
<div class="flex flex-col justify-between flex-1">
<div class="text-[15px] font-medium line-clamp-2">{{ item.title }}</div>
<div class="line-clamp-1 text-sm mt-[8px]">
{{ item.desc }}
</div>
<div class="text-[#999] text-xs w-full flex justify-between mt-[8px]">
<div>{{ item.create_time }}</div>
<div class="flex items-center">
<icon name="el-icon-View" />
<div class="ml-[5px]">{{ item.click }}</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import type { PropType } from 'vue'
import { getDecorateArticle } from '@/api/decoration'
import type options from './options'
type OptionsType = ReturnType<typeof options>
defineProps({
content: {
type: Object as PropType<OptionsType['content']>,
default: () => ({})
},
styles: {
type: Object as PropType<OptionsType['styles']>,
default: () => ({})
}
})
const newsList = ref<any[]>([])
const getData = async () => {
const data = await getDecorateArticle({
limit: 10
})
newsList.value = data
}
getData()
</script>
<style lang="scss" scoped>
.news {
.news-title {
&::before {
content: '';
width: 4px;
height: 17px;
display: block;
margin-right: 5px;
background: #4173ff;
}
}
}
</style>
@@ -0,0 +1,9 @@
import attr from './attr.vue'
import content from './content.vue'
import options from './options'
export default {
attr,
content,
options
}
@@ -0,0 +1,7 @@
export default () => ({
title: '资讯',
name: 'news',
disabled: 1,
content: {},
styles: {}
})
@@ -0,0 +1,71 @@
<template>
<el-form ref="form" label-width="80px" size="large">
<el-card shadow="never" class="!border-none flex mt-2">
<el-form-item label="页面标题">
<el-radio-group v-model="contentData.title_type">
<el-radio value="1">文字</el-radio>
<el-radio value="2">图片</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="content.title_type == 1">
<el-input
v-model="contentData.title"
maxlength="8"
show-word-limit
class="w-[300px]"
placeholder="请输入页面标题"
></el-input>
</el-form-item>
<el-form-item v-if="content.title_type == 2">
<material-picker v-model="contentData.title_img" :limit="1" size="100px" />
<div class="form-tips">建议图片尺寸300px*40px</div>
</el-form-item>
<el-form-item label="文字颜色" v-if="content.title_type == 1">
<el-radio-group v-model="contentData.text_color">
<el-radio value="1">白色</el-radio>
<el-radio value="2">黑色</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="页面背景">
<el-radio-group v-model="contentData.bg_type">
<el-radio value="1">背景颜色</el-radio>
<el-radio value="2">背景图片</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="content.bg_type == 1">
<color-picker v-model="contentData.bg_color" reset-color="#F5F5F5" />
</el-form-item>
<el-form-item v-if="content.bg_type == 2">
<material-picker v-model="contentData.bg_image" :limit="1" size="100px" />
<div class="form-tips">建议图片尺寸750px*高度不限</div>
</el-form-item>
</el-card>
</el-form>
</template>
<script lang="ts" setup>
import type { PropType } from 'vue'
import type options from './options'
type OptionsType = ReturnType<typeof options>
const emits = defineEmits<(event: 'update:content', data: OptionsType['content']) => void>()
const props = defineProps({
content: {
type: Object as PropType<OptionsType['content']>,
default: () => ({})
},
styles: {
type: Object as PropType<OptionsType['styles']>,
default: () => ({})
}
})
const contentData = computed({
get: () => props.content,
set: (newValue) => {
emits('update:content', newValue)
}
})
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,6 @@
<template>
<div class="page-mate"></div>
</template>
<script lang="ts" setup></script>
<style lang="scss" scoped></style>
@@ -0,0 +1,9 @@
import attr from './attr.vue'
import content from './content.vue'
import options from './options'
export default {
attr,
content,
options
}
@@ -0,0 +1,14 @@
export default () => ({
title: '页面设置',
name: 'page-meta',
content: {
title_type: 1,
title: '',
title_img: '',
bg_type: 1,
bg_color: '',
bg_image: '',
text_color: ''
},
styles: {}
})
@@ -0,0 +1,100 @@
<template>
<popup ref="popupRef" title="轮播图设置" :async="true" width="980px" @confirm="handleSubmit">
<el-alert title="最多可添加10张,建议图片尺寸750px*440px" type="warning" />
<el-scrollbar height="400px" class="mt-4">
<div class="flex flex-wrap p-4">
<div v-for="(item, index) in content.data" :key="index" class="w-[400px] mr-4 mb-4">
<del-wrap :key="index" @close="handleDelete(index)" class="w-full">
<div class="bg-fill-light w-full p-4">
<div class="flex items-center">
<material-picker
width="122px"
height="122px"
v-model="item.image"
upload-class="bg-body"
exclude-domain
>
<template #upload>
<div
class="w-[122px] h-[122px] flex justify-center items-center"
>
轮播图
</div>
</template>
</material-picker>
<link-picker v-model="item.link" />
</div>
</div>
</del-wrap>
</div>
</div>
<div class="mt-4 ml-4">
<el-button link type="primary" @click="handleAdd">+ 添加轮播图</el-button>
</div>
</el-scrollbar>
</popup>
</template>
<script lang="ts" setup>
import { cloneDeep } from 'lodash-es'
import type { PropType } from 'vue'
import Popup from '@/components/popup/index.vue'
import feedback from '@/utils/feedback'
import type options from './options'
type OptionsType = ReturnType<typeof options>
const emits = defineEmits<(event: 'update:content', data: OptionsType['content']) => void>()
const popupRef = shallowRef<InstanceType<typeof Popup>>()
const props = defineProps({
content: {
type: Object as PropType<OptionsType['content']>,
default: () => ({})
},
styles: {
type: Object as PropType<OptionsType['styles']>,
default: () => ({})
},
height: {
type: String,
default: '170px'
}
})
const open = () => {
popupRef.value?.open()
}
const handleSubmit = () => {
popupRef.value?.close()
}
const handleAdd = () => {
if (props.content.data?.length < 10) {
const content = cloneDeep(props.content)
content.data.push({
image: '',
name: '',
link: {}
})
emits('update:content', content)
} else {
feedback.msgError(`最多添加${10}张图片`)
}
}
const handleDelete = (index: number) => {
if (props.content.data?.length <= 1) {
return feedback.msgError('最少保留一个轮播图')
}
const content = cloneDeep(props.content)
content.data.splice(index, 1)
emits('update:content', content)
}
defineExpose({ open })
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,7 @@
import content from './content.vue'
import options from './options'
export default {
content,
options
}
@@ -0,0 +1,15 @@
export default () => ({
title: '首页轮播图',
name: 'pc-banner',
content: {
enabled: 1,
data: [
{
image: '',
name: '',
link: {}
}
]
},
styles: {}
})
@@ -0,0 +1,22 @@
<template>
<div></div>
</template>
<script lang="ts" setup>
import type { PropType } from 'vue'
import type options from './options'
type OptionsType = ReturnType<typeof options>
defineProps({
content: {
type: Object as PropType<OptionsType['content']>,
default: () => ({})
},
styles: {
type: Object as PropType<OptionsType['styles']>,
default: () => ({})
}
})
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,23 @@
<template>
<div class="search">
<div class="search-con flex items-center px-[15px]">
<icon name="el-icon-Search" :size="17" />
<span class="ml-[5px]">请输入关键词搜索</span>
</div>
</div>
</template>
<script lang="ts" setup></script>
<style lang="scss" scoped>
.search {
background-color: #fff;
padding: 7px 12px;
.search-con {
height: 100%;
height: 36px;
border-radius: 36px;
background: #f4f4f4;
color: #999999;
}
}
</style>
@@ -0,0 +1,9 @@
import attr from './attr.vue'
import content from './content.vue'
import options from './options'
export default {
attr,
content,
options
}
@@ -0,0 +1,7 @@
export default () => ({
title: '搜索',
name: 'search',
disabled: 1,
content: {},
styles: {}
})
@@ -0,0 +1,112 @@
<template>
<div>
<el-form label-width="70px">
<el-card shadow="never" class="!border-none flex mt-2">
<div class="flex items-end mb-4">
<div class="text-base text-[#101010] font-medium">菜单</div>
<div class="text-xs text-tx-secondary ml-2">
最多添加5张建议图片尺寸750px*200px
</div>
</div>
<draggable
class="draggable"
v-model="contentData.data"
animation="300"
handle=".drag-move"
item-key="index"
>
<template v-slot:item="{ element: item, index }">
<del-wrap :key="index" @close="handleDelete(index)" class="w-[467px]">
<div class="bg-fill-light flex items-center w-full p-4 mt-4">
<material-picker
v-model="item.image"
upload-class="bg-body"
exclude-domain
/>
<div class="ml-3 flex-1">
<el-form-item label="图片名称">
<el-input v-model="item.name" placeholder="请输入名称" />
</el-form-item>
<el-form-item class="mt-[18px]" label="图片链接">
<link-picker v-model="item.link" />
</el-form-item>
<el-form-item label="是否显示" class="mt-[18px]">
<div class="flex-1 flex items-center">
<el-switch
v-model="item.is_show"
active-value="1"
inactive-value="0"
/>
<div class="drag-move cursor-move ml-auto">
<icon name="el-icon-Rank" size="18" />
</div>
</div>
</el-form-item>
</div>
</div>
</del-wrap>
</template>
</draggable>
<div class="mt-4" v-if="content.data?.length < limit">
<el-button class="w-full" type="primary" @click="handleAdd">添加图片</el-button>
</div>
</el-card>
</el-form>
</div>
</template>
<script lang="ts" setup>
import { cloneDeep } from 'lodash-es'
import type { PropType } from 'vue'
import Draggable from 'vuedraggable'
import feedback from '@/utils/feedback'
import type options from './options'
type OptionsType = ReturnType<typeof options>
const emits = defineEmits<(event: 'update:content', data: OptionsType['content']) => void>()
const limit = 5
const props = defineProps({
content: {
type: Object as PropType<OptionsType['content']>,
default: () => ({})
},
styles: {
type: Object as PropType<OptionsType['styles']>,
default: () => ({})
}
})
const contentData = computed({
get: () => props.content,
set: (newValue) => {
emits('update:content', newValue)
}
})
const handleAdd = () => {
if (props.content.data?.length < limit) {
const content = cloneDeep(props.content)
content.data.push({
is_show: '1',
image: '',
name: '',
link: {}
})
emits('update:content', content)
} else {
feedback.msgError(`最多添加${limit}张图片`)
}
}
const handleDelete = (index: number) => {
if (props.content.data?.length <= 1) {
return feedback.msgError('最少保留一张图片')
}
const content = cloneDeep(props.content)
content.data.splice(index, 1)
emits('update:content', content)
}
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,36 @@
<template>
<div class="banner mx-[10px] mt-[10px]">
<div class="banner-image">
<decoration-img width="100%" height="100px" :src="getImage" fit="contain" />
</div>
</div>
</template>
<script lang="ts" setup>
import type { PropType } from 'vue'
import DecorationImg from '../../decoration-img.vue'
import type options from './options'
type OptionsType = ReturnType<typeof options>
const props = defineProps({
content: {
type: Object as PropType<OptionsType['content']>,
default: () => ({})
},
styles: {
type: Object as PropType<OptionsType['styles']>,
default: () => ({})
}
})
const showList = computed(() => {
return props.content.data?.filter((item: any) => item.is_show == '1') || []
})
const getImage = computed(() => {
if (Array.isArray(showList.value)) {
return showList.value[0] ? showList.value[0].image : ''
}
return ''
})
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,9 @@
import attr from './attr.vue'
import content from './content.vue'
import options from './options'
export default {
attr,
content,
options
}
@@ -0,0 +1,16 @@
export default () => ({
title: '个人中心广告图',
name: 'user-banner',
content: {
enabled: 1,
data: [
{
is_show: '1',
image: '',
name: '',
link: {}
}
]
},
styles: {}
})
@@ -0,0 +1,22 @@
<template>
<div></div>
</template>
<script lang="ts" setup>
import type { PropType } from 'vue'
import type options from './options'
type OptionsType = ReturnType<typeof options>
defineProps({
content: {
type: Object as PropType<OptionsType['content']>,
default: () => ({})
},
styles: {
type: Object as PropType<OptionsType['styles']>,
default: () => ({})
}
})
</script>
<style lang="scss" scoped></style>
@@ -0,0 +1,16 @@
<template>
<div class="user-info flex items-center px-[25px]">
<img src="./images/default_avatar.png" class="w-[60px] h-[60px]" alt="" />
<div class="text-white text-[18px] ml-[10px]">未登录</div>
</div>
</template>
<script lang="ts" setup></script>
<style lang="scss" scoped>
.user-info {
background: url(./images/my_topbg.png);
height: 115px;
background-position: bottom;
background-size: 100% auto;
}
</style>
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

@@ -0,0 +1,9 @@
import attr from './attr.vue'
import content from './content.vue'
import options from './options'
export default {
attr,
content,
options
}
@@ -0,0 +1,7 @@
export default () => ({
title: '用户信息',
name: 'user-info',
disabled: 1,
content: {},
styles: {}
})
Binary file not shown.

After

Width:  |  Height:  |  Size: 516 KiB

+149
View File
@@ -0,0 +1,149 @@
<template>
<div class="decoration-pages min-w-[1100px]">
<div class="flex flex-1 h-full justify-between">
<el-card
shadow="never"
class="!border-none flex scroll-view-content"
:body-style="{ 'padding-right': 0 }"
>
<Menu v-model="activeMenu" :menus="menus" />
</el-card>
<preview
class="flex-1 scroll-view-content"
v-model="selectWidgetIndex"
@updatePageData="updatePageData"
:pageData="getPageData"
:pageMeta="getPageMeta"
/>
<attr-setting
class="w-[560px] scroll-view-content"
:widget="getSelectWidget"
@update:content="updateContent"
/>
</div>
<footer-btns class="mt-4" :fixed="false" v-perms="['decorate:pages:save']">
<el-button type="primary" @click="setData">保存</el-button>
</footer-btns>
</div>
</template>
<script lang="ts" setup name="decorationPages">
import { getDecoratePages, setDecoratePages } from '@/api/decoration'
import { getNonDuplicateID } from '@/utils/util'
import AttrSetting from '../component/pages/attr-setting.vue'
import Menu from '../component/pages/menu.vue'
import Preview from '../component/pages/preview.vue'
import widgets from '../component/widgets'
enum pagesTypeEnum {
HOME = '1',
USER = '2',
SERVICE = '3'
}
const updatePageData = (value: any) => {
menus[activeMenu.value].pageData = [...value]
}
const generatePageData = (widgetNames: string[]) => {
return widgetNames.map((widgetName) => {
const options = {
id: getNonDuplicateID(),
...(widgets[widgetName]?.options() || {})
}
return options
})
}
const updateContent = (content: any) => {
if (menus[activeMenu.value]?.pageData) {
menus[activeMenu.value].pageData[selectWidgetIndex.value].content = content
}
}
const menus: Record<
string,
{
id: number
name: string
pageMeta?: any
pageData: any[]
}
> = reactive({
[pagesTypeEnum.HOME]: {
id: 1,
type: 1,
name: '首页装修',
pageMeta: generatePageData(['page-meta']),
pageData: generatePageData(['search', 'banner', 'nav', 'news'])
},
[pagesTypeEnum.USER]: {
id: 2,
type: 2,
name: '个人中心',
pageMeta: generatePageData(['page-meta']),
pageData: generatePageData(['user-info', 'my-service', 'user-banner'])
},
[pagesTypeEnum.SERVICE]: {
id: 3,
type: 3,
name: '客服设置',
pageMeta: null,
pageData: generatePageData(['customer-service'])
}
})
const activeMenu = ref<string>('1')
const selectWidgetIndex = ref<number>(-1)
const getPageData = computed(() => {
return menus[activeMenu.value]?.pageData ?? []
})
const getPageMeta = computed(() => {
return menus[activeMenu.value]?.pageMeta ?? null
})
const getSelectWidget = computed(() => {
if (selectWidgetIndex.value === -1) {
return menus[activeMenu.value]?.pageMeta[0] ?? ''
} else {
return menus[activeMenu.value]?.pageData[selectWidgetIndex.value] ?? ''
}
})
const getData = async () => {
const data = await getDecoratePages({ id: activeMenu.value })
menus[String(data.id)].pageData = JSON.parse(data.data)
menus[String(data.id)].pageMeta = data?.meta ? JSON.parse(data?.meta) : null
}
const setData = async () => {
const data = menus[activeMenu.value]
await setDecoratePages({
...data,
data: JSON.stringify(data.pageData),
meta: data?.pageMeta ? JSON.stringify(data?.pageMeta) : null
})
getData()
}
watch(
activeMenu,
() => {
selectWidgetIndex.value = getPageData.value.findIndex((item) => !item.disabled)
getData()
},
{
immediate: true
}
)
</script>
<style lang="scss" scoped>
$scroll-height: calc(100vh - var(--navbar-height) - 74px);
.decoration-pages {
height: $scroll-height;
@apply flex flex-col;
.scroll-view-content {
height: calc($scroll-height - 60px);
}
}
</style>
+42
View File
@@ -0,0 +1,42 @@
<template>
<div class="decoration-pc min-w-[1100px]">
<el-card shadow="never" class="!border-none flex-1 flex">
<div class="text-xl font-medium">首页装修</div>
<router-link
:to="{
path: '/decoration/pc_details',
query: {
url: state.pc_url
}
}"
>
<el-button class="m-5" type="primary" size="large">去装修</el-button>
</router-link>
<el-form>
<el-form-item label="最近更新">{{ state.update_time }}</el-form-item>
<el-form-item label="PC端链接">
<el-input style="width: 350px" v-model="state.pc_url" disabled></el-input>
<el-button type="primary" v-copy="state.pc_url">复制</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<script lang="ts" setup name="decorationPc">
import { getDecoratePc } from '@/api/decoration'
const state = ref({
update_time: '',
pc_url: ''
})
const getData = async () => {
try {
state.value = await getDecoratePc()
} catch (error) {
console.log(error)
}
}
getData()
</script>
+92
View File
@@ -0,0 +1,92 @@
<template>
<div class="decoration-pages min-w-[1100px]">
<el-card shadow="never" class="!border-none !rounded-none !bg-primary">
<div class="flex justify-between w-full">
<el-button
link
type="primary"
:icon="ArrowLeft"
style="color: #fff"
@click="handleBack"
>
返回
</el-button>
<el-button v-perms="['decorate:pages:save']" @click="setData"> 保存 </el-button>
</div>
</el-card>
<div class="flex-1 h-full">
<preview-pc v-model="selectWidgetIndex" :pageData="getPageData" />
</div>
</div>
</template>
<script lang="ts" setup name="decorationPc">
import { ArrowLeft } from '@element-plus/icons-vue'
import { getDecoratePages, setDecoratePages } from '@/api/decoration'
import feedback from '@/utils/feedback'
import PreviewPc from './component/pages/preview-pc.vue'
const router = useRouter()
enum pagesTypeEnum {
HOME = '4'
}
const menus: Record<
string,
{
id: number
name: string
pageData: any[]
}
> = reactive({
[pagesTypeEnum.HOME]: {
id: 4,
type: 4,
name: '首页装修',
pageData: []
}
})
const activeMenu = ref('4')
const selectWidgetIndex = ref(0)
const getPageData = computed(() => {
return menus[activeMenu.value]?.pageData ?? []
})
const handleBack = async () => {
await feedback.confirm('确定离开此页面?系统可能不会保存您所做的更改。')
router.back()
}
const getData = async () => {
const data = await getDecoratePages({ id: activeMenu.value })
menus[String(data.id)].pageData = JSON.parse(data.data)
selectWidgetIndex.value = getPageData.value.findIndex((item) => !item.disabled)
}
const setData = async () => {
await setDecoratePages({
...menus[activeMenu.value],
data: JSON.stringify(menus[activeMenu.value].pageData)
})
getData()
}
watch(
activeMenu,
() => {
selectWidgetIndex.value = getPageData.value.findIndex((item) => !item.disabled)
getData()
},
{
immediate: true
}
)
</script>
<style lang="scss" scoped>
.decoration-pages {
min-height: calc(100vh);
@apply flex flex-col;
}
</style>
@@ -0,0 +1,137 @@
<template>
<el-form label-width="140px">
<div>
<div class="text-xl font-medium mb-[20px]">系统主题色</div>
<el-form-item label-width="50">
<ThemePicker
v-model="formData.themeColorId"
:theme-colors="themeColors"
@change="selectThemeColor"
/>
</el-form-item>
</div>
<div>
<div class="text-xl font-medium mt-[40px] mb-[20px]">样式设置</div>
<el-form-item label="导航顶部文字颜色">
<div>
<el-radio-group v-model="formData.topTextColor">
<el-radio value="white" size="large">白色</el-radio>
<el-radio value="black" size="large">黑色</el-radio>
</el-radio-group>
<div>
<span class="form-tips">页面导航栏文字的颜色</span>
</div>
</div>
</el-form-item>
<el-form-item label="导航顶部背景颜色">
<div>
<color-picker
:resetColor="formData.themeColor1"
v-model="formData.navigationBarColor"
></color-picker>
<div>
<span class="form-tips"> 页面顶部导航栏背景颜色不设置则跟随主题色 </span>
</div>
</div>
</el-form-item>
<el-form-item label="自定义主题颜色" v-if="formData.themeColorId == 7">
<div>
<color-picker
v-model="formData.themeColor1"
default-color="#F8F8F8"
></color-picker>
<color-picker
v-model="formData.themeColor2"
default-color="#F8F8F8"
class="mt-2"
></color-picker>
<div>
<span class="form-tips">自定义主题渐变色用于按钮类和主要文字</span>
</div>
</div>
</el-form-item>
<el-form-item label="按钮文字颜色">
<div>
<el-radio-group v-model="formData.buttonColor">
<el-radio value="white" size="large">白色</el-radio>
<el-radio value="black" size="large">黑色</el-radio>
</el-radio-group>
</div>
</el-form-item>
</div>
<div>
<div class="flex" v-if="formData.themeColorId == 1">
<img class="w-[200px]" src="@/assets/images/blue1.png" />
<img class="w-[200px] ml-[30px]" src="@/assets/images/blue2.png" />
<img class="w-[200px] ml-[30px]" src="@/assets/images/blue3.png" />
</div>
<div class="flex" v-if="formData.themeColorId == 2">
<img class="w-[200px]" src="@/assets/images/green1.png" />
<img class="w-[200px] ml-[30px]" src="@/assets/images/green2.png" />
<img class="w-[200px] ml-[30px]" src="@/assets/images/green3.png" />
</div>
<div class="flex" v-if="formData.themeColorId == 3">
<img class="w-[200px]" src="@/assets/images/purple1.png" />
<img class="w-[200px] ml-[30px]" src="@/assets/images/purple2.png" />
<img class="w-[200px] ml-[30px]" src="@/assets/images/purple3.png" />
</div>
<div class="flex" v-if="formData.themeColorId == 4">
<img class="w-[200px]" src="@/assets/images/yellow1.png" />
<img class="w-[200px] ml-[30px]" src="@/assets/images/yellow2.png" />
<img class="w-[200px] ml-[30px]" src="@/assets/images/yellow3.png" />
</div>
<div class="flex" v-if="formData.themeColorId == 5">
<img class="w-[200px]" src="@/assets/images/red1.png" />
<img class="w-[200px] ml-[30px]" src="@/assets/images/red2.png" />
<img class="w-[200px] ml-[30px]" src="@/assets/images/red3.png" />
</div>
<div class="flex" v-if="formData.themeColorId == 6">
<img class="w-[200px]" src="@/assets/images/pink1.png" />
<img class="w-[200px] ml-[30px]" src="@/assets/images/pink2.png" />
<img class="w-[200px] ml-[30px]" src="@/assets/images/pink3.png" />
</div>
</div>
</el-form>
</template>
<script setup lang="ts">
import { useVModel } from '@vueuse/core'
import ThemePicker from './theme-picker.vue'
const props = defineProps<{
modelValue: Record<string, any>
}>()
const themeColors = ref([
{
id: 1,
name: '科技蓝',
color1: '#2F80ED',
color2: '#56CCF2',
btnColor: 'white'
},
{ id: 2, name: '偏绿蓝', color1: '#2EC840', color2: '#3DE650', btnColor: 'white' },
{ id: 3, name: '商务紫', color1: '#A74BFD', color2: '#CB60FF', btnColor: 'white' },
{ id: 4, name: '活力橙', color1: '#F7971E', color2: '#FFD200', btnColor: 'black' },
{ id: 5, name: '经典红', color1: '#FF2C3C', color2: '#EF1D2D', btnColor: 'white' },
{ id: 6, name: '美妆色', color1: '#FD498F', color2: '#FA444D', btnColor: 'white' },
{ id: 7, name: '自定义', color1: '#F8F8F8', color2: '#F5F5F5', btnColor: 'white' }
])
const emit = defineEmits<{
(event: 'update:modelValue', value: Record<string, any>): void
}>()
//选择主题颜色
const selectThemeColor = (item: any) => {
if (item.id != 7) {
formData.value.themeColor1 = item.color1
formData.value.themeColor2 = item.color2
formData.value.navigationBarColor = item.color1
formData.value.buttonColor = item.btnColor
formData.value.topTextColor = item.btnColor
}
}
//表单数据
const formData = useVModel(props, 'modelValue', emit)
</script>
@@ -0,0 +1,53 @@
<template>
<div class="flex flex-wrap">
<div
class="py-[18px] px-[20px] flex items-center text-[14px] shadow rounded-lg mr-[20px] mb-[20px] cursor-pointer"
:class="{ 'is-select': selectId == item.id, unselect: selectId != item.id }"
v-for="(item, index) in themeColors"
:key="index"
@click="handleSelect(item)"
>
<div
class="rounded-full h-[34px] w-[34px]"
:style="`background: linear-gradient(to right, ${item.color1}, ${item.color2})`"
></div>
<div class="ml-[14px]">{{ item.name }}</div>
</div>
</div>
</template>
<script setup lang="ts">
import { useVModel } from '@vueuse/core'
interface IThemeColors {
id: number
name: string
color1: string
color2: string
btnColor: string
}
const props = defineProps<{
themeColors: IThemeColors[]
modelValue: number | string
}>()
const emit = defineEmits<{
(event: 'update:modelValue', id: number | string): void
(event: 'change', item: IThemeColors): void
}>()
//主题色
const selectId = useVModel(props, 'modelValue', emit)
const handleSelect = (item: IThemeColors) => {
selectId.value = item.id
emit('change', item)
}
</script>
<style lang="scss" scoped>
.is-select {
border: 1px solid #4153ff;
}
.unselect {
border: 1px solid transparent;
}
</style>
@@ -0,0 +1,64 @@
<template>
<el-card shadow="never" class="!border-none">
<div class="mt-[-10px]">
<el-tabs v-model="currentTabIndex">
<el-tab-pane
v-for="(item, index) in tabsList"
:key="item.id"
:label="item.name"
:name="index"
>
<component :is="item.component" v-model="item.data" />
</el-tab-pane>
</el-tabs>
</div>
</el-card>
<footer-btns class="mt-4" :fixed="true">
<el-button type="primary" @click="setData">保存</el-button>
</footer-btns>
</template>
<script setup lang="ts">
import { markRaw } from 'vue'
import { getDecoratePages, setDecoratePages } from '@/api/decoration'
import MobileStyle from './components/mobile-style.vue'
const currentTabIndex = ref(0)
const tabsList = ref([
{
name: '移动端',
id: 5,
component: markRaw(MobileStyle),
data: {
themeColorId: 1,
topTextColor: 'white',
navigationBarColor: '',
themeColor1: '',
themeColor2: '',
buttonColor: 'white'
}
}
])
const currentTab = computed(() => tabsList.value[currentTabIndex.value] || {})
//获取数据
const getData = async () => {
const res = await getDecoratePages({ id: currentTab.value.id })
currentTab.value.data = JSON.parse(res.data)
}
//保存数据
const setData = async () => {
await setDecoratePages({
id: currentTab.value.id,
type: currentTab.value.id,
data: JSON.stringify(currentTab.value.data)
})
getData()
}
//初始化数据
onMounted(async () => {
await getData()
})
</script>
+148
View File
@@ -0,0 +1,148 @@
<template>
<div class="decoration-tabbar">
<div class="flex-1 min-h-0 w-full">
<div class="h-full">
<ElScrollbar>
<div class="flex min-w-[900px] scroll-view-content">
<el-card
shadow="never"
class="!border-none flex"
:body-style="{ 'padding-right': 0 }"
>
<Menu class="!h-full flex-none" v-model="activeMenu" :menus="menus" />
</el-card>
<div class="flex-1 pages-preview-box">
<div class="pages-preview mx-[30px]">
<el-scrollbar height="100%">
<component
:is="currentTab.component?.content"
v-bind="currentTab.data"
/>
</el-scrollbar>
</div>
</div>
<el-scrollbar class="attr-setting flex-none">
<div class="px-5 pb-5">
<component
:is="currentTab.component?.attr"
v-model="currentTab.data"
/>
</div>
</el-scrollbar>
</div>
</ElScrollbar>
</div>
</div>
<footer-btns :fixed="true" v-perms="['decorate:tabbar:save']">
<el-button type="primary" @click="setData">保存</el-button>
</footer-btns>
</div>
</template>
<script lang="ts" setup name="decorationTabbar">
import { markRaw } from 'vue'
import { getDecorateTabbar, setDecorateTabbar } from '@/api/decoration'
import Menu from './component/pages/menu.vue'
import mobileTab from './component/tabbar/mobile'
// import pcTab from './component/tabbar/pc'
enum ClientTypeEnum {
MOBILE = '1',
PC = '2'
}
const activeMenu = ref(ClientTypeEnum.MOBILE)
const menus: Record<
string,
{
id: number
name: string
type: number
data: any
component: any
}
> = reactive({
[ClientTypeEnum.MOBILE]: {
id: 1,
type: 1,
name: '移动端导航栏',
data: {
style: {
default_color: '',
selected_color: ''
},
list: []
},
component: markRaw(mobileTab)
}
})
const currentTab = computed(() => {
return menus[activeMenu.value]
})
const getData = async () => {
const data = await getDecorateTabbar({
type: activeMenu.value
})
menus['1'].data = data
}
const setData = async () => {
const { data } = currentTab.value
await setDecorateTabbar({
...data
})
getData()
}
watch(
activeMenu,
() => {
getData()
},
{
immediate: true
}
)
</script>
<style lang="scss" scoped>
$scroll-height: calc(100vh - var(--navbar-height) - 74px);
.decoration-tabbar {
height: $scroll-height;
width: 100%;
@apply flex flex-col;
.scroll-view-content {
height: calc($scroll-height - 60px);
}
.pages-preview-box {
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
height: 100%;
.pages-preview {
background-color: #ffffff;
width: 360px;
height: 615px;
color: #333;
position: relative;
}
}
.nav-item {
&:hover {
.drag-move {
display: block;
}
}
}
.attr-setting {
width: 450px;
height: 100%;
}
}
</style>
+623
View File
@@ -0,0 +1,623 @@
<template>
<div class="code-edit">
<el-card class="!border-none" shadow="never">
<el-page-header content="编辑数据表" @back="$router.back()" />
</el-card>
<el-card class="mt-4 !border-none" shadow="never">
<el-form
ref="formRef"
class="ls-form"
:model="formData"
label-width="100px"
:rules="rules"
>
<el-tabs v-model="activeName">
<el-tab-pane label="基础信息" name="base">
<el-form-item label="表名称" prop="table_name">
<div class="w-80">
<el-input
v-model="formData.table_name"
placeholder="请输入表名称"
clearable
/>
</div>
</el-form-item>
<el-form-item label="表描述" prop="table_comment">
<div class="w-80">
<el-input
v-model="formData.table_comment"
placeholder="请输入表描述"
clearable
/>
</div>
</el-form-item>
<el-form-item label="作者">
<div class="w-80">
<el-input v-model="formData.author" clearable />
</div>
</el-form-item>
<el-form-item label="备注">
<div class="w-80">
<el-input
v-model="formData.remark"
class="w-full"
type="textarea"
:autosize="{ minRows: 4, maxRows: 4 }"
maxlength="200"
show-word-limit
clearable
/>
</div>
</el-form-item>
</el-tab-pane>
<el-tab-pane label="字段管理" name="column">
<el-table :data="formData.table_column">
<el-table-column label="字段列名" prop="column_name" />
<el-table-column label="字段描述" prop="column_comment" min-width="120">
<template v-slot="{ row }">
<el-input v-model="row.column_comment" clearable />
</template>
</el-table-column>
<el-table-column label="物理类型" prop="column_type" />
<el-table-column label="必填" width="80">
<template #header>
<div class="header-checkbox">
<el-checkbox
v-model="isAllRequired"
@change="toggleAllRequired"
/>
<span>必填</span>
</div>
</template>
<template v-slot="{ row }">
<el-checkbox
v-model="row.is_required"
:true-value="1"
:false-value="0"
/>
</template>
</el-table-column>
<el-table-column label="插入" width="80">
<template #header>
<div class="header-checkbox">
<el-checkbox
v-model="isAllInsert"
@change="toggleAllInsert"
/>
<span>插入</span>
</div>
</template>
<template v-slot="{ row }">
<el-checkbox
v-model="row.is_insert"
:true-value="1"
:false-value="0"
/>
</template>
</el-table-column>
<el-table-column label="编辑" width="80">
<template #header>
<div class="header-checkbox">
<el-checkbox
v-model="isAllUpdate"
@change="toggleAllUpdate"
/>
<span>编辑</span>
</div>
</template>
<template v-slot="{ row }">
<el-checkbox
v-model="row.is_update"
:true-value="1"
:false-value="0"
/>
</template>
</el-table-column>
<el-table-column label="列表" width="80">
<template #header>
<div class="header-checkbox">
<el-checkbox
v-model="isAllLists"
@change="toggleAllLists"
/>
<span>列表</span>
</div>
</template>
<template v-slot="{ row }">
<el-checkbox
v-model="row.is_lists"
:true-value="1"
:false-value="0"
/>
</template>
</el-table-column>
<el-table-column label="查询" width="80">
<template #header>
<div class="header-checkbox">
<el-checkbox
v-model="isAllQuery"
@change="toggleAllQuery"
/>
<span>查询</span>
</div>
</template>
<template v-slot="{ row }">
<el-checkbox
v-model="row.is_query"
:true-value="1"
:false-value="0"
/>
</template>
</el-table-column>
<el-table-column label="查询方式">
<template v-slot="{ row }">
<el-select v-model="row.query_type">
<el-option label="=" value="=" />
<el-option label="!=" value="!=" />
<el-option label=">" value=">" />
<el-option label=">=" value=">=" />
<el-option label="<" value="<" />
<el-option label="<=" value="<=" />
<el-option label="LIKE" value="like" />
<el-option label="BETWEEN" value="between" />
</el-select>
</template>
</el-table-column>
<el-table-column label="显示类型" min-width="120">
<template v-slot="{ row }">
<el-select v-model="row.view_type">
<el-option label="文本框" value="input" />
<el-option label="文本域" value="textarea" />
<el-option label="下拉框" value="select" />
<el-option label="单选框" value="radio" />
<el-option label="复选框" value="checkbox" />
<el-option label="日期控件" value="datetime" />
<el-option label="图片选择控件" value="imageSelect" />
<el-option label="富文本控件" value="editor" />
</el-select>
</template>
</el-table-column>
<el-table-column label="字典类型" min-width="120">
<template v-slot="{ row }">
<el-select
v-model="row.dict_type"
clearable
:disabled="
!(
row.view_type == 'select' ||
row.view_type == 'radio' ||
row.view_type == 'checkbox'
)
"
placeholder="字典类型"
>
<el-option
v-for="(item, index) in optionsData.dict_type"
:key="index"
:label="item.name"
:value="item.type"
:disabled="!item.status"
/>
</el-select>
</template>
</el-table-column>
</el-table>
</el-tab-pane>
<el-tab-pane label="生成配置" name="config">
<el-form-item label="模板类型" prop="template_type">
<el-radio-group v-model="formData.template_type">
<el-radio :value="0">单表curd</el-radio>
<el-radio :value="1">树表curd</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="删除类型" prop="delete.type">
<el-radio-group v-model="formData.delete.type">
<el-radio :value="0">物理删除</el-radio>
<el-radio :value="1">软删除</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
label="删除字段"
prop="delete.name"
v-if="formData.delete.type == 1"
>
<el-select class="w-80" v-model="formData.delete.name" clearable>
<el-option
v-for="item in formData.table_column"
:key="item.id"
:value="item.column_name"
:label="`${item.column_name}${item.column_comment}`"
/>
</el-select>
</el-form-item>
<template v-if="formData.template_type == 1">
<el-form-item label="树表ID" prop="treePrimary">
<div>
<el-select
class="w-80"
v-model="formData.tree.tree_id"
clearable
>
<el-option
v-for="item in formData.table_column"
:key="item.id"
:value="item.column_name"
:label="`${item.column_name}${item.column_comment}`"
/>
</el-select>
<div class="form-tips">指定树表的主要ID一般为主键</div>
</div>
</el-form-item>
<el-form-item label="树表父ID" prop="treeParent">
<div>
<el-select
class="w-80"
v-model="formData.tree.tree_pid"
clearable
>
<el-option
v-for="item in formData.table_column"
:key="item.id"
:value="item.column_name"
:label="`${item.column_name}${item.column_comment}`"
/>
</el-select>
<div class="form-tips">指定树表的父ID比如parent_id</div>
</div>
</el-form-item>
<el-form-item label="树名称" prop="treeName">
<el-select class="w-80" v-model="formData.tree.tree_name" clearable>
<el-option
v-for="item in formData.table_column"
:key="item.id"
:value="item.column_name"
:label="`${item.column_name}${item.column_comment}`"
/>
</el-select>
</el-form-item>
</template>
<el-form-item label="类描述">
<div class="w-80">
<div>
<el-input
v-model="formData.class_comment"
placeholder="请输入文件描述"
clearable
/>
</div>
<div class="form-tips">
<div>
填写test,生成文件描述为test控制器(test逻辑/test模型)
</div>
</div>
</div>
</el-form-item>
<el-form-item label="生成方式" prop="generate_type">
<el-radio-group v-model="formData.generate_type">
<el-radio :value="0">压缩包下载</el-radio>
<el-radio :value="1">生成到模块</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="模块名" prop="module_name">
<div class="w-80">
<el-input
v-model="formData.module_name"
placeholder="请输入模块名"
clearable
/>
<div class="form-tips">生成文件所在模块</div>
</div>
</el-form-item>
<el-form-item label="类目录">
<div class="w-80">
<div>
<el-input
v-model="formData.class_dir"
placeholder="请输入文件所在目录"
clearable
/>
</div>
<div class="form-tips">
<div>
填写test,则在app/模块名/controller/test下生成控制器
</div>
</div>
</div>
</el-form-item>
<el-form-item label="父级菜单" prop="menu.pid">
<el-tree-select
class="w-80"
v-model="formData.menu.pid"
:data="optionsData.menu"
clearable
node-key="id"
:props="{
label: 'name'
}"
default-expand-all
placeholder="请选择父级菜单"
check-strictly
/>
</el-form-item>
<el-form-item label="菜单名称" prop="menu.name">
<div class="w-80">
<el-input
v-model="formData.menu.name"
placeholder="请输入菜单名称"
clearable
/>
</div>
</el-form-item>
<el-form-item label="菜单构建" prop="menu.type" required>
<div>
<el-radio-group v-model="formData.menu.type">
<el-radio :value="1">自动构建</el-radio>
<el-radio :value="0">手动添加</el-radio>
</el-radio-group>
<div class="form-tips">
自动构建自动执行生成菜单sql手动添加自行添加菜单
</div>
</div>
</el-form-item>
</el-tab-pane>
<el-tab-pane label="关联配置" name="relations">
<el-button type="primary" @click="showEditPopup('add')">
<template #icon>
<icon name="el-icon-Plus" />
</template>
新增关联
</el-button>
<div class="mt-4">
<el-table :data="formData.relations" size="small">
<el-table-column prop="type" label="关联类型">
<template #default="{ row }">
<dict-value :value="row.type" :options="relationTypes" />
</template>
</el-table-column>
<el-table-column prop="name" label="关联名称" />
<el-table-column prop="model" label="关联模型" />
<el-table-column prop="local_key" label="关联键">
<template #default="{ row }">
<dict-value
:value="row.local_key"
:options="formData.table_column"
:config="{
label: 'column_comment',
value: 'column_name'
}"
/>
</template>
</el-table-column>
<el-table-column prop="foreign_key" label="外键" />
<el-table-column label="操作">
<template #default="{ row, $index }">
<el-button
link
type="primary"
@click="showEditPopup('edit', row, $index)"
>
编辑
</el-button>
<el-button link type="danger" @click="handelDelete($index)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
<relations-add
:column="formData.table_column"
:types="relationTypes"
v-if="showEdit"
ref="editRef"
@add="handleAdd"
@edit="handleEdit"
@close="showEdit = false"
/>
</div>
</el-tab-pane>
</el-tabs>
</el-form>
</el-card>
<footer-btns>
<el-button type="primary" @click="onSubmit">保存</el-button>
</footer-btns>
</div>
</template>
<style scoped>
.header-checkbox {
display: flex;
align-items: center;
gap: 6px; /* 这个属性可以设置子元素之间的间距 */
}
.header-checkbox .el-checkbox {
margin-right: 2px;
}
</style>
<script lang="ts" setup name="tableEdit">
import type { FormInstance } from 'element-plus'
import { cloneDeep } from 'lodash'
import { menuAll } from '@/api/perms/menu'
import { dictTypeAll } from '@/api/setting/dict'
import { generateEdit, tableDetail } from '@/api/tools/code'
import { useDictOptions } from '@/hooks/useDictOptions'
import feedback from '@/utils/feedback'
import RelationsAdd from '../components/relations-add.vue'
const route = useRoute()
const router = useRouter()
const activeName = ref('column')
const showEdit = ref(false)
const relationTypes = [
{
name: '一对一',
value: 'has_one'
},
{
name: '一对多',
value: 'has_many'
}
]
const formData = reactive({
id: '',
table_name: '',
table_comment: '',
author: '',
remark: '',
template_type: 0,
generate_type: 0,
module_name: '',
class_dir: '',
class_comment: '',
table_column: [] as any[],
menu: {
pid: 0,
name: '',
type: 0
},
tree: {
tree_id: 0,
tree_pid: 0,
tree_name: 0
},
delete: {
name: '',
type: 0
},
relations: [] as any[]
})
const isAllRequired = ref(false)
const isAllInsert = ref(false)
const isAllUpdate = ref(false)
const isAllLists = ref(false)
const isAllQuery = ref(false)
// 全选功能
const toggleAllRequired = () => {
formData.table_column.forEach(item => {
item.is_required = isAllRequired.value ? 1 : 0
})
}
const toggleAllInsert = () => {
formData.table_column.forEach(item => {
item.is_insert = isAllInsert.value ? 1 : 0
})
}
const toggleAllUpdate = () => {
formData.table_column.forEach(item => {
item.is_update = isAllUpdate.value ? 1 : 0
})
}
const toggleAllLists = () => {
formData.table_column.forEach(item => {
item.is_lists = isAllLists.value ? 1 : 0
})
}
const toggleAllQuery = () => {
formData.table_column.forEach(item => {
item.is_query = isAllQuery.value ? 1 : 0
})
}
let editIndex = 0
const formRef = shallowRef<FormInstance>()
const editRef = shallowRef<InstanceType<typeof RelationsAdd>>()
const rules = reactive({
table_name: [{ required: true, message: '请输入表名称' }],
table_comment: [{ required: true, message: '请输入表描述' }],
module_name: [{ required: true, message: '请输入模块名' }],
generate_type: [{ required: true, trigger: 'change' }],
template_type: [{ required: true, trigger: 'change' }],
['menu.pid']: [{ required: true, message: '请选择父级菜单' }],
['menu.name']: [{ required: true, message: '请输入菜单名称' }],
['delete.type']: [{ required: true, trigger: 'change' }],
['delete.name']: [{ required: true, message: '请选择删除字段' }]
})
const showEditPopup = async (type: string, data?: any, index?: number) => {
showEdit.value = true
await nextTick()
if (data && index !== undefined) {
editRef.value?.setFormData(data)
editIndex = index
}
editRef.value?.open(type)
}
const handleAdd = (data: any) => {
const newData = cloneDeep(toRaw(data))
formData.relations.push(newData)
}
const handleEdit = async (data: any) => {
const newData = cloneDeep(toRaw(data))
console.log(editIndex)
formData.relations.splice(editIndex, 1, newData)
}
const handelDelete = (index: number) => {
formData.relations.splice(index, 1)
}
const getDetails = async () => {
const data = await tableDetail({
id: route.query.id
})
Object.keys(formData).forEach((key) => {
//@ts-ignore
formData[key] = data[key]
})
watch(
() => formData.generate_type,
(value) => {
if (value == 1) {
feedback
.confirm('生成到模块方式如遇同名文件会覆盖旧文件,确定要选择此方式吗?')
.catch(() => {
formData.generate_type = 0
})
}
}
)
}
const { optionsData } = useDictOptions<{
dict_type: any[]
menu: any[]
}>({
dict_type: {
api: dictTypeAll
},
menu: {
api: menuAll,
transformData(data) {
const menu = { id: 0, name: '顶级', children: [] }
menu.children = data
return [menu]
}
}
})
const onSubmit = async () => {
try {
await formRef.value?.validate()
await generateEdit(formData)
router.back()
} catch (error: any) {
for (const err in error) {
const isInRules = Object.keys(rules).includes(err)
isInRules && feedback.msgError(error[err][0]?.message)
}
}
}
getDetails()
</script>
+223
View File
@@ -0,0 +1,223 @@
<template>
<div class="code-generation">
<el-card class="!border-none" shadow="never">
<el-form class="mb-[-16px]" :model="formData" inline>
<el-form-item class="w-[280px]" label="表名称">
<el-input v-model="formData.table_name" clearable @keyup.enter="resetPage" />
</el-form-item>
<el-form-item class="w-[280px]" label="表描述">
<el-input v-model="formData.table_comment" clearable @keyup.enter="resetPage" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="resetPage">查询</el-button>
<el-button @click="resetParams">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card class="!border-none mt-4" shadow="never" v-loading="pager.loading">
<div class="flex">
<data-table
v-perms="['tools.generator/selectTable']"
class="inline-block mr-[10px]"
@success="getLists"
>
<el-button type="primary">
<template #icon>
<icon name="el-icon-Plus" />
</template>
导入数据表
</el-button>
</data-table>
<el-button
v-perms="['tools.generator/delete']"
:disabled="!selectData.length"
@click="handleDelete(selectData)"
type="danger"
>
<template #icon>
<icon name="el-icon-Delete" />
</template>
删除
</el-button>
<el-button
v-perms="['tools.generator/generate']"
:disabled="!selectData.length"
@click="handleGenerate(selectData)"
>
生成代码
</el-button>
</div>
<div class="mt-4">
<el-table
:data="pager.lists"
size="large"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column label="表名称" prop="table_name" min-width="180" />
<el-table-column label="表描述" prop="table_comment" min-width="180" />
<el-table-column label="创建时间" prop="create_time" min-width="180" />
<el-table-column label="更新时间" prop="update_time" min-width="180" />
<el-table-column label="操作" width="160" fixed="right">
<template #default="{ row }">
<div class="flex items-center">
<el-button
v-perms="['tools.generator/preview']"
type="primary"
link
@click="handlePreview(row.id)"
>
预览
</el-button>
<el-button type="primary" link>
<router-link
v-perms="['tools.generator/edit']"
:to="{
path: getRoutePath('tools.generator/edit'),
query: {
id: row.id
}
}"
>
编辑
</router-link>
</el-button>
<el-dropdown
class="ml-2"
@command="handleCommand($event, row)"
v-perms="[
'tools.generator/generate',
'tools.generator/syncColumn',
'tools.generator/delete'
]"
>
<el-button type="primary" link>
更多
<icon name="el-icon-ArrowDown" :size="14" />
</el-button>
<template #dropdown>
<el-dropdown-menu>
<div v-perms="['tools.generator/generate']">
<el-dropdown-item command="generate">
<el-button type="primary" link>
生成代码
</el-button>
</el-dropdown-item>
</div>
<div v-perms="['tools.generator/syncColumn']">
<el-dropdown-item command="sync">
<el-button type="primary" link>
同步
</el-button>
</el-dropdown-item>
</div>
<div v-perms="['tools.generator/delete']">
<el-dropdown-item command="delete">
<el-button type="danger" link> 删除 </el-button>
</el-dropdown-item>
</div>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
</el-table-column>
</el-table>
</div>
<div class="flex justify-end mt-4">
<pagination v-model="pager" @change="getLists" />
</div>
</el-card>
<code-preview
v-if="previewState.show"
v-model="previewState.show"
:code="previewState.code"
/>
</div>
</template>
<script lang="ts" setup name="codeGenerate">
import {
generateCode,
generateDelete,
generatePreview,
generateTable,
syncColumn
} from '@/api/tools/code'
import { usePaging } from '@/hooks/usePaging'
import { getRoutePath } from '@/router'
import { isProdMode } from '@/utils/env'
import feedback from '@/utils/feedback'
import CodePreview from '../components/code-preview.vue'
import DataTable from '../components/data-table.vue'
const formData = reactive({
table_name: '',
table_comment: ''
})
const previewState = reactive({
show: false,
loading: false,
code: []
})
const { pager, getLists, resetParams, resetPage } = usePaging({
fetchFun: generateTable,
params: formData
})
const selectData = ref<any[]>([])
const handleSelectionChange = (val: any[]) => {
selectData.value = val.map(({ id }) => id)
}
const handleSync = async (id: number) => {
await feedback.confirm('确定要同步表结构?')
await syncColumn({ id })
}
const handleDelete = async (id: number | any[]) => {
await feedback.confirm('确定要删除?')
await generateDelete({ id })
getLists()
}
const handlePreview = async (id: number) => {
const data: any = await generatePreview({ id })
previewState.code = data
previewState.show = true
}
const hasGenerateTypeInModule = (data: any[]) => {
return data.some((item) => item.generate_type == 1)
}
const handleGenerate = async (selectData: any[]) => {
if (isProdMode() && hasGenerateTypeInModule(selectData)) {
return feedback.msgError('生成方式为生成到模块,请在前端开发模式下使用,详细参考文档')
}
const data: any = await generateCode({ id: selectData })
if (data.file) {
window.open(data.file, '_blank')
}
}
const handleCommand = (command: any, row: any) => {
switch (command) {
case 'generate':
handleGenerate([row.id])
break
case 'sync':
handleSync(row.id)
break
case 'delete':
handleDelete(row.id)
}
}
getLists()
</script>
@@ -0,0 +1,64 @@
<template>
<div class="code-preview">
<el-dialog v-model="show" width="900px" title="代码预览">
<el-tabs v-model="activeTab">
<el-tab-pane
v-for="(item, index) in code"
:label="item.name"
:name="`index${index}`"
:key="index"
>
<div class="flex" style="height: 50vh">
<el-scrollbar class="flex-1">
<highlightjs autodetect :code="item.content" />
</el-scrollbar>
<div>
<el-button @click="handleCopy(item.content)" type="primary" link>
<template #icon>
<icon name="el-icon-CopyDocument" />
</template>
复制
</el-button>
</div>
</div>
</el-tab-pane>
</el-tabs>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import useClipboard from 'vue-clipboard3'
import feedback from '@/utils/feedback'
const props = defineProps<{
modelValue: boolean
code: any[]
}>()
const emit = defineEmits<{
(event: 'update:modelValue', value: boolean): void
}>()
const { toClipboard } = useClipboard()
const activeTab = ref('index0')
const handleCopy = async (text: string) => {
try {
await toClipboard(text)
feedback.msgSuccess('复制成功')
} catch (e) {
feedback.msgError('复制失败')
}
}
const show = computed<boolean>({
get() {
return props.modelValue
},
set(value) {
emit('update:modelValue', value)
}
})
</script>
@@ -0,0 +1,94 @@
<template>
<div class="data-table">
<popup
ref="popupRef"
:clickModalClose="false"
title="选择表"
width="900px"
:async="true"
@confirm="handleConfirm"
>
<template #trigger>
<slot></slot>
</template>
<el-form class="ls-form" :model="formData" inline>
<el-form-item class="w-[280px]" label="表名称">
<el-input v-model="formData.name" clearable @keyup.enter="resetPage" />
</el-form-item>
<el-form-item class="w-[280px]" label="表描述">
<el-input v-model="formData.comment" clearable @keyup.enter="resetPage" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="resetPage">查询</el-button>
<el-button @click="resetParams">重置</el-button>
</el-form-item>
</el-form>
<div class="m-4" v-loading="pager.loading">
<el-table
height="400"
size="large"
:data="pager.lists"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column label="表名称" prop="name" min-width="150" />
<el-table-column label="表描述" prop="comment" min-width="160" />
<el-table-column label="创建时间" prop="create_time" min-width="180" />
</el-table>
</div>
<div class="flex justify-end mt-4">
<pagination v-model="pager" @change="getLists" />
</div>
</popup>
</div>
</template>
<script lang="ts" setup>
import { dataTable, selectTable } from '@/api/tools/code'
import Pagination from '@/components/pagination/index.vue'
import Popup from '@/components/popup/index.vue'
import { usePaging } from '@/hooks/usePaging'
import feedback from '@/utils/feedback'
const emit = defineEmits<{
(event: 'success'): void
}>()
const popupRef = shallowRef<InstanceType<typeof Popup>>()
const formData = reactive({
name: '', // 表名称
comment: '' // 表描述
})
const { pager, getLists, resetParams, resetPage } = usePaging({
fetchFun: dataTable,
params: formData,
size: 10
})
const selectData = ref<any[]>([])
const handleSelectionChange = (val: any[]) => {
selectData.value = val.map(({ name, comment }) => ({
name,
comment
}))
}
const handleConfirm = async () => {
if (!selectData.value.length) return feedback.msgError('请选择数据表')
await selectTable({
table: selectData.value
})
popupRef.value?.close()
emit('success')
}
watch(
() => popupRef.value?.visible,
(value) => {
if (value) getLists()
}
)
</script>
@@ -0,0 +1,162 @@
<template>
<div class="edit-popup">
<popup
ref="popupRef"
:title="popupTitle"
:async="true"
width="550px"
@confirm="handleSubmit"
@close="handleClose"
>
<el-form ref="formRef" :model="formData" label-width="84px" :rules="formRules">
<el-form-item label="关联类型" prop="type">
<el-select class="flex-1" v-model="formData.type" placeholder="请选择关联类型">
<el-option
v-for="(item, index) in types"
:key="index"
:label="item.name"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="关联名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入关联名称" />
</el-form-item>
<el-form-item label="关联模型" prop="model">
<el-select class="flex-1" v-model="formData.model" placeholder="请选择关联模型">
<el-option
v-for="item in optionsData.models"
:label="item"
:value="item"
:key="item"
/>
</el-select>
</el-form-item>
<el-form-item label="关联健" prop="local_key">
<el-select
class="flex-1"
v-model="formData.local_key"
clearable
placeholder="请选择关联健"
>
<el-option
v-for="item in column"
:key="item.id"
:value="item.column_name"
:label="`${item.column_name}${item.column_comment}`"
/>
</el-select>
</el-form-item>
<el-form-item label="外键" prop="foreign_key">
<el-input
v-model="formData.foreign_key"
placeholder="关联表外键或中间表的外键"
/>
</el-form-item>
</el-form>
</popup>
</div>
</template>
<script lang="ts" setup>
import type { FormInstance } from 'element-plus'
import type { PropType } from 'vue'
import { getModels } from '@/api/tools/code'
import Popup from '@/components/popup/index.vue'
import { useDictOptions } from '@/hooks/useDictOptions'
defineProps({
column: {
type: Array as PropType<any[]>,
default: () => []
},
types: {
type: Array as PropType<any[]>,
default: () => []
}
})
const emit = defineEmits(['add', 'close', 'edit'])
const formRef = shallowRef<FormInstance>()
const popupRef = shallowRef<InstanceType<typeof Popup>>()
const mode = ref<'add' | 'edit'>('add')
const popupTitle = computed(() => {
return mode.value == 'edit' ? '编辑关联' : '新增关联'
})
const formData = reactive({
name: '',
model: '',
type: '',
local_key: '',
foreign_key: ''
})
const formRules = {
name: [
{
required: true,
message: '请输入关联名称'
}
],
type: [
{
required: true,
message: '请选择关联类型'
}
],
model: [
{
required: true,
message: '请选择关联模型'
}
],
local_key: [
{
required: true,
message: '请选择关联健'
}
],
foreign_key: [
{
required: true,
message: '请输入外键'
}
]
}
const { optionsData } = useDictOptions<{
models: any[]
}>({
models: {
api: getModels
}
})
const handleSubmit = async () => {
await formRef.value?.validate()
popupRef.value?.close()
emit(mode.value, formData)
}
const open = (type = 'add') => {
mode.value = type as 'add' | 'edit'
popupRef.value?.open()
}
const setFormData = (data: Record<any, any>) => {
for (const key in formData) {
if (data[key] != null && data[key] != undefined) {
//@ts-ignore
formData[key] = data[key]
}
}
}
const handleClose = () => {
emit('close')
}
defineExpose({
open,
setFormData
})
</script>
+15
View File
@@ -0,0 +1,15 @@
<template>
<div class="error404">
<error code="403" title="您的账号权限不足,请联系管理员添加权限!" :show-btn="false">
<template #content>
<div class="flex justify-center">
<img class="w-[150px] h-[150px]" src="@/assets/images/no_perms.png" alt="" />
</div>
</template>
</error>
</div>
</template>
<script lang="ts" setup>
import Error from './components/error.vue'
</script>
+9
View File
@@ -0,0 +1,9 @@
<template>
<div class="error404">
<error code="404" title="哎呀,出错了!您访问的页面不存在…"></error>
</div>
</template>
<script lang="ts" setup>
import Error from './components/error.vue'
</script>
@@ -0,0 +1,58 @@
<template>
<div class="error">
<div>
<slot name="content">
<div class="error-code">{{ code }}</div>
</slot>
<div class="text-lg text-tx-secondary mt-7 mb-7">{{ title }}</div>
<el-button v-if="showBtn" type="primary" @click="router.go(-1)">
{{ second }} 秒后返回上一页
</el-button>
</div>
</div>
</template>
<script lang="ts" setup>
import { onUnmounted, ref } from 'vue'
import { useRouter } from 'vue-router'
const props = defineProps({
code: String,
title: String,
showBtn: {
type: Boolean,
default: true
}
})
let timer: any = null
const second = ref(5)
const router = useRouter()
props.showBtn &&
(timer = setInterval(() => {
if (second.value === 0) {
clearInterval(timer)
router.go(-1)
} else {
second.value--
}
}, 1000))
onUnmounted(() => {
timer && clearInterval(timer)
})
</script>
<style lang="scss" scoped>
.error {
text-align: center;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
.error-code {
@apply text-primary;
font-size: 150px;
}
.el-button {
width: 176px;
}
}
</style>
+106
View File
@@ -0,0 +1,106 @@
<template>
<div>
<el-card class="!border-none" shadow="never">
<el-alert
type="warning"
title="温馨提示:用户账户变动记录"
:closable="false"
show-icon
></el-alert>
<el-form ref="formRef" class="mb-[-16px] mt-[16px]" :model="queryParams" :inline="true">
<el-form-item class="w-[280px]" label="用户信息">
<el-input
v-model="queryParams.user_info"
placeholder="请输入用户账号/昵称/手机号"
clearable
@keyup.enter="resetPage"
/>
</el-form-item>
<el-form-item class="w-[280px]" label="变动类型">
<el-select v-model="queryParams.change_type">
<el-option label="全部" value />
<el-option
v-for="(value, key) in optionsData.change_type"
:key="key"
:label="value"
:value="key"
/>
</el-select>
</el-form-item>
<el-form-item label="记录时间">
<daterange-picker
v-model:startTime="queryParams.start_time"
v-model:endTime="queryParams.end_time"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="resetPage">查询</el-button>
<el-button @click="resetParams">重置</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card class="!border-none mt-4" shadow="never">
<el-table size="large" v-loading="pager.loading" :data="pager.lists">
<el-table-column label="用户账号" prop="account" min-width="100" />
<el-table-column label="用户昵称" min-width="160">
<template #default="{ row }">
<div class="flex items-center">
<image-contain
class="flex-none mr-2"
:src="row.avatar"
:width="40"
:height="40"
preview-teleported
fit="contain"
/>
{{ row.nickname }}
</div>
</template>
</el-table-column>
<el-table-column label="手机号码" prop="mobile" min-width="100" />
<el-table-column label="变动金额" prop="change_amount" min-width="100">
<template #default="{ row }">
<span :class="{ 'text-error': row.action == 2 }">
{{ row.change_amount }}
</span>
</template>
</el-table-column>
<el-table-column label="剩余金额" prop="left_amount" min-width="100" />
<el-table-column label="变动类型" prop="change_type_desc" min-width="120" />
<el-table-column label="来源单号" prop="source_sn" min-width="100" />
<el-table-column label="记录时间" prop="create_time" min-width="120" />
</el-table>
<div class="flex justify-end mt-4">
<pagination v-model="pager" @change="getLists" />
</div>
</el-card>
</div>
</template>
<script lang="ts" setup name="balanceDetail">
import { accountLog, getUmChangeType } from '@/api/finance'
import { useDictOptions } from '@/hooks/useDictOptions'
import { usePaging } from '@/hooks/usePaging'
const queryParams = reactive({
user_info: '',
change_type: '',
start_time: '',
end_time: ''
})
const { pager, getLists, resetPage, resetParams } = usePaging({
fetchFun: accountLog,
params: queryParams
})
const { optionsData } = useDictOptions<{
change_type: Record<number, string>
}>({
change_type: {
api: getUmChangeType
}
})
getLists()
</script>

Some files were not shown because too many files have changed in this diff Show More