mirror of
https://gitee.com/hhyykk/ipms-sjy-ui.git
synced 2025-02-01 19:24:58 +08:00
Merge branch 'dev' of https://gitee.com/yudaocode/yudao-ui-admin-vue3
This commit is contained in:
commit
eb9f8c9f6f
@ -1,13 +1,14 @@
|
||||
import request from '@/config/axios'
|
||||
|
||||
export interface CustomerVO {
|
||||
id: number
|
||||
id?: number
|
||||
name: string
|
||||
industryId: number
|
||||
level: number
|
||||
source: number
|
||||
followUpStatus: boolean
|
||||
lockStatus: boolean
|
||||
followUpStatus?: boolean
|
||||
lockStatus?: boolean
|
||||
dealStatus?: boolean
|
||||
mobile: string
|
||||
telephone: string
|
||||
website: string
|
||||
@ -16,13 +17,20 @@ export interface CustomerVO {
|
||||
email: string
|
||||
description: string
|
||||
remark: string
|
||||
ownerUserId: number
|
||||
roUserIds: string
|
||||
rwUserIds: string
|
||||
areaId: number
|
||||
ownerUserId?: number
|
||||
ownerUserName?: string
|
||||
ownerUserDept?: string
|
||||
roUserIds?: string
|
||||
rwUserIds?: string
|
||||
areaId?: number
|
||||
areaName?: string
|
||||
detailAddress: string
|
||||
contactLastTime: Date
|
||||
contactLastTime?: Date
|
||||
contactNextTime: Date
|
||||
createTime?: Date
|
||||
updateTime?: Date
|
||||
creator?: string
|
||||
creatorName?: string
|
||||
}
|
||||
|
||||
// 查询客户列表
|
||||
|
@ -1,5 +1,6 @@
|
||||
import request from '@/config/axios'
|
||||
|
||||
// TODO @zange:挪到 product 下,建个 category 包,挪进去哈;
|
||||
export interface ProductCategoryVO {
|
||||
id: number
|
||||
name: string
|
||||
|
@ -27,6 +27,11 @@ export const getTenantIdByName = (name: string) => {
|
||||
return request.get({ url: '/system/tenant/get-id-by-name?name=' + name })
|
||||
}
|
||||
|
||||
// 使用租户域名,获得租户信息
|
||||
export const getTenantByWebsite = (website: string) => {
|
||||
return request.get({ url: '/system/tenant/get-by-website?website=' + website })
|
||||
}
|
||||
|
||||
// 登出
|
||||
export const loginOut = () => {
|
||||
return request.post({ url: '/system/auth/logout' })
|
||||
|
45
src/api/mall/promotion/diy/page.ts
Normal file
45
src/api/mall/promotion/diy/page.ts
Normal file
@ -0,0 +1,45 @@
|
||||
import request from '@/config/axios'
|
||||
|
||||
export interface DiyPageVO {
|
||||
id?: number
|
||||
templateId?: number
|
||||
name: string
|
||||
remark: string
|
||||
previewImageUrls: string[]
|
||||
property: string
|
||||
}
|
||||
|
||||
// 查询装修页面列表
|
||||
export const getDiyPagePage = async (params: any) => {
|
||||
return await request.get({ url: `/promotion/diy-page/page`, params })
|
||||
}
|
||||
|
||||
// 查询装修页面详情
|
||||
export const getDiyPage = async (id: number) => {
|
||||
return await request.get({ url: `/promotion/diy-page/get?id=` + id })
|
||||
}
|
||||
|
||||
// 新增装修页面
|
||||
export const createDiyPage = async (data: DiyPageVO) => {
|
||||
return await request.post({ url: `/promotion/diy-page/create`, data })
|
||||
}
|
||||
|
||||
// 修改装修页面
|
||||
export const updateDiyPage = async (data: DiyPageVO) => {
|
||||
return await request.put({ url: `/promotion/diy-page/update`, data })
|
||||
}
|
||||
|
||||
// 删除装修页面
|
||||
export const deleteDiyPage = async (id: number) => {
|
||||
return await request.delete({ url: `/promotion/diy-page/delete?id=` + id })
|
||||
}
|
||||
|
||||
// 获得装修页面属性
|
||||
export const getDiyPageProperty = async (id: number) => {
|
||||
return await request.get({ url: `/promotion/diy-page/get-property?id=` + id })
|
||||
}
|
||||
|
||||
// 更新装修页面属性
|
||||
export const updateDiyPageProperty = async (data: DiyPageVO) => {
|
||||
return await request.put({ url: `/promotion/diy-page/update-property`, data })
|
||||
}
|
58
src/api/mall/promotion/diy/template.ts
Normal file
58
src/api/mall/promotion/diy/template.ts
Normal file
@ -0,0 +1,58 @@
|
||||
import request from '@/config/axios'
|
||||
import { DiyPageVO } from '@/api/mall/promotion/diy/page'
|
||||
|
||||
export interface DiyTemplateVO {
|
||||
id?: number
|
||||
name: string
|
||||
used: boolean
|
||||
usedTime?: Date
|
||||
remark: string
|
||||
previewImageUrls: string[]
|
||||
property: string
|
||||
}
|
||||
|
||||
export interface DiyTemplatePropertyVO extends DiyTemplateVO {
|
||||
pages: DiyPageVO[]
|
||||
}
|
||||
|
||||
// 查询装修模板列表
|
||||
export const getDiyTemplatePage = async (params: any) => {
|
||||
return await request.get({ url: `/promotion/diy-template/page`, params })
|
||||
}
|
||||
|
||||
// 查询装修模板详情
|
||||
export const getDiyTemplate = async (id: number) => {
|
||||
return await request.get({ url: `/promotion/diy-template/get?id=` + id })
|
||||
}
|
||||
|
||||
// 新增装修模板
|
||||
export const createDiyTemplate = async (data: DiyTemplateVO) => {
|
||||
return await request.post({ url: `/promotion/diy-template/create`, data })
|
||||
}
|
||||
|
||||
// 修改装修模板
|
||||
export const updateDiyTemplate = async (data: DiyTemplateVO) => {
|
||||
return await request.put({ url: `/promotion/diy-template/update`, data })
|
||||
}
|
||||
|
||||
// 删除装修模板
|
||||
export const deleteDiyTemplate = async (id: number) => {
|
||||
return await request.delete({ url: `/promotion/diy-template/delete?id=` + id })
|
||||
}
|
||||
|
||||
// 使用装修模板
|
||||
export const useDiyTemplate = async (id: number) => {
|
||||
return await request.put({ url: `/promotion/diy-template/use?id=` + id })
|
||||
}
|
||||
|
||||
// 获得装修模板属性
|
||||
export const getDiyTemplateProperty = async (id: number) => {
|
||||
return await request.get<DiyTemplatePropertyVO>({
|
||||
url: `/promotion/diy-template/get-property?id=` + id
|
||||
})
|
||||
}
|
||||
|
||||
// 更新装修模板属性
|
||||
export const updateDiyTemplateProperty = async (data: DiyTemplateVO) => {
|
||||
return await request.put({ url: `/promotion/diy-template/update-property`, data })
|
||||
}
|
25
src/api/pay/demo/transfer/index.ts
Normal file
25
src/api/pay/demo/transfer/index.ts
Normal file
@ -0,0 +1,25 @@
|
||||
import request from '@/config/axios'
|
||||
|
||||
export interface DemoTransferVO {
|
||||
price: number
|
||||
type: number
|
||||
userName: string
|
||||
alipayLogonId: string
|
||||
openid: string
|
||||
}
|
||||
|
||||
// 创建示例转账单
|
||||
export function createDemoTransfer(data: DemoTransferVO) {
|
||||
return request.post({
|
||||
url: '/pay/demo-transfer/create',
|
||||
data: data
|
||||
})
|
||||
}
|
||||
|
||||
// 获得示例订单分页
|
||||
export function getDemoTransferPage(query: PageParam) {
|
||||
return request.get({
|
||||
url: '/pay/demo-transfer/page',
|
||||
params: query
|
||||
})
|
||||
}
|
27
src/api/pay/transfer/index.ts
Normal file
27
src/api/pay/transfer/index.ts
Normal file
@ -0,0 +1,27 @@
|
||||
import request from '@/config/axios'
|
||||
|
||||
export interface TransferVO {
|
||||
appId: number
|
||||
channelCode: string
|
||||
merchantTransferId: string
|
||||
type: number
|
||||
price: number
|
||||
subject: string
|
||||
userName: string
|
||||
alipayLogonId: string
|
||||
openid: string
|
||||
}
|
||||
|
||||
// 新增转账单
|
||||
export const createTransfer = async (data: TransferVO) => {
|
||||
return await request.post({ url: `/pay/transfer/create`, data })
|
||||
}
|
||||
|
||||
// 查询转账单列表
|
||||
export const getTransferPage = async (params) => {
|
||||
return await request.get({ url: `/pay/transfer/page`, params })
|
||||
}
|
||||
|
||||
export const getTransfer = async (id: number) => {
|
||||
return await request.get({ url: '/pay/transfer/get?id=' + id })
|
||||
}
|
BIN
src/assets/imgs/diy/statusBar.png
Normal file
BIN
src/assets/imgs/diy/statusBar.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8.7 KiB |
54
src/components/ColorInput/index.vue
Normal file
54
src/components/ColorInput/index.vue
Normal file
@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<el-input v-model="color">
|
||||
<template #prepend>
|
||||
<el-color-picker v-model="color" :predefine="COLORS" />
|
||||
</template>
|
||||
</el-input>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
|
||||
// 颜色输入框
|
||||
defineOptions({ name: 'ColorInput' })
|
||||
|
||||
// 预设颜色
|
||||
const COLORS = [
|
||||
'#ff4500',
|
||||
'#ff8c00',
|
||||
'#ffd700',
|
||||
'#90ee90',
|
||||
'#00ced1',
|
||||
'#1e90ff',
|
||||
'#c71585',
|
||||
'#409EFF',
|
||||
'#909399',
|
||||
'#C0C4CC',
|
||||
'#b7390b',
|
||||
'#ff7800',
|
||||
'#fad400',
|
||||
'#5b8c5f',
|
||||
'#00babd',
|
||||
'#1f73c3',
|
||||
'#711f57'
|
||||
]
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: propTypes.string.def('')
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const color = computed({
|
||||
get: () => {
|
||||
return props.modelValue
|
||||
},
|
||||
set: (val: string) => {
|
||||
emit('update:modelValue', val)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
:deep(.el-input-group__prepend) {
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
222
src/components/DiyEditor/components/ComponentContainer.vue
Normal file
222
src/components/DiyEditor/components/ComponentContainer.vue
Normal file
@ -0,0 +1,222 @@
|
||||
<template>
|
||||
<div :class="['component', { active: active }]">
|
||||
<div
|
||||
:style="{
|
||||
...style
|
||||
}"
|
||||
>
|
||||
<component :is="component.id" :property="component.property" />
|
||||
</div>
|
||||
<div class="component-wrap">
|
||||
<!-- 左侧组件名 -->
|
||||
<div class="component-name" v-if="component.name">
|
||||
{{ component.name }}
|
||||
</div>
|
||||
<!-- 左侧:组件操作工具栏 -->
|
||||
<div class="component-toolbar" v-if="showToolbar && component.name && active">
|
||||
<VerticalButtonGroup type="primary">
|
||||
<el-tooltip content="上移" placement="right">
|
||||
<el-button :disabled="!canMoveUp" @click.stop="handleMoveComponent(-1)">
|
||||
<Icon icon="ep:arrow-up" />
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="下移" placement="right">
|
||||
<el-button :disabled="!canMoveDown" @click.stop="handleMoveComponent(1)">
|
||||
<Icon icon="ep:arrow-down" />
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="复制" placement="right">
|
||||
<el-button @click.stop="handleCopyComponent()">
|
||||
<Icon icon="ep:copy-document" />
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="删除" placement="right">
|
||||
<el-button @click.stop="handleDeleteComponent()">
|
||||
<Icon icon="ep:delete" />
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</VerticalButtonGroup>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
// 注册所有的组件
|
||||
import { components } from '../components/mobile/index'
|
||||
export default {
|
||||
components: { ...components }
|
||||
}
|
||||
</script>
|
||||
<script setup lang="ts">
|
||||
import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { object } from 'vue-types'
|
||||
|
||||
/**
|
||||
* 组件容器
|
||||
* 用于包裹组件,为组件提供 背景、外边距、内边距、边框等样式
|
||||
*/
|
||||
defineOptions({ name: 'ComponentContainer' })
|
||||
|
||||
type DiyComponentWithStyle = DiyComponent<any> & { property: { style?: ComponentStyle } }
|
||||
const props = defineProps({
|
||||
component: object<DiyComponentWithStyle>().isRequired,
|
||||
active: propTypes.bool.def(false),
|
||||
canMoveUp: propTypes.bool.def(false),
|
||||
canMoveDown: propTypes.bool.def(false),
|
||||
showToolbar: propTypes.bool.def(true)
|
||||
})
|
||||
|
||||
/**
|
||||
* 组件样式
|
||||
*/
|
||||
const style = computed(() => {
|
||||
let componentStyle = props.component.property.style
|
||||
if (!componentStyle) {
|
||||
return {}
|
||||
}
|
||||
return {
|
||||
marginTop: `${componentStyle.marginTop || 0}px`,
|
||||
marginBottom: `${componentStyle.marginBottom || 0}px`,
|
||||
marginLeft: `${componentStyle.marginLeft || 0}px`,
|
||||
marginRight: `${componentStyle.marginRight || 0}px`,
|
||||
paddingTop: `${componentStyle.paddingTop || 0}px`,
|
||||
paddingRight: `${componentStyle.paddingRight || 0}px`,
|
||||
paddingBottom: `${componentStyle.paddingBottom || 0}px`,
|
||||
paddingLeft: `${componentStyle.paddingLeft || 0}px`,
|
||||
borderTopLeftRadius: `${componentStyle.borderTopLeftRadius || 0}px`,
|
||||
borderTopRightRadius: `${componentStyle.borderTopRightRadius || 0}px`,
|
||||
borderBottomRightRadius: `${componentStyle.borderBottomRightRadius || 0}px`,
|
||||
borderBottomLeftRadius: `${componentStyle.borderBottomLeftRadius || 0}px`,
|
||||
overflow: 'hidden',
|
||||
background:
|
||||
componentStyle.bgType === 'color' ? componentStyle.bgColor : `url(${componentStyle.bgImg})`
|
||||
}
|
||||
})
|
||||
|
||||
const emits = defineEmits<{
|
||||
(e: 'move', direction: number): void
|
||||
(e: 'copy'): void
|
||||
(e: 'delete'): void
|
||||
}>()
|
||||
/**
|
||||
* 移动组件
|
||||
* @param direction 移动方向
|
||||
*/
|
||||
const handleMoveComponent = (direction: number) => {
|
||||
emits('move', direction)
|
||||
}
|
||||
/**
|
||||
* 复制组件
|
||||
*/
|
||||
const handleCopyComponent = () => {
|
||||
emits('copy')
|
||||
}
|
||||
/**
|
||||
* 删除组件
|
||||
*/
|
||||
const handleDeleteComponent = () => {
|
||||
emits('delete')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
$active-border-width: 2px;
|
||||
$hover-border-width: 1px;
|
||||
$name-position: -85px;
|
||||
$toolbar-position: -55px;
|
||||
/* 组件 */
|
||||
.component {
|
||||
position: relative;
|
||||
cursor: move;
|
||||
.component-wrap {
|
||||
display: block;
|
||||
position: absolute;
|
||||
left: -$active-border-width;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
/* 鼠标放到组件上时 */
|
||||
&:hover {
|
||||
border: $hover-border-width dashed var(--el-color-primary);
|
||||
box-shadow: 0 0 5px 0 rgba(24, 144, 255, 0.3);
|
||||
.component-name {
|
||||
/* 防止加了边框之后,位置移动 */
|
||||
left: $name-position - $hover-border-width;
|
||||
top: $hover-border-width;
|
||||
}
|
||||
}
|
||||
/* 左侧:组件名称 */
|
||||
.component-name {
|
||||
display: block;
|
||||
position: absolute;
|
||||
width: 80px;
|
||||
text-align: center;
|
||||
line-height: 25px;
|
||||
height: 25px;
|
||||
background: #fff;
|
||||
font-size: 12px;
|
||||
left: $name-position;
|
||||
top: $active-border-width;
|
||||
box-shadow:
|
||||
0 0 4px #00000014,
|
||||
0 2px 6px #0000000f,
|
||||
0 4px 8px 2px #0000000a;
|
||||
/* 右侧小三角 */
|
||||
&:after {
|
||||
position: absolute;
|
||||
top: 7.5px;
|
||||
right: -10px;
|
||||
content: ' ';
|
||||
height: 0;
|
||||
width: 0;
|
||||
border: 5px solid transparent;
|
||||
border-left-color: #fff;
|
||||
}
|
||||
}
|
||||
/* 右侧:组件操作工具栏 */
|
||||
.component-toolbar {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: $toolbar-position;
|
||||
/* 左侧小三角 */
|
||||
&:before {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: -10px;
|
||||
content: ' ';
|
||||
height: 0;
|
||||
width: 0;
|
||||
border: 5px solid transparent;
|
||||
border-right-color: #2d8cf0;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* 组件选中时 */
|
||||
&.active {
|
||||
margin-bottom: 4px;
|
||||
|
||||
.component-wrap {
|
||||
border: $active-border-width solid var(--el-color-primary) !important;
|
||||
box-shadow: 0 0 10px 0 rgba(24, 144, 255, 0.3);
|
||||
margin-bottom: $active-border-width + $active-border-width;
|
||||
|
||||
.component-name {
|
||||
background: var(--el-color-primary);
|
||||
color: #fff;
|
||||
/* 防止加了边框之后,位置移动 */
|
||||
left: $name-position - $active-border-width !important;
|
||||
top: 0 !important;
|
||||
&:after {
|
||||
border-left-color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
.component-toolbar {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,163 @@
|
||||
<template>
|
||||
<el-tabs stretch>
|
||||
<el-tab-pane label="内容">
|
||||
<slot></slot>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="样式" lazy>
|
||||
<el-card header="组件样式" class="property-group">
|
||||
<el-form :model="formData" label-width="80px">
|
||||
<el-form-item label="组件背景" prop="bgType">
|
||||
<el-radio-group v-model="formData.bgType">
|
||||
<el-radio label="color">纯色</el-radio>
|
||||
<el-radio label="img">图片</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="选择颜色" prop="bgColor" v-if="formData.bgType === 'color'">
|
||||
<ColorInput v-model="formData.bgColor" />
|
||||
</el-form-item>
|
||||
<el-form-item label="上传图片" prop="bgImg" v-else>
|
||||
<UploadImg v-model="formData.bgImg" :limit="1">
|
||||
<template #tip>建议宽度 750px</template>
|
||||
</UploadImg>
|
||||
</el-form-item>
|
||||
<el-tree :data="treeData" :expand-on-click-node="false">
|
||||
<template #default="{ node, data }">
|
||||
<el-form-item
|
||||
:label="data.label"
|
||||
:prop="data.prop"
|
||||
:label-width="node.level === 1 ? '80px' : '62px'"
|
||||
class="w-full m-b-0!"
|
||||
>
|
||||
<el-slider
|
||||
v-model="formData[data.prop]"
|
||||
:max="100"
|
||||
:min="0"
|
||||
show-input
|
||||
input-size="small"
|
||||
:show-input-controls="false"
|
||||
@input="handleSliderChange(data.prop)"
|
||||
/>
|
||||
</el-form-item>
|
||||
</template>
|
||||
</el-tree>
|
||||
<slot name="style" :formData="formData"></slot>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ComponentStyle, usePropertyForm } from '@/components/DiyEditor/util'
|
||||
|
||||
/**
|
||||
* 组件容器属性
|
||||
* 用于包裹组件,为组件提供 背景、外边距、内边距、边框等样式
|
||||
*/
|
||||
defineOptions({ name: 'ComponentContainer' })
|
||||
|
||||
const props = defineProps<{ modelValue: ComponentStyle }>()
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const { formData } = usePropertyForm(props.modelValue, emit)
|
||||
|
||||
const treeData = [
|
||||
{
|
||||
label: '外部边距',
|
||||
prop: 'margin',
|
||||
children: [
|
||||
{
|
||||
label: '上',
|
||||
prop: 'marginTop'
|
||||
},
|
||||
{
|
||||
label: '右',
|
||||
prop: 'marginRight'
|
||||
},
|
||||
{
|
||||
label: '下',
|
||||
prop: 'marginBottom'
|
||||
},
|
||||
{
|
||||
label: '左',
|
||||
prop: 'marginLeft'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '内部边距',
|
||||
prop: 'padding',
|
||||
children: [
|
||||
{
|
||||
label: '上',
|
||||
prop: 'paddingTop'
|
||||
},
|
||||
{
|
||||
label: '右',
|
||||
prop: 'paddingRight'
|
||||
},
|
||||
{
|
||||
label: '下',
|
||||
prop: 'paddingBottom'
|
||||
},
|
||||
{
|
||||
label: '左',
|
||||
prop: 'paddingLeft'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
label: '边框圆角',
|
||||
prop: 'borderRadius',
|
||||
children: [
|
||||
{
|
||||
label: '上左',
|
||||
prop: 'borderTopLeftRadius'
|
||||
},
|
||||
{
|
||||
label: '上右',
|
||||
prop: 'borderTopRightRadius'
|
||||
},
|
||||
{
|
||||
label: '下右',
|
||||
prop: 'borderBottomRightRadius'
|
||||
},
|
||||
{
|
||||
label: '下左',
|
||||
prop: 'borderBottomLeftRadius'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
const handleSliderChange = (prop: string) => {
|
||||
switch (prop) {
|
||||
case 'margin':
|
||||
formData.value.marginTop = formData.value.margin
|
||||
formData.value.marginRight = formData.value.margin
|
||||
formData.value.marginBottom = formData.value.margin
|
||||
formData.value.marginLeft = formData.value.margin
|
||||
break
|
||||
case 'padding':
|
||||
formData.value.paddingTop = formData.value.padding
|
||||
formData.value.paddingRight = formData.value.padding
|
||||
formData.value.paddingBottom = formData.value.padding
|
||||
formData.value.paddingLeft = formData.value.padding
|
||||
break
|
||||
case 'borderRadius':
|
||||
formData.value.borderTopLeftRadius = formData.value.borderRadius
|
||||
formData.value.borderTopRightRadius = formData.value.borderRadius
|
||||
formData.value.borderBottomRightRadius = formData.value.borderRadius
|
||||
formData.value.borderBottomLeftRadius = formData.value.borderRadius
|
||||
break
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
:deep(.el-slider__runway) {
|
||||
margin-right: 16px;
|
||||
}
|
||||
:deep(.el-input-number) {
|
||||
width: 50px;
|
||||
}
|
||||
</style>
|
197
src/components/DiyEditor/components/ComponentLibrary.vue
Normal file
197
src/components/DiyEditor/components/ComponentLibrary.vue
Normal file
@ -0,0 +1,197 @@
|
||||
<template>
|
||||
<el-aside class="editor-left" width="261px">
|
||||
<el-scrollbar>
|
||||
<el-collapse v-model="extendGroups">
|
||||
<el-collapse-item
|
||||
v-for="group in groups"
|
||||
:key="group.name"
|
||||
:name="group.name"
|
||||
:title="group.name"
|
||||
>
|
||||
<draggable
|
||||
class="component-container"
|
||||
ghost-class="draggable-ghost"
|
||||
item-key="index"
|
||||
:list="group.components"
|
||||
:sort="false"
|
||||
:group="{ name: 'component', pull: 'clone', put: false }"
|
||||
:clone="handleCloneComponent"
|
||||
:animation="200"
|
||||
:force-fallback="true"
|
||||
>
|
||||
<template #item="{ element }">
|
||||
<div>
|
||||
<div class="drag-placement">组件放置区域</div>
|
||||
<div class="component">
|
||||
<Icon :icon="element.icon" :size="32" />
|
||||
<span class="mt-4px text-12px">{{ element.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</el-scrollbar>
|
||||
</el-aside>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import draggable from 'vuedraggable'
|
||||
import { componentConfigs } from '../components/mobile/index'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import { DiyComponent, DiyComponentLibrary } from '@/components/DiyEditor/util'
|
||||
|
||||
/** 组件库 */
|
||||
defineOptions({ name: 'ComponentLibrary' })
|
||||
|
||||
// 组件列表
|
||||
const props = defineProps<{
|
||||
list: DiyComponentLibrary[]
|
||||
}>()
|
||||
const groups = reactive<any[]>([])
|
||||
// 展开的折叠面板
|
||||
const extendGroups = reactive<string[]>([])
|
||||
watch(
|
||||
() => props.list,
|
||||
() => {
|
||||
// 清除旧数据
|
||||
extendGroups.length = 0
|
||||
groups.length = 0
|
||||
// 重新生成数据
|
||||
props.list.forEach((group) => {
|
||||
// 是否展开分组
|
||||
if (group.extended) {
|
||||
extendGroups.push(group.name)
|
||||
}
|
||||
// 查找组件
|
||||
const components = group.components
|
||||
.map((name) => componentConfigs[name] as DiyComponent<any>)
|
||||
.filter((component) => component)
|
||||
if (components.length > 0) {
|
||||
groups.push({
|
||||
name: group.name,
|
||||
components
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
|
||||
// 克隆组件
|
||||
const handleCloneComponent = (component: DiyComponent<any>) => {
|
||||
return cloneDeep(component)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.editor-left {
|
||||
z-index: 1;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 8px 0 8px -8px rgba(0, 0, 0, 0.12);
|
||||
|
||||
:deep(.el-collapse) {
|
||||
border-top: none;
|
||||
}
|
||||
:deep(.el-collapse-item__wrap) {
|
||||
border-bottom: none;
|
||||
}
|
||||
:deep(.el-collapse-item__content) {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
:deep(.el-collapse-item__header) {
|
||||
border-bottom: none;
|
||||
background-color: var(--el-bg-color-page);
|
||||
padding: 0 24px;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
.component-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.component {
|
||||
width: 86px;
|
||||
height: 86px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-right: 1px solid var(--el-border-color-lighter);
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
cursor: move;
|
||||
|
||||
.el-icon {
|
||||
margin-bottom: 4px;
|
||||
color: gray;
|
||||
}
|
||||
}
|
||||
.component.active,
|
||||
.component:hover {
|
||||
background: var(--el-color-primary);
|
||||
color: var(--el-color-white);
|
||||
|
||||
.el-icon {
|
||||
color: var(--el-color-white);
|
||||
}
|
||||
}
|
||||
|
||||
.component:nth-of-type(3n) {
|
||||
border-right: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* 拖拽占位提示,默认不显示 */
|
||||
.drag-placement {
|
||||
display: none;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.drag-area {
|
||||
/* 拖拽到手机区域时的样式 */
|
||||
.draggable-ghost {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
/* 条纹背景 */
|
||||
background: linear-gradient(
|
||||
45deg,
|
||||
#91a8d5 0,
|
||||
#91a8d5 10%,
|
||||
#94b4eb 10%,
|
||||
#94b4eb 50%,
|
||||
#91a8d5 50%,
|
||||
#91a8d5 60%,
|
||||
#94b4eb 60%,
|
||||
#94b4eb
|
||||
);
|
||||
background-size: 1rem 1rem;
|
||||
transition: all 0.5s;
|
||||
span {
|
||||
color: #fff;
|
||||
display: inline-block;
|
||||
width: 140px;
|
||||
height: 25px;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
line-height: 25px;
|
||||
background: #5487df;
|
||||
}
|
||||
/* 拖拽时隐藏组件 */
|
||||
.component {
|
||||
display: none;
|
||||
}
|
||||
/* 拖拽时显示占位提示 */
|
||||
.drag-placement {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,50 @@
|
||||
import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
|
||||
|
||||
/** 轮播图属性 */
|
||||
export interface CarouselProperty {
|
||||
// 类型:默认 | 卡片
|
||||
type: 'default' | 'card'
|
||||
// 指示器样式:点 | 数字
|
||||
indicator: 'dot' | 'number'
|
||||
// 是否自动播放
|
||||
autoplay: boolean
|
||||
// 播放间隔
|
||||
interval: number
|
||||
// 轮播内容
|
||||
items: CarouselItemProperty[]
|
||||
// 组件样式
|
||||
style: ComponentStyle
|
||||
}
|
||||
// 轮播内容属性
|
||||
export interface CarouselItemProperty {
|
||||
// 类型:图片 | 视频
|
||||
type: 'img' | 'video'
|
||||
// 图片链接
|
||||
imgUrl: string
|
||||
// 视频链接
|
||||
videoUrl: string
|
||||
// 跳转链接
|
||||
url: string
|
||||
}
|
||||
|
||||
// 定义组件
|
||||
export const component = {
|
||||
id: 'Carousel',
|
||||
name: '轮播图',
|
||||
icon: 'system-uicons:carousel',
|
||||
property: {
|
||||
type: 'default',
|
||||
indicator: 'dot',
|
||||
autoplay: false,
|
||||
interval: 3,
|
||||
items: [
|
||||
{ type: 'img', imgUrl: 'https://static.iocoder.cn/mall/banner-01.jpg', videoUrl: '' },
|
||||
{ type: 'img', imgUrl: 'https://static.iocoder.cn/mall/banner-02.jpg', videoUrl: '' }
|
||||
] as CarouselItemProperty[],
|
||||
style: {
|
||||
bgType: 'color',
|
||||
bgColor: '#fff',
|
||||
marginBottom: 8
|
||||
} as ComponentStyle
|
||||
}
|
||||
} as DiyComponent<CarouselProperty>
|
@ -0,0 +1,43 @@
|
||||
<template>
|
||||
<!-- 无图片 -->
|
||||
<div
|
||||
class="h-250px flex items-center justify-center bg-gray-3"
|
||||
v-if="property.items.length === 0"
|
||||
>
|
||||
<Icon icon="tdesign:image" class="text-gray-8 text-120px!" />
|
||||
</div>
|
||||
<div v-else class="relative">
|
||||
<el-carousel
|
||||
height="174px"
|
||||
:type="property.type === 'card' ? 'card' : ''"
|
||||
:autoplay="property.autoplay"
|
||||
:interval="property.interval * 1000"
|
||||
:indicator-position="property.indicator === 'number' ? 'none' : undefined"
|
||||
@change="handleIndexChange"
|
||||
>
|
||||
<el-carousel-item v-for="(item, index) in property.items" :key="index">
|
||||
<el-image class="h-full w-full" :src="item.imgUrl" />
|
||||
</el-carousel-item>
|
||||
</el-carousel>
|
||||
<div
|
||||
v-if="property.indicator === 'number'"
|
||||
class="absolute p-y-2px bottom-10px right-10px rounded-xl bg-black p-x-8px text-10px text-white opacity-40"
|
||||
>{{ currentIndex }} / {{ property.items.length }}</div
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { CarouselProperty } from './config'
|
||||
|
||||
/** 轮播图 */
|
||||
defineOptions({ name: 'Carousel' })
|
||||
|
||||
defineProps<{ property: CarouselProperty }>()
|
||||
|
||||
const currentIndex = ref(0)
|
||||
const handleIndexChange = (index: number) => {
|
||||
currentIndex.value = index + 1
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
142
src/components/DiyEditor/components/mobile/Carousel/property.vue
Normal file
142
src/components/DiyEditor/components/mobile/Carousel/property.vue
Normal file
@ -0,0 +1,142 @@
|
||||
<template>
|
||||
<ComponentContainerProperty v-model="formData.style">
|
||||
<el-form label-width="80px" :model="formData">
|
||||
<el-card header="样式设置" class="property-group" shadow="never">
|
||||
<el-form-item label="样式" prop="type">
|
||||
<el-radio-group v-model="formData.type">
|
||||
<el-tooltip class="item" content="默认" placement="bottom">
|
||||
<el-radio-button label="default">
|
||||
<Icon icon="system-uicons:carousel" />
|
||||
</el-radio-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip class="item" content="卡片" placement="bottom">
|
||||
<el-radio-button label="card">
|
||||
<Icon icon="ic:round-view-carousel" />
|
||||
</el-radio-button>
|
||||
</el-tooltip>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="指示器" prop="indicator">
|
||||
<el-radio-group v-model="formData.indicator">
|
||||
<el-radio label="dot">小圆点</el-radio>
|
||||
<el-radio label="number">数字</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="是否轮播" prop="autoplay">
|
||||
<el-switch v-model="formData.autoplay" />
|
||||
</el-form-item>
|
||||
<el-form-item label="播放间隔" prop="interval" v-if="formData.autoplay">
|
||||
<el-slider
|
||||
v-model="formData.interval"
|
||||
:max="10"
|
||||
:min="0.5"
|
||||
:step="0.5"
|
||||
show-input
|
||||
input-size="small"
|
||||
:show-input-controls="false"
|
||||
/>
|
||||
<el-text type="info">单位:秒</el-text>
|
||||
</el-form-item>
|
||||
</el-card>
|
||||
<el-card header="内容设置" class="property-group" shadow="never">
|
||||
<el-text type="info" size="small"> 拖动左上角的小圆点可对其排序 </el-text>
|
||||
<template v-if="formData.items[0]">
|
||||
<draggable
|
||||
:list="formData.items"
|
||||
:force-fallback="true"
|
||||
:animation="200"
|
||||
handle=".drag-icon"
|
||||
class="m-t-8px"
|
||||
item-key="index"
|
||||
>
|
||||
<template #item="{ element, index }">
|
||||
<div class="content mb-4px flex flex-col gap-4px rounded bg-gray-50 p-8px">
|
||||
<div
|
||||
class="m--8px m-b-8px flex flex-row items-center justify-between bg-gray-100 p-8px"
|
||||
>
|
||||
<Icon icon="ic:round-drag-indicator" class="drag-icon cursor-move" />
|
||||
<Icon
|
||||
icon="ep:delete"
|
||||
class="cursor-pointer text-red-5"
|
||||
@click="handleDeleteImage(index)"
|
||||
v-if="formData.items.length > 1"
|
||||
/>
|
||||
</div>
|
||||
<el-form-item label="类型" prop="type" class="m-b-8px!" label-width="50px">
|
||||
<el-radio-group v-model="element.type">
|
||||
<el-radio label="img">图片</el-radio>
|
||||
<el-radio label="video">视频</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
label="图片"
|
||||
class="m-b-8px!"
|
||||
label-width="50px"
|
||||
v-if="element.type === 'img'"
|
||||
>
|
||||
<UploadImg
|
||||
v-model="element.imgUrl"
|
||||
draggable="false"
|
||||
height="80px"
|
||||
width="100%"
|
||||
class="min-w-80px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<template v-else>
|
||||
<el-form-item label="封面" class="m-b-8px!" label-width="50px">
|
||||
<UploadImg
|
||||
v-model="element.imgUrl"
|
||||
draggable="false"
|
||||
height="80px"
|
||||
width="100%"
|
||||
class="min-w-80px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="视频" class="m-b-8px!" label-width="50px">
|
||||
<UploadFile
|
||||
v-model="element.videoUrl"
|
||||
:file-type="['mp4']"
|
||||
:limit="1"
|
||||
:file-size="100"
|
||||
class="min-w-80px"
|
||||
/>
|
||||
</el-form-item>
|
||||
</template>
|
||||
<el-form-item label="链接" class="m-b-8px!" label-width="50px">
|
||||
<el-input placeholder="链接" v-model="element.url" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
</template>
|
||||
<el-button @click="handleAddImage" type="primary" plain class="w-full">
|
||||
添加图片
|
||||
</el-button>
|
||||
</el-card>
|
||||
</el-form>
|
||||
</ComponentContainerProperty>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import draggable from 'vuedraggable' //拖拽组件
|
||||
import { CarouselItemProperty, CarouselProperty } from './config'
|
||||
import { usePropertyForm } from '@/components/DiyEditor/util'
|
||||
|
||||
// 轮播图属性面板
|
||||
defineOptions({ name: 'CarouselProperty' })
|
||||
|
||||
const props = defineProps<{ modelValue: CarouselProperty }>()
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const { formData } = usePropertyForm(props.modelValue, emit)
|
||||
|
||||
// 添加图片
|
||||
const handleAddImage = () => {
|
||||
formData.value.items.push({} as CarouselItemProperty)
|
||||
}
|
||||
// 删除图片
|
||||
const handleDeleteImage = (index: number) => {
|
||||
formData.value.items.splice(index, 1)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
29
src/components/DiyEditor/components/mobile/Divider/config.ts
Normal file
29
src/components/DiyEditor/components/mobile/Divider/config.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { DiyComponent } from '@/components/DiyEditor/util'
|
||||
|
||||
/** 分割线属性 */
|
||||
export interface DividerProperty {
|
||||
// 高度
|
||||
height: number
|
||||
// 线宽
|
||||
lineWidth: number
|
||||
// 边距类型
|
||||
paddingType: 'none' | 'horizontal'
|
||||
// 颜色
|
||||
lineColor: string
|
||||
// 类型
|
||||
borderType: 'solid' | 'dashed' | 'dotted' | 'none'
|
||||
}
|
||||
|
||||
// 定义组件
|
||||
export const component = {
|
||||
id: 'Divider',
|
||||
name: '分割线',
|
||||
icon: 'tdesign:component-divider-vertical',
|
||||
property: {
|
||||
height: 30,
|
||||
lineWidth: 1,
|
||||
paddingType: 'none',
|
||||
lineColor: '#dcdfe6',
|
||||
borderType: 'solid'
|
||||
}
|
||||
} as DiyComponent<DividerProperty>
|
29
src/components/DiyEditor/components/mobile/Divider/index.vue
Normal file
29
src/components/DiyEditor/components/mobile/Divider/index.vue
Normal file
@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex items-center"
|
||||
:style="{
|
||||
height: property.height + 'px'
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="w-full"
|
||||
:style="{
|
||||
borderTopStyle: property.borderType,
|
||||
borderTopColor: property.lineColor,
|
||||
borderTopWidth: `${property.lineWidth}px`,
|
||||
margin: property.paddingType === 'none' ? '0' : '0px 16px'
|
||||
}"
|
||||
></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { DividerProperty } from './config'
|
||||
|
||||
/** 页面顶部导航栏 */
|
||||
defineOptions({ name: 'Divider' })
|
||||
|
||||
defineProps<{ property: DividerProperty }>()
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<el-form label-width="80px" :model="formData">
|
||||
<el-form-item label="高度" prop="height">
|
||||
<el-slider v-model="formData.height" :min="1" :max="100" show-input input-size="small" />
|
||||
</el-form-item>
|
||||
<el-form-item label="选择样式" prop="borderType">
|
||||
<el-radio-group v-model="formData!.borderType">
|
||||
<el-tooltip
|
||||
placement="top"
|
||||
v-for="(item, index) in BORDER_TYPES"
|
||||
:key="index"
|
||||
:content="item.text"
|
||||
>
|
||||
<el-radio-button :label="item.type">
|
||||
<Icon :icon="item.icon" />
|
||||
</el-radio-button>
|
||||
</el-tooltip>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<template v-if="formData.borderType !== 'none'">
|
||||
<el-form-item label="线宽" prop="lineWidth">
|
||||
<el-slider v-model="formData.lineWidth" :min="1" :max="30" show-input input-size="small" />
|
||||
</el-form-item>
|
||||
<el-form-item label="左右边距" prop="paddingType">
|
||||
<el-radio-group v-model="formData!.paddingType">
|
||||
<el-tooltip content="无边距" placement="top">
|
||||
<el-radio-button label="none">
|
||||
<Icon icon="tabler:box-padding" />
|
||||
</el-radio-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="左右留边" placement="top">
|
||||
<el-radio-button label="horizontal">
|
||||
<Icon icon="vaadin:padding" />
|
||||
</el-radio-button>
|
||||
</el-tooltip>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="颜色">
|
||||
<!-- 分割线颜色 -->
|
||||
<ColorInput v-model="formData.lineColor" />
|
||||
</el-form-item>
|
||||
</template>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { DividerProperty } from './config'
|
||||
import { usePropertyForm } from '@/components/DiyEditor/util'
|
||||
// 导航栏属性面板
|
||||
defineOptions({ name: 'DividerProperty' })
|
||||
const props = defineProps<{ modelValue: DividerProperty }>()
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const { formData } = usePropertyForm(props.modelValue, emit)
|
||||
|
||||
//线类型
|
||||
const BORDER_TYPES = [
|
||||
{
|
||||
icon: 'vaadin:line-h',
|
||||
text: '实线',
|
||||
type: 'solid'
|
||||
},
|
||||
{
|
||||
icon: 'tabler:line-dashed',
|
||||
text: '虚线',
|
||||
type: 'dashed'
|
||||
},
|
||||
{
|
||||
icon: 'tabler:line-dotted',
|
||||
text: '点线',
|
||||
type: 'dotted'
|
||||
},
|
||||
{
|
||||
icon: 'entypo:progress-empty',
|
||||
text: '无',
|
||||
type: 'none'
|
||||
}
|
||||
]
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
@ -0,0 +1,27 @@
|
||||
import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
|
||||
|
||||
/** 图片展示属性 */
|
||||
export interface ImageBarProperty {
|
||||
// 图片链接
|
||||
imgUrl: string
|
||||
// 跳转链接
|
||||
url: string
|
||||
// 组件样式
|
||||
style: ComponentStyle
|
||||
}
|
||||
|
||||
// 定义组件
|
||||
export const component = {
|
||||
id: 'ImageBar',
|
||||
name: '图片展示',
|
||||
icon: 'ep:picture',
|
||||
property: {
|
||||
imgUrl: '',
|
||||
url: '',
|
||||
style: {
|
||||
bgType: 'color',
|
||||
bgColor: '#fff',
|
||||
marginBottom: 8
|
||||
} as ComponentStyle
|
||||
}
|
||||
} as DiyComponent<ImageBarProperty>
|
@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<!-- 无图片 -->
|
||||
<div class="h-50px flex items-center justify-center bg-gray-3" v-if="!property.imgUrl">
|
||||
<Icon icon="ep:picture" class="text-gray-8 text-30px!" />
|
||||
</div>
|
||||
<el-image class="min-h-30px" v-else :src="property.imgUrl" />
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ImageBarProperty } from './config'
|
||||
|
||||
/** 图片展示 */
|
||||
defineOptions({ name: 'ImageBar' })
|
||||
|
||||
defineProps<{ property: ImageBarProperty }>()
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
/* 图片 */
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<ComponentContainerProperty v-model="formData.style">
|
||||
<el-form label-width="80px" :model="formData">
|
||||
<el-form-item label="上传图片" prop="imgUrl">
|
||||
<UploadImg
|
||||
v-model="formData.imgUrl"
|
||||
draggable="false"
|
||||
height="80px"
|
||||
width="100%"
|
||||
class="min-w-80px"
|
||||
>
|
||||
<template #tip> 建议宽度750 </template>
|
||||
</UploadImg>
|
||||
</el-form-item>
|
||||
<el-form-item label="链接" prop="url">
|
||||
<el-input placeholder="链接" v-model="formData.url" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</ComponentContainerProperty>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ImageBarProperty } from './config'
|
||||
import { usePropertyForm } from '@/components/DiyEditor/util'
|
||||
|
||||
// 图片展示属性面板
|
||||
defineOptions({ name: 'ImageBarProperty' })
|
||||
|
||||
const props = defineProps<{ modelValue: ImageBarProperty }>()
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const { formData } = usePropertyForm(props.modelValue, emit)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
@ -0,0 +1,38 @@
|
||||
import { DiyComponent } from '@/components/DiyEditor/util'
|
||||
|
||||
/** 顶部导航栏属性 */
|
||||
export interface NavigationBarProperty {
|
||||
// 页面标题
|
||||
title: string
|
||||
// 页面描述
|
||||
description: string
|
||||
// 顶部导航高度
|
||||
navBarHeight: number
|
||||
// 页面背景颜色
|
||||
backgroundColor: string
|
||||
// 页面背景图片
|
||||
backgroundImage: string
|
||||
// 样式类型:默认 | 沉浸式
|
||||
styleType: 'default' | 'immersion'
|
||||
// 常驻显示
|
||||
alwaysShow: boolean
|
||||
// 是否显示返回按钮
|
||||
showGoBack: boolean
|
||||
}
|
||||
|
||||
// 定义组件
|
||||
export const component = {
|
||||
id: 'NavigationBar',
|
||||
name: '顶部导航栏',
|
||||
icon: 'tabler:layout-navbar',
|
||||
property: {
|
||||
title: '页面标题',
|
||||
description: '',
|
||||
navBarHeight: 35,
|
||||
backgroundColor: '#fff',
|
||||
backgroundImage: '',
|
||||
styleType: 'default',
|
||||
alwaysShow: true,
|
||||
showGoBack: true
|
||||
}
|
||||
} as DiyComponent<NavigationBarProperty>
|
@ -0,0 +1,59 @@
|
||||
<template>
|
||||
<div
|
||||
class="navigation-bar"
|
||||
:style="{
|
||||
height: `${property.navBarHeight}px`,
|
||||
backgroundColor: property.backgroundColor,
|
||||
backgroundImage: `url(${property.backgroundImage})`
|
||||
}"
|
||||
>
|
||||
<!-- 左侧 -->
|
||||
<div class="left">
|
||||
<Icon icon="ep:arrow-left" v-show="property.showGoBack" />
|
||||
</div>
|
||||
<!-- 中间 -->
|
||||
<div
|
||||
class="center"
|
||||
:style="{
|
||||
height: `${property.navBarHeight}px`,
|
||||
lineHeight: `${property.navBarHeight}px`
|
||||
}"
|
||||
>
|
||||
{{ property.title }}
|
||||
</div>
|
||||
<!-- 右侧 -->
|
||||
<div class="right"></div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { NavigationBarProperty } from './config'
|
||||
|
||||
/** 页面顶部导航栏 */
|
||||
defineOptions({ name: 'NavigationBar' })
|
||||
|
||||
defineProps<{ property: NavigationBarProperty }>()
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.navigation-bar {
|
||||
height: 35px;
|
||||
background: #fff;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
/* 左边 */
|
||||
.left {
|
||||
margin-left: 8px;
|
||||
}
|
||||
.center {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
line-height: 35px;
|
||||
color: #333333;
|
||||
}
|
||||
/* 右边 */
|
||||
.right {
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<el-form label-width="80px" :model="formData" :rules="rules">
|
||||
<el-form-item label="页面标题" prop="title">
|
||||
<el-input v-model="formData!.title" placeholder="页面标题" maxlength="25" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item label="页面描述" prop="description">
|
||||
<el-input
|
||||
type="textarea"
|
||||
v-model="formData!.description"
|
||||
placeholder="用户通过微信分享给朋友时,会自动显示页面描述"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="样式" prop="styleType">
|
||||
<el-radio-group v-model="formData!.styleType">
|
||||
<el-radio label="default">默认</el-radio>
|
||||
<el-radio label="immersion">沉浸式</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="常驻显示" prop="alwaysShow" v-if="formData.styleType === 'immersion'">
|
||||
<el-radio-group v-model="formData!.alwaysShow">
|
||||
<el-radio :label="false">关闭</el-radio>
|
||||
<el-radio :label="true">开启</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="高度" prop="navBarHeight">
|
||||
<el-slider
|
||||
v-model="formData!.navBarHeight"
|
||||
:max="100"
|
||||
:min="35"
|
||||
show-input
|
||||
input-size="small"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="返回按钮" prop="showGoBack">
|
||||
<el-switch v-model="formData!.showGoBack" />
|
||||
</el-form-item>
|
||||
<el-form-item label="背景颜色" prop="backgroundColor">
|
||||
<ColorInput v-model="formData!.backgroundColor" />
|
||||
</el-form-item>
|
||||
<el-form-item label="背景图片" prop="backgroundImage">
|
||||
<UploadImg v-model="formData!.backgroundImage" :limit="1">
|
||||
<template #tip>建议宽度 750px</template>
|
||||
</UploadImg>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { NavigationBarProperty } from './config'
|
||||
import { usePropertyForm } from '@/components/DiyEditor/util'
|
||||
// 导航栏属性面板
|
||||
defineOptions({ name: 'NavigationBarProperty' })
|
||||
// 表单校验
|
||||
const rules = {
|
||||
name: [{ required: true, message: '请输入页面名称', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const props = defineProps<{ modelValue: NavigationBarProperty }>()
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const { formData } = usePropertyForm(props.modelValue, emit)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
@ -0,0 +1,39 @@
|
||||
import { DiyComponent } from '@/components/DiyEditor/util'
|
||||
|
||||
/** 公告栏属性 */
|
||||
export interface NoticeBarProperty {
|
||||
// 图标地址
|
||||
iconUrl: string
|
||||
// 公告内容列表
|
||||
contents: NoticeContentProperty[]
|
||||
// 背景颜色
|
||||
backgroundColor: string
|
||||
// 文字颜色
|
||||
textColor: string
|
||||
}
|
||||
|
||||
/** 内容属性 */
|
||||
export interface NoticeContentProperty {
|
||||
// 内容文字
|
||||
text: string
|
||||
// 链接地址
|
||||
url: string
|
||||
}
|
||||
|
||||
// 定义组件
|
||||
export const component = {
|
||||
id: 'NoticeBar',
|
||||
name: '公告栏',
|
||||
icon: 'ep:bell',
|
||||
property: {
|
||||
iconUrl: 'http://mall.yudao.iocoder.cn/static/images/xinjian.png',
|
||||
contents: [
|
||||
{
|
||||
text: '',
|
||||
url: ''
|
||||
}
|
||||
],
|
||||
backgroundColor: '#fff',
|
||||
textColor: '#333'
|
||||
}
|
||||
} as DiyComponent<NoticeBarProperty>
|
@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<div
|
||||
class="flex items-center p-y-4px text-12px"
|
||||
:style="{ backgroundColor: property.backgroundColor, color: property.textColor }"
|
||||
>
|
||||
<el-image :src="property.iconUrl" class="h-18px" />
|
||||
<el-divider direction="vertical" />
|
||||
<el-carousel height="24px" direction="vertical" :autoplay="true" class="flex-1 p-r-8px">
|
||||
<el-carousel-item v-for="(item, index) in property.contents" :key="index">
|
||||
<div class="h-24px truncate leading-24px">{{ item.text }}</div>
|
||||
</el-carousel-item>
|
||||
</el-carousel>
|
||||
<Icon icon="ep:arrow-right" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { NoticeBarProperty } from './config'
|
||||
|
||||
/** 公告栏 */
|
||||
defineOptions({ name: 'NoticeBar' })
|
||||
|
||||
defineProps<{ property: NoticeBarProperty }>()
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
@ -0,0 +1,77 @@
|
||||
<template>
|
||||
<el-form label-width="80px" :model="formData" :rules="rules">
|
||||
<el-form-item label="公告图标" prop="iconUrl">
|
||||
<UploadImg v-model="formData.iconUrl" height="48px">
|
||||
<template #tip>建议尺寸:24 * 24</template>
|
||||
</UploadImg>
|
||||
</el-form-item>
|
||||
<el-form-item label="背景颜色" prop="backgroundColor">
|
||||
<ColorInput v-model="formData.backgroundColor" />
|
||||
</el-form-item>
|
||||
<el-form-item label="文字颜色" prop="文字颜色">
|
||||
<ColorInput v-model="formData.textColor" />
|
||||
</el-form-item>
|
||||
<el-text tag="p"> 公告内容 </el-text>
|
||||
<el-text type="info" size="small"> 拖动左上角的小圆点可以调整热词顺序 </el-text>
|
||||
<template v-if="formData.contents.length">
|
||||
<VueDraggable
|
||||
:list="formData.contents"
|
||||
item-key="index"
|
||||
handle=".drag-icon"
|
||||
:forceFallback="true"
|
||||
:animation="200"
|
||||
class="m-t-8px"
|
||||
>
|
||||
<template #item="{ element, index }">
|
||||
<div class="mb-4px flex flex-row gap-4px rounded bg-gray-100 p-8px">
|
||||
<div class="flex flex-col items-start justify-between">
|
||||
<Icon icon="ic:round-drag-indicator" class="drag-icon cursor-move" />
|
||||
<Icon
|
||||
icon="ep:delete"
|
||||
class="cursor-pointer text-red-5"
|
||||
@click="handleDeleteContent(index)"
|
||||
v-if="formData.contents.length > 1"
|
||||
/>
|
||||
</div>
|
||||
<div class="w-full flex flex-col gap-8px">
|
||||
<el-input v-model="element.text" placeholder="请输入公告" />
|
||||
<el-input v-model="element.url" placeholder="请输入链接" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</VueDraggable>
|
||||
</template>
|
||||
<el-form-item label-width="0">
|
||||
<el-button @click="handleAddContent" type="primary" plain class="m-t-8px w-full">
|
||||
添加内容
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { NoticeBarProperty, NoticeContentProperty } from './config'
|
||||
import { usePropertyForm } from '@/components/DiyEditor/util'
|
||||
import VueDraggable from 'vuedraggable'
|
||||
// 通知栏属性面板
|
||||
defineOptions({ name: 'NoticeBarProperty' })
|
||||
// 表单校验
|
||||
const rules = {
|
||||
content: [{ required: true, message: '请输入公告', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
const props = defineProps<{ modelValue: NoticeBarProperty }>()
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const { formData } = usePropertyForm(props.modelValue, emit)
|
||||
|
||||
/* 添加公告 */
|
||||
const handleAddContent = () => {
|
||||
formData.value.contents.push({} as NoticeContentProperty)
|
||||
}
|
||||
/* 删除公告 */
|
||||
const handleDeleteContent = (index: number) => {
|
||||
formData.value.contents.splice(index, 1)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
@ -0,0 +1,23 @@
|
||||
import { DiyComponent } from '@/components/DiyEditor/util'
|
||||
|
||||
/** 页面设置属性 */
|
||||
export interface PageConfigProperty {
|
||||
// 页面描述
|
||||
description: string
|
||||
// 页面背景颜色
|
||||
backgroundColor: string
|
||||
// 页面背景图片
|
||||
backgroundImage: string
|
||||
}
|
||||
|
||||
// 定义页面组件
|
||||
export const component = {
|
||||
id: 'PageConfig',
|
||||
name: '页面设置',
|
||||
icon: 'ep:document',
|
||||
property: {
|
||||
description: '',
|
||||
backgroundColor: '#f5f5f5',
|
||||
backgroundImage: ''
|
||||
}
|
||||
} as DiyComponent<PageConfigProperty>
|
@ -0,0 +1,34 @@
|
||||
<template>
|
||||
<el-form label-width="80px" :model="formData" :rules="rules">
|
||||
<el-form-item label="页面描述" prop="description">
|
||||
<el-input
|
||||
type="textarea"
|
||||
v-model="formData!.description"
|
||||
placeholder="用户通过微信分享给朋友时,会自动显示页面描述"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="背景颜色" prop="backgroundColor">
|
||||
<ColorInput v-model="formData!.backgroundColor" />
|
||||
</el-form-item>
|
||||
<el-form-item label="背景图片" prop="backgroundImage">
|
||||
<UploadImg v-model="formData!.backgroundImage" :limit="1">
|
||||
<template #tip>建议宽度 750px</template>
|
||||
</UploadImg>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { PageConfigProperty } from './config'
|
||||
import { usePropertyForm } from '@/components/DiyEditor/util'
|
||||
// 导航栏属性面板
|
||||
defineOptions({ name: 'PageConfigProperty' })
|
||||
// 表单校验
|
||||
const rules = {}
|
||||
|
||||
const props = defineProps<{ modelValue: PageConfigProperty }>()
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const { formData } = usePropertyForm(props.modelValue, emit)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
@ -0,0 +1,43 @@
|
||||
import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
|
||||
|
||||
/** 搜索框属性 */
|
||||
export interface SearchProperty {
|
||||
height: number // 搜索栏高度
|
||||
showScan: boolean // 显示扫一扫
|
||||
borderRadius: number // 框体样式
|
||||
placeholder: string // 占位文字
|
||||
placeholderPosition: PlaceholderPosition // 占位文字位置
|
||||
backgroundColor: string // 框体颜色
|
||||
textColor: string // 字体颜色
|
||||
hotKeywords: string[] // 热词
|
||||
style: ComponentStyle
|
||||
}
|
||||
|
||||
// 文字位置
|
||||
export type PlaceholderPosition = 'left' | 'center'
|
||||
|
||||
// 定义组件
|
||||
export const component = {
|
||||
id: 'SearchBar',
|
||||
name: '搜索框',
|
||||
icon: 'ep:search',
|
||||
property: {
|
||||
height: 28,
|
||||
showScan: false,
|
||||
borderRadius: 0,
|
||||
placeholder: '搜索商品',
|
||||
placeholderPosition: 'left',
|
||||
backgroundColor: 'rgb(238, 238, 238)',
|
||||
textColor: 'rgb(150, 151, 153)',
|
||||
hotKeywords: [],
|
||||
style: {
|
||||
bgType: 'color',
|
||||
bgColor: '#fff',
|
||||
marginBottom: 8,
|
||||
paddingTop: 8,
|
||||
paddingRight: 8,
|
||||
paddingBottom: 8,
|
||||
paddingLeft: 8
|
||||
} as ComponentStyle
|
||||
}
|
||||
} as DiyComponent<SearchProperty>
|
@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<div
|
||||
class="search-bar"
|
||||
:style="{
|
||||
color: property.textColor
|
||||
}"
|
||||
>
|
||||
<!-- 搜索框 -->
|
||||
<div
|
||||
class="inner"
|
||||
:style="{
|
||||
height: `${property.height}px`,
|
||||
background: property.backgroundColor,
|
||||
borderRadius: `${property.borderRadius}px`
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="placeholder"
|
||||
:style="{
|
||||
justifyContent: property.placeholderPosition
|
||||
}"
|
||||
>
|
||||
<Icon icon="ep:search" />
|
||||
<span>{{ property.placeholder || '搜索商品' }}</span>
|
||||
</div>
|
||||
<div class="right">
|
||||
<!-- 搜索热词 -->
|
||||
<span v-for="(keyword, index) in property.hotKeywords" :key="index">{{ keyword }}</span>
|
||||
<!-- 扫一扫 -->
|
||||
<Icon icon="ant-design:scan-outlined" v-show="property.showScan" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { SearchProperty } from './config'
|
||||
/** 搜索框 */
|
||||
defineOptions({ name: 'SearchBar' })
|
||||
defineProps<{ property: SearchProperty }>()
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.search-bar {
|
||||
/* 搜索框 */
|
||||
.inner {
|
||||
position: relative;
|
||||
min-height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
|
||||
.placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 0 8px;
|
||||
gap: 2px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
word-break: break-all;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.right {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
@ -0,0 +1,99 @@
|
||||
<template>
|
||||
<ComponentContainerProperty v-model="formData.style">
|
||||
<el-text tag="p"> 搜索热词 </el-text>
|
||||
<el-text type="info" size="small"> 拖动左侧的小圆点可以调整热词顺序 </el-text>
|
||||
|
||||
<!-- 表单 -->
|
||||
<el-form label-width="80px" :model="formData" class="m-t-8px">
|
||||
<div v-if="formData.hotKeywords.length">
|
||||
<VueDraggable
|
||||
:list="formData.hotKeywords"
|
||||
item-key="index"
|
||||
handle=".drag-icon"
|
||||
:forceFallback="true"
|
||||
:animation="200"
|
||||
>
|
||||
<template #item="{ index }">
|
||||
<div class="mb-4px flex flex-row items-center gap-4px rounded bg-gray-100 p-8px">
|
||||
<Icon icon="ic:round-drag-indicator" class="drag-icon cursor-move" />
|
||||
<el-input v-model="formData.hotKeywords[index]" placeholder="请输入热词" />
|
||||
<Icon icon="ep:delete" class="text-red-500" @click="deleteHotWord(index)" />
|
||||
</div>
|
||||
</template>
|
||||
</VueDraggable>
|
||||
</div>
|
||||
<el-form-item label-width="0">
|
||||
<el-button @click="handleAddHotWord" type="primary" plain class="m-t-8px w-full">
|
||||
添加热词
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
<el-form-item label="框体样式">
|
||||
<el-radio-group v-model="formData!.borderRadius">
|
||||
<el-tooltip content="方形" placement="top">
|
||||
<el-radio-button :label="0">
|
||||
<Icon icon="tabler:input-search" />
|
||||
</el-radio-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="圆形" placement="top">
|
||||
<el-radio-button :label="10">
|
||||
<Icon icon="iconoir:input-search" />
|
||||
</el-radio-button>
|
||||
</el-tooltip>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="提示文字" prop="placeholder">
|
||||
<el-input v-model="formData.placeholder" />
|
||||
</el-form-item>
|
||||
<el-form-item label="文本位置" prop="placeholderPosition">
|
||||
<el-radio-group v-model="formData!.placeholderPosition">
|
||||
<el-tooltip content="居左" placement="top">
|
||||
<el-radio-button label="left">
|
||||
<Icon icon="ant-design:align-left-outlined" />
|
||||
</el-radio-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="居中" placement="top">
|
||||
<el-radio-button label="center">
|
||||
<Icon icon="ant-design:align-center-outlined" />
|
||||
</el-radio-button>
|
||||
</el-tooltip>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="扫一扫" prop="showScan">
|
||||
<el-switch v-model="formData!.showScan" />
|
||||
</el-form-item>
|
||||
<el-form-item label="框体高度" prop="height">
|
||||
<el-slider v-model="formData!.height" :max="50" :min="28" show-input input-size="small" />
|
||||
</el-form-item>
|
||||
<el-form-item label="框体颜色" prop="backgroundColor">
|
||||
<ColorInput v-model="formData.backgroundColor" />
|
||||
</el-form-item>
|
||||
<el-form-item class="lef" label="文本颜色" prop="textColor">
|
||||
<ColorInput v-model="formData.textColor" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</ComponentContainerProperty>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import VueDraggable from 'vuedraggable'
|
||||
import { usePropertyForm } from '@/components/DiyEditor/util'
|
||||
import { SearchProperty } from '@/components/DiyEditor/components/mobile/SearchBar/config'
|
||||
|
||||
/** 搜索框属性面板 */
|
||||
defineOptions({ name: 'SearchProperty' })
|
||||
|
||||
const props = defineProps<{ modelValue: SearchProperty }>()
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const { formData } = usePropertyForm(props.modelValue, emit)
|
||||
|
||||
/* 添加热词 */
|
||||
const handleAddHotWord = () => {
|
||||
formData.value.hotKeywords.push('')
|
||||
}
|
||||
/* 删除热词 */
|
||||
const deleteHotWord = (index: number) => {
|
||||
formData.value.hotKeywords.splice(index, 1)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
97
src/components/DiyEditor/components/mobile/TabBar/config.ts
Normal file
97
src/components/DiyEditor/components/mobile/TabBar/config.ts
Normal file
@ -0,0 +1,97 @@
|
||||
import { DiyComponent } from '@/components/DiyEditor/util'
|
||||
|
||||
/** 底部导航菜单属性 */
|
||||
export interface TabBarProperty {
|
||||
// 选项列表
|
||||
items: TabBarItemProperty[]
|
||||
// 主题
|
||||
theme: string
|
||||
// 样式
|
||||
style: TabBarStyle
|
||||
}
|
||||
|
||||
// 选项属性
|
||||
export interface TabBarItemProperty {
|
||||
// 标签文字
|
||||
text: string
|
||||
// 链接
|
||||
url: string
|
||||
// 默认图标链接
|
||||
iconUrl: string
|
||||
// 选中的图标链接
|
||||
activeIconUrl: string
|
||||
}
|
||||
|
||||
// 样式
|
||||
export interface TabBarStyle {
|
||||
// 背景类型
|
||||
bgType: 'color' | 'img'
|
||||
// 背景颜色
|
||||
bgColor: string
|
||||
// 图片链接
|
||||
bgImg: string
|
||||
// 默认颜色
|
||||
color: string
|
||||
// 选中的颜色
|
||||
activeColor: string
|
||||
}
|
||||
|
||||
// 定义组件
|
||||
export const component = {
|
||||
id: 'TabBar',
|
||||
name: '底部导航',
|
||||
icon: 'fluent:table-bottom-row-16-filled',
|
||||
property: {
|
||||
theme: 'red',
|
||||
style: {
|
||||
bgType: 'color',
|
||||
bgColor: '#fff',
|
||||
color: '#282828',
|
||||
activeColor: '#fc4141'
|
||||
},
|
||||
items: [
|
||||
{
|
||||
text: '首页',
|
||||
url: '/pages/index/index',
|
||||
iconUrl: 'http://mall.yudao.iocoder.cn/static/images/1-001.png',
|
||||
activeIconUrl: 'http://mall.yudao.iocoder.cn/static/images/1-002.png'
|
||||
},
|
||||
{
|
||||
text: '分类',
|
||||
url: '/pages/index/category?id=3',
|
||||
iconUrl: 'http://mall.yudao.iocoder.cn/static/images/2-001.png',
|
||||
activeIconUrl: 'http://mall.yudao.iocoder.cn/static/images/2-002.png'
|
||||
},
|
||||
{
|
||||
text: '购物车',
|
||||
url: '/pages/index/cart',
|
||||
iconUrl: 'http://mall.yudao.iocoder.cn/static/images/3-001.png',
|
||||
activeIconUrl: 'http://mall.yudao.iocoder.cn/static/images/3-002.png'
|
||||
},
|
||||
{
|
||||
text: '我的',
|
||||
url: '/pages/index/user',
|
||||
iconUrl: 'http://mall.yudao.iocoder.cn/static/images/4-001.png',
|
||||
activeIconUrl: 'http://mall.yudao.iocoder.cn/static/images/4-002.png'
|
||||
}
|
||||
]
|
||||
}
|
||||
} as DiyComponent<TabBarProperty>
|
||||
|
||||
export const THEME_LIST = [
|
||||
{ id: 'red', name: '中国红', icon: 'icon-park-twotone:theme', color: '#d10019' },
|
||||
{ id: 'orange', name: '桔橙', icon: 'icon-park-twotone:theme', color: '#f37b1d' },
|
||||
{ id: 'gold', name: '明黄', icon: 'icon-park-twotone:theme', color: '#fbbd08' },
|
||||
{ id: 'green', name: '橄榄绿', icon: 'icon-park-twotone:theme', color: '#8dc63f' },
|
||||
{ id: 'cyan', name: '天青', icon: 'icon-park-twotone:theme', color: '#1cbbb4' },
|
||||
{ id: 'blue', name: '海蓝', icon: 'icon-park-twotone:theme', color: '#0081ff' },
|
||||
{ id: 'purple', name: '姹紫', icon: 'icon-park-twotone:theme', color: '#6739b6' },
|
||||
{ id: 'brightRed', name: '嫣红', icon: 'icon-park-twotone:theme', color: '#e54d42' },
|
||||
{ id: 'forestGreen', name: '森绿', icon: 'icon-park-twotone:theme', color: '#39b54a' },
|
||||
{ id: 'mauve', name: '木槿', icon: 'icon-park-twotone:theme', color: '#9c26b0' },
|
||||
{ id: 'pink', name: '桃粉', icon: 'icon-park-twotone:theme', color: '#e03997' },
|
||||
{ id: 'brown', name: '棕褐', icon: 'icon-park-twotone:theme', color: '#a5673f' },
|
||||
{ id: 'grey', name: '玄灰', icon: 'icon-park-twotone:theme', color: '#8799a3' },
|
||||
{ id: 'gray', name: '草灰', icon: 'icon-park-twotone:theme', color: '#aaaaaa' },
|
||||
{ id: 'black', name: '墨黑', icon: 'icon-park-twotone:theme', color: '#333333' }
|
||||
]
|
58
src/components/DiyEditor/components/mobile/TabBar/index.vue
Normal file
58
src/components/DiyEditor/components/mobile/TabBar/index.vue
Normal file
@ -0,0 +1,58 @@
|
||||
<template>
|
||||
<div class="tab-bar">
|
||||
<div
|
||||
class="tab-bar-bg"
|
||||
:style="{
|
||||
background:
|
||||
property.style.bgType === 'color'
|
||||
? property.style.bgColor
|
||||
: `url(${property.style.bgImg})`,
|
||||
backgroundSize: '100% 100%',
|
||||
backgroundRepeat: 'no-repeat'
|
||||
}"
|
||||
>
|
||||
<div v-for="(item, index) in property.items" :key="index" class="tab-bar-item">
|
||||
<img :src="index === 0 ? item.activeIconUrl : item.iconUrl" alt="" />
|
||||
<span :style="{ color: index === 0 ? property.style.activeColor : property.style.color }">
|
||||
{{ item.text }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { TabBarProperty } from './config'
|
||||
|
||||
/** 页面底部导航栏 */
|
||||
defineOptions({ name: 'TabBar' })
|
||||
|
||||
defineProps<{ property: TabBarProperty }>()
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.tab-bar {
|
||||
width: 100%;
|
||||
z-index: 2;
|
||||
.tab-bar-bg {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
padding: 8px 0;
|
||||
|
||||
.tab-bar-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 12px;
|
||||
width: 100%;
|
||||
|
||||
img {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
145
src/components/DiyEditor/components/mobile/TabBar/property.vue
Normal file
145
src/components/DiyEditor/components/mobile/TabBar/property.vue
Normal file
@ -0,0 +1,145 @@
|
||||
<template>
|
||||
<div class="tab-bar">
|
||||
<!-- 表单 -->
|
||||
<el-form :model="formData" label-width="80px">
|
||||
<el-form-item label="主题" prop="theme">
|
||||
<el-select v-model="formData!.theme" @change="handleThemeChange">
|
||||
<el-option
|
||||
v-for="(theme, index) in THEME_LIST"
|
||||
:key="index"
|
||||
:label="theme.name"
|
||||
:value="theme.id"
|
||||
>
|
||||
<template #default>
|
||||
<div class="flex items-center justify-between">
|
||||
<Icon :icon="theme.icon" :color="theme.color" />
|
||||
<span>{{ theme.name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="默认颜色">
|
||||
<ColorInput v-model="formData!.style.color" />
|
||||
</el-form-item>
|
||||
<el-form-item label="选中颜色">
|
||||
<ColorInput v-model="formData!.style.activeColor" />
|
||||
</el-form-item>
|
||||
<el-form-item label="导航背景">
|
||||
<el-radio-group v-model="formData!.style.bgType">
|
||||
<el-radio-button label="color">纯色</el-radio-button>
|
||||
<el-radio-button label="img">图片</el-radio-button>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="选择颜色" v-if="formData!.style.bgType === 'color'">
|
||||
<ColorInput v-model="formData!.style.bgColor" />
|
||||
</el-form-item>
|
||||
<el-form-item label="选择图片" v-if="formData!.style.bgType === 'img'">
|
||||
<UploadImg v-model="formData!.style.bgImg" width="100%" height="50px" class="min-w-200px">
|
||||
<template #tip> 建议尺寸 375 * 50 </template>
|
||||
</UploadImg>
|
||||
</el-form-item>
|
||||
|
||||
<el-text tag="p">图标设置</el-text>
|
||||
<el-text type="info" size="small"> 拖动左上角的小圆点可对其排序, 图标建议尺寸 44*44 </el-text>
|
||||
<draggable
|
||||
:list="formData!.items"
|
||||
item-key="index"
|
||||
:forceFallback="true"
|
||||
:animation="200"
|
||||
handle=".drag-icon"
|
||||
class="m-t-8px"
|
||||
>
|
||||
<template #item="{ element, index }">
|
||||
<div class="mb-4px flex flex-row gap-4px rounded bg-gray-100 p-8px">
|
||||
<div class="flex flex-col items-start justify-between">
|
||||
<Icon icon="ic:round-drag-indicator" class="drag-icon cursor-move" />
|
||||
<Icon
|
||||
icon="ep:delete"
|
||||
class="cursor-pointer text-red-5"
|
||||
@click="handleDeleteItem(index)"
|
||||
v-if="formData.items.length > 1"
|
||||
/>
|
||||
</div>
|
||||
<div class="w-full flex flex-col">
|
||||
<div class="m-b-8px flex items-center justify-around">
|
||||
<div class="flex flex-col items-center justify-between">
|
||||
<UploadImg
|
||||
v-model="element.iconUrl"
|
||||
width="40px"
|
||||
height="40px"
|
||||
:show-delete="false"
|
||||
:show-btn-text="false"
|
||||
/>
|
||||
<el-text size="small">默认图片</el-text>
|
||||
</div>
|
||||
<div>
|
||||
<UploadImg
|
||||
v-model="element.activeIconUrl"
|
||||
width="40px"
|
||||
height="40px"
|
||||
:show-delete="false"
|
||||
:show-btn-text="false"
|
||||
/>
|
||||
<el-text>选中图片</el-text>
|
||||
</div>
|
||||
</div>
|
||||
<el-form-item prop="text" label-width="0" class="m-b-8px!">
|
||||
<el-input v-model="element.text" placeholder="请输入文字" />
|
||||
</el-form-item>
|
||||
<el-form-item prop="url" label-width="0" class="m-b-0!">
|
||||
<el-input v-model="element.url" placeholder="请选择链接" />
|
||||
</el-form-item>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
|
||||
<el-form-item label-width="0">
|
||||
<!-- 添加导航按钮 -->
|
||||
<el-tooltip content="最多添加5个">
|
||||
<el-button
|
||||
@click="handleAddItem"
|
||||
class="m-b-16px w-full"
|
||||
type="primary"
|
||||
plain
|
||||
:disabled="formData!.items.length >= 5"
|
||||
>
|
||||
添加导航
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import draggable from 'vuedraggable' //拖拽组件
|
||||
import { TabBarItemProperty, TabBarProperty, THEME_LIST } from './config'
|
||||
import { usePropertyForm } from '@/components/DiyEditor/util'
|
||||
// 底部导航栏
|
||||
defineOptions({ name: 'TabBarProperty' })
|
||||
|
||||
const props = defineProps<{ modelValue: TabBarProperty }>()
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const { formData } = usePropertyForm(props.modelValue, emit)
|
||||
|
||||
/** 添加导航项 */
|
||||
const handleAddItem = () => {
|
||||
formData?.value?.items?.push({} as TabBarItemProperty)
|
||||
}
|
||||
/** 删除导航项 */
|
||||
const handleDeleteItem = (index: number) => {
|
||||
formData?.value?.items?.splice(index, 1)
|
||||
}
|
||||
|
||||
// 要的主题
|
||||
const handleThemeChange = () => {
|
||||
const theme = THEME_LIST.find((theme) => theme.id === formData.value.theme)
|
||||
if (theme?.color) {
|
||||
formData.value.style.activeColor = theme.color
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
@ -0,0 +1,65 @@
|
||||
import { DiyComponent } from '@/components/DiyEditor/util'
|
||||
|
||||
/** 标题栏属性 */
|
||||
export interface TitleBarProperty {
|
||||
// 主标题
|
||||
title: string
|
||||
// 副标题
|
||||
description: string
|
||||
// 标题大小
|
||||
titleSize: number
|
||||
// 描述大小
|
||||
descriptionSize: number
|
||||
// 标题粗细
|
||||
titleWeight: number
|
||||
// 显示位置
|
||||
position: 'left' | 'center'
|
||||
// 描述粗细
|
||||
descriptionWeight: number
|
||||
// 标题颜色
|
||||
titleColor: string
|
||||
// 描述颜色
|
||||
descriptionColor: string
|
||||
// 背景颜色
|
||||
backgroundColor: string
|
||||
// 底部分割线
|
||||
showBottomBorder: false
|
||||
// 查看更多
|
||||
more: {
|
||||
// 是否显示查看更多
|
||||
show: false
|
||||
// 样式选择
|
||||
type: 'text' | 'icon' | 'all'
|
||||
// 自定义文字
|
||||
text: string
|
||||
// 链接
|
||||
url: string
|
||||
}
|
||||
}
|
||||
|
||||
// 定义组件
|
||||
export const component = {
|
||||
id: 'TitleBar',
|
||||
name: '标题栏',
|
||||
icon: 'material-symbols:line-start',
|
||||
property: {
|
||||
title: '主标题',
|
||||
description: '副标题',
|
||||
titleSize: 16,
|
||||
descriptionSize: 12,
|
||||
titleWeight: 400,
|
||||
position: 'left',
|
||||
descriptionWeight: 200,
|
||||
titleColor: 'rgba(50, 50, 51, 10)',
|
||||
descriptionColor: 'rgba(150, 151, 153, 10)',
|
||||
backgroundColor: 'rgba(255, 255, 255, 10)',
|
||||
showBottomBorder: false,
|
||||
more: {
|
||||
//查看更多
|
||||
show: false,
|
||||
type: 'icon',
|
||||
text: '查看更多',
|
||||
url: ''
|
||||
}
|
||||
}
|
||||
} as DiyComponent<TitleBarProperty>
|
@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<div
|
||||
class="title-bar"
|
||||
:style="{
|
||||
background: property.backgroundColor,
|
||||
borderBottom: property.showBottomBorder ? '1px solid #F9F9F9' : '1px solid #fff'
|
||||
}"
|
||||
>
|
||||
<div>
|
||||
<!-- 标题 -->
|
||||
<div
|
||||
:style="{
|
||||
fontSize: `${property.titleSize}px`,
|
||||
fontWeight: property.titleWeight,
|
||||
color: property.titleColor,
|
||||
textAlign: property.position
|
||||
}"
|
||||
v-if="property.title"
|
||||
>
|
||||
{{ property.title }}
|
||||
</div>
|
||||
<!-- 副标题 -->
|
||||
<div
|
||||
:style="{
|
||||
fontSize: `${property.descriptionSize}px`,
|
||||
fontWeight: property.descriptionWeight,
|
||||
color: property.descriptionColor,
|
||||
textAlign: property.position
|
||||
}"
|
||||
class="m-t-8px"
|
||||
v-if="property.description"
|
||||
>
|
||||
{{ property.description }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- 更多 -->
|
||||
<div
|
||||
class="more"
|
||||
v-show="property.more.show"
|
||||
:style="{
|
||||
color: property.more.type === 'text' ? '#38f' : ''
|
||||
}"
|
||||
>
|
||||
{{ property.more.type === 'icon' ? '' : property.more.text }}
|
||||
<Icon icon="ep:arrow-right" v-if="property.more.type !== 'text'" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { TitleBarProperty } from './config'
|
||||
|
||||
/** 标题栏 */
|
||||
defineOptions({ name: 'TitleBar' })
|
||||
|
||||
defineProps<{ property: TitleBarProperty }>()
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.title-bar {
|
||||
border: 2px solid #fff;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
padding: 8px 16px;
|
||||
min-height: 20px;
|
||||
position: relative;
|
||||
|
||||
/* 更多 */
|
||||
.more {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
margin: auto;
|
||||
font-size: 10px;
|
||||
color: #969799;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
115
src/components/DiyEditor/components/mobile/TitleBar/property.vue
Normal file
115
src/components/DiyEditor/components/mobile/TitleBar/property.vue
Normal file
@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<section class="title-bar">
|
||||
<el-form label-width="85px" :model="formData" :rules="rules">
|
||||
<el-form-item label="主标题" prop="title">
|
||||
<el-input
|
||||
v-model="formData.title"
|
||||
placeholder="请输入主标题"
|
||||
show-word-limit
|
||||
maxlength="20"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="副标题" prop="description">
|
||||
<el-input
|
||||
type="textarea"
|
||||
v-model="formData.description"
|
||||
placeholder="请输入副标题"
|
||||
maxlength="50"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="显示位置" prop="position">
|
||||
<el-radio-group v-model="formData!.position">
|
||||
<el-tooltip content="居左" placement="top">
|
||||
<el-radio-button label="left">
|
||||
<Icon icon="ant-design:align-left-outlined" />
|
||||
</el-radio-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="居中" placement="top">
|
||||
<el-radio-button label="center">
|
||||
<Icon icon="ant-design:align-center-outlined" />
|
||||
</el-radio-button>
|
||||
</el-tooltip>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="标题大小" prop="titleSize">
|
||||
<el-slider v-model="formData.titleSize" :max="60" :min="10" show-input input-size="small" />
|
||||
</el-form-item>
|
||||
<el-form-item label="副标题大小" prop="descriptionSize">
|
||||
<el-slider
|
||||
v-model="formData.descriptionSize"
|
||||
:max="60"
|
||||
:min="10"
|
||||
show-input
|
||||
input-size="small"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="标题粗细" prop="titleWeight">
|
||||
<el-slider
|
||||
v-model="formData.titleWeight"
|
||||
:min="100"
|
||||
:max="900"
|
||||
:step="100"
|
||||
show-input
|
||||
input-size="small"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="副标题粗细" prop="descriptionWeight">
|
||||
<el-slider
|
||||
v-model="formData.descriptionWeight"
|
||||
:min="100"
|
||||
:max="900"
|
||||
:step="100"
|
||||
show-input
|
||||
input-size="small"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="标题颜色" prop="titleColor">
|
||||
<ColorInput v-model="formData.titleColor" />
|
||||
</el-form-item>
|
||||
<el-form-item label="副标题颜色" prop="descriptionColor">
|
||||
<ColorInput v-model="formData.descriptionColor" />
|
||||
</el-form-item>
|
||||
<el-form-item label="背景颜色" prop="backgroundColor">
|
||||
<ColorInput v-model="formData.backgroundColor" />
|
||||
</el-form-item>
|
||||
<el-form-item label="底部分割线" prop="showBottomBorder">
|
||||
<el-switch v-model="formData!.showBottomBorder" />
|
||||
</el-form-item>
|
||||
<el-form-item label="查看更多" prop="more.show">
|
||||
<el-checkbox v-model="formData.more.show" />
|
||||
</el-form-item>
|
||||
<!-- 更多样式选择 -->
|
||||
<template v-if="formData.more.show">
|
||||
<el-form-item label="样式" prop="more.type">
|
||||
<el-radio-group v-model="formData.more.type">
|
||||
<el-radio label="text">文字</el-radio>
|
||||
<el-radio label="icon">图标</el-radio>
|
||||
<el-radio label="all">文字+图标</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="更多文字" prop="more.text" v-show="formData.more.type !== 'icon'">
|
||||
<el-input v-model="formData.more.text" />
|
||||
</el-form-item>
|
||||
<el-form-item label="跳转链接" prop="more.url">
|
||||
<el-input v-model="formData.more.url" placeholder="请输入跳转链接" />
|
||||
</el-form-item>
|
||||
</template>
|
||||
</el-form>
|
||||
</section>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { TitleBarProperty } from './config'
|
||||
import { usePropertyForm } from '@/components/DiyEditor/util'
|
||||
// 导航栏属性面板
|
||||
defineOptions({ name: 'TitleBarProperty' })
|
||||
|
||||
const props = defineProps<{ modelValue: TitleBarProperty }>()
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const { formData } = usePropertyForm(props.modelValue, emit)
|
||||
|
||||
// 表单校验
|
||||
const rules = {}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
@ -0,0 +1,37 @@
|
||||
import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
|
||||
|
||||
/** 视频播放属性 */
|
||||
export interface VideoPlayerProperty {
|
||||
// 视频链接
|
||||
videoUrl: string
|
||||
// 封面链接
|
||||
posterUrl: string
|
||||
// 是否自动播放
|
||||
autoplay: boolean
|
||||
// 组件样式
|
||||
style: VideoPlayerStyle
|
||||
}
|
||||
|
||||
// 视频播放样式
|
||||
export interface VideoPlayerStyle extends ComponentStyle {
|
||||
// 视频高度
|
||||
height: number
|
||||
}
|
||||
|
||||
// 定义组件
|
||||
export const component = {
|
||||
id: 'VideoPlayer',
|
||||
name: '视频播放',
|
||||
icon: 'ep:video-play',
|
||||
property: {
|
||||
videoUrl: '',
|
||||
posterUrl: '',
|
||||
autoplay: false,
|
||||
style: {
|
||||
bgType: 'color',
|
||||
bgColor: '#fff',
|
||||
marginBottom: 8,
|
||||
height: 300
|
||||
} as ComponentStyle
|
||||
}
|
||||
} as DiyComponent<VideoPlayerProperty>
|
@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<div class="w-full" :style="{ height: `${property.style.height}px` }">
|
||||
<el-image class="w-full w-full" :src="property.posterUrl" v-if="property.posterUrl" />
|
||||
<video
|
||||
v-else
|
||||
class="w-full w-full"
|
||||
:src="property.videoUrl"
|
||||
:poster="property.posterUrl"
|
||||
:autoplay="property.autoplay"
|
||||
controls
|
||||
></video>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { VideoPlayerProperty } from './config'
|
||||
|
||||
/** 视频播放 */
|
||||
defineOptions({ name: 'VideoPlayer' })
|
||||
|
||||
defineProps<{ property: VideoPlayerProperty }>()
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
/* 图片 */
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<ComponentContainerProperty v-model="formData.style">
|
||||
<template #style="{ formData }">
|
||||
<el-form-item label="高度" prop="height">
|
||||
<el-slider
|
||||
v-model="formData.height"
|
||||
:max="500"
|
||||
:min="100"
|
||||
show-input
|
||||
input-size="small"
|
||||
:show-input-controls="false"
|
||||
/>
|
||||
</el-form-item>
|
||||
</template>
|
||||
<el-form label-width="80px" :model="formData">
|
||||
<el-form-item label="上传视频" prop="videoUrl">
|
||||
<UploadFile
|
||||
v-model="formData.videoUrl"
|
||||
:file-type="['mp4']"
|
||||
:limit="1"
|
||||
:file-size="100"
|
||||
class="min-w-80px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="上传封面" prop="posterUrl">
|
||||
<UploadImg
|
||||
v-model="formData.posterUrl"
|
||||
draggable="false"
|
||||
height="80px"
|
||||
width="100%"
|
||||
class="min-w-80px"
|
||||
>
|
||||
<template #tip> 建议宽度750 </template>
|
||||
</UploadImg>
|
||||
</el-form-item>
|
||||
<el-form-item label="自动播放" prop="autoplay">
|
||||
<el-switch v-model="formData.autoplay" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</ComponentContainerProperty>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { VideoPlayerProperty } from './config'
|
||||
import { usePropertyForm } from '@/components/DiyEditor/util'
|
||||
|
||||
// 视频播放属性面板
|
||||
defineOptions({ name: 'VideoPlayerProperty' })
|
||||
|
||||
const props = defineProps<{ modelValue: VideoPlayerProperty }>()
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const { formData } = usePropertyForm(props.modelValue, emit)
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
61
src/components/DiyEditor/components/mobile/index.ts
Normal file
61
src/components/DiyEditor/components/mobile/index.ts
Normal file
@ -0,0 +1,61 @@
|
||||
/*
|
||||
* 组件注册
|
||||
*
|
||||
* 组件规范:
|
||||
* 1. 每个子目录就是一个独立的组件,每个目录包括以下三个文件:
|
||||
* 2. config.ts:组件配置,必选,用于定义组件、组件默认的属性、定义属性的类型
|
||||
* 3. index.vue:组件展示,用于展示组件的渲染效果。可以不提供,如 Page(页面设置),只需要属性配置表单即可
|
||||
* 4. property.vue:组件属性表单,用于配置组件,必选,
|
||||
*
|
||||
* 注:
|
||||
* 组件ID以config.ts中配置的id为准,与组件目录的名称无关,但还是建议组件目录的名称与组件ID保持一致
|
||||
*/
|
||||
|
||||
// 导入组件界面模块
|
||||
const viewModules: Record<string, any> = import.meta.glob('./*/*.vue')
|
||||
// 导入配置模块
|
||||
const configModules: Record<string, any> = import.meta.glob('./*/config.ts', { eager: true })
|
||||
|
||||
// 界面模块
|
||||
const components = {}
|
||||
// 组件配置模块
|
||||
const componentConfigs = {}
|
||||
|
||||
// 组件界面的类型
|
||||
type ViewType = 'index' | 'property'
|
||||
|
||||
/**
|
||||
* 注册组件的界面模块
|
||||
*
|
||||
* @param componentId 组件ID
|
||||
* @param configPath 配置模块的文件路径
|
||||
* @param viewType 组件界面的类型
|
||||
*/
|
||||
const registerComponentViewModule = (
|
||||
componentId: string,
|
||||
configPath: string,
|
||||
viewType: ViewType
|
||||
) => {
|
||||
const viewPath = configPath.replace('config.ts', `${viewType}.vue`)
|
||||
const viewModule = viewModules[viewPath]
|
||||
if (viewModule) {
|
||||
// 定义异步组件
|
||||
components[componentId] = defineAsyncComponent(viewModule)
|
||||
}
|
||||
}
|
||||
|
||||
// 注册
|
||||
Object.keys(configModules).forEach((modulePath: string) => {
|
||||
const component = configModules[modulePath].component
|
||||
const componentId = component?.id
|
||||
if (componentId) {
|
||||
// 注册组件
|
||||
componentConfigs[componentId] = component
|
||||
// 注册预览界面
|
||||
registerComponentViewModule(componentId, modulePath, 'index')
|
||||
// 注册属性配置表单
|
||||
registerComponentViewModule(`${componentId}Property`, modulePath, 'property')
|
||||
}
|
||||
})
|
||||
|
||||
export { components, componentConfigs }
|
454
src/components/DiyEditor/index.vue
Normal file
454
src/components/DiyEditor/index.vue
Normal file
@ -0,0 +1,454 @@
|
||||
<template>
|
||||
<el-container class="editor">
|
||||
<!-- 顶部:工具栏 -->
|
||||
<el-header class="editor-header">
|
||||
<!-- 左侧操作区 -->
|
||||
<slot name="toolBarLeft"></slot>
|
||||
<!-- 中心操作区 -->
|
||||
<div class="header-center flex flex-1 items-center justify-center">
|
||||
<span>{{ title }}</span>
|
||||
</div>
|
||||
<!-- 右侧操作区 -->
|
||||
<el-button-group class="header-right">
|
||||
<el-tooltip content="重置">
|
||||
<el-button @click="handleReset">
|
||||
<Icon icon="system-uicons:reset-alt" :size="24" />
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="预览">
|
||||
<el-button @click="handlePreview">
|
||||
<Icon icon="ep:view" :size="24" />
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="保存">
|
||||
<el-button @click="handleSave">
|
||||
<Icon icon="ep:check" :size="24" />
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</el-button-group>
|
||||
</el-header>
|
||||
<!-- 中心区域 -->
|
||||
<el-container class="editor-container">
|
||||
<!-- 左侧:组件库 -->
|
||||
<ComponentLibrary ref="componentLibrary" :list="libs" v-if="libs && libs.length > 0" />
|
||||
<!-- 中心设计区域 -->
|
||||
<div class="editor-center page-prop-area" @click="handlePageSelected">
|
||||
<!-- 手机顶部 -->
|
||||
<div class="editor-design-top">
|
||||
<!-- 手机顶部状态栏 -->
|
||||
<img src="@/assets/imgs/diy/statusBar.png" alt="" class="status-bar" />
|
||||
<!-- 手机顶部导航栏 -->
|
||||
<ComponentContainer
|
||||
v-if="showNavigationBar"
|
||||
:component="navigationBarComponent"
|
||||
:show-toolbar="false"
|
||||
:active="selectedComponent?.id === navigationBarComponent.id"
|
||||
@click="handleNavigationBarSelected"
|
||||
class="cursor-pointer!"
|
||||
/>
|
||||
</div>
|
||||
<!-- 手机页面编辑区域 -->
|
||||
<el-scrollbar
|
||||
height="100%"
|
||||
wrap-class="editor-design-center page-prop-area"
|
||||
view-class="phone-container"
|
||||
:view-style="{
|
||||
backgroundColor: pageConfigComponent.property.backgroundColor,
|
||||
backgroundImage: `url(${pageConfigComponent.property.backgroundImage})`
|
||||
}"
|
||||
>
|
||||
<draggable
|
||||
class="page-prop-area drag-area"
|
||||
v-model="pageComponents"
|
||||
item-key="index"
|
||||
:animation="200"
|
||||
filter=".component-toolbar"
|
||||
ghost-class="draggable-ghost"
|
||||
:force-fallback="true"
|
||||
group="component"
|
||||
@change="handleComponentChange"
|
||||
>
|
||||
<template #item="{ element, index }">
|
||||
<ComponentContainer
|
||||
:component="element"
|
||||
:active="selectedComponentIndex === index"
|
||||
:can-move-up="index > 0"
|
||||
:can-move-down="index < pageComponents.length - 1"
|
||||
@move="(direction) => handleMoveComponent(index, direction)"
|
||||
@copy="handleCopyComponent(index)"
|
||||
@delete="handleDeleteComponent(index)"
|
||||
@click="handleComponentSelected(element, index)"
|
||||
/>
|
||||
</template>
|
||||
</draggable>
|
||||
</el-scrollbar>
|
||||
<!-- 手机底部导航 -->
|
||||
<div v-if="showTabBar" :class="['editor-design-bottom', 'component', 'cursor-pointer!']">
|
||||
<ComponentContainer
|
||||
:component="tabBarComponent"
|
||||
:show-toolbar="false"
|
||||
:active="selectedComponent?.id === tabBarComponent.id"
|
||||
@click="handleTabBarSelected"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 右侧属性面板 -->
|
||||
<el-aside class="editor-right" width="350px" v-if="selectedComponent?.property">
|
||||
<el-card
|
||||
shadow="never"
|
||||
body-class="h-[calc(100%-var(--el-card-padding)-var(--el-card-padding))]"
|
||||
class="h-full"
|
||||
>
|
||||
<!-- 组件名称 -->
|
||||
<template #header>
|
||||
<div class="flex items-center gap-8px">
|
||||
<Icon :icon="selectedComponent.icon" color="gray" />
|
||||
<span>{{ selectedComponent.name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-scrollbar
|
||||
class="m-[calc(0px-var(--el-card-padding))]"
|
||||
view-class="p-[var(--el-card-padding)] p-b-[calc(var(--el-card-padding)+var(--el-card-padding))] property"
|
||||
>
|
||||
<component
|
||||
:is="selectedComponent.id + 'Property'"
|
||||
v-model="selectedComponent.property"
|
||||
/>
|
||||
</el-scrollbar>
|
||||
</el-card>
|
||||
</el-aside>
|
||||
</el-container>
|
||||
</el-container>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
// 注册所有的组件
|
||||
import { components } from './components/mobile/index'
|
||||
export default {
|
||||
components: { ...components }
|
||||
}
|
||||
</script>
|
||||
<script lang="ts" setup>
|
||||
import draggable from 'vuedraggable'
|
||||
import ComponentLibrary from './components/ComponentLibrary.vue'
|
||||
import { cloneDeep, includes } from 'lodash-es'
|
||||
import { component as PAGE_CONFIG_COMPONENT } from '@/components/DiyEditor/components/mobile/PageConfig/config'
|
||||
import { component as NAVIGATION_BAR_COMPONENT } from './components/mobile/NavigationBar/config'
|
||||
import { component as TAB_BAR_COMPONENT } from './components/mobile/TabBar/config'
|
||||
import { isString } from '@/utils/is'
|
||||
import { DiyComponent, DiyComponentLibrary, PageConfig } from '@/components/DiyEditor/util'
|
||||
import { componentConfigs } from '@/components/DiyEditor/components/mobile'
|
||||
|
||||
/** 页面装修详情页 */
|
||||
defineOptions({ name: 'DiyPageDetail' })
|
||||
|
||||
// 消息弹窗
|
||||
const message = useMessage()
|
||||
// 左侧组件库
|
||||
const componentLibrary = ref()
|
||||
// 页面设置组件
|
||||
const pageConfigComponent = ref<DiyComponent<any>>(cloneDeep(PAGE_CONFIG_COMPONENT))
|
||||
// 顶部导航栏
|
||||
const navigationBarComponent = ref<DiyComponent<any>>(cloneDeep(NAVIGATION_BAR_COMPONENT))
|
||||
// 底部导航菜单
|
||||
const tabBarComponent = ref<DiyComponent<any>>(cloneDeep(TAB_BAR_COMPONENT))
|
||||
|
||||
// 选中的组件,默认选中顶部导航栏
|
||||
const selectedComponent = ref<DiyComponent<any>>()
|
||||
// 选中的组件索引
|
||||
const selectedComponentIndex = ref<number>(-1)
|
||||
// 组件列表
|
||||
const pageComponents = ref<DiyComponent<any>[]>([])
|
||||
// 定义属性
|
||||
const props = defineProps<{
|
||||
// 页面配置,支持Json字符串
|
||||
modelValue: string | PageConfig
|
||||
// 标题
|
||||
title: string
|
||||
// 组件库
|
||||
libs: DiyComponentLibrary[]
|
||||
// 是否显示顶部导航栏
|
||||
showNavigationBar: boolean
|
||||
// 是否显示底部导航菜单
|
||||
showTabBar: boolean
|
||||
// 是否显示页面配置
|
||||
showPageConfig: boolean
|
||||
}>()
|
||||
|
||||
// 监听传入的页面配置
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
() => {
|
||||
const modelValue = isString(props.modelValue)
|
||||
? (JSON.parse(props.modelValue) as PageConfig)
|
||||
: props.modelValue
|
||||
pageConfigComponent.value.property = modelValue?.page || PAGE_CONFIG_COMPONENT.property
|
||||
navigationBarComponent.value.property =
|
||||
modelValue?.navigationBar || NAVIGATION_BAR_COMPONENT.property
|
||||
tabBarComponent.value.property = modelValue?.tabBar || TAB_BAR_COMPONENT.property
|
||||
// 查找对应的页面组件
|
||||
pageComponents.value = (modelValue?.components || []).map((item) => {
|
||||
const component = componentConfigs[item.id]
|
||||
return { ...component, property: item.property }
|
||||
})
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
// 保存
|
||||
const handleSave = () => {
|
||||
const pageConfig = {
|
||||
page: pageConfigComponent.value.property,
|
||||
navigationBar: navigationBarComponent.value.property,
|
||||
tabBar: tabBarComponent.value.property,
|
||||
components: pageComponents.value.map((component) => {
|
||||
// 只保留APP有用的字段
|
||||
return { id: component.id, property: component.property }
|
||||
})
|
||||
} as PageConfig
|
||||
if (!props.showTabBar) {
|
||||
delete pageConfig.tabBar
|
||||
}
|
||||
// 发送数据更新通知
|
||||
const modelValue = isString(props.modelValue) ? JSON.stringify(pageConfig) : pageConfig
|
||||
emits('update:modelValue', modelValue)
|
||||
// 发送保存通知
|
||||
emits('save', pageConfig)
|
||||
}
|
||||
|
||||
// 处理页面选中:显示属性表单
|
||||
const handlePageSelected = (event: any) => {
|
||||
if (!props.showPageConfig) return
|
||||
|
||||
// 配置了样式 page-prop-area 的元素,才显示页面设置
|
||||
if (includes(event?.target?.classList, 'page-prop-area')) {
|
||||
handleComponentSelected(unref(pageConfigComponent))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 选中组件
|
||||
*
|
||||
* @param component 组件
|
||||
* @param index 组件的索引
|
||||
*/
|
||||
const handleComponentSelected = (component: DiyComponent<any>, index: number = -1) => {
|
||||
selectedComponent.value = component
|
||||
selectedComponentIndex.value = index
|
||||
}
|
||||
|
||||
// 选中顶部导航栏
|
||||
const handleNavigationBarSelected = () => {
|
||||
handleComponentSelected(unref(navigationBarComponent))
|
||||
}
|
||||
|
||||
// 选中底部导航菜单
|
||||
const handleTabBarSelected = () => {
|
||||
handleComponentSelected(unref(tabBarComponent))
|
||||
}
|
||||
|
||||
// 组件变动
|
||||
const handleComponentChange = (dragEvent: any) => {
|
||||
// 新增,即从组件库拖拽添加组件
|
||||
if (dragEvent.added) {
|
||||
const { element, newIndex } = dragEvent.added
|
||||
handleComponentSelected(element, newIndex)
|
||||
} else if (dragEvent.moved) {
|
||||
// 拖拽排序
|
||||
const { newIndex } = dragEvent.moved
|
||||
// 保持选中
|
||||
selectedComponentIndex.value = newIndex
|
||||
}
|
||||
}
|
||||
|
||||
// 交换组件
|
||||
const swapComponent = (oldIndex: number, newIndex: number) => {
|
||||
;[pageComponents.value[oldIndex], pageComponents.value[newIndex]] = [
|
||||
pageComponents.value[newIndex],
|
||||
pageComponents.value[oldIndex]
|
||||
]
|
||||
// 保持选中
|
||||
selectedComponentIndex.value = newIndex
|
||||
}
|
||||
|
||||
/** 移动组件 */
|
||||
const handleMoveComponent = (index: number, direction: number) => {
|
||||
const newIndex = index + direction
|
||||
if (newIndex < 0 || newIndex >= pageComponents.value.length) return
|
||||
|
||||
swapComponent(index, newIndex)
|
||||
}
|
||||
/** 复制组件 */
|
||||
const handleCopyComponent = (index: number) => {
|
||||
const component = cloneDeep(pageComponents.value[index])
|
||||
pageComponents.value.splice(index + 1, 0, component)
|
||||
}
|
||||
/**
|
||||
* 删除组件
|
||||
* @param index 当前组件index
|
||||
*/
|
||||
const handleDeleteComponent = (index: number) => {
|
||||
// 删除组件
|
||||
pageComponents.value.splice(index, 1)
|
||||
if (index < pageComponents.value.length) {
|
||||
// 1. 不是最后一个组件时,删除后选中下面的组件
|
||||
let bottomIndex = index
|
||||
handleComponentSelected(pageComponents.value[bottomIndex], bottomIndex)
|
||||
} else if (pageComponents.value.length > 0) {
|
||||
// 2. 不是第一个组件时,删除后选中上面的组件
|
||||
let topIndex = index - 1
|
||||
handleComponentSelected(pageComponents.value[topIndex], topIndex)
|
||||
} else {
|
||||
// 3. 组件全部删除之后,显示页面设置
|
||||
handleComponentSelected(unref(pageConfigComponent))
|
||||
}
|
||||
}
|
||||
|
||||
// 工具栏操作
|
||||
const emits = defineEmits(['reset', 'preview', 'save', 'update:modelValue'])
|
||||
// 重置
|
||||
const handleReset = () => {
|
||||
message.warning('开发中~')
|
||||
emits('reset')
|
||||
}
|
||||
// 预览
|
||||
const handlePreview = () => {
|
||||
message.warning('开发中~')
|
||||
emits('preview')
|
||||
}
|
||||
|
||||
// 设置默认选中的组件
|
||||
const setDefaultSelectedComponent = () => {
|
||||
if (props.showPageConfig) {
|
||||
selectedComponent.value = unref(pageConfigComponent)
|
||||
} else if (props.showNavigationBar) {
|
||||
selectedComponent.value = unref(navigationBarComponent)
|
||||
} else if (props.showTabBar) {
|
||||
selectedComponent.value = unref(tabBarComponent)
|
||||
}
|
||||
}
|
||||
watch(
|
||||
() => [props.showPageConfig, props.showNavigationBar, props.showTabBar],
|
||||
() => setDefaultSelectedComponent()
|
||||
)
|
||||
onMounted(() => setDefaultSelectedComponent())
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
/* 手机宽度 */
|
||||
$phone-width: 375px;
|
||||
$toolbar-height: 42px;
|
||||
/* 根节点样式 */
|
||||
.editor {
|
||||
height: 100%;
|
||||
margin: calc(0px - var(--app-content-padding));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* 顶部:工具栏 */
|
||||
.editor-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: $toolbar-height;
|
||||
padding: 0;
|
||||
border-bottom: solid 1px var(--el-border-color);
|
||||
background-color: var(--el-bg-color);
|
||||
/* 工具栏:右侧按钮 */
|
||||
.header-right {
|
||||
height: 100%;
|
||||
.el-button {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
/* 隐藏工具栏按钮的边框 */
|
||||
:deep(.el-radio-button__inner),
|
||||
:deep(.el-button) {
|
||||
border-top: none !important;
|
||||
border-bottom: none !important;
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
}
|
||||
/* 中心操作区 */
|
||||
.editor-container {
|
||||
height: calc(
|
||||
100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) -
|
||||
$toolbar-height
|
||||
);
|
||||
/* 右侧属性面板 */
|
||||
.editor-right {
|
||||
flex-shrink: 0;
|
||||
box-shadow: -8px 0 8px -8px rgba(0, 0, 0, 0.12);
|
||||
overflow: hidden;
|
||||
/* 属性面板顶部:减少内边距 */
|
||||
:deep(.el-card__header) {
|
||||
padding: 8px 16px;
|
||||
}
|
||||
/* 属性面板分组 */
|
||||
:deep(.property-group) {
|
||||
margin: 0 -20px;
|
||||
&.el-card {
|
||||
border: none;
|
||||
}
|
||||
/* 属性分组名称 */
|
||||
.el-card__header {
|
||||
border: none;
|
||||
background: var(--el-bg-color-page);
|
||||
padding: 8px 32px;
|
||||
}
|
||||
.el-card__body {
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 中心区域 */
|
||||
.editor-center {
|
||||
position: relative;
|
||||
flex: 1 1 0;
|
||||
background-color: var(--app-content-bg-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
margin: 16px 0 0 0;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
|
||||
/* 手机顶部 */
|
||||
.editor-design-top {
|
||||
width: $phone-width;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* 手机顶部状态栏 */
|
||||
.status-bar {
|
||||
height: 20px;
|
||||
width: $phone-width;
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
/* 手机底部导航 */
|
||||
.editor-design-bottom {
|
||||
width: $phone-width;
|
||||
margin: 0 auto;
|
||||
}
|
||||
/* 手机页面编辑区域 */
|
||||
:deep(.editor-design-center) {
|
||||
width: 100%;
|
||||
|
||||
/* 主体内容 */
|
||||
.phone-container {
|
||||
position: relative;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 100%;
|
||||
height: 100%;
|
||||
width: $phone-width;
|
||||
margin: 0 auto;
|
||||
.drag-area {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
120
src/components/DiyEditor/util.ts
Normal file
120
src/components/DiyEditor/util.ts
Normal file
@ -0,0 +1,120 @@
|
||||
import { ref, Ref } from 'vue'
|
||||
import { PageConfigProperty } from '@/components/DiyEditor/components/mobile/PageConfig/config'
|
||||
import { NavigationBarProperty } from '@/components/DiyEditor/components/mobile/NavigationBar/config'
|
||||
import { TabBarProperty } from '@/components/DiyEditor/components/mobile/TabBar/config'
|
||||
|
||||
// 页面装修组件
|
||||
export interface DiyComponent<T> {
|
||||
// 组件唯一标识
|
||||
id: string
|
||||
// 组件名称
|
||||
name: string
|
||||
// 组件图标
|
||||
icon: string
|
||||
// 组件属性
|
||||
property: T
|
||||
}
|
||||
|
||||
// 页面装修组件库
|
||||
export interface DiyComponentLibrary {
|
||||
// 组件库名称
|
||||
name: string
|
||||
// 是否展开
|
||||
extended: boolean
|
||||
// 组件列表
|
||||
components: string[]
|
||||
}
|
||||
|
||||
// 组件样式
|
||||
export interface ComponentStyle {
|
||||
// 背景类型
|
||||
bgType: 'color' | 'img'
|
||||
// 背景颜色
|
||||
bgColor: string
|
||||
// 背景图片
|
||||
bgImg: string
|
||||
// 外边距
|
||||
margin: number
|
||||
marginTop: number
|
||||
marginRight: number
|
||||
marginBottom: number
|
||||
marginLeft: number
|
||||
// 内边距
|
||||
padding: number
|
||||
paddingTop: number
|
||||
paddingRight: number
|
||||
paddingBottom: number
|
||||
paddingLeft: number
|
||||
// 边框圆角
|
||||
borderRadius: number
|
||||
borderTopLeftRadius: number
|
||||
borderTopRightRadius: number
|
||||
borderBottomRightRadius: number
|
||||
borderBottomLeftRadius: number
|
||||
}
|
||||
|
||||
// 页面配置
|
||||
export interface PageConfig {
|
||||
// 页面属性
|
||||
page: PageConfigProperty
|
||||
// 顶部导航栏属性
|
||||
navigationBar: NavigationBarProperty
|
||||
// 底部导航菜单属性
|
||||
tabBar?: TabBarProperty
|
||||
// 页面组件列表
|
||||
components: PageComponent[]
|
||||
}
|
||||
// 页面组件,只保留组件ID,组件属性
|
||||
export interface PageComponent extends Pick<DiyComponent<any>, 'id' | 'property'> {}
|
||||
|
||||
// 属性表单监听
|
||||
export function usePropertyForm<T>(modelValue: T, emit: Function): { formData: Ref<T> } {
|
||||
const formData = ref<T>()
|
||||
// 监听属性数据变动
|
||||
watch(
|
||||
() => modelValue,
|
||||
() => {
|
||||
formData.value = modelValue
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
immediate: true
|
||||
}
|
||||
)
|
||||
// 监听表单数据变动
|
||||
watch(
|
||||
() => formData.value,
|
||||
() => {
|
||||
emit('update:modelValue', formData.value)
|
||||
},
|
||||
{
|
||||
deep: true
|
||||
}
|
||||
)
|
||||
|
||||
return { formData }
|
||||
}
|
||||
|
||||
// 页面组件库
|
||||
export const PAGE_LIBS = [
|
||||
{
|
||||
name: '基础组件',
|
||||
extended: true,
|
||||
components: [
|
||||
'SearchBar',
|
||||
'NoticeBar',
|
||||
'GridNavigation',
|
||||
'ListNavigation',
|
||||
'Divider',
|
||||
'TitleBar'
|
||||
]
|
||||
},
|
||||
{ name: '图文组件', extended: true, components: ['ImageBar', 'Carousel', 'VideoPlayer'] },
|
||||
{ name: '商品组件', extended: true, components: ['ProductCard'] },
|
||||
{
|
||||
name: '会员组件',
|
||||
extended: true,
|
||||
components: ['UserCard', 'OrderCard', 'WalletCard', 'CouponCard']
|
||||
},
|
||||
{ name: '营销组件', extended: true, components: ['Combination', 'Seckill', 'Point', 'Coupon'] }
|
||||
] as DiyComponentLibrary[]
|
@ -33,11 +33,10 @@
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { PropType } from 'vue'
|
||||
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { getAccessToken, getTenantId } from '@/utils/auth'
|
||||
import type { UploadInstance, UploadUserFile, UploadProps, UploadRawFile } from 'element-plus'
|
||||
import { isArray, isString } from '@/utils/is'
|
||||
|
||||
defineOptions({ name: 'UploadFile' })
|
||||
|
||||
@ -45,10 +44,7 @@ const message = useMessage() // 消息弹窗
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Array as PropType<UploadUserFile[]>,
|
||||
required: true
|
||||
},
|
||||
modelValue: propTypes.oneOfType<string | string[]>([String, Array<String>]).isRequired,
|
||||
title: propTypes.string.def('文件上传'),
|
||||
updateUrl: propTypes.string.def(import.meta.env.VITE_UPLOAD_URL),
|
||||
fileType: propTypes.array.def(['doc', 'xls', 'ppt', 'txt', 'pdf']), // 文件类型, 例如['png', 'jpg', 'jpeg']
|
||||
@ -62,7 +58,7 @@ const props = defineProps({
|
||||
const valueRef = ref(props.modelValue)
|
||||
const uploadRef = ref<UploadInstance>()
|
||||
const uploadList = ref<UploadUserFile[]>([])
|
||||
const fileList = ref<UploadUserFile[]>(props.modelValue)
|
||||
const fileList = ref<UploadUserFile[]>([])
|
||||
const uploadNumber = ref<number>(0)
|
||||
const uploadHeaders = ref({
|
||||
Authorization: 'Bearer ' + getAccessToken(),
|
||||
@ -109,7 +105,7 @@ const handleFileSuccess: UploadProps['onSuccess'] = (res: any): void => {
|
||||
fileList.value = fileList.value.concat(uploadList.value)
|
||||
uploadList.value = []
|
||||
uploadNumber.value = 0
|
||||
emit('update:modelValue', listToString(fileList.value))
|
||||
emitUpdateModelValue()
|
||||
}
|
||||
}
|
||||
// 文件数超出提示
|
||||
@ -125,20 +121,47 @@ const handleRemove = (file) => {
|
||||
const findex = fileList.value.map((f) => f.name).indexOf(file.name)
|
||||
if (findex > -1) {
|
||||
fileList.value.splice(findex, 1)
|
||||
emit('update:modelValue', listToString(fileList.value))
|
||||
emitUpdateModelValue()
|
||||
}
|
||||
}
|
||||
const handlePreview: UploadProps['onPreview'] = (uploadFile) => {
|
||||
console.log(uploadFile)
|
||||
}
|
||||
// 对象转成指定字符串分隔
|
||||
const listToString = (list: UploadUserFile[], separator?: string) => {
|
||||
let strs = ''
|
||||
separator = separator || ','
|
||||
for (let i in list) {
|
||||
strs += list[i].url + separator
|
||||
|
||||
// 监听模型绑定值变动
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
() => {
|
||||
const files: string[] = []
|
||||
// 情况1:字符串
|
||||
if (isString(props.modelValue)) {
|
||||
// 情况1.1:逗号分隔的多值
|
||||
if (props.modelValue.includes(',')) {
|
||||
files.concat(props.modelValue.split(','))
|
||||
} else if (props.modelValue.length > 0) {
|
||||
files.push(props.modelValue)
|
||||
}
|
||||
} else if (isArray(props.modelValue)) {
|
||||
// 情况2:字符串
|
||||
files.concat(props.modelValue)
|
||||
} else {
|
||||
throw new Error('不支持的 modelValue 类型')
|
||||
}
|
||||
fileList.value = files.map((url: string) => {
|
||||
return { url, name: url.substring(url.lastIndexOf('/') + 1) } as UploadUserFile
|
||||
})
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
// 发送文件链接列表更新
|
||||
const emitUpdateModelValue = () => {
|
||||
// 情况1:数组结果
|
||||
let result: string | string[] = fileList.value.map((file) => file.url!)
|
||||
// 情况2:逗号分隔的字符串
|
||||
if (isString(props.modelValue)) {
|
||||
result = result.join(',')
|
||||
}
|
||||
return strs != '' ? strs.substr(0, strs.length - 1) : ''
|
||||
emit('update:modelValue', result)
|
||||
}
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
|
@ -18,15 +18,15 @@
|
||||
<div class="upload-handle" @click.stop>
|
||||
<div class="handle-icon" @click="editImg">
|
||||
<Icon icon="ep:edit" />
|
||||
<span>{{ t('action.edit') }}</span>
|
||||
<span v-if="showBtnText">{{ t('action.edit') }}</span>
|
||||
</div>
|
||||
<div class="handle-icon" @click="imgViewVisible = true">
|
||||
<Icon icon="ep:zoom-in" />
|
||||
<span>{{ t('action.detail') }}</span>
|
||||
<span v-if="showBtnText">{{ t('action.detail') }}</span>
|
||||
</div>
|
||||
<div class="handle-icon" @click="deleteImg">
|
||||
<div class="handle-icon" @click="deleteImg" v-if="showDelete">
|
||||
<Icon icon="ep:delete" />
|
||||
<span>{{ t('action.del') }}</span>
|
||||
<span v-if="showBtnText">{{ t('action.del') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -81,7 +81,11 @@ const props = defineProps({
|
||||
fileType: propTypes.array.def(['image/jpeg', 'image/png', 'image/gif']), // 图片类型限制 ==> 非必传(默认为 ["image/jpeg", "image/png", "image/gif"])
|
||||
height: propTypes.string.def('150px'), // 组件高度 ==> 非必传(默认为 150px)
|
||||
width: propTypes.string.def('150px'), // 组件宽度 ==> 非必传(默认为 150px)
|
||||
borderradius: propTypes.string.def('8px') // 组件边框圆角 ==> 非必传(默认为 8px)
|
||||
borderradius: propTypes.string.def('8px'), // 组件边框圆角 ==> 非必传(默认为 8px)
|
||||
// 是否显示删除按钮
|
||||
showDelete: propTypes.bool.def(true),
|
||||
// 是否显示按钮文字
|
||||
showBtnText: propTypes.bool.def(true)
|
||||
})
|
||||
const { t } = useI18n() // 国际化
|
||||
const message = useMessage() // 消息弹窗
|
||||
|
40
src/components/VerticalButtonGroup/index.vue
Normal file
40
src/components/VerticalButtonGroup/index.vue
Normal file
@ -0,0 +1,40 @@
|
||||
<template>
|
||||
<el-button-group v-bind="$attrs">
|
||||
<slot></slot>
|
||||
</el-button-group>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* 垂直按钮组
|
||||
* Element官方的按钮组只支持水平显示,通过重写样式实现垂直布局
|
||||
*/
|
||||
defineOptions({ name: 'VerticalButtonGroup' })
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.el-button-group {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.el-button-group > :deep(.el-button:first-child) {
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
border-top-right-radius: var(--el-border-radius-base);
|
||||
border-bottom-color: var(--el-button-divide-border-color);
|
||||
}
|
||||
.el-button-group > :deep(.el-button:last-child) {
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-left-radius: var(--el-border-radius-base);
|
||||
border-top-color: var(--el-button-divide-border-color);
|
||||
}
|
||||
.el-button-group :deep(.el-button--primary:not(:first-child):not(:last-child)) {
|
||||
border-top-color: var(--el-button-divide-border-color);
|
||||
border-bottom-color: var(--el-button-divide-border-color);
|
||||
}
|
||||
.el-button-group > :deep(.el-button:not(:last-child)) {
|
||||
margin-bottom: -1px;
|
||||
margin-right: 0;
|
||||
}
|
||||
</style>
|
@ -19,6 +19,6 @@ const title = computed(() => appStore.getTitle)
|
||||
:class="prefixCls"
|
||||
class="h-[var(--app-footer-height)] bg-[var(--app-content-bg-color)] text-center leading-[var(--app-footer-height)] text-[var(--el-text-color-placeholder)] dark:bg-[var(--el-bg-color)]"
|
||||
>
|
||||
<p style="font-size: 14px">Copyright ©2022-{{ title }}</p>
|
||||
<span class="text-14px">Copyright ©2022-{{ title }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -459,6 +459,52 @@ const remainingRouter: AppRouteRecordRaw[] = [
|
||||
component: () => import('@/views/pay/cashier/index.vue')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/diy',
|
||||
name: 'DiyCenter',
|
||||
meta: { hidden: true },
|
||||
component: Layout,
|
||||
children: [
|
||||
{
|
||||
path: 'template/decorate/:id',
|
||||
name: 'DiyTemplateDecorate',
|
||||
meta: {
|
||||
title: '模板装修',
|
||||
noCache: true,
|
||||
hidden: true
|
||||
},
|
||||
component: () => import('@/views/mall/promotion/diy/template/decorate.vue')
|
||||
},
|
||||
{
|
||||
path: 'page/decorate/:id',
|
||||
name: 'DiyPageDecorate',
|
||||
meta: {
|
||||
title: '页面装修',
|
||||
noCache: true,
|
||||
hidden: true
|
||||
},
|
||||
component: () => import('@/views/mall/promotion/diy/page/decorate.vue')
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/crm',
|
||||
component: Layout,
|
||||
name: 'CrmCenter',
|
||||
meta: { hidden: true },
|
||||
children: [
|
||||
{
|
||||
path: 'customer/detail/:id',
|
||||
name: 'CrmCustomerDetail',
|
||||
meta: {
|
||||
title: '客户详情',
|
||||
noCache: true,
|
||||
hidden: true
|
||||
},
|
||||
component: () => import('@/views/crm/customer/detail/index.vue')
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
|
@ -144,6 +144,8 @@ export enum DICT_TYPE {
|
||||
PAY_REFUND_STATUS = 'pay_refund_status', // 退款订单状态
|
||||
PAY_NOTIFY_STATUS = 'pay_notify_status', // 商户支付回调状态
|
||||
PAY_NOTIFY_TYPE = 'pay_notify_type', // 商户支付回调状态
|
||||
PAY_TRANSFER_STATUS = 'pay_transfer_status', // 转账订单状态
|
||||
PAY_TRANSFER_TYPE = 'pay_transfer_type', // 转账订单状态
|
||||
|
||||
// ========== MP 模块 ==========
|
||||
MP_AUTO_REPLY_REQUEST_MATCH = 'mp_auto_reply_request_match', // 自动回复请求匹配类型
|
||||
|
@ -193,10 +193,10 @@ const loginData = reactive({
|
||||
})
|
||||
|
||||
const socialList = [
|
||||
{ icon: 'ant-design:github-filled', type: 0 },
|
||||
{ icon: 'ant-design:wechat-filled', type: 30 },
|
||||
{ icon: 'ant-design:alipay-circle-filled', type: 0 },
|
||||
{ icon: 'ant-design:dingtalk-circle-filled', type: 20 }
|
||||
{ icon: 'ant-design:dingtalk-circle-filled', type: 20 },
|
||||
{ icon: 'ant-design:github-filled', type: 0 },
|
||||
{ icon: 'ant-design:alipay-circle-filled', type: 0 }
|
||||
]
|
||||
|
||||
// 获取验证码
|
||||
@ -210,7 +210,7 @@ const getCode = async () => {
|
||||
verify.value.show()
|
||||
}
|
||||
}
|
||||
//获取租户ID
|
||||
// 获取租户 ID
|
||||
const getTenantId = async () => {
|
||||
if (loginData.tenantEnable === 'true') {
|
||||
const res = await LoginApi.getTenantIdByName(loginData.loginForm.tenantName)
|
||||
@ -230,6 +230,15 @@ const getCookie = () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
// 根据域名,获得租户信息
|
||||
const getTenantByWebsite = async () => {
|
||||
const website = location.host
|
||||
const res = await LoginApi.getTenantByWebsite(website)
|
||||
if (res) {
|
||||
loginData.loginForm.tenantName = res.name
|
||||
authUtil.setTenantId(res.id)
|
||||
}
|
||||
}
|
||||
const loading = ref() // ElLoading.service 返回的实例
|
||||
// 登录
|
||||
const handleLogin = async (params) => {
|
||||
@ -278,10 +287,15 @@ const doSocialLogin = async (type: number) => {
|
||||
} else {
|
||||
loginLoading.value = true
|
||||
if (loginData.tenantEnable === 'true') {
|
||||
await message.prompt('请输入租户名称', t('common.reminder')).then(async ({ value }) => {
|
||||
const res = await LoginApi.getTenantIdByName(value)
|
||||
authUtil.setTenantId(res)
|
||||
})
|
||||
// 尝试先通过 tenantName 获取租户
|
||||
await getTenantId()
|
||||
// 如果获取不到,则需要弹出提示,进行处理
|
||||
if (!authUtil.getTenantId()) {
|
||||
await message.prompt('请输入租户名称', t('common.reminder')).then(async ({ value }) => {
|
||||
const res = await LoginApi.getTenantIdByName(value)
|
||||
authUtil.setTenantId(res)
|
||||
})
|
||||
}
|
||||
}
|
||||
// 计算 redirectUri
|
||||
// tricky: type、redirect需要先encode一次,否则钉钉回调会丢失。
|
||||
@ -307,6 +321,7 @@ watch(
|
||||
)
|
||||
onMounted(() => {
|
||||
getCookie()
|
||||
getTenantByWebsite()
|
||||
})
|
||||
</script>
|
||||
|
||||
|
@ -10,7 +10,7 @@
|
||||
<el-form-item label="线索名称" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入线索名称" />
|
||||
</el-form-item>
|
||||
<!-- TODO 客户选择 -->
|
||||
<!-- TODO wanwan 客户选择 -->
|
||||
<el-form-item label="客户" prop="customerId">
|
||||
<el-input v-model="formData.customerId" placeholder="请选择客户" />
|
||||
</el-form-item>
|
||||
@ -31,7 +31,7 @@
|
||||
<el-form-item label="地址" prop="address">
|
||||
<el-input v-model="formData.address" placeholder="请输入地址" />
|
||||
</el-form-item>
|
||||
<!-- TODO 负责人选择 -->
|
||||
<!-- TODO wanwan 负责人选择 -->
|
||||
<el-form-item label="负责人" prop="ownerUserId">
|
||||
<el-input v-model="formData.ownerUserId" placeholder="请输入负责人" />
|
||||
</el-form-item>
|
||||
@ -46,7 +46,6 @@
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { DICT_TYPE, getBoolDictOptions } from '@/utils/dict'
|
||||
import * as ClueApi from '@/api/crm/clue'
|
||||
|
||||
const { t } = useI18n() // 国际化
|
||||
|
@ -96,7 +96,7 @@
|
||||
:formatter="dateFormatter"
|
||||
width="180px"
|
||||
/>
|
||||
<el-table-column label="操作" align="center">
|
||||
<el-table-column label="操作" align="center" min-width="110" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
link
|
||||
|
@ -166,7 +166,7 @@ import download from '@/utils/download'
|
||||
import * as ContractApi from '@/api/crm/contract'
|
||||
import ContractForm from './ContractForm.vue'
|
||||
|
||||
defineOptions({ name: 'Contract' })
|
||||
defineOptions({ name: 'CrmContract' })
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
const { t } = useI18n() // 国际化
|
||||
|
@ -8,91 +8,135 @@
|
||||
v-loading="formLoading"
|
||||
>
|
||||
<el-row>
|
||||
<el-form-item label="客户名称" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入客户名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="所属行业" prop="industryId">
|
||||
<el-select v-model="formData.industryId" placeholder="请选择所属行业">
|
||||
<el-option
|
||||
v-for="dict in getStrDictOptions(DICT_TYPE.CRM_CUSTOMER_INDUSTRY)"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
<el-col :span="12">
|
||||
<el-form-item label="客户名称" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入客户名称" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="所属行业" prop="industryId">
|
||||
<el-select v-model="formData.industryId" placeholder="请选择所属行业">
|
||||
<el-option
|
||||
v-for="dict in getStrDictOptions(DICT_TYPE.CRM_CUSTOMER_INDUSTRY)"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="客户来源" prop="source">
|
||||
<el-select v-model="formData.source" placeholder="请选择客户来源">
|
||||
<el-option
|
||||
v-for="dict in getStrDictOptions(DICT_TYPE.CRM_CUSTOMER_SOURCE)"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="客户等级" prop="level">
|
||||
<el-select v-model="formData.level" placeholder="请选择客户等级">
|
||||
<el-option
|
||||
v-for="dict in getStrDictOptions(DICT_TYPE.CRM_CUSTOMER_LEVEL)"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="手机" prop="mobile">
|
||||
<el-input v-model="formData.mobile" placeholder="请输入手机" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="电话" prop="telephone">
|
||||
<el-input v-model="formData.telephone" placeholder="请输入电话" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="邮箱" prop="email">
|
||||
<el-input v-model="formData.email" placeholder="请输入邮箱" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="QQ" prop="qq">
|
||||
<el-input v-model="formData.qq" placeholder="请输入QQ" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="微信" prop="wechat">
|
||||
<el-input v-model="formData.wechat" placeholder="请输入微信" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="网址" prop="website">
|
||||
<el-input v-model="formData.website" placeholder="请输入网址" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="所在地" prop="areaId">
|
||||
<el-tree-select
|
||||
v-model="formData.areaId"
|
||||
:data="areaList"
|
||||
:props="defaultProps"
|
||||
:render-after-expand="true"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="详细地址" prop="detailAddress">
|
||||
<el-input v-model="formData.detailAddress" placeholder="请输入详细地址" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-form-item label="客户来源" prop="source">
|
||||
<el-select v-model="formData.source" placeholder="请选择客户来源">
|
||||
<el-option
|
||||
v-for="dict in getStrDictOptions(DICT_TYPE.CRM_CUSTOMER_SOURCE)"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="客户等级" prop="level">
|
||||
<el-select v-model="formData.level" placeholder="请选择客户等级">
|
||||
<el-option
|
||||
v-for="dict in getStrDictOptions(DICT_TYPE.CRM_CUSTOMER_LEVEL)"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-form-item label="手机" prop="mobile">
|
||||
<el-input v-model="formData.mobile" placeholder="请输入手机" />
|
||||
</el-form-item>
|
||||
<el-form-item label="电话" prop="telephone">
|
||||
<el-input v-model="formData.telephone" placeholder="请输入电话" />
|
||||
</el-form-item>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-form-item label="邮箱" prop="email">
|
||||
<el-input v-model="formData.email" placeholder="请输入邮箱" />
|
||||
</el-form-item>
|
||||
<el-form-item label="QQ" prop="qq">
|
||||
<el-input v-model="formData.qq" placeholder="请输入QQ" />
|
||||
</el-form-item>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-form-item label="微信" prop="wechat">
|
||||
<el-input v-model="formData.wechat" placeholder="请输入微信" />
|
||||
</el-form-item>
|
||||
<el-form-item label="网址" prop="website">
|
||||
<el-input v-model="formData.website" placeholder="请输入网址" />
|
||||
</el-form-item>
|
||||
</el-row>
|
||||
<el-row>
|
||||
<el-form-item label="地区编号" prop="areaId">
|
||||
<el-input v-model="formData.areaId" placeholder="请输入地区编号" />
|
||||
</el-form-item>
|
||||
<el-form-item label="详细地址" prop="detailAddress">
|
||||
<el-input v-model="formData.detailAddress" placeholder="请输入详细地址" />
|
||||
</el-form-item>
|
||||
</el-row>
|
||||
<!-- TODO @Wanwan 少一个负责人字段,默认先选中自己 -->
|
||||
<el-row>
|
||||
<el-form-item label="下次联系时间" prop="contactNextTime">
|
||||
<el-date-picker
|
||||
v-model="formData.contactNextTime"
|
||||
type="date"
|
||||
value-format="x"
|
||||
placeholder="选择下次联系时间"
|
||||
<el-form-item v-if="formType === 'create'" label="负责人" prop="userIds" span="24">
|
||||
<el-select v-model="formData.ownerUserId">
|
||||
<el-option
|
||||
v-for="item in userOptions"
|
||||
:key="parseInt(item.id)"
|
||||
:label="item.nickname"
|
||||
:value="parseInt(item.id)"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-row>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="下次联系时间" prop="contactNextTime">
|
||||
<el-date-picker
|
||||
v-model="formData.contactNextTime"
|
||||
type="date"
|
||||
value-format="x"
|
||||
placeholder="选择下次联系时间"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-form-item label="客户描述" prop="description">
|
||||
<el-input v-model="formData.description" placeholder="请输入客户描述" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="formData.remark" placeholder="请输入备注" />
|
||||
</el-form-item>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="客户描述" prop="description">
|
||||
<el-input v-model="formData.description" placeholder="请输入客户描述" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="formData.remark" placeholder="请输入备注" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
|
||||
@ -103,6 +147,10 @@
|
||||
<script setup lang="ts">
|
||||
import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
|
||||
import * as CustomerApi from '@/api/crm/customer'
|
||||
import * as AreaApi from '@/api/system/area'
|
||||
import { defaultProps } from '@/utils/tree'
|
||||
import * as UserApi from '@/api/system/user'
|
||||
import { CACHE_KEY, useCache } from '@/hooks/web/useCache'
|
||||
|
||||
const { t } = useI18n() // 国际化
|
||||
const message = useMessage() // 消息弹窗
|
||||
@ -111,6 +159,8 @@ const dialogVisible = ref(false) // 弹窗的是否展示
|
||||
const dialogTitle = ref('') // 弹窗的标题
|
||||
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
|
||||
const formType = ref('') // 表单的类型:create - 新增;update - 修改
|
||||
const areaList = ref([]) // 地区列表
|
||||
const userOptions = ref<UserApi.UserVO[]>([]) // 用户列表
|
||||
const formData = ref({
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
@ -127,10 +177,12 @@ const formData = ref({
|
||||
remark: undefined,
|
||||
areaId: undefined,
|
||||
detailAddress: undefined,
|
||||
contactNextTime: undefined
|
||||
contactNextTime: undefined,
|
||||
ownerUserId: undefined
|
||||
})
|
||||
const formRules = reactive({
|
||||
name: [{ require: true, message: '客户名称不能为空', trigger: 'blur' }]
|
||||
name: [{ required: true, message: '客户名称不能为空', trigger: 'blur' }],
|
||||
ownerUserId: [{ required: true, message: '负责人不能为空', trigger: 'blur' }]
|
||||
})
|
||||
const formRef = ref() // 表单 Ref
|
||||
|
||||
@ -149,6 +201,16 @@ const open = async (type: string, id?: number) => {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
// 获得地区列表
|
||||
areaList.value = await AreaApi.getAreaTree()
|
||||
// 获得用户列表
|
||||
userOptions.value = await UserApi.getSimpleUserList()
|
||||
// 默认新建时选中自己
|
||||
if (formType.value === 'create') {
|
||||
const { wsCache } = useCache()
|
||||
const user = wsCache.get(CACHE_KEY.USER).user
|
||||
formData.value.ownerUserId = user.id
|
||||
}
|
||||
}
|
||||
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
|
||||
|
||||
@ -196,7 +258,8 @@ const resetForm = () => {
|
||||
remark: undefined,
|
||||
areaId: undefined,
|
||||
detailAddress: undefined,
|
||||
contactNextTime: undefined
|
||||
contactNextTime: undefined,
|
||||
ownerUserId: undefined
|
||||
}
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
16
src/views/crm/customer/detail/CustomerBasicInfo.vue
Normal file
16
src/views/crm/customer/detail/CustomerBasicInfo.vue
Normal file
@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<el-col>
|
||||
<el-row>
|
||||
<span class="text-xl font-bold">{{ customer.name }}</span>
|
||||
</el-row>
|
||||
</el-col>
|
||||
<el-col class="mt-10px">
|
||||
<!-- TODO 标签 -->
|
||||
<!-- <Icon icon="ant-design:tag-filled" />-->
|
||||
</el-col>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import * as CustomerApi from '@/api/crm/customer'
|
||||
|
||||
const { customer } = defineProps<{ customer: CustomerApi.CustomerVO }>()
|
||||
</script>
|
92
src/views/crm/customer/detail/CustomerDetails.vue
Normal file
92
src/views/crm/customer/detail/CustomerDetails.vue
Normal file
@ -0,0 +1,92 @@
|
||||
<template>
|
||||
<el-collapse v-model="activeNames">
|
||||
<el-collapse-item name="basicInfo">
|
||||
<template #title>
|
||||
<span class="text-base font-bold">基本信息</span>
|
||||
</template>
|
||||
<el-descriptions :column="4">
|
||||
<el-descriptions-item label="客户名称">
|
||||
{{ customer.name }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="所属行业">
|
||||
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_INDUSTRY" :value="customer.industryId" />
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="客户来源">
|
||||
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_SOURCE" :value="customer.source" />
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="客户等级">
|
||||
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_LEVEL" :value="customer.level" />
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="手机">
|
||||
{{ customer.mobile }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="电话">
|
||||
{{ customer.telephone }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="邮箱">
|
||||
{{ customer.email }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="QQ">
|
||||
{{ customer.qq }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="微信">
|
||||
{{ customer.wechat }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="网址">
|
||||
{{ customer.website }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="所在地">
|
||||
{{ customer.areaName }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="详细地址">
|
||||
{{ customer.detailAddress }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="下次联系时间">
|
||||
{{ customer.contactNextTime ? formatDate(customer.contactNextTime, 'YYYY-MM-DD') : '空' }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-descriptions :column="1">
|
||||
<el-descriptions-item label="客户描述">
|
||||
{{ customer.description }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="备注">
|
||||
{{ customer.remark }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-collapse-item>
|
||||
<el-collapse-item name="systemInfo">
|
||||
<template #title>
|
||||
<span class="text-base font-bold">系统信息</span>
|
||||
</template>
|
||||
<el-descriptions :column="2">
|
||||
<el-descriptions-item label="负责人">
|
||||
{{ customer.ownerUserName }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建人">
|
||||
{{ customer.creatorName }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">
|
||||
{{ customer.createTime ? formatDate(customer.createTime) : '空' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="更新时间">
|
||||
{{ customer.updateTime ? formatDate(customer.updateTime) : '空' }}
|
||||
</el-descriptions-item>
|
||||
<!-- TODO wanwan:要不把“最后跟进时间”放到“下次联系时间”后面 -->
|
||||
<el-descriptions-item label="最后跟进时间">
|
||||
{{ customer.contactLastTime ? formatDate(customer.contactLastTime) : '空' }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import * as CustomerApi from '@/api/crm/customer'
|
||||
import { DICT_TYPE } from '@/utils/dict'
|
||||
import { formatDate } from '@/utils/formatTime'
|
||||
|
||||
const { customer } = defineProps<{ customer: CustomerApi.CustomerVO }>()
|
||||
|
||||
// 展示的折叠面板
|
||||
const activeNames = ref(['basicInfo', 'systemInfo'])
|
||||
</script>
|
||||
<style scoped lang="scss"></style>
|
154
src/views/crm/customer/detail/index.vue
Normal file
154
src/views/crm/customer/detail/index.vue
Normal file
@ -0,0 +1,154 @@
|
||||
<template>
|
||||
<div v-loading="loading">
|
||||
<div class="flex items-start justify-between">
|
||||
<div>
|
||||
<!-- 左上:客户基本信息 -->
|
||||
<CustomerBasicInfo :customer="customer" />
|
||||
</div>
|
||||
<div>
|
||||
<!-- 右上:按钮 -->
|
||||
<el-button @click="openForm('update', customer.id)" v-hasPermi="['crm:customer:update']">
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button>更改成交状态</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<el-row class="mt-10px">
|
||||
<el-button>
|
||||
<Icon icon="ph:calendar-fill" class="mr-5px" />
|
||||
创建任务
|
||||
</el-button>
|
||||
<el-button>
|
||||
<Icon icon="carbon:email" class="mr-5px" />
|
||||
发送邮件
|
||||
</el-button>
|
||||
<el-button>
|
||||
<Icon icon="system-uicons:contacts" class="mr-5px" />
|
||||
创建联系人
|
||||
</el-button>
|
||||
<el-button>
|
||||
<Icon icon="ep:opportunity" class="mr-5px" />
|
||||
创建商机
|
||||
</el-button>
|
||||
<el-button>
|
||||
<Icon icon="clarity:contract-line" class="mr-5px" />
|
||||
创建合同
|
||||
</el-button>
|
||||
<el-button>
|
||||
<Icon icon="icon-park:income-one" class="mr-5px" />
|
||||
创建回款
|
||||
</el-button>
|
||||
<el-button>
|
||||
<Icon icon="fluent:people-team-add-20-filled" class="mr-5px" />
|
||||
添加团队成员
|
||||
</el-button>
|
||||
</el-row>
|
||||
</div>
|
||||
<ContentWrap class="mt-10px">
|
||||
<el-descriptions :column="5" direction="vertical">
|
||||
<el-descriptions-item label="客户级别">
|
||||
<dict-tag :type="DICT_TYPE.CRM_CUSTOMER_LEVEL" :value="customer.level" />
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="成交状态">
|
||||
{{ customer.dealStatus ? '已成交' : '未成交' }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="负责人">
|
||||
{{ customer.ownerUserName }}
|
||||
</el-descriptions-item>
|
||||
<!-- TODO wanwan 首要联系人? -->
|
||||
<el-descriptions-item label="首要联系人" />
|
||||
<!-- TODO wanwan 首要联系人电话? -->
|
||||
<el-descriptions-item label="首要联系人电话">
|
||||
{{ customer.mobile }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</ContentWrap>
|
||||
<!-- TODO wanwan:这个 tab 拉满哈,可以更好看; -->
|
||||
<el-col :span="18">
|
||||
<el-tabs>
|
||||
<el-tab-pane label="详细资料">
|
||||
<!-- TODO wanwan:这个 ml-2 是不是可以优化下,不要整个左移,而是里面的内容有个几 px 的偏移,不顶在框里 -->
|
||||
<CustomerDetails class="ml-2" :customer="customer" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="活动" lazy> 活动</el-tab-pane>
|
||||
<el-tab-pane label="邮件" lazy> 邮件</el-tab-pane>
|
||||
<el-tab-pane label="工商信息" lazy> 工商信息</el-tab-pane>
|
||||
<el-tab-pane label="客户关系" lazy> 客户关系</el-tab-pane>
|
||||
<!-- TODO wanwan 以下标签上的数量需要接口统计返回 -->
|
||||
<el-tab-pane label="联系人" lazy>
|
||||
<template #label> 联系人<el-badge :value="12" class="item" type="primary" /> </template>
|
||||
联系人
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="团队成员" lazy>
|
||||
<template #label> 团队成员<el-badge :value="2" class="item" type="primary" /> </template>
|
||||
团队成员
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="商机" lazy> 商机</el-tab-pane>
|
||||
<el-tab-pane label="合同" lazy>
|
||||
<template #label> 合同<el-badge :value="3" class="item" type="primary" /> </template>
|
||||
合同
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="回款" lazy>
|
||||
<template #label> 回款<el-badge :value="4" class="item" type="primary" /> </template>
|
||||
回款
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="回访" lazy> 回访</el-tab-pane>
|
||||
<el-tab-pane label="发票" lazy> 发票</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-col>
|
||||
|
||||
<!-- 表单弹窗:添加/修改 -->
|
||||
<CustomerForm ref="formRef" @success="getCustomerData(id)" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useTagsViewStore } from '@/store/modules/tagsView'
|
||||
import * as CustomerApi from '@/api/crm/customer'
|
||||
import CustomerBasicInfo from '@/views/crm/customer/detail/CustomerBasicInfo.vue'
|
||||
import { DICT_TYPE } from '@/utils/dict'
|
||||
import CustomerDetails from '@/views/crm/customer/detail/CustomerDetails.vue'
|
||||
import CustomerForm from '@/views/crm/customer/CustomerForm.vue'
|
||||
|
||||
defineOptions({ name: 'CustomerDetail' })
|
||||
|
||||
const { delView } = useTagsViewStore() // 视图操作
|
||||
const route = useRoute()
|
||||
const { currentRoute } = useRouter() // 路由
|
||||
const id = Number(route.params.id)
|
||||
const loading = ref(true) // 加载中
|
||||
|
||||
// 客户详情
|
||||
const customer = ref<CustomerApi.CustomerVO>({} as CustomerApi.CustomerVO)
|
||||
|
||||
/**
|
||||
* 获取详情
|
||||
*
|
||||
* @param id
|
||||
*/
|
||||
const getCustomerData = async (id: number) => {
|
||||
loading.value = true
|
||||
try {
|
||||
customer.value = await CustomerApi.getCustomer(id)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const formRef = ref()
|
||||
const openForm = (type: string, id?: number) => {
|
||||
formRef.value.open(type, id)
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化
|
||||
*/
|
||||
onMounted(() => {
|
||||
if (!id) {
|
||||
ElMessage.warning('参数错误,客户不能为空!')
|
||||
delView(unref(currentRoute))
|
||||
return
|
||||
}
|
||||
getCustomerData(id)
|
||||
})
|
||||
</script>
|
@ -26,6 +26,51 @@
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="所属行业" prop="industryId">
|
||||
<el-select
|
||||
v-model="queryParams.industryId"
|
||||
placeholder="请选择所属行业"
|
||||
clearable
|
||||
class="!w-240px"
|
||||
>
|
||||
<el-option
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_INDUSTRY)"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="客户等级" prop="level">
|
||||
<el-select
|
||||
v-model="queryParams.level"
|
||||
placeholder="请选择客户等级"
|
||||
clearable
|
||||
class="!w-240px"
|
||||
>
|
||||
<el-option
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_LEVEL)"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="客户来源" prop="source">
|
||||
<el-select
|
||||
v-model="queryParams.source"
|
||||
placeholder="请选择客户来源"
|
||||
clearable
|
||||
class="!w-240px"
|
||||
>
|
||||
<el-option
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.CRM_CUSTOMER_SOURCE)"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
|
||||
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
|
||||
@ -67,8 +112,9 @@
|
||||
</el-table-column>
|
||||
<el-table-column label="手机" align="center" prop="mobile" width="120" />
|
||||
<el-table-column label="详细地址" align="center" prop="detailAddress" width="200" />
|
||||
<!-- TODO @Wanwan 负责人回显,所属部门,创建人 -->
|
||||
<el-table-column label="负责人" align="center" prop="ownerUserId" />
|
||||
<el-table-column label="负责人" align="center" prop="ownerUserName" />
|
||||
<el-table-column label="所属部门" align="center" prop="ownerUserDept" />
|
||||
<el-table-column label="创建人" align="center" prop="creatorName" />
|
||||
<el-table-column
|
||||
label="创建时间"
|
||||
align="center"
|
||||
@ -100,9 +146,10 @@
|
||||
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.lockStatus" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<!-- TODO @Wanwan 距进入公海天数 -->
|
||||
<el-table-column label="操作" align="center" width="160">
|
||||
<!-- TODO @wanwan 距进入公海天数 -->
|
||||
<el-table-column label="操作" align="center" min-width="150" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button link type="primary" @click="openDetail(scope.row.id)">详情</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
@ -136,7 +183,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { DICT_TYPE, getBoolDictOptions } from '@/utils/dict'
|
||||
import { DICT_TYPE, getBoolDictOptions, getIntDictOptions } from '@/utils/dict'
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
import download from '@/utils/download'
|
||||
import * as CustomerApi from '@/api/crm/customer'
|
||||
@ -154,7 +201,10 @@ const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
name: null,
|
||||
mobile: null
|
||||
mobile: null,
|
||||
industryId: null,
|
||||
level: null,
|
||||
source: null
|
||||
})
|
||||
const queryFormRef = ref() // 搜索的表单
|
||||
const exportLoading = ref(false) // 导出的加载中
|
||||
@ -183,6 +233,12 @@ const resetQuery = () => {
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 打开客户详情 */
|
||||
const { push } = useRouter()
|
||||
const openDetail = (id: number) => {
|
||||
push({ name: 'CrmCustomerDetail', params: { id } })
|
||||
}
|
||||
|
||||
/** 添加/修改操作 */
|
||||
const formRef = ref()
|
||||
const openForm = (type: string, id?: number) => {
|
||||
|
@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<Dialog :title="dialogTitle" v-model="dialogVisible">
|
||||
<!-- TODO @zange:改成每行两个哈; -->
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
|
@ -1,4 +1,5 @@
|
||||
<template>
|
||||
<!-- TODO @zange:挪到 product 下,建个 category 包,挪进去哈; -->
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
|
@ -229,6 +229,7 @@
|
||||
/>
|
||||
<el-table-column label="操作" align="center" width="180px">
|
||||
<template #default="scope">
|
||||
<!-- todo @liuhongfeng:用路径参数哈,receivableId -->
|
||||
<!--<router-link :to="'/crm/receivable-plan?receivableId=' + scope.row.receivableId">
|
||||
<el-button link type="primary">详情</el-button>
|
||||
</router-link>-->
|
||||
|
114
src/views/mall/promotion/diy/page/DiyPageForm.vue
Normal file
114
src/views/mall/promotion/diy/page/DiyPageForm.vue
Normal file
@ -0,0 +1,114 @@
|
||||
<template>
|
||||
<Dialog :title="dialogTitle" v-model="dialogVisible">
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-width="100px"
|
||||
v-loading="formLoading"
|
||||
>
|
||||
<el-form-item label="页面名称" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入页面名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="formData.remark" placeholder="请输入备注" />
|
||||
</el-form-item>
|
||||
<el-form-item label="预览图" prop="previewImageUrls">
|
||||
<UploadImgs v-model="formData.previewImageUrls" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
|
||||
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import * as DiyPageApi from '@/api/mall/promotion/diy/page'
|
||||
|
||||
/** 装修页面表单 */
|
||||
defineOptions({ name: 'DiyPageForm' })
|
||||
|
||||
const { t } = useI18n() // 国际化
|
||||
const message = useMessage() // 消息弹窗
|
||||
|
||||
const dialogVisible = ref(false) // 弹窗的是否展示
|
||||
const dialogTitle = ref('') // 弹窗的标题
|
||||
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
|
||||
const formType = ref('') // 表单的类型:create - 新增;update - 修改
|
||||
const formData = ref({
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
remark: undefined,
|
||||
previewImageUrls: []
|
||||
})
|
||||
const formRules = reactive({
|
||||
name: [{ required: true, message: '页面名称不能为空', trigger: 'blur' }]
|
||||
})
|
||||
const formRef = ref() // 表单 Ref
|
||||
|
||||
/** 打开弹窗 */
|
||||
const open = async (type: string, id?: number) => {
|
||||
dialogVisible.value = true
|
||||
dialogTitle.value = t('action.' + type)
|
||||
formType.value = type
|
||||
resetForm()
|
||||
// 修改时,设置数据
|
||||
if (id) {
|
||||
formLoading.value = true
|
||||
try {
|
||||
const diyPage = await DiyPageApi.getDiyPage(id) // 处理预览图
|
||||
if (diyPage?.previewImageUrls?.length > 0) {
|
||||
diyPage.previewImageUrls = diyPage.previewImageUrls.map((url: string) => {
|
||||
return { url }
|
||||
})
|
||||
}
|
||||
formData.value = diyPage
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
|
||||
|
||||
/** 提交表单 */
|
||||
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
|
||||
const submitForm = async () => {
|
||||
// 校验表单
|
||||
if (!formRef) return
|
||||
const valid = await formRef.value.validate()
|
||||
if (!valid) return
|
||||
// 提交请求
|
||||
formLoading.value = true
|
||||
try {
|
||||
// 处理预览图
|
||||
const previewImageUrls = formData.value.previewImageUrls.map((item) => {
|
||||
return item['url'] ? item['url'] : item
|
||||
})
|
||||
const data = { ...formData.value, previewImageUrls } as unknown as DiyPageApi.DiyPageVO
|
||||
if (formType.value === 'create') {
|
||||
await DiyPageApi.createDiyPage(data)
|
||||
message.success(t('common.createSuccess'))
|
||||
} else {
|
||||
await DiyPageApi.updateDiyPage(data)
|
||||
message.success(t('common.updateSuccess'))
|
||||
}
|
||||
dialogVisible.value = false
|
||||
// 发送操作成功的事件
|
||||
emit('success')
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 重置表单 */
|
||||
const resetForm = () => {
|
||||
formData.value = {
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
remark: undefined,
|
||||
previewImageUrls: []
|
||||
}
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
</script>
|
76
src/views/mall/promotion/diy/page/decorate.vue
Normal file
76
src/views/mall/promotion/diy/page/decorate.vue
Normal file
@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<DiyEditor
|
||||
v-if="formData && !formLoading"
|
||||
v-model="formData.property"
|
||||
:title="formData.name"
|
||||
:libs="PAGE_LIBS"
|
||||
:show-page-config="true"
|
||||
:show-navigation-bar="true"
|
||||
:show-tab-bar="false"
|
||||
@save="submitForm"
|
||||
/>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import * as DiyPageApi from '@/api/mall/promotion/diy/page'
|
||||
import { useTagsViewStore } from '@/store/modules/tagsView'
|
||||
import { PAGE_LIBS } from '@/components/DiyEditor/util'
|
||||
|
||||
/** 装修页面表单 */
|
||||
defineOptions({ name: 'DiyPageDecorate' })
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
|
||||
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
|
||||
const formData = ref<DiyPageApi.DiyPageVO>()
|
||||
const formRef = ref() // 表单 Ref
|
||||
|
||||
// 获取详情
|
||||
const getPageDetail = async (id: any) => {
|
||||
formLoading.value = true
|
||||
try {
|
||||
formData.value = await DiyPageApi.getDiyPageProperty(id)
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
// 提交表单
|
||||
const submitForm = async () => {
|
||||
// 校验表单
|
||||
if (!formRef) return
|
||||
// 提交请求
|
||||
formLoading.value = true
|
||||
try {
|
||||
await DiyPageApi.updateDiyPageProperty(unref(formData)!)
|
||||
message.success('保存成功')
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
formData.value = {
|
||||
id: undefined,
|
||||
templateId: undefined,
|
||||
name: '',
|
||||
remark: '',
|
||||
previewImageUrls: [],
|
||||
property: ''
|
||||
} as DiyPageApi.DiyPageVO
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
/** 初始化 **/
|
||||
const { currentRoute } = useRouter() // 路由
|
||||
const { delView } = useTagsViewStore() // 视图操作
|
||||
const route = useRoute()
|
||||
onMounted(() => {
|
||||
resetForm()
|
||||
if (!route.params.id) {
|
||||
message.warning('参数错误,页面编号不能为空!')
|
||||
delView(unref(currentRoute))
|
||||
return
|
||||
}
|
||||
getPageDetail(route.params.id)
|
||||
})
|
||||
</script>
|
189
src/views/mall/promotion/diy/page/index.vue
Normal file
189
src/views/mall/promotion/diy/page/index.vue
Normal file
@ -0,0 +1,189 @@
|
||||
<template>
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
class="-mb-15px"
|
||||
:model="queryParams"
|
||||
ref="queryFormRef"
|
||||
:inline="true"
|
||||
label-width="68px"
|
||||
>
|
||||
<el-form-item label="页面名称" prop="name">
|
||||
<el-input
|
||||
v-model="queryParams.name"
|
||||
placeholder="请输入页面名称"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="创建时间" prop="createTime">
|
||||
<el-date-picker
|
||||
v-model="queryParams.createTime"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
type="daterange"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
|
||||
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
@click="openForm('create')"
|
||||
v-hasPermi="['promotion:diy-page:create']"
|
||||
>
|
||||
<Icon icon="ep:plus" class="mr-5px" /> 新增
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 列表 -->
|
||||
<ContentWrap>
|
||||
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
|
||||
<el-table-column label="编号" align="center" prop="id" />
|
||||
<el-table-column label="预览图" align="center" prop="previewImageUrls">
|
||||
<template #default="scope">
|
||||
<el-image
|
||||
class="h-40px max-w-40px"
|
||||
v-for="(url, index) in scope.row.previewImageUrls"
|
||||
:key="index"
|
||||
:src="url"
|
||||
:preview-src-list="scope.row.previewImageUrls"
|
||||
:initial-index="index"
|
||||
preview-teleported
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="页面名称" align="center" prop="name" />
|
||||
<el-table-column label="备注" align="center" prop="remark" />
|
||||
<el-table-column
|
||||
label="创建时间"
|
||||
align="center"
|
||||
prop="createTime"
|
||||
:formatter="dateFormatter"
|
||||
width="180px"
|
||||
/>
|
||||
<el-table-column label="操作" align="center">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
@click="handleDecorate(scope.row.id)"
|
||||
v-hasPermi="['promotion:diy-page:update']"
|
||||
>
|
||||
装修
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
@click="openForm('update', scope.row.id)"
|
||||
v-hasPermi="['promotion:diy-page:update']"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
@click="handleDelete(scope.row.id)"
|
||||
v-hasPermi="['promotion:diy-page:delete']"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!-- 分页 -->
|
||||
<Pagination
|
||||
:total="total"
|
||||
v-model:page="queryParams.pageNo"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 表单弹窗:添加/修改 -->
|
||||
<DiyPageForm ref="formRef" @success="getList" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
import * as DiyPageApi from '@/api/mall/promotion/diy/page'
|
||||
import DiyPageForm from './DiyPageForm.vue'
|
||||
|
||||
/** 装修页面 */
|
||||
defineOptions({ name: 'DiyPage' })
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
const { t } = useI18n() // 国际化
|
||||
|
||||
const loading = ref(true) // 列表的加载中
|
||||
const total = ref(0) // 列表的总页数
|
||||
const list = ref([]) // 列表的数据
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
name: null,
|
||||
createTime: []
|
||||
})
|
||||
const queryFormRef = ref() // 搜索的表单
|
||||
|
||||
/** 查询列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await DiyPageApi.getDiyPagePage(queryParams)
|
||||
list.value = data.list
|
||||
total.value = data.total
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value.resetFields()
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 添加/修改操作 */
|
||||
const formRef = ref()
|
||||
const openForm = (type: string, id?: number) => {
|
||||
formRef.value.open(type, id)
|
||||
}
|
||||
|
||||
/** 删除按钮操作 */
|
||||
const handleDelete = async (id: number) => {
|
||||
try {
|
||||
// 删除的二次确认
|
||||
await message.delConfirm()
|
||||
// 发起删除
|
||||
await DiyPageApi.deleteDiyPage(id)
|
||||
message.success(t('common.delSuccess'))
|
||||
// 刷新列表
|
||||
await getList()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/** 打开装修页面 */
|
||||
const { push } = useRouter()
|
||||
const handleDecorate = (id: number) => {
|
||||
push({ name: 'DiyPageDecorate', params: { id } })
|
||||
}
|
||||
|
||||
/** 初始化 **/
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
115
src/views/mall/promotion/diy/template/DiyTemplateForm.vue
Normal file
115
src/views/mall/promotion/diy/template/DiyTemplateForm.vue
Normal file
@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<Dialog :title="dialogTitle" v-model="dialogVisible">
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-width="100px"
|
||||
v-loading="formLoading"
|
||||
>
|
||||
<el-form-item label="模板名称" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入模板名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="formData.remark" placeholder="请输入备注" type="textarea" />
|
||||
</el-form-item>
|
||||
<el-form-item label="预览图" prop="previewImageUrls">
|
||||
<UploadImgs v-model="formData.previewImageUrls" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
|
||||
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import * as DiyTemplateApi from '@/api/mall/promotion/diy/template'
|
||||
|
||||
/** 装修模板表单 */
|
||||
defineOptions({ name: 'DiyTemplateForm' })
|
||||
|
||||
const { t } = useI18n() // 国际化
|
||||
const message = useMessage() // 消息弹窗
|
||||
|
||||
const dialogVisible = ref(false) // 弹窗的是否展示
|
||||
const dialogTitle = ref('') // 弹窗的标题
|
||||
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
|
||||
const formType = ref('') // 表单的类型:create - 新增;update - 修改
|
||||
const formData = ref({
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
remark: undefined,
|
||||
previewImageUrls: []
|
||||
})
|
||||
const formRules = reactive({
|
||||
name: [{ required: true, message: '模板名称不能为空', trigger: 'blur' }]
|
||||
})
|
||||
const formRef = ref() // 表单 Ref
|
||||
|
||||
/** 打开弹窗 */
|
||||
const open = async (type: string, id?: number) => {
|
||||
dialogVisible.value = true
|
||||
dialogTitle.value = t('action.' + type)
|
||||
formType.value = type
|
||||
resetForm()
|
||||
// 修改时,设置数据
|
||||
if (id) {
|
||||
formLoading.value = true
|
||||
try {
|
||||
const diyTemplate = await DiyTemplateApi.getDiyTemplate(id)
|
||||
// 处理预览图
|
||||
if (diyTemplate?.previewImageUrls?.length > 0) {
|
||||
diyTemplate.previewImageUrls = diyTemplate.previewImageUrls.map((url: string) => {
|
||||
return { url }
|
||||
})
|
||||
}
|
||||
formData.value = diyTemplate
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
|
||||
|
||||
/** 提交表单 */
|
||||
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
|
||||
const submitForm = async () => {
|
||||
// 校验表单
|
||||
if (!formRef) return
|
||||
const valid = await formRef.value.validate()
|
||||
if (!valid) return
|
||||
// 提交请求
|
||||
formLoading.value = true
|
||||
try {
|
||||
// 处理预览图
|
||||
const previewImageUrls = formData.value.previewImageUrls.map((item) => {
|
||||
return item['url'] ? item['url'] : item
|
||||
})
|
||||
const data = { ...formData.value, previewImageUrls } as unknown as DiyTemplateApi.DiyTemplateVO
|
||||
if (formType.value === 'create') {
|
||||
await DiyTemplateApi.createDiyTemplate(data)
|
||||
message.success(t('common.createSuccess'))
|
||||
} else {
|
||||
await DiyTemplateApi.updateDiyTemplate(data)
|
||||
message.success(t('common.updateSuccess'))
|
||||
}
|
||||
dialogVisible.value = false
|
||||
// 发送操作成功的事件
|
||||
emit('success')
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 重置表单 */
|
||||
const resetForm = () => {
|
||||
formData.value = {
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
remark: undefined,
|
||||
previewImageUrls: []
|
||||
}
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
</script>
|
132
src/views/mall/promotion/diy/template/decorate.vue
Normal file
132
src/views/mall/promotion/diy/template/decorate.vue
Normal file
@ -0,0 +1,132 @@
|
||||
<template>
|
||||
<DiyEditor
|
||||
v-if="formData && !formLoading"
|
||||
v-model="currentFormData!.property"
|
||||
:title="templateItems[selectedTemplateItem].name"
|
||||
:libs="libs"
|
||||
:show-page-config="selectedTemplateItem !== 0"
|
||||
:show-tab-bar="selectedTemplateItem === 0"
|
||||
:show-navigation-bar="selectedTemplateItem !== 0"
|
||||
@save="submitForm"
|
||||
>
|
||||
<template #toolBarLeft>
|
||||
<el-radio-group
|
||||
v-model="selectedTemplateItem"
|
||||
class="h-full!"
|
||||
@change="handleTemplateItemChange"
|
||||
>
|
||||
<el-tooltip v-for="(item, index) in templateItems" :key="index" :content="item.name">
|
||||
<el-radio-button :label="index">
|
||||
<Icon :icon="item.icon" :size="24" />
|
||||
</el-radio-button>
|
||||
</el-tooltip>
|
||||
</el-radio-group>
|
||||
</template>
|
||||
</DiyEditor>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import * as DiyTemplateApi from '@/api/mall/promotion/diy/template'
|
||||
import * as DiyPageApi from '@/api/mall/promotion/diy/page'
|
||||
import { useTagsViewStore } from '@/store/modules/tagsView'
|
||||
import { DiyComponentLibrary, PAGE_LIBS } from '@/components/DiyEditor/util'
|
||||
|
||||
/** 装修模板表单 */
|
||||
defineOptions({ name: 'DiyTemplateDecorate' })
|
||||
|
||||
// 左上角工具栏操作按钮
|
||||
const selectedTemplateItem = ref(0)
|
||||
const templateItems = reactive([
|
||||
{ name: '基础设置', icon: 'ep:iphone' },
|
||||
{ name: '首页', icon: 'ep:home-filled' },
|
||||
{ name: '我的', icon: 'ep:user-filled' }
|
||||
])
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
|
||||
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
|
||||
const formData = ref<DiyTemplateApi.DiyTemplatePropertyVO>()
|
||||
const formRef = ref() // 表单 Ref
|
||||
// 当前编辑的属性
|
||||
const currentFormData = ref<DiyTemplateApi.DiyTemplatePropertyVO | DiyPageApi.DiyPageVO>()
|
||||
|
||||
// 获取详情
|
||||
const getPageDetail = async (id: any) => {
|
||||
formLoading.value = true
|
||||
try {
|
||||
formData.value = await DiyTemplateApi.getDiyTemplateProperty(id)
|
||||
currentFormData.value = formData.value
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 模板组件库
|
||||
const templateLibs = [] as DiyComponentLibrary[]
|
||||
// 当前组件库
|
||||
const libs = ref<DiyComponentLibrary[]>(templateLibs)
|
||||
// 模板选项切换
|
||||
const handleTemplateItemChange = () => {
|
||||
// 编辑模板
|
||||
if (selectedTemplateItem.value === 0) {
|
||||
libs.value = templateLibs
|
||||
currentFormData.value = formData.value
|
||||
return
|
||||
}
|
||||
|
||||
// 编辑页面
|
||||
libs.value = PAGE_LIBS
|
||||
currentFormData.value = formData.value!.pages.find(
|
||||
(page: DiyPageApi.DiyPageVO) => page.name === templateItems[selectedTemplateItem.value].name
|
||||
)
|
||||
console.log(currentFormData.value)
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const submitForm = async () => {
|
||||
// 校验表单
|
||||
if (!formRef) return
|
||||
// 提交请求
|
||||
formLoading.value = true
|
||||
try {
|
||||
if (selectedTemplateItem.value === 0) {
|
||||
// 提交模板属性
|
||||
await DiyTemplateApi.updateDiyTemplateProperty(unref(formData)!)
|
||||
} else {
|
||||
// 提交页面属性
|
||||
await DiyPageApi.updateDiyPageProperty(unref(currentFormData)!)
|
||||
}
|
||||
message.success('保存成功')
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
formData.value = {
|
||||
id: undefined,
|
||||
name: '',
|
||||
used: false,
|
||||
usedTime: undefined,
|
||||
remark: '',
|
||||
previewImageUrls: [],
|
||||
property: '',
|
||||
pages: []
|
||||
} as DiyTemplateApi.DiyTemplatePropertyVO
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
/** 初始化 **/
|
||||
const { currentRoute } = useRouter() // 路由
|
||||
const { delView } = useTagsViewStore() // 视图操作
|
||||
const route = useRoute()
|
||||
onMounted(() => {
|
||||
resetForm()
|
||||
if (!route.params.id) {
|
||||
message.warning('参数错误,页面编号不能为空!')
|
||||
delView(unref(currentRoute))
|
||||
return
|
||||
}
|
||||
getPageDetail(route.params.id)
|
||||
})
|
||||
</script>
|
225
src/views/mall/promotion/diy/template/index.vue
Normal file
225
src/views/mall/promotion/diy/template/index.vue
Normal file
@ -0,0 +1,225 @@
|
||||
<template>
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
class="-mb-15px"
|
||||
:model="queryParams"
|
||||
ref="queryFormRef"
|
||||
:inline="true"
|
||||
label-width="68px"
|
||||
>
|
||||
<el-form-item label="模板名称" prop="name">
|
||||
<el-input
|
||||
v-model="queryParams.name"
|
||||
placeholder="请输入模板名称"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="创建时间" prop="createTime">
|
||||
<el-date-picker
|
||||
v-model="queryParams.createTime"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
type="daterange"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
|
||||
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
@click="openForm('create')"
|
||||
v-hasPermi="['promotion:diy-template:create']"
|
||||
>
|
||||
<Icon icon="ep:plus" class="mr-5px" /> 新增
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 列表 -->
|
||||
<ContentWrap>
|
||||
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
|
||||
<el-table-column label="编号" align="center" prop="id" />
|
||||
<el-table-column label="预览图" align="center" prop="previewImageUrls">
|
||||
<template #default="scope">
|
||||
<el-image
|
||||
class="h-40px max-w-40px"
|
||||
v-for="(url, index) in scope.row.previewImageUrls"
|
||||
:key="index"
|
||||
:src="url"
|
||||
:preview-src-list="scope.row.previewImageUrls"
|
||||
:initial-index="index"
|
||||
preview-teleported
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="模板名称" align="center" prop="name" />
|
||||
<el-table-column label="是否使用" align="center" prop="used">
|
||||
<template #default="scope">
|
||||
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.used" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="使用时间"
|
||||
align="center"
|
||||
prop="usedTime"
|
||||
:formatter="dateFormatter"
|
||||
width="180px"
|
||||
/>
|
||||
<el-table-column label="备注" align="center" prop="remark" />
|
||||
<el-table-column
|
||||
label="创建时间"
|
||||
align="center"
|
||||
prop="createTime"
|
||||
:formatter="dateFormatter"
|
||||
width="180px"
|
||||
/>
|
||||
<el-table-column label="操作" align="center" width="200">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
@click="handleDecorate(scope.row.id)"
|
||||
v-hasPermi="['promotion:diy-template:update']"
|
||||
>
|
||||
装修
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
@click="openForm('update', scope.row.id)"
|
||||
v-hasPermi="['promotion:diy-template:update']"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<template v-if="!scope.row.used">
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
@click="handleUse(scope.row)"
|
||||
v-hasPermi="['promotion:diy-template:use']"
|
||||
>
|
||||
使用
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
@click="handleDelete(scope.row.id)"
|
||||
v-hasPermi="['promotion:diy-template:delete']"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!-- 分页 -->
|
||||
<Pagination
|
||||
:total="total"
|
||||
v-model:page="queryParams.pageNo"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 表单弹窗:添加/修改 -->
|
||||
<DiyTemplateForm ref="formRef" @success="getList" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
import * as DiyTemplateApi from '@/api/mall/promotion/diy/template'
|
||||
import DiyTemplateForm from './DiyTemplateForm.vue'
|
||||
import { DICT_TYPE } from '@/utils/dict'
|
||||
|
||||
/** 装修模板 */
|
||||
defineOptions({ name: 'DiyTemplate' })
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
const { t } = useI18n() // 国际化
|
||||
|
||||
const loading = ref(true) // 列表的加载中
|
||||
const total = ref(0) // 列表的总页数
|
||||
const list = ref([]) // 列表的数据
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
name: null,
|
||||
createTime: []
|
||||
})
|
||||
const queryFormRef = ref() // 搜索的表单
|
||||
|
||||
/** 查询列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await DiyTemplateApi.getDiyTemplatePage(queryParams)
|
||||
list.value = data.list
|
||||
total.value = data.total
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value.resetFields()
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 添加/修改操作 */
|
||||
const formRef = ref()
|
||||
const openForm = (type: string, id?: number) => {
|
||||
formRef.value.open(type, id)
|
||||
}
|
||||
|
||||
/** 删除按钮操作 */
|
||||
const handleDelete = async (id: number) => {
|
||||
try {
|
||||
// 删除的二次确认
|
||||
await message.delConfirm()
|
||||
// 发起删除
|
||||
await DiyTemplateApi.deleteDiyTemplate(id)
|
||||
message.success(t('common.delSuccess'))
|
||||
// 刷新列表
|
||||
await getList()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/** 使用模板 */
|
||||
const handleUse = async (row: DiyTemplateApi.DiyTemplateVO) => {
|
||||
try {
|
||||
// 使用模板的二次确认
|
||||
await message.confirm(`是否使用模板“${row.name}”?`)
|
||||
// 发起删除
|
||||
await DiyTemplateApi.useDiyTemplate(row.id)
|
||||
message.success('使用成功')
|
||||
// 刷新列表
|
||||
await getList()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/** 打开装修页面 */
|
||||
const { push } = useRouter()
|
||||
const handleDecorate = (id: number) => {
|
||||
push({ name: 'DiyTemplateDecorate', params: { id } })
|
||||
}
|
||||
|
||||
/** 初始化 **/
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
122
src/views/pay/demo/transfer/DemoTransferForm.vue
Normal file
122
src/views/pay/demo/transfer/DemoTransferForm.vue
Normal file
@ -0,0 +1,122 @@
|
||||
<template>
|
||||
<Dialog :title="dialogTitle" v-model="dialogVisible" width="800px">
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-width="120px"
|
||||
v-loading="formLoading"
|
||||
>
|
||||
<el-form-item label="转账类型" prop="type">
|
||||
<el-radio-group v-model="formData.type">
|
||||
<el-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.PAY_TRANSFER_TYPE)"
|
||||
:key="dict.value"
|
||||
:label="dict.value"
|
||||
:disabled="dict.value === 2 || dict.value === 3 || dict.value === 4"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="转账金额(元)" prop="price">
|
||||
<el-input-number
|
||||
v-model="formData.price"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
:step="0.01"
|
||||
placeholder="请输入转账金额"
|
||||
style="width: 200px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="收款人姓名" prop="userName">
|
||||
<el-input v-model="formData.userName" placeholder="请输入收款人姓名" />
|
||||
</el-form-item>
|
||||
<el-form-item v-show="formData.type === 1" label="支付宝登录账号" prop="alipayLogonId">
|
||||
<el-input v-model="formData.alipayLogonId" placeholder="请输入支付宝登录账号" />
|
||||
</el-form-item>
|
||||
<el-form-item v-show="formData.type === 2" label="微信 openid" prop="openid">
|
||||
<el-input v-model="formData.openid" placeholder="请输入微信 openid" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
|
||||
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import * as DemoTransferApi from '@/api/pay/demo/transfer'
|
||||
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
||||
import { yuanToFen } from '@/utils'
|
||||
const { t } = useI18n() // 国际化
|
||||
const message = useMessage() // 消息弹窗
|
||||
|
||||
const dialogVisible = ref(false) // 弹窗的是否展示
|
||||
const dialogTitle = ref('') // 弹窗的标题
|
||||
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
|
||||
const formType = ref('') // 表单的类型:create - 新增;update - 修改
|
||||
const formData = ref({
|
||||
id: undefined,
|
||||
price: undefined,
|
||||
type: undefined,
|
||||
userName: undefined,
|
||||
alipayLogonId: undefined,
|
||||
openid: undefined
|
||||
})
|
||||
const formRules = reactive({
|
||||
price: [{ required: true, message: '转账金额不能为空', trigger: 'blur' }],
|
||||
type: [{ required: true, message: '转账类型不能为空', trigger: 'change' }]
|
||||
})
|
||||
const formRef = ref() // 表单 Ref
|
||||
|
||||
/** 打开弹窗 */
|
||||
const open = async (type: string) => {
|
||||
dialogVisible.value = true
|
||||
dialogTitle.value = t('action.' + type)
|
||||
formType.value = type
|
||||
resetForm()
|
||||
}
|
||||
/** 关闭弹窗 */
|
||||
const close = async () => {
|
||||
dialogVisible.value = false
|
||||
resetForm()
|
||||
}
|
||||
defineExpose({ open, close }) // 提供 open, close 方法,用于打开, 关闭弹窗
|
||||
|
||||
/** 提交表单 */
|
||||
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
|
||||
const submitForm = async () => {
|
||||
// 校验表单
|
||||
if (!formRef) return
|
||||
const valid = await formRef.value.validate()
|
||||
if (!valid) return
|
||||
// 提交请求
|
||||
formLoading.value = true
|
||||
try {
|
||||
const data = { ...formData.value }
|
||||
data.price = yuanToFen(data.price)
|
||||
if (formType.value === 'create') {
|
||||
await DemoTransferApi.createDemoTransfer(data)
|
||||
message.success(t('common.createSuccess'))
|
||||
}
|
||||
dialogVisible.value = false
|
||||
// 发送操作成功的事件
|
||||
emit('success')
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 重置表单 */
|
||||
const resetForm = () => {
|
||||
formData.value = {
|
||||
id: undefined,
|
||||
price: undefined,
|
||||
userName: undefined,
|
||||
alipayLogonId: undefined,
|
||||
openid: undefined
|
||||
}
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
</script>
|
159
src/views/pay/demo/transfer/index.vue
Normal file
159
src/views/pay/demo/transfer/index.vue
Normal file
@ -0,0 +1,159 @@
|
||||
<template>
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
class="-mb-15px"
|
||||
:model="queryParams"
|
||||
ref="queryFormRef"
|
||||
:inline="true"
|
||||
label-width="68px"
|
||||
>
|
||||
<el-form-item>
|
||||
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
|
||||
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
|
||||
<el-button type="primary" plain @click="openForm('create')"
|
||||
><Icon icon="ep:plus" />创建业务转账单
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 列表 -->
|
||||
<ContentWrap>
|
||||
<el-table v-loading="loading" :data="list" :show-overflow-tooltip="true">
|
||||
<el-table-column label="订单编号" align="center" prop="id" />
|
||||
<el-table-column label="转账类型" align="center" prop="type" width="120">
|
||||
<template #default="scope">
|
||||
<dict-tag :type="DICT_TYPE.PAY_TRANSFER_TYPE" :value="scope.row.type" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="转账金额" align="center" prop="price">
|
||||
<template #default="scope">
|
||||
<span>¥{{ (scope.row.price / 100.0).toFixed(2) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="收款人姓名" align="center" prop="userName" width="120" />
|
||||
<el-table-column label="支付宝登录账号" align="center" prop="alipayLogonId" width="180" />
|
||||
<el-table-column label="微信 openid" align="center" prop="openid" width="120" />
|
||||
<el-table-column label="转账状态" align="center" prop="transferStatus">
|
||||
<template #default="scope">
|
||||
<dict-tag :type="DICT_TYPE.PAY_TRANSFER_STATUS" :value="scope.row.transferStatus" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="转账单号" align="center" prop="payTransferId" />
|
||||
<el-table-column label="支付渠道" align="center" prop="payChannelCode" />
|
||||
<el-table-column
|
||||
label="转账时间"
|
||||
align="center"
|
||||
prop="transferTime"
|
||||
:formatter="dateFormatter"
|
||||
width="180px"
|
||||
/>
|
||||
<el-table-column
|
||||
label="操作"
|
||||
align="center"
|
||||
class-name="small-padding fixed-width"
|
||||
width="100"
|
||||
fixed="right"
|
||||
>
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
@click="handleTransfer(scope.row)"
|
||||
v-if="scope.row.transferStatus === 0"
|
||||
v-hasPermi="['pay:transfer:create']"
|
||||
>
|
||||
发起转账
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!-- 分页 -->
|
||||
<Pagination
|
||||
:total="total"
|
||||
v-model:page="queryParams.pageNo"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 表单弹窗:添加/修改 -->
|
||||
<DemoTransferForm ref="demoFormRef" @success="getList" />
|
||||
<CreatePayTransfer ref="payTransferRef" @success="getList" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
import * as DemoTransferApi from '@/api/pay/demo/transfer'
|
||||
import * as PayTransferApi from '@/api/pay/transfer'
|
||||
import DemoTransferForm from './DemoTransferForm.vue'
|
||||
import CreatePayTransfer from '../../transfer/CreatePayTransfer.vue'
|
||||
import { DICT_TYPE } from '@/utils/dict'
|
||||
const message = useMessage() // 消息弹窗
|
||||
const { t } = useI18n() // 国际化
|
||||
|
||||
const loading = ref(true) // 列表的加载中
|
||||
const total = ref(0) // 列表的总页数
|
||||
const list = ref([]) // 列表的数据
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10
|
||||
})
|
||||
const queryFormRef = ref() // 搜索的表单
|
||||
|
||||
let payTransfer = {
|
||||
appId: undefined,
|
||||
merchantTransferId: undefined,
|
||||
type: undefined,
|
||||
price: undefined,
|
||||
subject: undefined,
|
||||
userName: undefined,
|
||||
alipayLogonId: undefined,
|
||||
openid: undefined
|
||||
} as PayTransferApi.TransferVO // 传递给转账订单的数据
|
||||
|
||||
/** 查询列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await DemoTransferApi.getDemoTransferPage(queryParams)
|
||||
list.value = data.list
|
||||
total.value = data.total
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value.resetFields()
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 创建业务转账单操作 */
|
||||
const demoFormRef = ref()
|
||||
const payTransferRef = ref()
|
||||
const openForm = (type: string) => {
|
||||
demoFormRef.value.open(type)
|
||||
}
|
||||
|
||||
/** 发起转账操作 */
|
||||
const handleTransfer = (row: any) => {
|
||||
payTransfer = { ...row }
|
||||
payTransfer.merchantTransferId = row.id.toString()
|
||||
payTransfer.subject = '示例转账'
|
||||
payTransferRef.value.showPayTransfer(payTransfer)
|
||||
}
|
||||
|
||||
/** 初始化 **/
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
@ -62,7 +62,7 @@
|
||||
<el-divider />
|
||||
<el-descriptions :column="1" label-class-name="desc-label" direction="vertical" border>
|
||||
<el-descriptions-item label="支付通道异步回调内容">
|
||||
{{ detailData.extension.channelNotifyData }}
|
||||
<el-text>{{ detailData.extension.channelNotifyData }}</el-text>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</Dialog>
|
||||
|
135
src/views/pay/transfer/CreatePayTransfer.vue
Normal file
135
src/views/pay/transfer/CreatePayTransfer.vue
Normal file
@ -0,0 +1,135 @@
|
||||
<template>
|
||||
<Dialog title="发起转账" v-model="dialogVisible" width="800px">
|
||||
<el-card style="margin-top: 10px">
|
||||
<el-descriptions title="转账信息" :column="2" border>
|
||||
<el-descriptions-item label="转账类型">
|
||||
{{ typeName }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="转账金额(元)">
|
||||
¥{{ (transfer.price / 100.0).toFixed(2) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="收款人姓名">
|
||||
{{ transfer.userName }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="支付宝登录账号" v-if="transfer.type === 1">
|
||||
{{ transfer.alipayLogonId }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="微信 openid" v-if="transfer.type === 2">
|
||||
{{ transfer.openid }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</el-card>
|
||||
<el-card style="margin-top: 20px">
|
||||
<template #header>
|
||||
<div class="card-header">
|
||||
<span>选择转账渠道</span>
|
||||
</div>
|
||||
</template>
|
||||
<div>
|
||||
<el-radio-group v-model="channelCode">
|
||||
<el-radio
|
||||
label="alipay_pc"
|
||||
:disabled="transfer.type === 2 || transfer.type === 3 || transfer.type === 4"
|
||||
>
|
||||
<img :src="svg_alipay_app" />
|
||||
</el-radio>
|
||||
<el-radio
|
||||
label="wx_app"
|
||||
:disabled="transfer.type === 1 || transfer.type === 3 || transfer.type === 4"
|
||||
>
|
||||
<img :src="svg_wx_app" />
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
</el-card>
|
||||
<el-divider />
|
||||
<div style="text-align: right">
|
||||
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
|
||||
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||
</div>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import * as PayTransferApi from '@/api/pay/transfer'
|
||||
import { computed, PropType } from 'vue'
|
||||
import { DICT_TYPE, getDictLabel } from '@/utils/dict'
|
||||
// 导入图标
|
||||
import svg_alipay_app from '@/assets/svgs/pay/icon/alipay_app.svg'
|
||||
import svg_wx_app from '@/assets/svgs/pay/icon/wx_app.svg'
|
||||
import { yuanToFen } from '@/utils'
|
||||
const { t } = useI18n() // 国际化
|
||||
const message = useMessage() // 消息弹窗
|
||||
const formLoading = ref(false) // 提交的按钮禁用
|
||||
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
|
||||
defineOptions({ name: 'CreatePayTransfer' })
|
||||
|
||||
// 提交数据
|
||||
let submitTransferData: PayTransferApi.TransferVO
|
||||
|
||||
const transfer = reactive({
|
||||
appId: undefined,
|
||||
channelCode: undefined,
|
||||
merchantTransferId: undefined,
|
||||
type: undefined,
|
||||
price: undefined,
|
||||
subject: undefined,
|
||||
userName: undefined,
|
||||
alipayLogonId: undefined,
|
||||
openid: undefined
|
||||
})
|
||||
const dialogVisible = ref(false)
|
||||
const typeName = computed(() => {
|
||||
return getDictLabel(DICT_TYPE.PAY_TRANSFER_TYPE, transfer.type)
|
||||
})
|
||||
const channelCode = computed(() => {
|
||||
let channelCode = 'alipay_pc'
|
||||
if (transfer.type === 2) {
|
||||
channelCode = 'wx_app'
|
||||
}
|
||||
// TODO 银行卡和钱包 转账待实现
|
||||
return channelCode
|
||||
})
|
||||
|
||||
/** 打开弹窗 */
|
||||
const showPayTransfer = async (payTransfer: PayTransferApi.TransferVO) => {
|
||||
dialogVisible.value = true
|
||||
submitTransferData = payTransfer
|
||||
transfer.merchantTransferId = payTransfer.merchantTransferId
|
||||
transfer.price = payTransfer.price
|
||||
transfer.userName = payTransfer.userName
|
||||
transfer.type = payTransfer.type
|
||||
transfer.appId = payTransfer.appId
|
||||
transfer.subject = payTransfer.subject
|
||||
transfer.alipayLogonId = payTransfer.alipayLogonId
|
||||
transfer.openid = payTransfer.openid
|
||||
}
|
||||
/** 关闭弹窗 */
|
||||
const close = async () => {
|
||||
dialogVisible.value = false
|
||||
}
|
||||
defineExpose({ showPayTransfer, close }) // 提供 showPayTransfer, close 方法,用于打开, 关闭弹窗
|
||||
|
||||
const submitForm = async () => {
|
||||
// 校验表单
|
||||
formLoading.value = true
|
||||
try {
|
||||
submitTransferData.channelCode = channelCode.value
|
||||
await PayTransferApi.createTransfer(submitTransferData)
|
||||
message.success('发起转账成功. 是否转账成功,以转账订单状态为准')
|
||||
// 发送操作成功的事件
|
||||
emit('success')
|
||||
dialogVisible.value = false
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
80
src/views/pay/transfer/TransferDetail.vue
Normal file
80
src/views/pay/transfer/TransferDetail.vue
Normal file
@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<Dialog v-model="dialogVisible" title="转账单详情" width="700px">
|
||||
<el-descriptions :column="2" label-class-name="desc-label">
|
||||
<el-descriptions-item label="商户单号">
|
||||
<el-tag size="small">{{ detailData.merchantTransferId }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="转账单号">
|
||||
<el-tag type="warning" size="small" v-if="detailData.no">{{ detailData.no }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="应用编号">{{ detailData.appId }}</el-descriptions-item>
|
||||
<el-descriptions-item label="转账状态">
|
||||
<dict-tag :type="DICT_TYPE.PAY_TRANSFER_STATUS" :value="detailData.status" size="small" />
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="转账金额">
|
||||
<el-tag type="success" size="small">¥{{ (detailData.price / 100.0).toFixed(2) }}</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="转账时间">
|
||||
{{ formatDate(detailData.successTime) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">
|
||||
{{ formatDate(detailData.createTime) }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<!-- 分割线 -->
|
||||
<el-divider />
|
||||
<el-descriptions :column="2" label-class-name="desc-label">
|
||||
<el-descriptions-item label="收款人姓名">{{ detailData.userName }}</el-descriptions-item>
|
||||
<el-descriptions-item label="支付宝登录账号" v-if="detailData.type === 1">
|
||||
{{ detailData.alipayLogonId }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="微信 openid" v-if="detailData.type === 2">
|
||||
{{ detailData.openid }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="支付渠道">
|
||||
<dict-tag :type="DICT_TYPE.PAY_CHANNEL_CODE" :value="detailData.channelCode" />
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="支付 IP">{{ detailData.userIp }}</el-descriptions-item>
|
||||
<el-descriptions-item label="渠道单号">
|
||||
<el-tag size="mini" type="success" v-if="detailData.channelTransferNo">
|
||||
{{ detailData.channelTransferNo }}
|
||||
</el-tag>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="通知 URL">{{ detailData.notifyUrl }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-divider />
|
||||
<el-descriptions :column="1" label-class-name="desc-label" direction="vertical" border>
|
||||
<el-descriptions-item label="转账渠道通知内容">
|
||||
<el-text>{{ detailData.channelNotifyData }}</el-text>
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
<el-divider />
|
||||
<div style="text-align: right">
|
||||
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||
</div>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { DICT_TYPE } from '@/utils/dict'
|
||||
import * as TransferApi from '@/api/pay/transfer'
|
||||
import { formatDate } from '@/utils/formatTime'
|
||||
defineOptions({ name: 'PayTransferDetail' })
|
||||
const dialogVisible = ref(false) // 弹窗的是否展示
|
||||
const detailLoading = ref(false) // 表单的加载中
|
||||
const detailData = ref({})
|
||||
/** 打开弹窗 */
|
||||
const open = async (id: number) => {
|
||||
dialogVisible.value = true
|
||||
// 设置数据
|
||||
detailLoading.value = true
|
||||
try {
|
||||
detailData.value = await TransferApi.getTransfer(id)
|
||||
} finally {
|
||||
detailLoading.value = false
|
||||
}
|
||||
}
|
||||
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
267
src/views/pay/transfer/index.vue
Normal file
267
src/views/pay/transfer/index.vue
Normal file
@ -0,0 +1,267 @@
|
||||
<template>
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
class="-mb-15px"
|
||||
:model="queryParams"
|
||||
ref="queryFormRef"
|
||||
:inline="true"
|
||||
label-width="100px"
|
||||
>
|
||||
<el-form-item label="转账单号" prop="no">
|
||||
<el-input
|
||||
v-model="queryParams.no"
|
||||
placeholder="请输入转账单号"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="转账渠道" prop="channelCode">
|
||||
<el-select
|
||||
v-model="queryParams.channelCode"
|
||||
placeholder="请选择支付渠道"
|
||||
clearable
|
||||
class="!w-240px"
|
||||
>
|
||||
<el-option
|
||||
v-for="dict in getStrDictOptions(DICT_TYPE.PAY_CHANNEL_CODE)"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="商户单号" prop="merchantTransferId">
|
||||
<el-input
|
||||
v-model="queryParams.merchantTransferId"
|
||||
placeholder="请输入商户单号"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="类型" prop="type">
|
||||
<el-select v-model="queryParams.type" placeholder="请选择类型" clearable class="!w-240px">
|
||||
<el-option
|
||||
v-for="dict in getStrDictOptions(DICT_TYPE.PAY_TRANSFER_TYPE)"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="转账状态" prop="status">
|
||||
<el-select
|
||||
v-model="queryParams.status"
|
||||
placeholder="请选择转账状态"
|
||||
clearable
|
||||
class="!w-240px"
|
||||
>
|
||||
<el-option
|
||||
v-for="dict in getStrDictOptions(DICT_TYPE.PAY_TRANSFER_STATUS)"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="收款人姓名" prop="userName">
|
||||
<el-input
|
||||
v-model="queryParams.userName"
|
||||
placeholder="请输入收款人姓名"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="渠道单号" prop="channelTransferNo">
|
||||
<el-input
|
||||
v-model="queryParams.channelTransferNo"
|
||||
placeholder="渠道单号"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="创建时间" prop="createTime">
|
||||
<el-date-picker
|
||||
v-model="queryParams.createTime"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
type="daterange"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
|
||||
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 列表 -->
|
||||
<ContentWrap>
|
||||
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
|
||||
<el-table-column label="编号" align="center" prop="id" />
|
||||
<el-table-column
|
||||
label="创建时间"
|
||||
align="center"
|
||||
prop="createTime"
|
||||
:formatter="dateFormatter"
|
||||
width="180px"
|
||||
/>
|
||||
<el-table-column label="应用编号" align="center" prop="appId" />
|
||||
<el-table-column label="类型" align="center" prop="type" width="120">
|
||||
<template #default="scope">
|
||||
<dict-tag :type="DICT_TYPE.PAY_TRANSFER_TYPE" :value="scope.row.type" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="转账金额" align="center" prop="price">
|
||||
<template #default="scope">
|
||||
<span>¥{{ (scope.row.price / 100.0).toFixed(2) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="转账状态" align="center" prop="status" width="120">
|
||||
<template #default="scope">
|
||||
<dict-tag :type="DICT_TYPE.PAY_TRANSFER_STATUS" :value="scope.row.status" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="订单号" align="left" width="300">
|
||||
<template #default="scope">
|
||||
<p class="transfer-font">
|
||||
<el-tag size="small"> 商户</el-tag>
|
||||
{{ scope.row.merchantTransferId }}
|
||||
</p>
|
||||
<p class="transfer-font" v-if="scope.row.no">
|
||||
<el-tag size="small" type="warning">转账</el-tag>
|
||||
{{ scope.row.no }}
|
||||
</p>
|
||||
<p class="transfer-font" v-if="scope.row.channelTransferNo">
|
||||
<el-tag size="small" type="success">渠道</el-tag>
|
||||
{{ scope.row.channelTransferNo }}
|
||||
</p>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="收款人姓名" align="center" prop="userName" width="120" />
|
||||
<el-table-column label="收款账号" align="left" width="200">
|
||||
<template #default="scope">
|
||||
<p class="transfer-font" v-if="scope.row.alipayLogonId">
|
||||
<el-tag size="small">支付宝登录号</el-tag>
|
||||
{{ scope.row.alipayLogonId }}
|
||||
</p>
|
||||
<p class="transfer-font" v-if="scope.row.openid">
|
||||
<el-tag size="small">微信 openId</el-tag>
|
||||
{{ scope.row.openid }}
|
||||
</p>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="转账标题" align="center" prop="subject" width="120" />
|
||||
<el-table-column label="转账渠道" align="center" prop="channelCode">
|
||||
<template #default="scope">
|
||||
<dict-tag :type="DICT_TYPE.PAY_CHANNEL_CODE" :value="scope.row.channelCode" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="转账成功时间"
|
||||
align="center"
|
||||
prop="successTime"
|
||||
:formatter="dateFormatter"
|
||||
width="180px"
|
||||
/>
|
||||
<el-table-column label="操作" align="center" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button link type="primary" @click="openDetail(scope.row.id)"> 详情 </el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!-- 分页 -->
|
||||
<Pagination
|
||||
:total="total"
|
||||
v-model:page="queryParams.pageNo"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
<TransferDetail ref="detailRef" />
|
||||
</ContentWrap>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
import * as TransferApi from '@/api/pay/transfer'
|
||||
import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
|
||||
import TransferDetail from './TransferDetail.vue'
|
||||
defineOptions({ name: 'PayTransfer' })
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
const { t } = useI18n() // 国际化
|
||||
|
||||
const loading = ref(true) // 列表的加载中
|
||||
const total = ref(0) // 列表的总页数
|
||||
const list = ref([]) // 列表的数据
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
no: null,
|
||||
appId: null,
|
||||
channelId: null,
|
||||
channelCode: null,
|
||||
merchantTransferId: null,
|
||||
type: null,
|
||||
status: null,
|
||||
successTime: [],
|
||||
price: null,
|
||||
subject: null,
|
||||
userName: null,
|
||||
alipayLogonId: null,
|
||||
openid: null,
|
||||
createTime: []
|
||||
})
|
||||
const queryFormRef = ref() // 搜索的表单
|
||||
const exportLoading = ref(false) // 导出的加载中
|
||||
|
||||
/** 查询列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await TransferApi.getTransferPage(queryParams)
|
||||
list.value = data.list
|
||||
total.value = data.total
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value.resetFields()
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 添加/修改操作 */
|
||||
const detailRef = ref()
|
||||
const openDetail = (id: number) => {
|
||||
detailRef.value.open(id)
|
||||
}
|
||||
|
||||
/** 初始化 **/
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.transfer-font {
|
||||
padding: 2px 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
@ -90,7 +90,7 @@ const submitForm = async () => {
|
||||
// 提交请求
|
||||
formLoading.value = true
|
||||
try {
|
||||
const data = formData.value as unknown as WalletRechargePackageApi.WalletRechargePackageVO
|
||||
const data = { ...formData.value }
|
||||
data.payPrice = yuanToFen(data.payPrice)
|
||||
data.bonusPrice = yuanToFen(data.bonusPrice)
|
||||
if (formType.value === 'create') {
|
||||
|
@ -54,8 +54,8 @@
|
||||
value-format="x"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="绑定域名" prop="domain">
|
||||
<el-input v-model="formData.domain" placeholder="请输入绑定域名" />
|
||||
<el-form-item label="绑定域名" prop="website">
|
||||
<el-input v-model="formData.website" placeholder="请输入绑定域名" />
|
||||
</el-form-item>
|
||||
<el-form-item label="租户状态" prop="status">
|
||||
<el-radio-group v-model="formData.status">
|
||||
@ -97,7 +97,7 @@ const formData = ref({
|
||||
contactMobile: undefined,
|
||||
accountCount: undefined,
|
||||
expireTime: undefined,
|
||||
domain: undefined,
|
||||
website: undefined,
|
||||
status: CommonStatusEnum.ENABLE
|
||||
})
|
||||
const formRules = reactive({
|
||||
@ -107,7 +107,7 @@ const formRules = reactive({
|
||||
status: [{ required: true, message: '租户状态不能为空', trigger: 'blur' }],
|
||||
accountCount: [{ required: true, message: '账号额度不能为空', trigger: 'blur' }],
|
||||
expireTime: [{ required: true, message: '过期时间不能为空', trigger: 'blur' }],
|
||||
domain: [{ required: true, message: '绑定域名不能为空', trigger: 'blur' }],
|
||||
website: [{ required: true, message: '绑定域名不能为空', trigger: 'blur' }],
|
||||
username: [{ required: true, message: '用户名称不能为空', trigger: 'blur' }],
|
||||
password: [{ required: true, message: '用户密码不能为空', trigger: 'blur' }]
|
||||
})
|
||||
@ -170,7 +170,7 @@ const resetForm = () => {
|
||||
contactMobile: undefined,
|
||||
accountCount: undefined,
|
||||
expireTime: undefined,
|
||||
domain: undefined,
|
||||
website: undefined,
|
||||
status: CommonStatusEnum.ENABLE
|
||||
}
|
||||
formRef.value?.resetFields()
|
||||
|
@ -125,7 +125,7 @@
|
||||
width="180"
|
||||
:formatter="dateFormatter"
|
||||
/>
|
||||
<el-table-column label="绑定域名" align="center" prop="domain" width="180" />
|
||||
<el-table-column label="绑定域名" align="center" prop="website" width="180" />
|
||||
<el-table-column label="租户状态" align="center" prop="status">
|
||||
<template #default="scope">
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
|
||||
|
Loading…
Reference in New Issue
Block a user