Merge branch 'release/1.2.1'

This commit is contained in:
TinyAnts 2022-09-09 10:34:48 +08:00
commit 3868962b1e
918 changed files with 61701 additions and 1324 deletions

1
.gitignore vendored
View File

@ -1,4 +1,3 @@
.vscode
.idea
*.iws
*.iml

View File

@ -1,4 +1,4 @@
NODE_ENV = 'development'
# Base API
# 请求域名
VITE_APP_BASE_URL='https://likeadmin-java.yixiangonline.com'

View File

@ -1,3 +1,4 @@
NODE_ENV = 'production'
# Base API
# 请求域名
VITE_APP_BASE_URL=''

View File

@ -34,7 +34,8 @@ module.exports = {
'no-prototype-builtins': 'off',
'prefer-spread': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/no-non-null-asserted-optional-chain': 'off'
'@typescript-eslint/no-non-null-asserted-optional-chain': 'off',
'vue/no-mutating-props': 'off'
},
globals: {
module: 'readonly'

4
admin/.gitignore vendored
View File

@ -30,7 +30,3 @@ components.d.ts
*.sln
*.sw?
# lock
package-lock.json
yarn.lock

3
admin/.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
}

11
admin/.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,11 @@
{
"editor.detectIndentation": false,
"editor.tabSize": 4,
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"css.validate": false,
"less.validate": false,
"scss.validate": false
}

69
admin/src/api/article.ts Normal file
View File

@ -0,0 +1,69 @@
import request from '@/utils/request'
// 文章分类列表
export function articleCateLists(params?: any) {
return request.get({ url: '/article/cate/list', params })
}
// 文章分类列表
export function articleCateAll(params?: any) {
return request.get({ url: '/article/cate/all', params })
}
// 添加文章分类
export function articleCateAdd(params: any) {
return request.post({ url: '/article/cate/add', params })
}
// 编辑文章分类
export function articleCateEdit(params: any) {
return request.post({ url: '/article/cate/edit', params })
}
// 删除文章分类
export function articleCateDelete(params: any) {
return request.post({ url: '/article/cate/del', params })
}
// 文章分类详情
export function articleCateDetail(params: any) {
return request.get({ url: '/article/cate/detail', params })
}
// 文章分类状态
export function articleCateStatus(params: any) {
return request.post({ url: '/article/cate/change', params })
}
// 文章列表
export function articleLists(params?: any) {
return request.get({ url: '/article/list', params })
}
// 文章列表
export function articleAll(params?: any) {
return request.get({ url: '/article/all', params })
}
// 添加文章
export function articleAdd(params: any) {
return request.post({ url: '/article/add', params })
}
// 编辑文章
export function articleEdit(params: any) {
return request.post({ url: '/article/edit', params })
}
// 删除文章
export function articleDelete(params: any) {
return request.post({ url: '/article/del', params })
}
// 文章详情
export function articleDetail(params: any) {
return request.get({ url: '/article/detail', params })
}
// 文章分类状态
export function articleStatus(params: any) {
return request.post({ url: '/article/change', params })
}

View File

@ -0,0 +1,11 @@
import request from '@/utils/request'
// H5渠道配置保存
export function setH5Config(params: any) {
return request.post({ url: '/channel/h5/save', params })
}
// H5渠道配置详情
export function getH5Config() {
return request.get({ url: '/channel/h5/detail' })
}

View File

@ -0,0 +1,11 @@
import request from '@/utils/request'
// 微信小程序配置保存
export function setWeappConfig(params: any) {
return request.post({ url: '/channel/mp/save', params })
}
// 微信小程序配置详情
export function getWeappConfig() {
return request.get({ url: '/channel/mp/detail' })
}

View File

@ -0,0 +1,11 @@
import request from '@/utils/request'
// 微信开发平台配置保存
export function setWxDevConfig(params: any) {
return request.post({ url: '/channel/wx/save', params })
}
// 微信开发平台配置详情
export function getWxDevConfig() {
return request.get({ url: '/channel/wx/detail' })
}

View File

@ -0,0 +1,11 @@
import request from '@/utils/request'
// 微信公众号配置保存
export function setOaConfig(params: any) {
return request.post({ url: '/channel/oa/save', params })
}
// 微信公众号配置详情
export function getOaConfig() {
return request.get({ url: '/channel/oa/detail' })
}

16
admin/src/api/consumer.ts Normal file
View File

@ -0,0 +1,16 @@
import request from '@/utils/request'
// 用户列表
export function getUserList(params: any) {
return request.get({ url: '/user/list', params })
}
// 用户详情
export function getUserDetail(params: any) {
return request.get({ url: '/user/detail', params })
}
// 用户编辑
export function userEdit(params: any) {
return request.post({ url: '/user/edit', params })
}

View File

@ -0,0 +1,21 @@
import request from '@/utils/request'
// 页面装修详情
export function getDecoratePages(params: any) {
return request.get({ url: '/decorate/pages/detail', params }, { ignoreCancelToken: true })
}
// 页面装修保存
export function setDecoratePages(params: any) {
return request.post({ url: '/decorate/pages/save', params })
}
// 底部导航详情
export function getDecorateTabbar(params?: any) {
return request.get({ url: '/decorate/tabbar/detail', params })
}
// 底部导航保存
export function setDecorateTabbar(params: any) {
return request.post({ url: '/decorate/tabbar/save', params })
}

31
admin/src/api/message.ts Normal file
View File

@ -0,0 +1,31 @@
import request from '@/utils/request'
// 通知设置列表
export function noticeLists(params: any) {
return request.get({ url: '/setting/notice/list', params })
}
// 通知设置详情
export function noticeDetail(params: any) {
return request.get({ url: '/setting/notice/detail', params })
}
// 通知设置保存
export function setNoticeConfig(params: any) {
return request.post({ url: '/setting/notice/save', params })
}
// 短信设置列表
export function smsLists() {
return request.get({ url: '/setting/sms/list' })
}
// 短信设置详情
export function smsDetail(params: any) {
return request.get({ url: '/setting/sms/detail', params })
}
// 短信设置保存
export function setSmsConfig(params: any) {
return request.post({ url: '/setting/sms/save', params })
}

View File

