Merge branch 'release/1.2.1'
This commit is contained in:
commit
3868962b1e
|
|
@ -1,4 +1,3 @@
|
|||
.vscode
|
||||
.idea
|
||||
*.iws
|
||||
*.iml
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
NODE_ENV = 'development'
|
||||
|
||||
# Base API
|
||||
# 请求域名
|
||||
VITE_APP_BASE_URL='https://likeadmin-java.yixiangonline.com'
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
NODE_ENV = 'production'
|
||||
# Base API
|
||||
|
||||
# 请求域名
|
||||
VITE_APP_BASE_URL=''
|
||||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -30,7 +30,3 @@ components.d.ts
|
|||
*.sln
|
||||
*.sw?
|
||||
|
||||
# lock
|
||||
|
||||
package-lock.json
|
||||
yarn.lock
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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 })
|
||||
}
|
||||
|
|
@ -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' })
|
||||
}
|
||||
|
|
@ -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' })
|
||||
}
|
||||
|
|
@ -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' })
|
||||
}
|
||||
|
|
@ -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' })
|
||||
}
|
||||
|
|
@ -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 })
|
||||
}
|
||||
|
|
@ -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 })
|
||||
}
|
||||
|
|
@ -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 })
|
||||
}
|
||||
|
|
@ -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 })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -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 })
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@
|
|||
*/
|
||||
|
||||
import useUserStore from '@/stores/modules/user'
|
||||
|
||||
export default {
|
||||
mounted: (el: HTMLElement, binding: any) => {
|
||||
const { value } = binding
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -78,6 +78,6 @@ router.beforeEach(async (to, from, next) => {
|
|||
})
|
||||
|
||||
router.afterEach(() => {
|
||||
// console.log(router.getRoutes())
|
||||
console.log(router.getRoutes())
|
||||
NProgress.done()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,9 @@ const useAppStore = defineStore({
|
|||
}
|
||||
},
|
||||
actions: {
|
||||
getImageUrl(url: string) {
|
||||
return url ? `${this.config.ossDomain}${url}` : ''
|
||||
},
|
||||
getConfig() {
|
||||
return new Promise((resolve, reject) => {
|
||||
getConfig()
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 数据
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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*高400px。jpg,jpeg,png格式</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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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*高400px。jpg,jpeg,png格式</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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1 @@
|
|||
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
import attr from './attr.vue'
|
||||
import content from './content.vue'
|
||||
import options from './options'
|
||||
export default {
|
||||
attr,
|
||||
content,
|
||||
options
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
export default () => ({
|
||||
title: '首页轮播图',
|
||||
name: 'banner',
|
||||
content: {
|
||||
enabled: 1,
|
||||
data: [
|
||||
{
|
||||
image: '',
|
||||
name: '',
|
||||
link: {}
|
||||
}
|
||||
]
|
||||
},
|
||||
styles: {}
|
||||
})
|
||||
|
|
@ -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像素;图片格式:jpg、png、jpeg</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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
import attr from './attr.vue'
|
||||
import content from './content.vue'
|
||||
import options from './options'
|
||||
export default {
|
||||
attr,
|
||||
content,
|
||||
options
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
export default () => ({
|
||||
title: '客服设置',
|
||||
name: 'customer-service',
|
||||
content: {
|
||||
title: '添加客服二维码',
|
||||
time: '',
|
||||
mobile: '',
|
||||
qrcode: ''
|
||||
},
|
||||
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
|
||||
}
|
||||
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
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
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,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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
import attr from './attr.vue'
|
||||
import content from './content.vue'
|
||||
import options from './options'
|
||||
export default {
|
||||
attr,
|
||||
content,
|
||||
options
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
export default () => ({
|
||||
title: '导航菜单',
|
||||
name: 'nav',
|
||||
content: {
|
||||
enabled: 1,
|
||||
data: [
|
||||
{
|
||||
image: '',
|
||||
name: '导航',
|
||||
link: {}
|
||||
}
|
||||
]
|
||||
},
|
||||
styles: {}
|
||||
})
|
||||
|
|
@ -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>
|
||||
|
|
@ -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,8 @@
|
|||
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,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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
import attr from './attr.vue'
|
||||
import content from './content.vue'
|
||||
import options from './options'
|
||||
export default {
|
||||
attr,
|
||||
content,
|
||||
options
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
export default () => ({
|
||||
title: '个人中心广告图',
|
||||
name: 'user-banner',
|
||||
content: {
|
||||
enabled: 1,
|
||||
data: [
|
||||
{
|
||||
image: '',
|
||||
name: '',
|
||||
link: {}
|
||||
}
|
||||
]
|
||||
},
|
||||
styles: {}
|
||||
})
|
||||
|
|
@ -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>
|
||||
|
|
@ -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,8 @@
|
|||
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: {}
|
||||
})
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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: '',
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in New Issue