@ -19,3 +19,8 @@ export function deptEdit(params: any) {
export function deptDelete(params: any) {
return request.post({ url: '/system/dept/del', params })
}
// 部门详情
export function deptDetail(params?: any) {
return request.get({ url: '/system/dept/detail', params })
}

View File

@ -23,3 +23,8 @@ export function postEdit(params: any) {
export function postDelete(params: any) {
return request.post({ url: '/system/post/del', params })
}
// 岗位详情
export function postDetail(params: any) {
return request.get({ url: '/system/post/detail', params })
}

View File

@ -19,3 +19,8 @@ export function menuEdit(params: Record<string, any>) {
export function menuDelete(params: Record<string, any>) {
return request.post({ url: '/system/menu/del', params })
}
// 菜单删除
export function menuDetail(params: Record<string, any>) {
return request.get({ url: '/system/menu/detail', params })
}

View File

@ -0,0 +1,29 @@
import request from '@/utils/request'
/**
* @return { Promise }
* @description
*/
export function getSearch() {
return request.get({ url: '/setting/search/detail' })
}
export interface List {
name: string // 搜索关键字
sort: number // 热门搜索排序
}
export interface Search {
isHotSearch: number // 是否开启搜索0/1
list: List[]
}
/**
* @return { Promise }
* @param { Search } Search
* @description
*/
export function setSearch(params: Search) {
return request.post({ url: '/setting/search/save', params })
}

View File

@ -0,0 +1,45 @@
import request from '@/utils/request'
/**
* @return { Promise }
* @description
*/
export function getUserSetup() {
return request.get({ url: '/setting/user/detail' })
}
/**
* @return { Promise }
* @param { string } defaultAvatar
* @description
*/
export function setUserSetup(params: { defaultAvatar: string }) {
return request.post({ url: '/setting/user/save', params })
}
/**
* @return { Promise }
* @description
*/
export function getLogin() {
return request.get({ url: '/setting/login/detail' })
}
export interface LoginSetup {
loginWay: number[] | any // 登录方式, 逗号隔开
forceBindMobile: number // 强制绑定手机 0/1
openAgreement: number // 是否开启协议 0/1
openOtherAuth: number // 第三方登录 0/1
autoLoginAuth: number[] | any // 第三方自动登录 逗号隔开
}
/**
* @return { Promise }
* @param { LoginSetup } LoginSetup
* @description
*/
export function setLogin(params: LoginSetup) {
return request.post({ url: '/setting/login/save', params })
}

View File

@ -0,0 +1,33 @@
<template>
<div class="color-picker flex flex-1">
<el-color-picker v-model="color" :predefine="predefineColors" />
<el-input v-model="color" class="mx-[10px] flex-1" type="text" readonly />
<el-button type="text" @click="reset">重置</el-button>
</div>
</template>
<script lang="ts" setup>
const props = defineProps({
modelValue: {
type: String
},
defaultColor: {
type: String
}
})
const emit = defineEmits<{
(event: 'update:modelValue', value: any): void
}>()
const color = computed({
get() {
return props.modelValue
},
set(value) {
emit('update:modelValue', value)
}
})
const predefineColors = ['#409EFF', '#28C76F', '#EA5455', '#FF9F43', '#01CFE8', '#4A5DFF']
const reset = () => {
color.value = props.defaultColor
}
</script>

View File

@ -1,21 +1,24 @@
<template>
<div class="footer-btns">
<div class="footer-btns__content">
<div class="footer-btns__content" :style="fixed ? 'position: fixed' : ''">
<slot></slot>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({})
<script lang="ts" setup>
defineProps({
fixed: {
type: Boolean,
default: true
}
})
</script>
<style scoped lang="scss">
.footer-btns {
height: 60px;
&__content {
position: fixed;
bottom: 0;
height: 60px;
right: 0;

View File

@ -1,6 +1,10 @@
<template>
<div class="icon-select">
<el-popover v-model:visible="state.popoverVisible" :width="state.popoverWidth">
<el-popover
trigger="contextmenu"
v-model:visible="state.popoverVisible"
:width="state.popoverWidth"
>
<div
@mouseover.stop="state.mouseoverSelect = true"
@mouseout.stop="state.mouseoverSelect = false"
@ -41,6 +45,7 @@
ref="inputRef"
v-model.trim="state.inputValue"
placeholder="搜索图标"
:autofocus="false"
:disabled="disabled"
@focus="handleFocus"
@blur="handleBlur"
@ -112,7 +117,7 @@ const handleFocus = () => {
state.inputFocus = state.popoverVisible = true
}
// // input
// input
const handleBlur = () => {
state.inputFocus = false
state.popoverVisible = state.mouseoverSelect

View File

@ -1,5 +1,11 @@
<template>
<el-image :style="styles" v-bind="props"> </el-image>
<el-image :style="styles" v-bind="props">
<template v-slot:error>
<div class="text-tx-secondary flex items-center justify-center bg-white h-full">
<icon name="el-icon-Picture" :size="30" />
</div>
</template>
</el-image>
</template>
<script lang="ts" setup>

View File

@ -0,0 +1,39 @@
<template>
<div class="custom-link mt-[30px]">
<div class="flex flex-wrap items-center">
自定义链接
<div class="ml-4 flex-1 min-w-[100px]">
<el-input
:model-value="modelValue.path"
placeholder="请输入链接地址"
@input="handleInput"
/>
</div>
</div>
<div class="form-tips">
请填写完整的带有https://http://
</div>
</div>
</template>
<script lang="ts" setup>
import type { PropType } from 'vue'
import { LinkTypeEnum, type Link } from '.'
defineProps({
modelValue: {
type: Object as PropType<Link>,
default: () => ({})
}
})
const emit = defineEmits<{
(event: 'update:modelValue', value: Link): void
}>()
const handleInput = (value: string) => {
emit('update:modelValue', {
path: value,
type: LinkTypeEnum.CUSTOM_LINK
})
}
</script>

View File

@ -0,0 +1,11 @@
export enum LinkTypeEnum {
'SHOP_PAGES' = 'shop',
'CUSTOM_LINK' = 'custom'
}
export interface Link {
path: string
name?: string
type: string
params?: Record<string, any>
}

View File

@ -0,0 +1,103 @@
<template>
<div class="link flex">
<el-menu
:default-active="activeMenu"
class="w-[160px] min-h-[350px] link-menu"
@select="handleSelect"
>
<el-menu-item v-for="(item, index) in menus" :index="item.type" :key="index">
<span>{{ item.name }}</span>
</el-menu-item>
</el-menu>
<div class="flex-1 pl-4">
<shop-pages
v-model="activeLink"
v-if="LinkTypeEnum.SHOP_PAGES == activeMenu"
@update:model-value="updateLink"
/>
<custom-link
v-model="activeLink"
v-if="LinkTypeEnum.CUSTOM_LINK == activeMenu"
@update:model-value="updateLink"
/>
</div>
</div>
</template>
<script lang="ts" setup>
import type { PropType } from 'vue'
import { LinkTypeEnum, type Link } from '.'
import ShopPages from './shop-pages.vue'
import CustomLink from './custom-link.vue'
const props = defineProps({
modelValue: {
type: Object as PropType<Link>,
required: true
}
})
const emit = defineEmits<{
(event: 'update:modelValue', value: any): void
}>()
const menus = ref([
{
name: '商城页面',
type: LinkTypeEnum.SHOP_PAGES,
link: {}
},
{
name: '自定义链接',
type: LinkTypeEnum.CUSTOM_LINK,
link: {}
}
])
const activeLink = computed({
get() {
return menus.value.find((item) => item.type == activeMenu.value)?.link as Link
},
set(value) {
menus.value.forEach((item) => {
if (item.type == activeMenu.value) {
item.link = value
}
})
}
})
const activeMenu = ref<string>(LinkTypeEnum.SHOP_PAGES)
const handleSelect = (index: string) => {
activeMenu.value = index
}
const updateLink = (value: any) => {
emit('update:modelValue', value)
}
watch(
() => props.modelValue,
(value) => {
activeMenu.value = value.type
activeLink.value = value
},
{
immediate: true
}
)
</script>
<style lang="scss" scoped>
.link-menu {
--el-menu-item-height: 40px;
: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>

View File

@ -0,0 +1,49 @@
<template>
<div class="link-picker flex-1" @click="popupRef?.open()">
<el-input
class="cursor-pointer"
:model-value="modelValue?.name ?? modelValue?.path"
placeholder="请选择链接"
readonly
>
<template #suffix>
<icon v-if="!modelValue?.path" name="el-icon-ArrowRight" />
<icon v-else name="el-icon-Close" @click.stop="emit('update:modelValue', {})" />
</template>
</el-input>
<popup ref="popupRef" width="700px" title="链接选择" @confirm="handleConfirm">
<link-content v-model="activeLink" />
</popup>
</div>
</template>
<script lang="ts" setup>
import { LinkTypeEnum, type Link } from '.'
import LinkContent from './index.vue'
import Popup from '@/components/popup/index.vue'
const props = defineProps({
modelValue: {
type: Object
}
})
const emit = defineEmits<{
(event: 'update:modelValue', value: any): void
}>()
const popupRef = shallowRef<InstanceType<typeof Popup>>()
const activeLink = ref<Link>({ path: '', type: LinkTypeEnum.SHOP_PAGES })
const handleConfirm = () => {
emit('update:modelValue', activeLink.value)
}
watch(
() => props.modelValue,
(value) => {
if (value?.type) {
activeLink.value = value as Link
}
},
{
immediate: true
}
)
</script>

View File

@ -0,0 +1,52 @@
<template>
<div class="shop-pages">
<div class="link-list flex flex-wrap">
<div
class="link-item border border-br px-5 py-[5px] rounded-[3px] cursor-pointer mr-[10px] mb-[10px]"
v-for="(item, index) in linkList"
:class="{ 'border-primary text-primary': modelValue.path == item.path }"
:key="index"
@click="handleSelect(item)"
>
{{ item.name }}
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import type { PropType } from 'vue'
import { LinkTypeEnum, type Link } from '.'
defineProps({
modelValue: {
type: Object as PropType<Link>,
default: () => ({})
}
})
const emit = defineEmits<{
(event: 'update:modelValue', value: Link): void
}>()
const linkList = ref([
{
path: '/pages/index/index',
name: '商城首页',
type: LinkTypeEnum.SHOP_PAGES
},
{
path: '/pages/news/news',
name: '文章资讯',
type: LinkTypeEnum.SHOP_PAGES
},
{
path: '/pages/user/user',
name: '个人中心',
type: LinkTypeEnum.SHOP_PAGES
}
])
const handleSelect = (value: Link) => {
emit('update:modelValue', value)
}
</script>

View File

@ -22,7 +22,7 @@
>
<del-wrap @close="deleteImg(index)">
<file-item
:uri="element"
:uri="excludeDomain ? getImageUrl(element) : element"
:file-size="size"
:type="type"
></file-item>
@ -41,7 +41,8 @@
v-show="showUpload"
:class="{
'is-disabled': disabled,
'is-one': limit == 1
'is-one': limit == 1,
[uploadClass]: true
}"
>
<slot name="upload">
@ -81,6 +82,7 @@ import Popup from '@/components/popup/index.vue'
import FileItem from './file.vue'
import Material from './index.vue'
import Preview from './preview.vue'
import useAppStore from '@/stores/modules/app'
export default defineComponent({
components: {
Popup,
@ -123,6 +125,15 @@ export default defineComponent({
hiddenUpload: {
type: Boolean,
default: false
},
uploadClass: {
type: String,
default: ''
},
//url
excludeDomain: {
type: Boolean,
default: false
}
},
@ -137,6 +148,7 @@ export default defineComponent({
const isAdd = ref(true)
const currentIndex = ref(-1)
const { disabled, limit, modelValue } = toRefs(props)
const { getImageUrl } = useAppStore()
const tipsText = computed(() => {
switch (props.type) {
case 'image':
@ -159,7 +171,9 @@ export default defineComponent({
return limit.value - fileList.value.length
})
const handleConfirm = () => {
const selectUri = select.value.map((item) => item.uri)
const selectUri = select.value.map((item) =>
props.excludeDomain ? item.path : item.uri
)
if (!isAdd.value) {
fileList.value.splice(currentIndex.value, 1, selectUri.shift())
} else {
@ -229,7 +243,8 @@ export default defineComponent({
previewUrl,
showPreview,
handlePreview,
handleClose
handleClose,
getImageUrl
}
}
})
@ -269,8 +284,8 @@ export default defineComponent({
}
}
.material-upload {
.upload-btn {
@apply box-border rounded border-br border-dashed border flex flex-col justify-center items-center;
:deep(.upload-btn) {
@apply text-tx-secondary box-border rounded border-br border-dashed border flex flex-col justify-center items-center;
}
}
}

View File

@ -0,0 +1,108 @@
<template>
<div @mouseenter="inPopover = true" @mouseleave="inPopover = false">
<el-popover
placement="top"
v-model:visible="visible"
:width="width"
trigger="contextmenu"
class="popover-input"
:teleported="false"
>
<div class="flex">
<div class="popover-input__input mr-[10px] flex-1">
<el-select
class="flex-1"
size="small"
v-if="type == 'select'"
v-model="inputValue"
:teleported="false"
>
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value"
></el-option>
</el-select>
<el-input
v-else
v-model="inputValue"
:type="type"
size="small"
:placeholder="placeholder"
/>
</div>
<div class="popover-input__btns flex-none">
<el-button link @click="visible = false">取消</el-button>
<el-button type="primary" size="small" @click="handleConfirm">确定</el-button>
</div>
</div>
<template #reference>
<div class="inline" @click.stop="handleOpen">
<slot></slot>
</div>
</template>
</el-popover>
</div>
</template>
<script lang="ts" setup>
import { useEventListener } from '@vueuse/core'
import type { PropType } from 'vue'
const props = defineProps({
value: {
type: String
},
type: {
type: String,
default: 'text'
},
width: {
type: [Number, String],
default: 250
},
placeholder: String,
disabled: {
type: Boolean,
default: false
},
options: {
type: Array as PropType<any[]>,
default: () => []
}
})
const emit = defineEmits(['confirm'])
const visible = ref(false)
const inPopover = ref(false)
const inputValue = ref()
const handleConfirm = () => {
close()
emit('confirm', inputValue.value)
}
const handleOpen = () => {
if (props.disabled) {
return
}
visible.value = true
}
const close = () => {
visible.value = false
}
watch(
() => props.value,
(value) => {
inputValue.value = value
},
{
immediate: true
}
)
useEventListener(document.body, 'click', () => {
if (inPopover.value) return
close()
})
</script>
<style scoped lang="scss"></style>

View File

@ -22,7 +22,7 @@
:close-on-click-modal="false"
width="500px"
:modal="false"
:before-close="handleClose"
@close="handleClose"
>
<div class="file-list p-4">
<template v-for="(item, index) in fileList" :key="index">
@ -44,6 +44,7 @@ import useUserStore from '@/stores/modules/user'
import config from '@/config'
import feedback from '@/utils/feedback'
import type { ElUpload } from 'element-plus'
import { RequestCodeEnum } from '@/enums/requestEnums'
export default defineComponent({
components: {},
props: {
@ -87,7 +88,7 @@ export default defineComponent({
const handleProgress = (event: any, file: any, fileLists: any[]) => {
visible.value = true
fileList.value = fileLists
fileList.value = toRaw(fileLists)
}
const handleSuccess = (response: any, file: any, fileLists: any[]) => {
@ -95,9 +96,9 @@ export default defineComponent({
if (allSuccess) {
uploadRefs.value?.clearFiles()
visible.value = false
emit('change')
}
emit('change')
if (response.code == 0 && response.show && response.msg) {
if (response.code == RequestCodeEnum.FAILED && response.msg) {
feedback.msgError(response.msg)
}
}

View File

@ -19,3 +19,22 @@ export enum ScreenEnum {
XL = 1280,
'2XL' = 1536
}
// 客户端类型
export enum ClientEnum {
MP_WEIXIN = 1, // 微信-小程序
OA_WEIXIN = 2, // 微信-公众号
H5 = 3, // H5
PC = 4, // PC
IOS = 5, //苹果
ANDROID = 6 //安卓
}
export const ClientMap = {
[ClientEnum.MP_WEIXIN]: '微信小程序',
[ClientEnum.OA_WEIXIN]: '微信公众号',
[ClientEnum.H5]: '手机H5Z',
[ClientEnum.PC]: '电脑PC',
[ClientEnum.IOS]: '苹果APP',
[ClientEnum.ANDROID]: '安卓APP'
}

View File

@ -5,7 +5,6 @@
*/
import useUserStore from '@/stores/modules/user'
export default {
mounted: (el: HTMLElement, binding: any) => {
const { value } = binding

View File

@ -33,7 +33,7 @@ import useTabsStore, { getRouteParams } from '@/stores/modules/multipleTabs'
const router = useRouter()
const tabsStore = useTabsStore()
const { route } = useWatchRoute((route) => {
tabsStore.addTab(route)
tabsStore.addTab(route, router)
})
const currentTab = computed(() => {

View File

@ -1,5 +1,9 @@
<template>
<div class="menu flex-1 min-h-0" :class="themeClass" :style="`--aside-width: ${width}px`">
<div
class="menu flex-1 min-h-0"
:class="themeClass"
:style="isCollapsed ? '' : `--aside-width: ${width}px`"
>
<el-scrollbar>
<el-menu
v-bind="config"
@ -48,7 +52,7 @@ const props = defineProps({
defineEmits(['select'])
const route = useRoute()
const activeMenu = computed<string>(() => (route.meta?.activeMenu as string) ?? route.path)
const activeMenu = computed<string>(() => route.meta?.activeMenu || route.path)
const themeClass = computed(() => `theme-${props.theme}`)
</script>

View File

@ -78,6 +78,6 @@ router.beforeEach(async (to, from, next) => {
})
router.afterEach(() => {
// console.log(router.getRoutes())
console.log(router.getRoutes())
NProgress.done()
})

View File

@ -7,6 +7,11 @@ import useUserStore from '@/stores/modules/user'
// 匹配views里面所有的.vue文件动态引入
const modules = import.meta.glob('/src/views/**/*.vue')
//
export function getModulesKey() {
return Object.keys(modules).map((item) => item.replace('/src/views/', '').replace('.vue', ''))
}
// 过滤路由所需要的数据
export function filterAsyncRoutes(routes: any[], firstRoute = true) {
return routes.map((route) => {
@ -31,7 +36,8 @@ export function createRouteRecord(route: any, firstRoute: boolean): RouteRecordR
perms: route.perms,
query: route.params,
icon: route.menuIcon,
type: route.menuType
type: route.menuType,
activeMenu: route.selected
}
}
switch (route.menuType) {
@ -79,6 +85,12 @@ export function findFirstValidRoute(routes: RouteRecordRaw[]): string | undefine
}
}
export function getRoutePath(perms: string) {
console.log(router.getRoutes())
console.log(router.getRoutes().find((item) => item.meta?.perms == perms)?.path)
return router.getRoutes().find((item) => item.meta?.perms == perms)?.path || ''
}
// 重置路由
export function resetRouter() {
router.removeRoute(INDEX_ROUTE_NAME)

View File

@ -40,40 +40,13 @@ export const constantRoutes: Array<RouteRecordRaw> = [
children: [
{
path: 'setting',
name: Symbol(),
component: () => import('@/views/user/setting.vue'),
meta: {
title: '个人设置'
}
}
]
},
{
path: '/dev_tools',
component: LAYOUT,
children: [
{
path: 'code/edit',
component: () => import('@/views/dev_tools/code/edit.vue'),
meta: {
title: '编辑数据表',
activeMenu: '/dev_tools/code'
}
}
]
},
{
path: '/setting',
component: LAYOUT,
children: [
{
path: 'dict/data',
component: () => import('@/views/setting/dict/data/index.vue'),
meta: {
title: '数据管理',
activeMenu: '/setting/dict'
}
}
]
}
]

View File

@ -18,6 +18,9 @@ const useAppStore = defineStore({
}
},
actions: {
getImageUrl(url: string) {
return url ? `${this.config.ossDomain}${url}` : ''
},
getConfig() {
return new Promise((resolve, reject) => {
getConfig()

View File

@ -28,10 +28,11 @@ const getHasTabIndex = (path: string, tabList: TabItem[]) => {
return tabList.findIndex((item) => item.path == path)
}
const isCannotAddRoute = (route: RouteLocationNormalized) => {
const { path, meta } = route
const isCannotAddRoute = (route: RouteLocationNormalized, router: Router) => {
const { path, meta, name } = route
if (!path || isExternal(path)) return true
if (meta?.hideTab) return true
if (!router.hasRoute(name!)) return true
if (([PageEnum.LOGIN, PageEnum.ERROR_403] as string[]).includes(path)) {
return true
}
@ -74,9 +75,9 @@ const useTabsStore = defineStore({
this.tasMap = {}
this.indexRouteName = ''
},
addTab(route: RouteLocationNormalized) {
addTab(route: RouteLocationNormalized, router: Router) {
const { name, path, query, meta, params } = route
if (isCannotAddRoute(route)) return
if (isCannotAddRoute(route, router)) return
const hasTabIndex = getHasTabIndex(path!, this.tabList)
const tabItem = {

View File

@ -10,6 +10,7 @@ export interface UserState {
token: string
userInfo: Record<string, any>
routes: RouteRecordRaw[]
menu: any[]
perms: string[]
}
@ -21,6 +22,7 @@ const useUserStore = defineStore({
userInfo: {},
// 路由
routes: [],
menu: [],
// 权限
perms: []
}),
@ -30,6 +32,7 @@ const useUserStore = defineStore({
this.token = ''
this.userInfo = {}
this.perms = []
this.menu = []
},
login(playload: any) {
const { account, password } = playload
@ -78,6 +81,7 @@ const useUserStore = defineStore({
return new Promise((resolve, reject) => {
getMenu()
.then((data) => {
this.menu = data
this.routes = filterAsyncRoutes(data)
resolve(data)
})

View File

@ -32,7 +32,7 @@
--el-border-color-dark: #d4d7de;
--el-border-color-darker: #cdd0d6;
--el-fill-color: #f0f2f5;
--el-fill-color-light: #f5f7fa;
--el-fill-color-light: #f8f8f8;
--el-fill-color-lighter: #fafafa;
--el-fill-color-extra-light: #fafcff;
--el-fill-color-dark: #ebedf0;

View File

@ -1,4 +1,5 @@
import { isObject } from '@vue/shared'
import { cloneDeep } from 'lodash'
/**
* @description
@ -25,7 +26,7 @@ export const isEmpty = (value: unknown) => {
*/
export const treeToArray = (data: any[], props = { children: 'children' }) => {
data = JSON.parse(JSON.stringify(data))
data = cloneDeep(data)
const { children } = props
const newData = []
const queue: any[] = []
@ -41,6 +42,33 @@ export const treeToArray = (data: any[], props = { children: 'children' }) => {
return newData
}
/**
* @description
* @param {Array} data
* @param {Object} props `{ parent: 'pid', children: 'children' }`
*/
export const arrayToTree = (
data: any[],
props = { id: 'id', parentId: 'pid', children: 'children' }
) => {
data = cloneDeep(data)
const { id, parentId, children } = props
const result: any[] = []
const map = new Map()
data.forEach((item) => {
map.set(item[id], item)
const parent = map.get(item[parentId])
if (parent) {
parent[children] = parent[children] ?? []
parent[children].push(item)
} else {
result.push(item)
}
})
return result
}
/**
* @description
* @param {String} path

View File

@ -0,0 +1,96 @@
<template>
<div class="edit-popup">
<popup
ref="popupRef"
:title="popupTitle"
:async="true"
width="550px"
:clickModalClose="true"
@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="请输入栏目名称" />
</el-form-item>
<el-form-item label="排序" prop="sort">
<div>
<el-input-number v-model="formData.sort" />
<div class="form-tips">默认为0 数值越大越排前</div>
</div>
</el-form-item>
<el-form-item label="状态" prop="isShow">
<el-switch v-model="formData.isShow" :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 { articleCateEdit, articleCateAdd, articleCateDetail } from '@/api/article'
import Popup from '@/components/popup/index.vue'
import feedback from '@/utils/feedback'
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,
isShow: 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)
feedback.msgSuccess('操作成功')
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>

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:cate: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="number" min-width="120" />
<el-table-column label="状态" min-width="120">
<template #default="{ row }">
<el-switch
v-perms="['article:cate:change']"
v-model="row.isShow"
:active-value="0"
:inactive-value="1"
@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
v-perms="['article:cate:edit']"
type="primary"
link
@click="handleEdit(row)"
>
编辑
</el-button>
<el-button
v-perms="['article:cate:del']"
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 { 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 })
feedback.msgSuccess('删除成功')
getLists()
}
const changeStatus = async (id: number) => {
try {
await articleCateStatus({ id })
feedback.msgSuccess('修改成功')
getLists()
} catch (error) {
getLists()
}
}
getLists()
</script>

View File

@ -0,0 +1,144 @@
<template>
<div class="article-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="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="请输入文章标题" />
</div>
</el-form-item>
<el-form-item label="文章栏目" prop="cid">
<el-select
class="w-80"
v-model="formData.cid"
placeholder="请选择文章栏目"
>
<el-option
v-for="item in optionsData.articleCate"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="文章简介" prop="intro">
<div class="w-80">
<el-input v-model="formData.intro" placeholder="请输入文章简介" />
</div>
</el-form-item>
<el-form-item label="摘要" prop="summary">
<div class="w-80">
<el-input type="textarea" :rows="6" v-model="formData.summary" />
</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" />
<div class="form-tips">默认为0 数值越大越排前</div>
</div>
</el-form-item>
<el-form-item label="初始浏览量" prop="visit">
<div>
<el-input-number v-model="formData.visit" />
</div>
</el-form-item>
<el-form-item label="文章状态" required prop="isShow">
<el-radio-group v-model="formData.isShow">
<el-radio :label="1">显示</el-radio>
<el-radio :label="0">隐藏</el-radio>
</el-radio-group>
</el-form-item>
</div>
<div class="xl:ml-20">
<el-form-item label="文章内容" required 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>
import type { FormInstance } from 'element-plus'
import feedback from '@/utils/feedback'
import { useDictOptions } from '@/hooks/useDictOptions'
import { articleCateAll, articleDetail, articleEdit } from '@/api/article'
const route = useRoute()
const router = useRouter()
const formData = reactive({
id: '',
title: '',
image: '',
cid: '',
intro: '',
author: '',
content: '',
visit: 0,
sort: 0,
isShow: '',
summary: ''
})
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<{
articleCate: any[]
}>({
articleCate: {
api: articleCateAll
}
})
const handleSave = async () => {
await formRef.value?.validate()
await articleEdit(formData)
feedback.msgSuccess('操作成功')
router.back()
}
route.query.id && getDetails()
</script>

View File

@ -0,0 +1,156 @@
<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 label="文章标题">
<el-input class="w-56" v-model="queryParams.title" />
</el-form-item>
<el-form-item label="栏目名称">
<el-select class="w-56" v-model="queryParams.cid">
<el-option label="全部" value />
<el-option
v-for="item in optionsData.articleCate"
:key="item.id"
:label="item.name"
:value="item.id"
/>
</el-select>
</el-form-item>
<el-form-item label="文章状态">
<el-select class="w-56" v-model="queryParams.isShow">
<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:add']"
:to="{
path: getRoutePath('article: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 }">
<el-image
v-if="row.image"
:src="row.image"
class="w-[60px] h-[45px]"
:preview-src-list="[row.image]"
/>
</template>
</el-table-column>
<el-table-column
label="标题"
prop="title"
min-width="160"
show-tooltip-when-overflow
/>
<el-table-column label="栏目" prop="category" min-width="100" />
<el-table-column label="作者" prop="author" min-width="120" />
<el-table-column label="浏览量" prop="visit" min-width="100" />
<el-table-column label="状态" min-width="100">
<template #default="{ row }">
<el-switch
v-perms="['article:cate:change']"
v-model="row.isShow"
:active-value="1"
:inactive-value="0"
@change="changeStatus(row.id)"
/>
</template>
</el-table-column>
<el-table-column label="排序" prop="sort" min-width="100" />
<el-table-column label="发布时间" prop="createTime" min-width="120" />
<el-table-column label="操作" width="120" fixed="right">
<template #default="{ row }">
<el-button v-perms="['article:edit']" type="primary" link>
<router-link
:to="{
path: getRoutePath('article:edit'),
query: {
id: row.id
}
}"
>
编辑
</router-link>
</el-button>
<el-button
v-perms="['article:del']"
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>
import { articleLists, articleDelete, articleStatus, articleCateAll } 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: '',
isShow: ''
})
const { pager, getLists, resetPage, resetParams } = usePaging({
fetchFun: articleLists,
params: queryParams
})
const { optionsData } = useDictOptions<{
articleCate: any[]
}>({
articleCate: {
api: articleCateAll
}
})
const changeStatus = async (id: number) => {
try {
await articleStatus({ id })
feedback.msgSuccess('修改成功')
getLists()
} catch (error) {
getLists()
}
}
const handleDelete = async (id: number) => {
await feedback.confirm('确定要删除?')
await articleDelete({ id })
feedback.msgSuccess('删除成功')
getLists()
}
getLists()
</script>

View File

@ -0,0 +1,67 @@
<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="160px">
<el-form-item label="渠道状态" required prop="status">
<div>
<el-radio-group v-model="formData.status">
<el-radio :label="1">开启</el-radio>
<el-radio :label="0">关闭</el-radio>
</el-radio-group>
<div class="form-tips">状态为关闭时将不对外提供服务请谨慎操作</div>
</div>
</el-form-item>
<el-form-item label="关闭后访问页面" prop="close">
<el-radio-group v-model="formData.close">
<el-radio :label="0">空页面</el-radio>
<el-radio :label="1">自定义链接</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="" prop="url" v-if="formData.close == 1">
<div class="w-80">
<el-input v-model="formData.url" placeholder="请输入完整的url" />
</div>
</el-form-item>
<el-form-item label="访问链接">
<div>{{ formData.accessLink }}</div>
<el-button class="ml-4" @click="copy(formData.accessLink)">复制</el-button>
</el-form-item>
</el-form>
</el-card>
<footer-btns v-perms="['channel:h5:save']">
<el-button type="primary" @click="handelSave">保存</el-button>
</footer-btns>
</div>
</template>
<script lang="ts" setup>
import { getH5Config, setH5Config } from '@/api/channel/h5'
import feedback from '@/utils/feedback'
import { useClipboard } from '@vueuse/core'
const formData = reactive({
status: 0,
close: 0,
url: '',
accessLink: ''
})
const { copy } = useClipboard()
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()
feedback.msgSuccess('操作成功')
}
getDetail()
</script>

View File

@ -0,0 +1,179 @@
<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" label-width="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="primaryId">
<div class="w-80">
<el-input v-model="formData.primaryId" placeholder="请输入原始ID" />
</div>
</el-form-item>
<el-form-item label="小程序码" prop="qrCode">
<div>
<div>
<material-picker v-model="formData.qrCode" :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="appId">
<div class="w-80">
<el-input v-model="formData.appId" placeholder="请输入AppID" />
</div>
</el-form-item>
<el-form-item label="AppSecret" prop="appSecret">
<div>
<div class="w-80">
<el-input v-model="formData.appSecret" placeholder="请输入AppSecret" />
</div>
</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>
<div class="flex">
<div class="mr-4 w-80">
<el-input v-model="formData.requestDomain" disabled />
</div>
<el-button @click="copy(formData.requestDomain)">复制</el-button>
</div>
<div class="form-tips">
小程序账号登录微信公众平台点击开发>开发设置->服务器域名填写https协议域名
</div>
</div>
</el-form-item>
<el-form-item label="socket合法域名">
<div>
<div class="flex">
<div class="mr-4 w-80">
<el-input v-model="formData.socketDomain" disabled />
</div>
<el-button @click="copy(formData.socketDomain)">复制</el-button>
</div>
<div class="form-tips">
小程序账号登录微信公众平台点击开发>开发设置->服务器域名填写wss协议域名
</div>
</div>
</el-form-item>
<el-form-item label="uploadFile合法域名">
<div>
<div class="flex">
<div class="mr-4 w-80">
<el-input v-model="formData.uploadFileDomain" disabled />
</div>
<el-button @click="copy(formData.uploadFileDomain)">复制</el-button>
</div>
<div class="form-tips">
小程序账号登录微信公众平台点击开发>开发设置->服务器域名填写https协议域名
</div>
</div>
</el-form-item>
<el-form-item label="downloadFile合法域名">
<div>
<div class="flex">
<div class="mr-4 w-80">
<el-input v-model="formData.downloadFileDomain" disabled />
</div>
<el-button @click="copy(formData.downloadFileDomain)">复制</el-button>
</div>
<div class="form-tips">
小程序账号登录微信公众平台点击开发>开发设置->服务器域名填写https协议域名
</div>
</div>
</el-form-item>
<el-form-item label="udp合法域名">
<div>
<div class="flex">
<div class="mr-4 w-80">
<el-input v-model="formData.udpDomain" disabled />
</div>
<el-button @click="copy(formData.udpDomain)">复制</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>
<div class="flex">
<div class="mr-4 w-80">
<el-input v-model="formData.businessDomain" disabled />
</div>
<el-button @click="copy(formData.businessDomain)">复制</el-button>
</div>
<div class="form-tips">
小程序账号登录微信公众平台点击开发>开发设置->业务域名填写业务域名
</div>
</div>
</el-form-item>
</el-card>
</el-form>
<footer-btns v-perms="['channel:h5:save']">
<el-button type="primary" @click="handelSave">保存</el-button>
</footer-btns>
</div>
</template>
<script lang="ts" setup>
import { getWeappConfig, setWeappConfig } from '@/api/channel/weapp'
import feedback from '@/utils/feedback'
import { useClipboard } from '@vueuse/core'
const formData = reactive({
name: '',
primaryId: '',
qrCode: '',
appId: '',
appSecret: '',
businessDomain: '',
downloadFileDomain: '',
requestDomain: '',
socketDomain: '',
tcpDomain: '',
udpDomain: '',
uploadFileDomain: ''
})
const { copy } = useClipboard()
const getDetail = async () => {
const data = await getWeappConfig()
for (const key in formData) {
//@ts-ignore
formData[key] = data[key]
}
}
const handelSave = async () => {
await setWeappConfig(formData)
getDetail()
feedback.msgSuccess('操作成功')
}
getDetail()
</script>

View File

@ -0,0 +1,62 @@
<template>
<div>
<el-card class="!border-none" shadow="never">
<el-alert
type="warning"
title="温馨提示填写微信开放平台开发配置请前往微信开放平台创建应用并完成认证APP应用配置主要用于APP微信登录和微信支付"
:closable="false"
show-icon
/>
</el-card>
<el-form ref="formRef" :model="formData" label-width="160px">
<el-card class="!border-none mt-4" shadow="never">
<div class="font-medium mb-7">APP应用</div>
<el-form-item label="AppID" prop="appId">
<div class="w-80">
<el-input v-model="formData.appId" placeholder="请输入AppID" />
</div>
</el-form-item>
<el-form-item label="AppSecret" prop="appSecret">
<div>
<div class="w-80">
<el-input v-model="formData.appSecret" placeholder="请输入AppSecret" />
</div>
</div>
</el-form-item>
<el-form-item>
<div class="form-tips">
小程序账号登录微信公众平台点击开发>开发设置->开发者ID设置AppID和AppSecret
</div>
</el-form-item>
</el-card>
</el-form>
<footer-btns v-perms="['channel:h5:save']">
<el-button type="primary" @click="handelSave">保存</el-button>
</footer-btns>
</div>
</template>
<script lang="ts" setup>
import { getWxDevConfig, setWxDevConfig } from '@/api/channel/wx_dev'
import feedback from '@/utils/feedback'
const formData = reactive({
appId: '',
appSecret: ''
})
const getDetail = async () => {
const data = await getWxDevConfig()
for (const key in formData) {
//@ts-ignore
formData[key] = data[key]
}
}
const handelSave = async () => {
await setWxDevConfig(formData)
getDetail()
feedback.msgSuccess('操作成功')
}
getDetail()
</script>

View File

@ -0,0 +1,184 @@
<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" label-width="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="primaryId">
<div class="w-80">
<el-input v-model="formData.primaryId" placeholder="请输入原始ID" />
</div>
</el-form-item>
<el-form-item label="公众号二维码" prop="qrCode">
<div>
<div>
<material-picker v-model="formData.qrCode" :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="appId">
<div class="w-80">
<el-input v-model="formData.appId" placeholder="请输入AppID" />
</div>
</el-form-item>
<el-form-item label="AppSecret" prop="appSecret">
<div>
<div class="w-80">
<el-input v-model="formData.appSecret" placeholder="请输入AppSecret" />
</div>
</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>
<div class="flex">
<div class="mr-4 w-80">
<el-input v-model="formData.url" disabled />
</div>
<el-button @click="copy(formData.url)">复制</el-button>
</div>
<div class="form-tips">
登录微信公众平台点击开发>基本配置>服务器配置填写服务器地址URL
</div>
</div>
</el-form-item>
<el-form-item label="Token" prop="Token">
<div>
<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="EncodingAESKey">
<div>
<div class="w-80">
<el-input v-model="formData.encodingAesKey" 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="status">
<div>
<el-radio-group class="flex-col !items-start" v-model="formData.encryptionType">
<el-radio :label="1">
明文模式 (不使用消息体加解密功能安全系数较低)
</el-radio>
<el-radio :label="2">
兼容模式 (明文密文将共存方便开发者调试和维护)
</el-radio>
<el-radio :label="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>
<div class="flex">
<div class="mr-4 w-80">
<el-input v-model="formData.businessDomain" disabled />
</div>
<el-button @click="copy(formData.businessDomain)">复制</el-button>
</div>
<div class="form-tips">
登录微信公众平台点击设置>公众号设置>功能设置填写业务域名
</div>
</div>
</el-form-item>
<el-form-item label="JS接口安全域名">
<div>
<div class="flex">
<div class="mr-4 w-80">
<el-input v-model="formData.jsDomain" disabled />
</div>
<el-button @click="copy(formData.jsDomain)">复制</el-button>
</div>
<div class="form-tips">
登录微信公众平台点击设置>公众号设置>功能设置填写JS接口安全域名
</div>
</div>
</el-form-item>
<el-form-item label="网页授权域名">
<div>
<div class="flex">
<div class="mr-4 w-80">
<el-input v-model="formData.webDomain" disabled />
</div>
<el-button @click="copy(formData.webDomain)">复制</el-button>
</div>
<div class="form-tips">
登录微信公众平台点击设置>公众号设置>功能设置填写网页授权域名
</div>
</div>
</el-form-item>
</el-card>
</el-form>
<footer-btns v-perms="['channel:h5:save']">
<el-button type="primary" @click="handelSave">保存</el-button>
</footer-btns>
</div>
</template>
<script lang="ts" setup>
import { getOaConfig, setOaConfig } from '@/api/channel/wx_oa'
import feedback from '@/utils/feedback'
import { useClipboard } from '@vueuse/core'
const formData = reactive({
name: "",
primaryId: " ",
qrCode: "",
appId: "",
appSecret: "",
url: "",
token: "",
encodingAesKey: "",
encryptionType: 1,
businessDomain: "",
jsDomain: "",
webDomain: ""
})
const { copy } = useClipboard()
const getDetail = async () => {
const data = await getOaConfig()
for (const key in formData) {
//@ts-ignore
formData[key] = data[key]
}
}
const handelSave = async () => {
await setOaConfig(formData)
getDetail()
feedback.msgSuccess('操作成功')
}
getDetail()
</script>

View File

@ -0,0 +1,27 @@
<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">
<!-- Phone -->
<oa-phone></oa-phone>
</el-card>
<footer-btns v-perms="['channel:h5:save']">
<el-button type="primary" @click="">保存</el-button>
</footer-btns>
</div>
</template>
<script setup lang="ts">
import OaPhone from "./menu_component/oa-phone.vue"
</script>
<style lang="scss" scoped>
.menu-oa {}
</style>

View File

@ -0,0 +1 @@

View File

@ -0,0 +1,37 @@
<template>
<!-- Phone -->
<div class="oa-phone">
<div class="oa-phone-content"></div>
<div class="oa-phone-menu">
<div class="flex items-center justify-center oa-phone-switch">
<el-icon>
<Grid />
</el-icon>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.oa-phone {
width: 260px;
height: 460px;
border: 1px solid #E5E5EA;
&-content {
height: 420px;
border-bottom: 1px solid #E5E5EA;
}
&-menu {
height: 40px;
.oa-phone-switch {
width: 40px;
height: 100%;
border-right: 1px solid #E5E5EA;
}
}
}
</style>

View File

@ -0,0 +1,117 @@
<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 py-5 pl-20 mb-10">
<div class="mb-3 text-tx-regular">用户头像</div>
<el-avatar :src="formData.avatar" :size="58" />
</div>
<el-form-item label="用户编号:"> {{ formData.sn }} </el-form-item>
<el-form-item label="用户昵称:">
{{ formData.nickname }}
</el-form-item>
<el-form-item label="账号:">
{{ formData.username }}
<popover-input class="ml-[10px]" @confirm="handleEdit($event, 'username')">
<el-button type="primary" link v-perms="['user:edit']">
<icon name="el-icon-EditPen" />
</el-button>
</popover-input>
</el-form-item>
<el-form-item label="真实姓名:">
{{ formData.realName || '-' }}
<popover-input class="ml-[10px]" @confirm="handleEdit($event, 'realName')">
<el-button type="primary" link v-perms="['user:edit']">
<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: 0
}
]"
@confirm="handleEdit($event, 'sex')"
>
<el-button type="primary" link v-perms="['user:edit']">
<icon name="el-icon-EditPen" />
</el-button>
</popover-input>
</el-form-item>
<el-form-item label="联系电话:">
{{ formData.mobile || '-' }}
<popover-input class="ml-[10px]" @confirm="handleEdit($event, 'mobile')">
<el-button type="primary" link v-perms="['user:edit']">
<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.createTime }} </el-form-item>
<el-form-item label="最近登录时间:"> {{ formData.lastLoginTime }} </el-form-item>
</el-form>
</el-card>
</div>
</template>
<script lang="ts" setup>
import type { FormInstance } from 'element-plus'
import { getUserDetail, userEdit } from '@/api/consumer'
import feedback from '@/utils/feedback'
const route = useRoute()
const formData = reactive({
avatar: '',
channel: '',
createTime: '',
lastLoginIp: '',
lastLoginTime: '',
mobile: '',
nickname: '',
realName: 0,
sex: 0,
sn: '',
username: ''
})
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) => {
await userEdit({
id: route.query.id,
field,
value
})
feedback.msgSuccess('编辑成功')
getDetails()
}
getDetails()
</script>

View File

@ -0,0 +1,89 @@
<template>
<div>
<el-card class="!border-none" shadow="never">
<el-form ref="formRef" class="mb-[-16px]" :model="queryParams" :inline="true">
<el-form-item label="用户信息">
<el-input
class="w-56"
v-model="queryParams.keyword"
placeholder="用户编号/昵称/手机号码"
/>
</el-form-item>
<el-form-item label="注册时间">
<daterange-picker
v-model:startTime="queryParams.startTime"
v-model:endTime="queryParams.endTime"
/>
</el-form-item>
<el-form-item label="注册来源">
<el-select class="w-56" 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>
</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="sn" min-width="120" />
<el-table-column label="头像" min-width="100">
<template #default="{ row }">
<el-avatar :src="row.image" :size="50" />
</template>
</el-table-column>
<el-table-column label="昵称" prop="nickname" min-width="100" />
<el-table-column label="账号" prop="username" min-width="120" />
<el-table-column label="手机号码" prop="mobile" min-width="100" />
<el-table-column label="性别" prop="sex" min-width="100" />
<el-table-column label="注册来源" prop="channel" min-width="100" />
<el-table-column label="注册时间" prop="createTime" min-width="120" />
<el-table-column label="操作" width="120" fixed="right">
<template #default="{ row }">
<el-button v-perms="['user:detail']" type="primary" link>
<router-link
:to="{
path: getRoutePath('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>
import { usePaging } from '@/hooks/usePaging'
import { getRoutePath } from '@/router'
import { getUserList } from '@/api/consumer'
import { ClientMap } from '@/enums/appEnums'
const queryParams = reactive({
keyword: '',
channel: '',
startTime: '',
endTime: ''
})
const { pager, getLists, resetPage, resetParams } = usePaging({
fetchFun: getUserList,
params: queryParams
})
getLists()
</script>

View File

@ -0,0 +1,79 @@
<template>
<div>
<div>
<del-wrap
class="max-w-[400px]"
v-for="(item, index) in modelValue"
: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">
<span class="text-tx-regular flex-none mr-3">名称</span>
<el-input v-model="item.name" placeholder="请输入名称" />
</div>
<div class="flex mt-[18px]">
<span class="text-tx-regular flex-none mr-3">链接</span>
<link-picker v-model="item.link" />
</div>
</div>
</div>
</del-wrap>
</div>
<div>
<el-button type="primary" @click="handleAdd">添加</el-button>
</div>
</div>
</template>
<script lang="ts" setup>
import feedback from '@/utils/feedback'
import type { PropType } from 'vue'
const props = defineProps({
modelValue: {
type: Array as PropType<any[]>,
default: () => []
},
max: {
type: Number,
default: 10
},
min: {
type: Number,
default: 1
}
})
const handleAdd = () => {
if (props.modelValue?.length < props.max) {
props.modelValue.push({
image: '',
name: '',
link: {}
})
} else {
feedback.msgError(`最多添加${props.max}`)
}
}
const handleDelete = (index: number) => {
if (props.modelValue?.length <= props.min) {
return feedback.msgError(`最少保留${props.min}`)
}
props.modelValue.splice(index, 1)
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,28 @@
<template>
<div class="pages-setting">
<div
class="title flex items-center before:w-[3px] before:h-[14px] before:block before:bg-primary before:mr-2"
>
{{ widget?.title }}
</div>
<keep-alive>
<component
class="pt-5 pr-4"
:is="widgets[widget?.name]?.attr"
:content="widget?.content"
:styles="widget?.styles"
/>
</keep-alive>
</div>
</template>
<script lang="ts" setup>
import type { PropType } from 'vue'
import widgets from '../widgets'
const props = defineProps({
widget: {
type: Object as PropType<Record<string, any>>,
default: () => ({})
}
})
</script>

View File

@ -0,0 +1,44 @@
<template>
<el-menu
:default-active="modelValue"
class="w-[160px] min-h-[668px] pages-menu"
@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>
</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-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>

View File

@ -0,0 +1,66 @@
<template>
<div class="shadow mx-[30px] pages-preview">
<div
v-for="(widget, index) in pageData"
:key="widget"
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,
'border-br border-2': !widget?.disabled
}"
></div>
<slot>
<keep-alive>
<component
:is="widgets[widget?.name]?.content"
:content="widget.content"
:styles="widget.styles"
/>
</keep-alive>
</slot>
</div>
</div>
</template>
<script lang="ts" setup>
import widgets from '../widgets'
import type { PropType } from 'vue'
defineProps({
pageData: {
type: Array as PropType<any[]>,
default: () => []
},
modelValue: {
type: Number,
default: 0
}
})
const emit = defineEmits<{
(event: 'update:modelValue', value: number): void
}>()
const handleClick = (widget: any, index: number) => {
if (widget.disabled) return
emit('update:modelValue', index)
}
</script>
<style lang="scss" scoped>
.pages-preview {
background-color: #f8f8f8;
width: 360px;
height: 615px;
color: #333;
.select {
@apply border-primary border-solid;
}
}
</style>

View File

@ -0,0 +1,79 @@
<template>
<div>
<el-form label-width="70px">
<el-form-item label="是否启用">
<el-radio-group v-model="content.enabled">
<el-radio :label="1">开启</el-radio>
<el-radio :label="0">停用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="图片设置">
<div class="flex-1">
<div class="form-tips">最多添加5张建议图片尺寸750px*240px</div>
<del-wrap
v-for="(item, index) in content.data"
:key="index"
@close="handleDelete(index)"
class="max-w-[400px]"
>
<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>
</div>
</div>
</del-wrap>
</div>
</el-form-item>
<el-form-item v-if="content.data?.length < limit">
<el-button type="primary" @click="handleAdd">添加图片</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script lang="ts" setup>
import feedback from '@/utils/feedback'
import type { PropType } from 'vue'
import type options from './options'
const limit = 5
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 handleAdd = () => {
if (props.content.data?.length < limit) {
props.content.data.push({
image: '',
name: '',
link: {}
})
} else {
feedback.msgError(`最多添加${limit}张图片`)
}
}
const handleDelete = (index: number) => {
if (props.content.data?.length <= 1) {
return feedback.msgError('最少保留一张图片')
}
props.content.data.splice(index, 1)
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,33 @@
<template>
<div class="banner">
<div class="banner-image">
<image-contain width="100%" height="170px" :src="getImageUrl(getImage)" fit="contain" />
</div>
</div>
</template>
<script lang="ts" setup>
import useAppStore from '@/stores/modules/app'
import type { PropType } from '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 { getImageUrl } = useAppStore()
const getImage = computed(() => {
const { data } = props.content
if (Array.isArray(data)) {
return data[0] ? data[0].image : ''
}
return ''
})
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,8 @@
import attr from './attr.vue'
import content from './content.vue'
import options from './options'
export default {
attr,
content,
options
}

View File

@ -0,0 +1,15 @@
export default () => ({
title: '首页轮播图',
name: 'banner',
content: {
enabled: 1,
data: [
{
image: '',
name: '',
link: {}
}
]
},
styles: {}
})

View File

@ -0,0 +1,38 @@
<template>
<div>
<el-form label-width="90px">
<el-form-item label="客服标题">
<el-input class="w-[400px]" v-model="content.title" />
</el-form-item>
<el-form-item label="服务时间">
<el-input class="w-[400px]" v-model="content.time" />
</el-form-item>
<el-form-item label="联系电话">
<el-input class="w-[400px]" v-model="content.mobile" />
</el-form-item>
<el-form-item label="客服二维码">
<div>
<material-picker v-model="content.qrcode" exclude-domain />
<div class="form-tips">建议图片尺寸200*200像素图片格式jpgpngjpeg</div>
</div>
</el-form-item>
</el-form>
</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>

View File

@ -0,0 +1,40 @@
<template>
<div class="customer-service">
<image-contain width="140px" height="140px" :src="getImageUrl(content.qrcode)" alt="" />
<div class="text-[15px] mt-[7px] font-medium">{{ content.title }}</div>
<div class="text-[#666] mt-[20px]">服务时间{{ content.time }}</div>
<div class="text-[#666] mt-[7px]">客服电话{{ content.mobile }}</div>
<div
class="text-white text-[16px] rounded-[42px] bg-[#4173FF] w-full h-[42px] flex justify-center items-center mt-[50px]"
>
保存二维码图片
</div>
</div>
</template>
<script lang="ts" setup>
import useAppStore from '@/stores/modules/app'
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: () => ({})
}
})
const { getImageUrl } = useAppStore()
</script>
<style lang="scss" scoped>
.customer-service {
margin: 10px 18px;
border-radius: 10px;
padding: 50px 55px 80px;
background: #fff;
@apply flex flex-col justify-center items-center;
}
</style>

View File

@ -0,0 +1,8 @@
import attr from './attr.vue'
import content from './content.vue'
import options from './options'
export default {
attr,
content,
options
}

View File

@ -0,0 +1,11 @@
export default () => ({
title: '客服设置',
name: 'customer-service',
content: {
title: '添加客服二维码',
time: '',
mobile: '',
qrcode: ''
},
styles: {}
})

View File

@ -0,0 +1,14 @@
const widgets: Record<string, any> = import.meta.glob('./**/index.ts', { eager: true })
interface Widget {
attr: any
content: any
options: any
}
console.log(widgets)
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

View File

@ -0,0 +1,38 @@
<template>
<div>
<el-form label-width="70px">
<el-form-item label="排版样式">
<el-radio-group v-model="content.style">
<el-radio :label="1">横排</el-radio>
<el-radio :label="2">竖排</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="标题名称">
<el-input class="w-[400px]" v-model="content.title" />
</el-form-item>
<el-form-item label="菜单设置">
<div class="flex-1">
<AddNav v-model="content.data" />
</div>
</el-form-item>
</el-form>
</div>
</template>
<script lang="ts" setup>
import type { PropType } from 'vue'
import type options from './options'
import AddNav from '../../add-nav.vue'
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>

View File

@ -0,0 +1,60 @@
<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 content.data"
:key="index"
class="flex flex-col items-center w-1/4 mb-[15px]"
>
<image-contain 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 content.data"
:key="index"
class="flex items-center border-b border-[#e5e5e5] h-[50px] px-[12px]"
>
<image-contain width="24px" height="24px" :src="getImageUrl(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 useAppStore from '@/stores/modules/app'
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: () => ({})
}
})
const { getImageUrl } = useAppStore()
</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>

View File

@ -0,0 +1,8 @@
import attr from './attr.vue'
import content from './content.vue'
import options from './options'
export default {
attr,
content,
options
}

View File

@ -0,0 +1,16 @@
export default () => ({
title: '我的服务',
name: 'my-service',
content: {
style: 1,
title: '我的服务',
data: [
{
image: '',
name: '',
link: {}
}
]
},
styles: {}
})

View File

@ -0,0 +1,36 @@
<template>
<div>
<el-form label-width="70px">
<el-form-item label="是否启用">
<el-radio-group v-model="content.enabled">
<el-radio :label="1">开启</el-radio>
<el-radio :label="0">停用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="菜单设置">
<div class="flex-1">
<div class="form-tips mb-4">最多可添加10个建议图片尺寸100px*100px</div>
<AddNav v-model="content.data" />
</div>
</el-form-item>
</el-form>
</div>
</template>
<script lang="ts" setup>
import type { PropType } from 'vue'
import type options from './options'
import AddNav from '../../add-nav.vue'
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>

View File

@ -0,0 +1,37 @@
<template>
<div class="nav pt-[15px] pb-[8px]">
<div class="flex flex-wrap">
<div
v-for="(item, index) in content.data"
:key="index"
class="flex flex-col items-center w-1/5 mb-[15px]"
>
<image-contain width="41px" height="41px" :src="getImageUrl(item.image)" alt="" />
<div class="mt-[7px]">{{ item.name }}</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import useAppStore from '@/stores/modules/app'
import type { PropType } from '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 { getImageUrl } = useAppStore()
</script>
<style lang="scss" scoped>
.nav {
background-color: #fff;
}
</style>

View File

@ -0,0 +1,8 @@
import attr from './attr.vue'
import content from './content.vue'
import options from './options'
export default {
attr,
content,
options
}

View File

@ -0,0 +1,15 @@
export default () => ({
title: '导航菜单',
name: 'nav',
content: {
enabled: 1,
data: [
{
image: '',
name: '导航',
link: {}
}
]
},
styles: {}
})

View File

@ -0,0 +1,20 @@
<template>
<div></div>
</template>
<script lang="ts" setup>
import type { PropType } from '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: () => ({})
}
})
</script>
<style lang="scss" scoped></style>

View File

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

View File

@ -0,0 +1,8 @@
import attr from './attr.vue'
import content from './content.vue'
import options from './options'
export default {
attr,
content,
options
}

View File

@ -0,0 +1,7 @@
export default () => ({
title: '搜索',
name: 'search',
disabled: 1,
content: {},
styles: {}
})

View File

@ -0,0 +1,79 @@
<template>
<div>
<el-form label-width="70px">
<el-form-item label="是否启用">
<el-radio-group v-model="content.enabled">
<el-radio :label="1">开启</el-radio>
<el-radio :label="0">停用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="图片设置">
<div class="flex-1">
<div class="form-tips">最多添加5张建议图片尺寸750px*200px</div>
<del-wrap
v-for="(item, index) in content.data"
:key="index"
@close="handleDelete(index)"
class="max-w-[400px]"
>
<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>
</div>
</div>
</del-wrap>
</div>
</el-form-item>
<el-form-item v-if="content.data?.length < limit">
<el-button type="primary" @click="handleAdd">添加图片</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script lang="ts" setup>
import feedback from '@/utils/feedback'
import type { PropType } from 'vue'
import type options from './options'
const limit = 5
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 handleAdd = () => {
if (props.content.data?.length < limit) {
props.content.data.push({
image: '',
name: '',
link: {}
})
} else {
feedback.msgError(`最多添加${limit}张图片`)
}
}
const handleDelete = (index: number) => {
if (props.content.data?.length <= 1) {
return feedback.msgError('最少保留一张图片')
}
props.content.data.splice(index, 1)
}
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,33 @@
<template>
<div class="banner mx-[10px] mt-[10px]">
<div class="banner-image">
<image-contain width="100%" height="100px" :src="getImageUrl(getImage)" fit="contain" />
</div>
</div>
</template>
<script lang="ts" setup>
import useAppStore from '@/stores/modules/app'
import type { PropType } from '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 { getImageUrl } = useAppStore()
const getImage = computed(() => {
const { data } = props.content
if (Array.isArray(data)) {
return data[0] ? data[0].image : ''
}
return ''
})
</script>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,8 @@
import attr from './attr.vue'
import content from './content.vue'
import options from './options'
export default {
attr,
content,
options
}

View File

@ -0,0 +1,15 @@
export default () => ({
title: '个人中心广告图',
name: 'user-banner',
content: {
enabled: 1,
data: [
{
image: '',
name: '',
link: {}
}
]
},
styles: {}
})

View File

@ -0,0 +1,20 @@
<template>
<div></div>
</template>
<script lang="ts" setup>
import type { PropType } from '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: () => ({})
}
})
</script>
<style lang="scss" scoped></style>

View File

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

View File

@ -0,0 +1,8 @@
import attr from './attr.vue'
import content from './content.vue'
import options from './options'
export default {
attr,
content,
options
}

View File

@ -0,0 +1,7 @@
export default () => ({
title: '用户信息',
name: 'user-info',
disabled: 1,
content: {},
styles: {}
})

View File

@ -0,0 +1,97 @@
<template>
<div class="decoration-pages min-w-[1100px]">
<el-card shadow="never" class="!border-none flex-1" :body-style="{ height: '100%' }">
<div class="flex h-full items-start">
<Menu v-model="activeMenu" :menus="menus" />
<preview v-model="selectWidgetIndex" :pageData="getPageData" />
<attr-setting class="flex-1" :widget="getSelectWidget" />
</div>
</el-card>
<footer-btns class="mt-4" :fixed="false">
<el-button type="primary" @click="setData">保存</el-button>
</footer-btns>
</div>
</template>
<script lang="ts" setup>
import Menu from '../component/pages/menu.vue'
import Preview from '../component/pages/preview.vue'
import AttrSetting from '../component/pages/attr-setting.vue'
import widgets from '../component/widgets'
import { getDecoratePages, setDecoratePages } from '@/api/decoration'
import feedback from '@/utils/feedback'
enum pagesTypeEnum {
HOME = '1',
USER = '2',
SERVICE = '3'
}
const generatePageData = (widgetNames: string[]) => {
return widgetNames.map((widgetName) => widgets[widgetName]?.options() || {})
}
const menus: Record<
string,
{
id: number
name: string
pageData: any[]
}
> = reactive({
[pagesTypeEnum.HOME]: {
id: 1,
pageType: 1,
name: '商城首页',
pageData: generatePageData(['search', 'banner', 'nav'])
},
[pagesTypeEnum.USER]: {
id: 2,
pageType: 2,
name: '个人中心',
pageData: generatePageData(['user-info', 'my-service', 'user-banner'])
},
[pagesTypeEnum.SERVICE]: {
id: 3,
pageType: 3,
name: '客服设置',
pageData: generatePageData(['customer-service'])
}
})
const activeMenu = ref('1')
const selectWidgetIndex = ref(-1)
const getPageData = computed(() => {
return menus[activeMenu.value]?.pageData ?? []
})
const getSelectWidget = computed(() => {
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.pageData)
}
const setData = async () => {
await setDecoratePages({
...menus[activeMenu.value],
pageData: JSON.stringify(menus[activeMenu.value].pageData)
})
getData()
feedback.msgSuccess('保存成功')
}
watch(
activeMenu,
() => {
selectWidgetIndex.value = getPageData.value.findIndex((item) => !item.disabled)
getData()
},
{
immediate: true
}
)
</script>
<style lang="scss" scoped>
.decoration-pages {
min-height: calc(100vh - var(--navbar-height) - 80px);
@apply flex flex-col;
}
</style>

View File

@ -0,0 +1,182 @@
<template>
<div class="decoration-tabbar min-w-[800px]">
<el-card shadow="never" class="!border-none flex-1" :body-style="{ height: '100%' }">
<div class="flex h-full items-start">
<div class="pages-preview mx-[30px]">
<div class="tabbar flex">
<div
class="tabbar-item flex flex-col justify-center items-center flex-1"
v-for="(item, index) in tabbar.list"
:key="index"
:style="{ color: tabbar.style.defaultColor }"
>
<img class="w-[22px] h-[22px]" :src="item.unselected" alt="" />
<div class="leading-3 text-[12px] mt-[4px]">{{ item.name }}</div>
</div>
</div>
</div>
<div class="flex-1">
<div
class="title flex items-center before:w-[3px] before:h-[14px] before:block before:bg-primary before:mr-2"
>
底部导航设置
<span class="form-tips ml-[10px] !mt-0">
至少添加2个导航最多添加5个导航
</span>
</div>
<el-form label-width="70px">
<el-tabs model-value="content">
<el-tab-pane label="导航图片" name="content">
<div class="mb-[18px]">
<del-wrap
v-for="(item, index) in tabbar.list"
:key="index"
@close="handleDelete(index)"
class="max-w-[400px]"
>
<div class="bg-fill-light w-full p-4 mt-4">
<el-form-item label="导航图标">
<material-picker
v-model="item.unselected"
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>
<material-picker
v-model="item.selected"
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="item.name"
placeholder="请输入名称"
/>
</el-form-item>
<el-form-item label="链接地址">
<link-picker v-model="item.link" />
</el-form-item>
</div>
</del-wrap>
</div>
<el-form-item v-if="tabbar.list?.length < max" label-width="0">
<el-button type="primary" @click="handleAdd">
添加导航
</el-button>
</el-form-item>
</el-tab-pane>
<el-tab-pane label="样式设置" name="styles">
<el-form-item label="默认颜色">
<color-picker
class="max-w-[400px]"
v-model="tabbar.style.defaultColor"
/>
</el-form-item>
<el-form-item label="选中颜色">
<color-picker
class="max-w-[400px]"
v-model="tabbar.style.selectedColor"
/>
</el-form-item>
</el-tab-pane>
</el-tabs>
</el-form>
</div>
</div>
</el-card>
<footer-btns class="mt-4" :fixed="false">
<el-button type="primary" @click="setData">保存</el-button>
</footer-btns>
</div>
</template>
<script lang="ts" setup>
import { getDecorateTabbar, setDecorateTabbar } from '@/api/decoration'
import feedback from '@/utils/feedback'
const max = 5
const min = 2
const tabbar = reactive({
style: {
defaultColor: '',
selectedColor: ''
},
list: [
{
name: '',
selected: '',
unselected: '',
link: {}
}
]
})
const handleAdd = () => {
if (tabbar.list?.length < max) {
tabbar.list.push({
name: '',
selected: '',
unselected: '',
link: {}
})
} else {
feedback.msgError(`最多添加${max}`)
}
}
const handleDelete = (index: number) => {
if (tabbar.list?.length <= min) {
return feedback.msgError(`最少保留${min}`)
}
tabbar.list.splice(index, 1)
}
const getData = async () => {
const data = await getDecorateTabbar()
tabbar.list = data.list
tabbar.style = data.style
}
const setData = async () => {
await setDecorateTabbar(toRaw(tabbar))
getData()
feedback.msgSuccess('保存成功')
}
getData()
</script>
<style lang="scss" scoped>
.decoration-tabbar {
min-height: calc(100vh - var(--navbar-height) - 80px);
@apply flex flex-col;
.pages-preview {
background-color: #f7f7f7;
width: 360px;
height: 615px;
color: #333;
position: relative;
.tabbar {
position: absolute;
height: 50px;
background-color: #fff;
bottom: 0;
width: 100%;
border: 2px solid var(--el-color-primary);
}
}
}
</style>

View File

@ -73,15 +73,15 @@
<el-button type="primary" link v-perms="['gen:editTable']">
<router-link
:to="{
path: '/dev_tools/code/edit',
path: getRoutePath('gen:editTable'),
query: {
id: row.id
}
}"
>
编辑
</router-link></el-button
>
</router-link>
</el-button>
<el-dropdown
class="ml-2"
@command="handleCommand($event, row)"
@ -152,6 +152,7 @@ import DataTable from '../components/data-table.vue'
import CodePreview from '../components/code-preview.vue'
import feedback from '@/utils/feedback'
import { streamFileDownload } from '@/utils/file'
import { getRoutePath } from '@/router'
const formData = reactive({
tableName: '',

View File

@ -0,0 +1,121 @@
<template>
<div>
<el-card class="!border-none" shadow="never">
<el-page-header content="编辑通知设置" @back="$router.back()" />
</el-card>
<el-form
ref="formRef"
:model="formData"
label-width="120px"
:rules="rules"
v-loading="loading"
>
<el-card class="!border-none mt-4" shadow="never">
<div class="font-medium mb-7">通知名称</div>
<el-form-item label="通知名称" prop="name"> {{ formData.name }} </el-form-item>
<el-form-item label="通知类型" prop="name"> {{ formData.type }} </el-form-item>
<el-form-item label="通知业务" prop="name"> {{ formData.remarks }} </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="开启状态" prop="smsNotice.status" required>
<el-radio-group v-model="formData.smsNotice.status">
<el-radio label="0">关闭</el-radio>
<el-radio label="1">开启</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="模板ID" prop="smsNotice.templateId">
<div class="w-80">
<el-input
v-model="formData.smsNotice.templateId"
placeholder="请输入模板ID"
/>
</div>
</el-form-item>
<el-form-item label="短信内容" prop="smsNotice.content">
<div class="flex-1">
<div class="w-full max-w-[320px]">
<el-input
type="textarea"
:rows="6"
v-model="formData.smsNotice.content"
/>
</div>
<div class="form-tips">
可选变量 用户昵称:nickname 订单编号:order_sn 支付时间:pay_time
<br />
示例亲爱的${nickname}您的订单${order_sn}已支付成功商家正在快马加鞭为您安排发货
<br />
生效条件1管理后台完成短信设置2第三方短信平台申请模板
</div>
</div>
</el-form-item>
</el-card>
</el-form>
<footer-btns>
<el-button type="primary" @click="handleSave">保存</el-button>
</footer-btns>
</div>
</template>
<script lang="ts" setup>
import type { FormInstance } from 'element-plus'
import feedback from '@/utils/feedback'
import { noticeDetail, setNoticeConfig } from '@/api/message'
const route = useRoute()
const router = useRouter()
const loading = ref(false)
const formData = reactive({
id: '',
name: '',
type: '',
remarks: '',
smsNotice: {
status: 0,
templateId: '',
content: ''
}
})
const rules = {
'smsNotice.templateId': [
{
required: true,
message: '请输入模板ID',
trigger: 'blur'
}
],
'smsNotice.content': [
{
required: true,
message: '请输入短信内容',
trigger: 'blur'
}
]
}
const formRef = shallowRef<FormInstance>()
const getDetails = async () => {
loading.value = true
const data = await noticeDetail({
id: route.query.id
})
Object.keys(data).forEach((key) => {
//@ts-ignore
formData[key] = data[key]
})
loading.value = false
}
const handleSave = async () => {
await formRef.value?.validate()
await setNoticeConfig(formData)
feedback.msgSuccess('操作成功')
router.back()
}
route.query.id && getDetails()
</script>

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