Merge remote-tracking branch 'origin/dev' into dev
This commit is contained in:
commit
57474c0644
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@ -102,6 +102,7 @@
|
||||
"codemirror",
|
||||
"commitlint",
|
||||
"cropperjs",
|
||||
"echart",
|
||||
"echarts",
|
||||
"esnext",
|
||||
"esno",
|
||||
@ -116,10 +117,12 @@
|
||||
"sider",
|
||||
"sortablejs",
|
||||
"stylelint",
|
||||
"svgs",
|
||||
"unocss",
|
||||
"unplugin",
|
||||
"unref",
|
||||
"videojs",
|
||||
"VITE",
|
||||
"vitejs",
|
||||
"vueuse",
|
||||
"wangeditor",
|
||||
|
33
package.json
33
package.json
@ -36,20 +36,20 @@
|
||||
"@wangeditor/editor-for-vue": "^5.1.10",
|
||||
"@zxcvbn-ts/core": "^3.0.4",
|
||||
"animate.css": "^4.1.1",
|
||||
"axios": "^1.5.1",
|
||||
"axios": "^1.6.0",
|
||||
"benz-amr-recorder": "^1.1.5",
|
||||
"bpmn-js-token-simulation": "^0.10.0",
|
||||
"camunda-bpmn-moddle": "^7.0.1",
|
||||
"cropperjs": "^1.6.1",
|
||||
"crypto-js": "^4.2.0",
|
||||
"dayjs": "^1.11.10",
|
||||
"diagram-js": "^12.5.0",
|
||||
"diagram-js": "^12.6.0",
|
||||
"driver.js": "^1.3.0",
|
||||
"echarts": "^5.4.3",
|
||||
"echarts-wordcloud": "^2.1.0",
|
||||
"element-plus": "2.4.1",
|
||||
"fast-xml-parser": "^4.3.2",
|
||||
"highlight.js": "^11.9.0",
|
||||
"intro.js": "^7.2.0",
|
||||
"jsencrypt": "^3.3.2",
|
||||
"lodash-es": "^4.17.21",
|
||||
"min-dash": "^4.1.1",
|
||||
@ -64,7 +64,7 @@
|
||||
"video.js": "^7.21.5",
|
||||
"vue": "^3.3.7",
|
||||
"vue-dompurify-html": "^4.1.4",
|
||||
"vue-i18n": "^9.5.0",
|
||||
"vue-i18n": "^9.6.2",
|
||||
"vue-router": "^4.2.5",
|
||||
"vue-types": "^5.1.1",
|
||||
"vuedraggable": "^4.1.0",
|
||||
@ -72,20 +72,19 @@
|
||||
"xml-js": "^1.6.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^18.0.0",
|
||||
"@commitlint/config-conventional": "^18.0.0",
|
||||
"@iconify/json": "^2.2.132",
|
||||
"@commitlint/cli": "^18.2.0",
|
||||
"@commitlint/config-conventional": "^18.1.0",
|
||||
"@iconify/json": "^2.2.135",
|
||||
"@intlify/unplugin-vue-i18n": "^1.4.0",
|
||||
"@purge-icons/generated": "^0.9.0",
|
||||
"@types/intro.js": "^5.1.3",
|
||||
"@types/lodash-es": "^4.17.10",
|
||||
"@types/node": "^20.8.8",
|
||||
"@types/node": "^20.8.9",
|
||||
"@types/nprogress": "^0.2.2",
|
||||
"@types/qrcode": "^1.5.4",
|
||||
"@types/qs": "^6.9.9",
|
||||
"@types/sortablejs": "^1.15.4",
|
||||
"@typescript-eslint/eslint-plugin": "^6.9.0",
|
||||
"@typescript-eslint/parser": "^6.9.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.9.1",
|
||||
"@typescript-eslint/parser": "^6.9.1",
|
||||
"@unocss/transformer-variant-group": "^0.57.1",
|
||||
"@unocss/eslint-config": "^0.57.1",
|
||||
"@vitejs/plugin-legacy": "^4.1.1",
|
||||
@ -99,24 +98,24 @@
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-define-config": "^1.24.1",
|
||||
"eslint-plugin-prettier": "^5.0.1",
|
||||
"eslint-plugin-vue": "^9.18.0",
|
||||
"eslint-plugin-vue": "^9.18.1",
|
||||
"lint-staged": "^15.0.2",
|
||||
"postcss": "^8.4.31",
|
||||
"postcss-html": "^1.5.0",
|
||||
"postcss-scss": "^4.0.9",
|
||||
"prettier": "^3.0.3",
|
||||
"rimraf": "^5.0.5",
|
||||
"rollup": "^4.1.4",
|
||||
"sass": "^1.69.4",
|
||||
"rollup": "^4.1.5",
|
||||
"sass": "^1.69.5",
|
||||
"stylelint": "^15.11.0",
|
||||
"stylelint-config-html": "^1.1.0",
|
||||
"stylelint-config-recommended": "^13.0.0",
|
||||
"stylelint-config-standard": "^34.0.0",
|
||||
"stylelint-order": "^6.0.3",
|
||||
"terser": "^5.22.0",
|
||||
"terser": "^5.23.0",
|
||||
"typescript": "5.2.2",
|
||||
"unocss": "^0.57.1",
|
||||
"unplugin-auto-import": "^0.16.6",
|
||||
"unplugin-auto-import": "^0.16.7",
|
||||
"unplugin-element-plus": "^0.8.0",
|
||||
"unplugin-vue-components": "^0.25.2",
|
||||
"vite": "4.5.0",
|
||||
@ -128,7 +127,7 @@
|
||||
"vite-plugin-svg-icons": "^2.0.1",
|
||||
"vite-plugin-top-level-await": "^1.3.1",
|
||||
"vue-eslint-parser": "^9.3.2",
|
||||
"vue-tsc": "^1.8.20"
|
||||
"vue-tsc": "^1.8.22"
|
||||
},
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
43
src/api/crm/product/index.ts
Normal file
43
src/api/crm/product/index.ts
Normal file
@ -0,0 +1,43 @@
|
||||
import request from '@/config/axios'
|
||||
|
||||
export interface ProductVO {
|
||||
id: number
|
||||
name: string
|
||||
no: string
|
||||
unit: string
|
||||
price: number
|
||||
status: number
|
||||
categoryId: number
|
||||
description: string
|
||||
ownerUserId: number
|
||||
}
|
||||
|
||||
// 查询产品列表
|
||||
export const getProductPage = async (params) => {
|
||||
return await request.get({ url: `/crm/product/page`, params })
|
||||
}
|
||||
|
||||
// 查询产品详情
|
||||
export const getProduct = async (id: number) => {
|
||||
return await request.get({ url: `/crm/product/get?id=` + id })
|
||||
}
|
||||
|
||||
// 新增产品
|
||||
export const createProduct = async (data: ProductVO) => {
|
||||
return await request.post({ url: `/crm/product/create`, data })
|
||||
}
|
||||
|
||||
// 修改产品
|
||||
export const updateProduct = async (data: ProductVO) => {
|
||||
return await request.put({ url: `/crm/product/update`, data })
|
||||
}
|
||||
|
||||
// 删除产品
|
||||
export const deleteProduct = async (id: number) => {
|
||||
return await request.delete({ url: `/crm/product/delete?id=` + id })
|
||||
}
|
||||
|
||||
// 导出产品 Excel
|
||||
export const exportProduct = async (params) => {
|
||||
return await request.download({ url: `/crm/product/export-excel`, params })
|
||||
}
|
33
src/api/crm/productCategory/index.ts
Normal file
33
src/api/crm/productCategory/index.ts
Normal file
@ -0,0 +1,33 @@
|
||||
import request from '@/config/axios'
|
||||
|
||||
// TODO @zange:挪到 product 下,建个 category 包,挪进去哈;
|
||||
export interface ProductCategoryVO {
|
||||
id: number
|
||||
name: string
|
||||
parentId: number
|
||||
}
|
||||
|
||||
// 查询产品分类详情
|
||||
export const getProductCategory = async (id: number) => {
|
||||
return await request.get({ url: `/crm/product-category/get?id=` + id })
|
||||
}
|
||||
|
||||
// 新增产品分类
|
||||
export const createProductCategory = async (data: ProductCategoryVO) => {
|
||||
return await request.post({ url: `/crm/product-category/create`, data })
|
||||
}
|
||||
|
||||
// 修改产品分类
|
||||
export const updateProductCategory = async (data: ProductCategoryVO) => {
|
||||
return await request.put({ url: `/crm/product-category/update`, data })
|
||||
}
|
||||
|
||||
// 删除产品分类
|
||||
export const deleteProductCategory = async (id: number) => {
|
||||
return await request.delete({ url: `/crm/product-category/delete?id=` + id })
|
||||
}
|
||||
|
||||
// 产品分类列表
|
||||
export const getProductCategoryList = async (params) => {
|
||||
return await request.get({ url: `/crm/product-category/list`, params })
|
||||
}
|
@ -2,7 +2,7 @@ import request from '@/config/axios'
|
||||
|
||||
export interface ReceivablePlanVO {
|
||||
id: number
|
||||
indexNo: number
|
||||
period: number
|
||||
receivableId: number
|
||||
status: number
|
||||
checkStatus: 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' })
|
||||
|
37
src/api/system/social/client/index.ts
Normal file
37
src/api/system/social/client/index.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import request from '@/config/axios'
|
||||
|
||||
export interface SocialClientVO {
|
||||
id: number
|
||||
name: string
|
||||
socialType: number
|
||||
userType: number
|
||||
clientId: string
|
||||
clientSecret: string
|
||||
agentId: string
|
||||
status: number
|
||||
}
|
||||
|
||||
// 查询社交客户端列表
|
||||
export const getSocialClientPage = async (params) => {
|
||||
return await request.get({ url: `/system/social-client/page`, params })
|
||||
}
|
||||
|
||||
// 查询社交客户端详情
|
||||
export const getSocialClient = async (id: number) => {
|
||||
return await request.get({ url: `/system/social-client/get?id=` + id })
|
||||
}
|
||||
|
||||
// 新增社交客户端
|
||||
export const createSocialClient = async (data: SocialClientVO) => {
|
||||
return await request.post({ url: `/system/social-client/create`, data })
|
||||
}
|
||||
|
||||
// 修改社交客户端
|
||||
export const updateSocialClient = async (data: SocialClientVO) => {
|
||||
return await request.put({ url: `/system/social-client/update`, data })
|
||||
}
|
||||
|
||||
// 删除社交客户端
|
||||
export const deleteSocialClient = async (id: number) => {
|
||||
return await request.delete({ url: `/system/social-client/delete?id=` + id })
|
||||
}
|
24
src/api/system/social/user/index.ts
Normal file
24
src/api/system/social/user/index.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import request from '@/config/axios'
|
||||
|
||||
export interface SocialUserVO {
|
||||
id: number
|
||||
type: number
|
||||
openid: string
|
||||
token: string
|
||||
rawTokenInfo: string
|
||||
nickname: string
|
||||
avatar: string
|
||||
rawUserInfo: string
|
||||
code: string
|
||||
state: string
|
||||
}
|
||||
|
||||
// 查询社交用户列表
|
||||
export const getSocialUserPage = async (params) => {
|
||||
return await request.get({ url: `/system/social-user/page`, params })
|
||||
}
|
||||
|
||||
// 查询社交用户详情
|
||||
export const getSocialUser = async (id: number) => {
|
||||
return await request.get({ url: `/system/social-user/get?id=` + id })
|
||||
}
|
@ -3,7 +3,7 @@ defineComponent({
|
||||
name: 'CardTitle'
|
||||
})
|
||||
|
||||
const { title } = defineProps({
|
||||
defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
|
@ -1,20 +1,19 @@
|
||||
<script lang="ts" setup>
|
||||
<script setup lang="ts">
|
||||
import { provide, computed, watch, onMounted } from 'vue'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { ComponentSize, ElConfigProvider } from 'element-plus'
|
||||
import { useLocaleStore } from '@/store/modules/locale'
|
||||
import { useWindowSize } from '@vueuse/core'
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
import { setCssVar } from '@/utils'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
import { ElementPlusSize } from '@/types/elementPlus'
|
||||
import { useWindowSize } from '@vueuse/core'
|
||||
|
||||
defineOptions({ name: 'ConfigGlobal' })
|
||||
|
||||
const { variables } = useDesign()
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
const props = defineProps({
|
||||
size: propTypes.oneOf<ElementPlusSize>(['default', 'small', 'large']).def('default')
|
||||
size: propTypes.oneOf<ComponentSize>(['default', 'small', 'large']).def('default')
|
||||
})
|
||||
|
||||
provide('configGlobal', props)
|
||||
@ -53,9 +52,9 @@ const currentLocale = computed(() => localeStore.currentLocale)
|
||||
|
||||
<template>
|
||||
<ElConfigProvider
|
||||
:namespace="variables.elNamespace"
|
||||
:locale="currentLocale.elLocale"
|
||||
:message="{ max: 1 }"
|
||||
:namespace="variables.elNamespace"
|
||||
:size="size"
|
||||
>
|
||||
<slot></slot>
|
||||
|
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>
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<el-aside class="editor-left" width="260px">
|
||||
<el-aside class="editor-left" width="261px">
|
||||
<el-scrollbar>
|
||||
<el-collapse v-model="extendGroups">
|
||||
<el-collapse-item
|
||||
@ -11,6 +11,7 @@
|
||||
<draggable
|
||||
class="component-container"
|
||||
ghost-class="draggable-ghost"
|
||||
item-key="index"
|
||||
:list="group.components"
|
||||
:sort="false"
|
||||
:group="{ name: 'component', pull: 'clone', put: false }"
|
||||
|
@ -1,27 +1,30 @@
|
||||
import { DiyComponent } from '@/components/DiyEditor/util'
|
||||
import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
|
||||
|
||||
/** 轮播图属性 */
|
||||
export interface CarouselProperty {
|
||||
// 选择模板
|
||||
swiperType: number
|
||||
// 图片圆角
|
||||
borderRadius: number
|
||||
// 页面边距
|
||||
pageMargin: number
|
||||
// 图片边距
|
||||
imageMargin: number
|
||||
// 分页类型
|
||||
pagingType: 'bullets' | 'fraction' | 'progressbar'
|
||||
// 一行个数
|
||||
rowIndividual: number
|
||||
// 添加图片
|
||||
// 类型:默认 | 卡片
|
||||
type: 'default' | 'card'
|
||||
// 指示器样式:点 | 数字
|
||||
indicator: 'dot' | 'number'
|
||||
// 是否自动播放
|
||||
autoplay: boolean
|
||||
// 播放间隔
|
||||
interval: number
|
||||
// 轮播内容
|
||||
items: CarouselItemProperty[]
|
||||
// 组件样式
|
||||
style: ComponentStyle
|
||||
}
|
||||
|
||||
// 轮播内容属性
|
||||
export interface CarouselItemProperty {
|
||||
title: string
|
||||
// 类型:图片 | 视频
|
||||
type: 'img' | 'video'
|
||||
// 图片链接
|
||||
imgUrl: string
|
||||
link: string
|
||||
// 视频链接
|
||||
videoUrl: string
|
||||
// 跳转链接
|
||||
url: string
|
||||
}
|
||||
|
||||
// 定义组件
|
||||
@ -30,15 +33,18 @@ export const component = {
|
||||
name: '轮播图',
|
||||
icon: 'system-uicons:carousel',
|
||||
property: {
|
||||
swiperType: 0, // 选择模板
|
||||
borderRadius: 0, // 图片圆角
|
||||
pageMargin: 0, // 页面边距
|
||||
imageMargin: 0, // 图片边距
|
||||
pagingType: 'bullets', // 分页类型
|
||||
rowIndividual: 2, // 一行个数
|
||||
type: 'default',
|
||||
indicator: 'dot',
|
||||
autoplay: false,
|
||||
interval: 3,
|
||||
items: [
|
||||
{ imgUrl: 'https://static.iocoder.cn/mall/banner-01.jpg' },
|
||||
{ imgUrl: 'https://static.iocoder.cn/mall/banner-02.jpg' }
|
||||
] as CarouselItemProperty[]
|
||||
{ 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>
|
||||
|
@ -6,70 +6,38 @@
|
||||
>
|
||||
<Icon icon="tdesign:image" class="text-gray-8 text-120px!" />
|
||||
</div>
|
||||
<!-- 一行一个 -->
|
||||
<div
|
||||
v-if="property.swiperType === 0"
|
||||
class="flex flex-col"
|
||||
:style="{
|
||||
paddingLeft: property.pageMargin + 'px',
|
||||
paddingRight: property.pageMargin + 'px'
|
||||
}"
|
||||
>
|
||||
<div v-for="(item, index) in property.items" :key="index">
|
||||
<div
|
||||
class="img-item"
|
||||
:style="{
|
||||
marginBottom: property.imageMargin + 'px',
|
||||
borderRadius: property.borderRadius + 'px'
|
||||
}"
|
||||
>
|
||||
<img alt="" :src="item.imgUrl" />
|
||||
<div v-if="item.title" class="title">{{ item.title }}</div>
|
||||
</div>
|
||||
</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>
|
||||
<el-carousel height="174px" v-else :type="property.swiperType === 3 ? 'card' : ''">
|
||||
<el-carousel-item v-for="(item, index) in property.items" :key="index">
|
||||
<div class="img-item" :style="{ borderRadius: property.borderRadius + 'px' }">
|
||||
<img alt="" :src="item.imgUrl" />
|
||||
<div v-if="item.title" class="title">{{ item.title }}</div>
|
||||
</div>
|
||||
</el-carousel-item>
|
||||
</el-carousel>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { CarouselProperty } from './config'
|
||||
|
||||
/** 页面顶部导航栏 */
|
||||
defineOptions({ name: 'NavigationBar' })
|
||||
/** 轮播图 */
|
||||
defineOptions({ name: 'Carousel' })
|
||||
|
||||
const props = defineProps<{ property: CarouselProperty }>()
|
||||
defineProps<{ property: CarouselProperty }>()
|
||||
|
||||
const currentIndex = ref(0)
|
||||
const handleIndexChange = (index: number) => {
|
||||
currentIndex.value = index + 1
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.img-item {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
&:last-child {
|
||||
margin: 0 !important;
|
||||
}
|
||||
/* 图片 */
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.title {
|
||||
height: 36px;
|
||||
width: 100%;
|
||||
background-color: rgba(51, 51, 51, 0.8);
|
||||
text-align: center;
|
||||
line-height: 36px;
|
||||
color: #fff;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style scoped lang="scss"></style>
|
||||
|
@ -1,103 +1,120 @@
|
||||
<template>
|
||||
<el-form label-width="80px" :model="formData">
|
||||
<el-form-item label="选择模板" prop="swiperType">
|
||||
<el-radio-group v-model="formData.swiperType">
|
||||
<el-tooltip class="item" content="一行一个" placement="bottom">
|
||||
<el-radio-button :label="0">
|
||||
<Icon icon="icon-park-twotone:multi-picture-carousel" />
|
||||
</el-radio-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip class="item" content="轮播海报" placement="bottom">
|
||||
<el-radio-button :label="1">
|
||||
<Icon icon="system-uicons:carousel" />
|
||||
</el-radio-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip class="item" content="多图单行" placement="bottom">
|
||||
<el-radio-button :label="2">
|
||||
<Icon icon="icon-park-twotone:carousel" />
|
||||
</el-radio-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip class="item" content="立体轮播" placement="bottom">
|
||||
<el-radio-button :label="3">
|
||||
<Icon icon="ic:round-view-carousel" />
|
||||
</el-radio-button>
|
||||
</el-tooltip>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
|
||||
<el-text tag="p">添加图片</el-text>
|
||||
<el-text type="info" size="small"> 拖动左上角的小圆点可对其排序 </el-text>
|
||||
|
||||
<!-- 图片广告 -->
|
||||
<div v-if="formData.items[0]">
|
||||
<draggable
|
||||
:list="formData.items"
|
||||
:force-fallback="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="handleDeleteImage(index)"
|
||||
v-if="formData.items.length > 1"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-1 flex-col items-center justify-between gap-8px">
|
||||
<UploadImg
|
||||
v-model="element.imgUrl"
|
||||
draggable="false"
|
||||
height="80px"
|
||||
width="100%"
|
||||
class="min-w-80px"
|
||||
/>
|
||||
<!-- 标题 -->
|
||||
<el-input v-model="element.title" placeholder="标题,选填" />
|
||||
<!-- 输入链接 -->
|
||||
<el-input placeholder="链接,选填" v-model="element.link" />
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
</draggable>
|
||||
</div>
|
||||
<el-button @click="handleAddImage" type="primary" plain class="w-full"> 添加图片 </el-button>
|
||||
<el-form-item label="一行个数" prop="rowIndividual" v-show="formData.swiperType === 2">
|
||||
<!-- 单选框 -->
|
||||
<el-radio-group v-model="formData.rowIndividual">
|
||||
<el-radio :label="2">2个</el-radio>
|
||||
<el-radio :label="3">3个</el-radio>
|
||||
<el-radio :label="4">4个</el-radio>
|
||||
<el-radio :label="5">5个</el-radio>
|
||||
<el-radio :label="6">6个</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="分页类型" prop="pagingType">
|
||||
<el-radio-group v-model="formData.pagingType">
|
||||
<el-radio :label="0">不显示</el-radio>
|
||||
<el-radio label="bullets">样式一</el-radio>
|
||||
<el-radio label="fraction">样式二</el-radio>
|
||||
<el-radio label="progressbar">样式三</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="图片圆角" prop="borderRadius">
|
||||
<el-slider v-model="formData.borderRadius" :max="30" />
|
||||
</el-form-item>
|
||||
<el-form-item label="页面边距" prop="pageMargin" v-show="formData.swiperType === 0">
|
||||
<el-slider v-model="formData.pageMargin" :max="20" />
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
label="图片边距"
|
||||
prop="imageMargin"
|
||||
v-show="formData.swiperType === 0 || formData.swiperType === 2"
|
||||
>
|
||||
<el-slider v-model="formData.imageMargin" :max="20" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<el-button @click="handleAddImage" type="primary" plain class="w-full">
|
||||
添加图片
|
||||
</el-button>
|
||||
</el-card>
|
||||
</el-form>
|
||||
</ComponentContainerProperty>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@ -117,7 +134,7 @@ const handleAddImage = () => {
|
||||
formData.value.items.push({} as CarouselItemProperty)
|
||||
}
|
||||
// 删除图片
|
||||
const handleDeleteImage = (index) => {
|
||||
const handleDeleteImage = (index: number) => {
|
||||
formData.value.items.splice(index, 1)
|
||||
}
|
||||
</script>
|
||||
|
@ -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>
|
@ -29,7 +29,7 @@ export const component = {
|
||||
title: '页面标题',
|
||||
description: '',
|
||||
navBarHeight: 35,
|
||||
backgroundColor: '#f5f5f5',
|
||||
backgroundColor: '#fff',
|
||||
backgroundImage: '',
|
||||
styleType: 'default',
|
||||
alwaysShow: true,
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { DiyComponent } from '@/components/DiyEditor/util'
|
||||
import { ComponentStyle, DiyComponent } from '@/components/DiyEditor/util'
|
||||
|
||||
/** 搜索框属性 */
|
||||
export interface SearchProperty {
|
||||
@ -7,10 +7,10 @@ export interface SearchProperty {
|
||||
borderRadius: number // 框体样式
|
||||
placeholder: string // 占位文字
|
||||
placeholderPosition: PlaceholderPosition // 占位文字位置
|
||||
backgroundColor: string // 背景颜色
|
||||
borderColor: string // 框体颜色
|
||||
backgroundColor: string // 框体颜色
|
||||
textColor: string // 字体颜色
|
||||
hotKeywords: string[] // 热词
|
||||
style: ComponentStyle
|
||||
}
|
||||
|
||||
// 文字位置
|
||||
@ -27,9 +27,17 @@ export const component = {
|
||||
borderRadius: 0,
|
||||
placeholder: '搜索商品',
|
||||
placeholderPosition: 'left',
|
||||
backgroundColor: 'rgb(249, 249, 249)',
|
||||
borderColor: 'rgb(255, 255, 255)',
|
||||
backgroundColor: 'rgb(238, 238, 238)',
|
||||
textColor: 'rgb(150, 151, 153)',
|
||||
hotKeywords: []
|
||||
hotKeywords: [],
|
||||
style: {
|
||||
bgType: 'color',
|
||||
bgColor: '#fff',
|
||||
marginBottom: 8,
|
||||
paddingTop: 8,
|
||||
paddingRight: 8,
|
||||
paddingBottom: 8,
|
||||
paddingLeft: 8
|
||||
} as ComponentStyle
|
||||
}
|
||||
} as DiyComponent<SearchProperty>
|
||||
|
@ -2,8 +2,6 @@
|
||||
<div
|
||||
class="search-bar"
|
||||
:style="{
|
||||
background: property.backgroundColor,
|
||||
border: `1px solid ${property.backgroundColor}`,
|
||||
color: property.textColor
|
||||
}"
|
||||
>
|
||||
@ -12,7 +10,7 @@
|
||||
class="inner"
|
||||
:style="{
|
||||
height: `${property.height}px`,
|
||||
background: property.borderColor,
|
||||
background: property.backgroundColor,
|
||||
borderRadius: `${property.borderRadius}px`
|
||||
}"
|
||||
>
|
||||
@ -44,13 +42,10 @@ defineProps<{ property: SearchProperty }>()
|
||||
|
||||
<style scoped lang="scss">
|
||||
.search-bar {
|
||||
position: relative;
|
||||
/* 搜索框 */
|
||||
.inner {
|
||||
position: relative;
|
||||
width: calc(100% - 16px);
|
||||
min-height: 28px;
|
||||
margin: 5px auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
|
@ -1,78 +1,77 @@
|
||||
<template>
|
||||
<el-text tag="p"> 搜索热词 </el-text>
|
||||
<el-text type="info" size="small"> 拖动左侧的小圆点可以调整热词顺序 </el-text>
|
||||
<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 label="框体颜色" prop="borderColor">
|
||||
<ColorInput v-model="formData.borderColor" />
|
||||
</el-form-item>
|
||||
<el-form-item class="lef" label="文本颜色" prop="textColor">
|
||||
<ColorInput v-model="formData.textColor" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<!-- 表单 -->
|
||||
<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">
|
||||
|
@ -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>
|
@ -33,111 +33,63 @@
|
||||
<ComponentLibrary ref="componentLibrary" :list="libs" v-if="libs && libs.length > 0" />
|
||||
<!-- 中心设计区域 -->
|
||||
<div class="editor-center page-prop-area" @click="handlePageSelected">
|
||||
<div class="editor-design">
|
||||
<!-- 手机顶部 -->
|
||||
<div class="editor-design-top">
|
||||
<!-- 手机顶部状态栏 -->
|
||||
<img src="@/assets/imgs/diy/statusBar.png" alt="" class="status-bar" />
|
||||
<!-- 手机顶部导航栏 -->
|
||||
<NavigationBar
|
||||
v-if="showNavigationBar"
|
||||
:property="navigationBarComponent.property"
|
||||
@click="handleNavigationBarSelected"
|
||||
:class="[
|
||||
'component',
|
||||
'cursor-pointer!',
|
||||
{ active: selectedComponent?.id === navigationBarComponent.id }
|
||||
]"
|
||||
/>
|
||||
</div>
|
||||
<!-- 手机页面编辑区域 -->
|
||||
<el-scrollbar class="editor-design-center" height="100%" view-class="page-prop-area">
|
||||
<div
|
||||
class="phone-container"
|
||||
: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 }">
|
||||
<div class="component-container" @click="handleComponentSelected(element, index)">
|
||||
<!-- 左侧组件名 -->
|
||||
<div
|
||||
:class="['component-name', { active: selectedComponentIndex === index }]"
|
||||
v-if="element.name"
|
||||
>
|
||||
{{ element.name }}
|
||||
</div>
|
||||
<!-- 组件内容区 -->
|
||||
<div :class="['component', { active: selectedComponentIndex === index }]">
|
||||
<component
|
||||
:is="element.id"
|
||||
:property="element.property"
|
||||
:data-type="element.id"
|
||||
/>
|
||||
</div>
|
||||
<!-- 左侧:组件操作工具栏 -->
|
||||
<div
|
||||
class="component-toolbar"
|
||||
v-if="element.name && selectedComponentIndex === index"
|
||||
>
|
||||
<el-button-group type="primary">
|
||||
<el-tooltip content="上移" placement="right">
|
||||
<el-button
|
||||
:disabled="index === 0"
|
||||
@click.stop="handleMoveComponent(index, -1)"
|
||||
>
|
||||
<Icon icon="ep:arrow-up" />
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="下移" placement="right">
|
||||
<el-button
|
||||
:disabled="index === pageComponents.length - 1"
|
||||
@click.stop="handleMoveComponent(index, 1)"
|
||||
>
|
||||
<Icon icon="ep:arrow-down" />
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="复制" placement="right">
|
||||
<el-button @click.stop="handleCopyComponent(index)">
|
||||
<Icon icon="ep:copy-document" />
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip content="删除" placement="right">
|
||||
<el-button @click.stop="handleDeleteComponent(index)">
|
||||
<Icon icon="ep:delete" />
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</el-button-group>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
<!-- 手机底部导航 -->
|
||||
<div
|
||||
v-if="showTabBar"
|
||||
:class="[
|
||||
'editor-design-bottom',
|
||||
'component',
|
||||
'cursor-pointer!',
|
||||
{ active: selectedComponent?.id === tabBarComponent.id }
|
||||
]"
|
||||
<!-- 手机顶部 -->
|
||||
<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"
|
||||
>
|
||||
<TabBar :property="tabBarComponent.property" @click="handleTabBarSelected" />
|
||||
</div>
|
||||
<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>
|
||||
<!-- 右侧属性面板 -->
|
||||
@ -178,8 +130,6 @@ export default {
|
||||
<script lang="ts" setup>
|
||||
import draggable from 'vuedraggable'
|
||||
import ComponentLibrary from './components/ComponentLibrary.vue'
|
||||
import NavigationBar from './components/mobile/NavigationBar/index.vue'
|
||||
import TabBar from './components/mobile/TabBar/index.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'
|
||||
@ -256,6 +206,9 @@ const handleSave = () => {
|
||||
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)
|
||||
@ -383,6 +336,7 @@ onMounted(() => setDefaultSelectedComponent())
|
||||
<style lang="scss" scoped>
|
||||
/* 手机宽度 */
|
||||
$phone-width: 375px;
|
||||
$toolbar-height: 42px;
|
||||
/* 根节点样式 */
|
||||
.editor {
|
||||
height: 100%;
|
||||
@ -394,7 +348,7 @@ $phone-width: 375px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: auto;
|
||||
height: $toolbar-height;
|
||||
padding: 0;
|
||||
border-bottom: solid 1px var(--el-border-color);
|
||||
background-color: var(--el-bg-color);
|
||||
@ -416,176 +370,81 @@ $phone-width: 375px;
|
||||
/* 中心操作区 */
|
||||
.editor-container {
|
||||
height: calc(
|
||||
100vh - var(--top-tool-height) - var(--tags-view-height) - var(--app-footer-height) - 42px
|
||||
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;
|
||||
}
|
||||
/* 属性面板分组 */
|
||||
.property-group {
|
||||
/* 属性分组 */
|
||||
:deep(.el-card__header) {
|
||||
: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;
|
||||
padding: 16px 0;
|
||||
background-color: var(--app-content-bg-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
/* 中心设计区域 */
|
||||
.editor-design {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
margin: 16px 0 0 0;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
|
||||
/* 手机顶部 */
|
||||
.editor-design-top {
|
||||
width: $phone-width;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
/* 手机顶部状态栏 */
|
||||
.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%;
|
||||
|
||||
/* 组件 */
|
||||
.component {
|
||||
border: 1px solid #fff;
|
||||
/* 主体内容 */
|
||||
.phone-container {
|
||||
position: relative;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 100%;
|
||||
height: 100%;
|
||||
width: $phone-width;
|
||||
cursor: move;
|
||||
/* 鼠标放到组件上时 */
|
||||
&:hover {
|
||||
border: 1px dashed var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
/* 组件选中 */
|
||||
.component.active {
|
||||
border: 2px solid var(--el-color-primary);
|
||||
}
|
||||
/* 手机顶部 */
|
||||
.editor-design-top {
|
||||
width: $phone-width;
|
||||
/* 手机顶部状态栏 */
|
||||
.status-bar {
|
||||
height: 20px;
|
||||
width: $phone-width;
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
/* 手机底部导航 */
|
||||
.editor-design-bottom {
|
||||
width: $phone-width;
|
||||
}
|
||||
/* 手机页面编辑区域 */
|
||||
.editor-design-center {
|
||||
width: 100%;
|
||||
flex: 1 1 0;
|
||||
|
||||
:deep(.el-scrollbar__view) {
|
||||
margin: 0 auto;
|
||||
.drag-area {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 主体内容 */
|
||||
.phone-container {
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
background-repeat: no-repeat;
|
||||
background-size: 100% 100%;
|
||||
width: $phone-width;
|
||||
margin: 0 auto;
|
||||
.drag-area {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 组件容器(左侧:组件名称,中间:组件,右侧:操作工具栏) */
|
||||
.component-container {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
/* 左侧:组件名称 */
|
||||
.component-name {
|
||||
position: absolute;
|
||||
width: 80px;
|
||||
text-align: center;
|
||||
line-height: 25px;
|
||||
height: 25px;
|
||||
background: #fff;
|
||||
font-size: 12px;
|
||||
left: -85px;
|
||||
top: 0;
|
||||
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-name.active {
|
||||
background: var(--el-color-primary);
|
||||
color: #fff;
|
||||
&:after {
|
||||
border-left-color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
/* 右侧:组件操作工具栏 */
|
||||
.component-toolbar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: -57px;
|
||||
/* 左侧小三角 */
|
||||
&:before {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: -10px;
|
||||
content: ' ';
|
||||
height: 0;
|
||||
width: 0;
|
||||
border: 5px solid transparent;
|
||||
border-right-color: #2d8cf0;
|
||||
}
|
||||
|
||||
/* 重写 Element 按钮组的样式(官方只支持水平显示,增加垂直显示的样式) */
|
||||
.el-button-group {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.el-button-group > .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 > .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 .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 > .el-button:not(:last-child) {
|
||||
margin-bottom: -1px;
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,19 +3,56 @@ import { PageConfigProperty } from '@/components/DiyEditor/components/mobile/Pag
|
||||
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 {
|
||||
// 页面属性
|
||||
@ -23,7 +60,7 @@ export interface PageConfig {
|
||||
// 顶部导航栏属性
|
||||
navigationBar: NavigationBarProperty
|
||||
// 底部导航菜单属性
|
||||
tabBar: TabBarProperty
|
||||
tabBar?: TabBarProperty
|
||||
// 页面组件列表
|
||||
components: PageComponent[]
|
||||
}
|
||||
@ -57,3 +94,27 @@ export function usePropertyForm<T>(modelValue: T, emit: Function): { formData: R
|
||||
|
||||
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[]
|
||||
|
@ -27,7 +27,7 @@ import * as DateUtil from '@/utils/formatTime'
|
||||
defineOptions({ name: 'ShortcutDateRangePicker' })
|
||||
|
||||
const shortcutDays = ref(7) // 日期快捷天数(单选按钮组), 默认7天
|
||||
const times = ref<[dayjs.ConfigType, dayjs.ConfigType]>(['', '']) // 时间范围参数
|
||||
const times = ref<[string, string]>(['', '']) // 时间范围参数
|
||||
defineExpose({ times }) // 暴露时间范围参数
|
||||
/** 日期快捷选择 */
|
||||
const shortcuts = [
|
||||
|
@ -32,7 +32,7 @@ onMounted(() => {
|
||||
|
||||
scrollContainer.value = getScrollContainer(refSticky.value!, true)
|
||||
useEventListener(scrollContainer, 'scroll', handleScroll)
|
||||
useEventListener('resize', handleReize)
|
||||
useEventListener('resize', handleResize)
|
||||
handleScroll()
|
||||
})
|
||||
onActivated(() => {
|
||||
@ -103,7 +103,7 @@ const handleScroll = () => {
|
||||
reset()
|
||||
}
|
||||
}
|
||||
const handleReize = () => {
|
||||
const handleResize = () => {
|
||||
if (isSticky.value && refSticky.value) {
|
||||
width.value = refSticky.value.getBoundingClientRect().width + 'px'
|
||||
}
|
||||
|
@ -4,13 +4,13 @@ import { propTypes } from '@/utils/propTypes'
|
||||
defineOptions({ name: 'Tooltip' })
|
||||
|
||||
defineProps({
|
||||
titel: propTypes.string.def(''),
|
||||
title: propTypes.string.def(''),
|
||||
message: propTypes.string.def(''),
|
||||
icon: propTypes.string.def('ep:question-filled')
|
||||
})
|
||||
</script>
|
||||
<template>
|
||||
<span>{{ titel }}</span>
|
||||
<span>{{ title }}</span>
|
||||
<ElTooltip :content="message" placement="top">
|
||||
<Icon :icon="icon" class="relative top-1px ml-1px" />
|
||||
</ElTooltip>
|
||||
|
@ -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">
|
||||
|
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>
|
49
src/hooks/web/useGuide.ts
Normal file
49
src/hooks/web/useGuide.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import { Config, driver } from 'driver.js'
|
||||
import 'driver.js/dist/driver.css'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
import { useI18n } from '@/hooks/web/useI18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const { variables } = useDesign()
|
||||
|
||||
export const useGuide = (options?: Config) => {
|
||||
const driverObj = driver(
|
||||
options || {
|
||||
showProgress: true,
|
||||
nextBtnText: t('common.nextLabel'),
|
||||
prevBtnText: t('common.prevLabel'),
|
||||
doneBtnText: t('common.doneLabel'),
|
||||
steps: [
|
||||
{
|
||||
element: `#${variables.namespace}-menu`,
|
||||
popover: {
|
||||
title: t('common.menu'),
|
||||
description: t('common.menuDes'),
|
||||
side: 'right'
|
||||
}
|
||||
},
|
||||
{
|
||||
element: `#${variables.namespace}-tool-header`,
|
||||
popover: {
|
||||
title: t('common.tool'),
|
||||
description: t('common.toolDes'),
|
||||
side: 'left'
|
||||
}
|
||||
},
|
||||
{
|
||||
element: `#${variables.namespace}-tags-view`,
|
||||
popover: {
|
||||
title: t('common.tagsView'),
|
||||
description: t('common.tagsViewDes'),
|
||||
side: 'bottom'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
...driverObj
|
||||
}
|
||||
}
|
@ -1,47 +0,0 @@
|
||||
import introJs from 'intro.js'
|
||||
import { IntroJs, Step, Options } from 'intro.js'
|
||||
import 'intro.js/introjs.css'
|
||||
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
|
||||
export const useIntro = (setps?: Step[], options?: Options) => {
|
||||
const { t } = useI18n()
|
||||
|
||||
const { variables } = useDesign()
|
||||
|
||||
const defaultSetps: Step[] = setps || [
|
||||
{
|
||||
element: `#${variables.namespace}-menu`,
|
||||
title: t('common.menu'),
|
||||
intro: t('common.menuDes'),
|
||||
position: 'right'
|
||||
},
|
||||
{
|
||||
element: `#${variables.namespace}-tool-header`,
|
||||
title: t('common.tool'),
|
||||
intro: t('common.toolDes'),
|
||||
position: 'left'
|
||||
},
|
||||
{
|
||||
element: `#${variables.namespace}-tags-view`,
|
||||
title: t('common.tagsView'),
|
||||
intro: t('common.tagsViewDes'),
|
||||
position: 'bottom'
|
||||
}
|
||||
]
|
||||
|
||||
const defaultOptions: Options = options || {
|
||||
prevLabel: t('common.prevLabel'),
|
||||
nextLabel: t('common.nextLabel'),
|
||||
skipLabel: t('common.skipLabel'),
|
||||
doneLabel: t('common.doneLabel')
|
||||
}
|
||||
|
||||
const introRef: IntroJs = introJs()
|
||||
|
||||
introRef.addSteps(defaultSetps).setOptions(defaultOptions)
|
||||
|
||||
return {
|
||||
introRef
|
||||
}
|
||||
}
|
21
src/hooks/web/useNetwork.ts
Normal file
21
src/hooks/web/useNetwork.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { ref, onBeforeUnmount } from 'vue'
|
||||
|
||||
const useNetwork = () => {
|
||||
const online = ref(true)
|
||||
|
||||
const updateNetwork = () => {
|
||||
online.value = navigator.onLine
|
||||
}
|
||||
|
||||
window.addEventListener('online', updateNetwork)
|
||||
window.addEventListener('offline', updateNetwork)
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
window.removeEventListener('online', updateNetwork)
|
||||
window.removeEventListener('offline', updateNetwork)
|
||||
})
|
||||
|
||||
return { online }
|
||||
}
|
||||
|
||||
export { useNetwork }
|
60
src/hooks/web/useNow.ts
Normal file
60
src/hooks/web/useNow.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import { dateUtil } from '@/utils/dateUtil'
|
||||
import { reactive, toRefs } from 'vue'
|
||||
import { tryOnMounted, tryOnUnmounted } from '@vueuse/core'
|
||||
|
||||
export const useNow = (immediate = true) => {
|
||||
let timer: IntervalHandle
|
||||
|
||||
const state = reactive({
|
||||
year: 0,
|
||||
month: 0,
|
||||
week: '',
|
||||
day: 0,
|
||||
hour: '',
|
||||
minute: '',
|
||||
second: 0,
|
||||
meridiem: ''
|
||||
})
|
||||
|
||||
const update = () => {
|
||||
const now = dateUtil()
|
||||
|
||||
const h = now.format('HH')
|
||||
const m = now.format('mm')
|
||||
const s = now.get('s')
|
||||
|
||||
state.year = now.get('y')
|
||||
state.month = now.get('M') + 1
|
||||
state.week = '星期' + ['日', '一', '二', '三', '四', '五', '六'][now.day()]
|
||||
state.day = now.get('date')
|
||||
state.hour = h
|
||||
state.minute = m
|
||||
state.second = s
|
||||
|
||||
state.meridiem = now.format('A')
|
||||
}
|
||||
|
||||
function start() {
|
||||
update()
|
||||
clearInterval(timer)
|
||||
timer = setInterval(() => update(), 1000)
|
||||
}
|
||||
|
||||
function stop() {
|
||||
clearInterval(timer)
|
||||
}
|
||||
|
||||
tryOnMounted(() => {
|
||||
immediate && start()
|
||||
})
|
||||
|
||||
tryOnUnmounted(() => {
|
||||
stop()
|
||||
})
|
||||
|
||||
return {
|
||||
...toRefs(state),
|
||||
start,
|
||||
stop
|
||||
}
|
||||
}
|
63
src/hooks/web/useTagsView.ts
Normal file
63
src/hooks/web/useTagsView.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import { useTagsViewStoreWithOut } from '@/store/modules/tagsView'
|
||||
import { RouteLocationNormalizedLoaded, useRouter } from 'vue-router'
|
||||
import { computed, nextTick, unref } from 'vue'
|
||||
|
||||
export const useTagsView = () => {
|
||||
const tagsViewStore = useTagsViewStoreWithOut()
|
||||
|
||||
const { replace, currentRoute } = useRouter()
|
||||
|
||||
const selectedTag = computed(() => tagsViewStore.getSelectedTag)
|
||||
|
||||
const closeAll = (callback?: Fn) => {
|
||||
tagsViewStore.delAllViews()
|
||||
callback?.()
|
||||
}
|
||||
|
||||
const closeLeft = (callback?: Fn) => {
|
||||
tagsViewStore.delLeftViews(unref(selectedTag) as RouteLocationNormalizedLoaded)
|
||||
callback?.()
|
||||
}
|
||||
|
||||
const closeRight = (callback?: Fn) => {
|
||||
tagsViewStore.delRightViews(unref(selectedTag) as RouteLocationNormalizedLoaded)
|
||||
callback?.()
|
||||
}
|
||||
|
||||
const closeOther = (callback?: Fn) => {
|
||||
tagsViewStore.delOthersViews(unref(selectedTag) as RouteLocationNormalizedLoaded)
|
||||
callback?.()
|
||||
}
|
||||
|
||||
const closeCurrent = (view?: RouteLocationNormalizedLoaded, callback?: Fn) => {
|
||||
if (view?.meta?.affix) return
|
||||
tagsViewStore.delView(view || unref(currentRoute))
|
||||
|
||||
callback?.()
|
||||
}
|
||||
|
||||
const refreshPage = async (view?: RouteLocationNormalizedLoaded, callback?: Fn) => {
|
||||
tagsViewStore.delCachedView()
|
||||
const { path, query } = view || unref(currentRoute)
|
||||
await nextTick()
|
||||
replace({
|
||||
path: '/redirect' + path,
|
||||
query: query
|
||||
})
|
||||
callback?.()
|
||||
}
|
||||
|
||||
const setTitle = (title: string, path?: string) => {
|
||||
tagsViewStore.setTitle(title, path)
|
||||
}
|
||||
|
||||
return {
|
||||
closeAll,
|
||||
closeLeft,
|
||||
closeRight,
|
||||
closeOther,
|
||||
closeCurrent,
|
||||
refreshPage,
|
||||
setTitle
|
||||
}
|
||||
}
|
@ -1,54 +1,53 @@
|
||||
const { t } = useI18n()
|
||||
import { useI18n } from '@/hooks/web/useI18n'
|
||||
import { FormItemRule } from 'element-plus'
|
||||
|
||||
type Callback = (error?: string | Error | undefined) => void
|
||||
const { t } = useI18n()
|
||||
|
||||
interface LengthRange {
|
||||
min: number
|
||||
max: number
|
||||
message: string
|
||||
message?: string
|
||||
}
|
||||
|
||||
export const useValidator = () => {
|
||||
const required = (message?: string) => {
|
||||
const required = (message?: string): FormItemRule => {
|
||||
return {
|
||||
required: true,
|
||||
message: message || t('common.required')
|
||||
}
|
||||
}
|
||||
|
||||
const lengthRange = (val: any, callback: Callback, options: LengthRange) => {
|
||||
const lengthRange = (options: LengthRange): FormItemRule => {
|
||||
const { min, max, message } = options
|
||||
if (val.length < min || val.length > max) {
|
||||
callback(new Error(message))
|
||||
} else {
|
||||
callback()
|
||||
|
||||
return {
|
||||
min,
|
||||
max,
|
||||
message: message || t('common.lengthRange', { min, max })
|
||||
}
|
||||
}
|
||||
|
||||
const notSpace = (val: any, callback: Callback, message: string) => {
|
||||
// 用户名不能有空格
|
||||
if (val.indexOf(' ') !== -1) {
|
||||
callback(new Error(message))
|
||||
} else {
|
||||
callback()
|
||||
const notSpace = (message?: string): FormItemRule => {
|
||||
return {
|
||||
validator: (_, val, callback) => {
|
||||
if (val?.indexOf(' ') !== -1) {
|
||||
callback(new Error(message || t('common.notSpace')))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const notSpecialCharacters = (val: any, callback: Callback, message: string) => {
|
||||
// 密码不能是特殊字符
|
||||
if (/[`~!@#$%^&*()_+<>?:"{},.\/;'[\]]/gi.test(val)) {
|
||||
callback(new Error(message))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
// 两个字符串是否想等
|
||||
const isEqual = (val1: string, val2: string, callback: Callback, message: string) => {
|
||||
if (val1 === val2) {
|
||||
callback()
|
||||
} else {
|
||||
callback(new Error(message))
|
||||
const notSpecialCharacters = (message?: string): FormItemRule => {
|
||||
return {
|
||||
validator: (_, val, callback) => {
|
||||
if (/[`~!@#$%^&*()_+<>?:"{},.\/;'[\]]/gi.test(val)) {
|
||||
callback(new Error(message || t('common.notSpecialCharacters')))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -56,7 +55,6 @@ export const useValidator = () => {
|
||||
required,
|
||||
lengthRange,
|
||||
notSpace,
|
||||
notSpecialCharacters,
|
||||
isEqual
|
||||
notSpecialCharacters
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -53,7 +53,7 @@ onMounted(() => {
|
||||
</template>
|
||||
<ElTabs v-model="activeName">
|
||||
<ElTabPane label="我的站内信" name="notice">
|
||||
<div class="message-list">
|
||||
<el-scrollbar class="message-list">
|
||||
<template v-for="item in list" :key="item.id">
|
||||
<div class="message-item">
|
||||
<img alt="" class="message-icon" src="@/assets/imgs/avatar.gif" />
|
||||
@ -67,7 +67,7 @@ onMounted(() => {
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</ElTabPane>
|
||||
</ElTabs>
|
||||
<!-- 更多 -->
|
||||
@ -88,6 +88,7 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.message-list {
|
||||
height: 400px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
|
@ -116,6 +116,7 @@ export enum DICT_TYPE {
|
||||
SYSTEM_OAUTH2_GRANT_TYPE = 'system_oauth2_grant_type',
|
||||
SYSTEM_MAIL_SEND_STATUS = 'system_mail_send_status',
|
||||
SYSTEM_NOTIFY_TEMPLATE_TYPE = 'system_notify_template_type',
|
||||
SYSTEM_SOCIAL_TYPE = 'system_social_type',
|
||||
|
||||
// ========== INFRA 模块 ==========
|
||||
INFRA_BOOLEAN_STRING = 'infra_boolean_string',
|
||||
@ -193,5 +194,6 @@ export enum DICT_TYPE {
|
||||
CRM_RETURN_TYPE = 'crm_return_type',
|
||||
CRM_CUSTOMER_INDUSTRY = 'crm_customer_industry',
|
||||
CRM_CUSTOMER_LEVEL = 'crm_customer_level',
|
||||
CRM_CUSTOMER_SOURCE = 'crm_customer_source'
|
||||
CRM_CUSTOMER_SOURCE = 'crm_customer_source',
|
||||
CRM_PRODUCT_STATUS = 'crm_product_status'
|
||||
}
|
||||
|
@ -334,6 +334,6 @@ export function getLast1Year(): [dayjs.ConfigType, dayjs.ConfigType] {
|
||||
export function getDateRange(
|
||||
beginDate: dayjs.ConfigType,
|
||||
endDate: dayjs.ConfigType
|
||||
): [dayjs.ConfigType, dayjs.ConfigType] {
|
||||
return [dayjs(beginDate).startOf('d'), dayjs(endDate).endOf('d')]
|
||||
): [string, string] {
|
||||
return [dayjs(beginDate).startOf('d').toString(), dayjs(endDate).endOf('d').toString()]
|
||||
}
|
||||
|
@ -34,6 +34,13 @@ export const underlineToHump = (str: string): string => {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 驼峰转横杠
|
||||
*/
|
||||
export const humpToDash = (str: string): string => {
|
||||
return str.replace(/([A-Z])/g, '-$1').toLowerCase()
|
||||
}
|
||||
|
||||
export const setCssVar = (prop: string, val: any, dom = document.documentElement) => {
|
||||
dom.style.setProperty(prop, val)
|
||||
}
|
||||
@ -67,7 +74,7 @@ export const trim = (str: string) => {
|
||||
* @param {Date | number | string} time 需要转换的时间
|
||||
* @param {String} fmt 需要转换的格式 如 yyyy-MM-dd、yyyy-MM-dd HH:mm:ss
|
||||
*/
|
||||
export const formatTime = (time: Date | number | string, fmt: string) => {
|
||||
export function formatTime(time: Date | number | string, fmt: string) {
|
||||
if (!time) return ''
|
||||
else {
|
||||
const date = new Date(time)
|
||||
@ -98,7 +105,7 @@ export const formatTime = (time: Date | number | string, fmt: string) => {
|
||||
/**
|
||||
* 生成随机字符串
|
||||
*/
|
||||
export const toAnyString = () => {
|
||||
export function toAnyString() {
|
||||
const str: string = 'xxxxx-xxxxx-4xxxx-yxxxx-xxxxx'.replace(/[xy]/g, (c: string) => {
|
||||
const r: number = (Math.random() * 16) | 0
|
||||
const v: number = c === 'x' ? r : (r & 0x3) | 0x8
|
||||
@ -107,6 +114,13 @@ export const toAnyString = () => {
|
||||
return str
|
||||
}
|
||||
|
||||
/**
|
||||
* 首字母大写
|
||||
*/
|
||||
export function firstUpperCase(str: string) {
|
||||
return str.toLowerCase().replace(/( |^)[a-z]/g, (L) => L.toUpperCase())
|
||||
}
|
||||
|
||||
export const generateUUID = () => {
|
||||
if (typeof crypto === 'object') {
|
||||
if (typeof crypto.randomUUID === 'function') {
|
||||
|
@ -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>
|
||||
|
||||
|
@ -15,7 +15,7 @@
|
||||
</div>
|
||||
</template>
|
||||
<div>
|
||||
<el-tabs v-model="activeName" tab-position="top" style="height: 400px" class="profile-tabs">
|
||||
<el-tabs v-model="activeName" class="profile-tabs" style="height: 400px" tab-position="top">
|
||||
<el-tab-pane :label="t('profile.info.basicInfo')" name="basicInfo">
|
||||
<BasicInfo />
|
||||
</el-tab-pane>
|
||||
@ -23,17 +23,18 @@
|
||||
<ResetPwd />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="t('profile.info.userSocial')" name="userSocial">
|
||||
<UserSocial />
|
||||
<UserSocial v-model:activeName="activeName" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts" name="Profile">
|
||||
import { BasicInfo, ProfileUser, ResetPwd, UserSocial } from './components/'
|
||||
const { t } = useI18n()
|
||||
<script lang="ts" setup>
|
||||
import { BasicInfo, ProfileUser, ResetPwd, UserSocial } from './components'
|
||||
|
||||
const { t } = useI18n()
|
||||
defineOptions({ name: 'Profile' })
|
||||
const activeName = ref('basicInfo')
|
||||
</script>
|
||||
<style scoped>
|
||||
|
@ -27,12 +27,15 @@ import { getUserProfile, ProfileVO } from '@/api/system/user/profile'
|
||||
import { socialAuthRedirect, socialBind, socialUnbind } from '@/api/system/user/socialUser'
|
||||
|
||||
defineOptions({ name: 'UserSocial' })
|
||||
|
||||
defineProps<{
|
||||
activeName: string
|
||||
}>()
|
||||
const message = useMessage()
|
||||
const socialUsers = ref<any[]>([])
|
||||
const userInfo = ref<ProfileVO>()
|
||||
|
||||
const initSocial = async () => {
|
||||
socialUsers.value = [] // 重置避免无限增长
|
||||
const res = await getUserProfile()
|
||||
userInfo.value = res
|
||||
for (const i in SystemUserSocialTypeEnum) {
|
||||
@ -49,9 +52,12 @@ const initSocial = async () => {
|
||||
}
|
||||
}
|
||||
const route = useRoute()
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:activeName', v: string): void
|
||||
}>()
|
||||
const bindSocial = () => {
|
||||
// 社交绑定
|
||||
const type = route.query.type
|
||||
const type = getUrlValue('type')
|
||||
const code = route.query.code
|
||||
const state = route.query.state
|
||||
if (!code) {
|
||||
@ -59,11 +65,20 @@ const bindSocial = () => {
|
||||
}
|
||||
socialBind(type, code, state).then(() => {
|
||||
message.success('绑定成功')
|
||||
emit('update:activeName', 'userSocial')
|
||||
initSocial()
|
||||
})
|
||||
}
|
||||
|
||||
// 双层 encode 需要在回调后进行 decode
|
||||
function getUrlValue(key: string): string {
|
||||
const url = new URL(decodeURIComponent(location.href))
|
||||
return url.searchParams.get(key) ?? ''
|
||||
}
|
||||
|
||||
const bind = (row) => {
|
||||
const redirectUri = location.origin + '/user/profile?type=' + row.type
|
||||
// 双层 encode 解决钉钉回调 type 参数丢失的问题
|
||||
const redirectUri = location.origin + '/user/profile?' + encodeURIComponent(`type=${row.type}`)
|
||||
// 进行跳转
|
||||
socialAuthRedirect(row.type, encodeURIComponent(redirectUri)).then((res) => {
|
||||
window.location.href = res
|
||||
@ -83,9 +98,8 @@ onMounted(async () => {
|
||||
|
||||
watch(
|
||||
() => route,
|
||||
(newRoute) => {
|
||||
() => {
|
||||
bindSocial()
|
||||
console.log(newRoute)
|
||||
},
|
||||
{
|
||||
immediate: true
|
||||
|
70
src/views/crm/product/ProductDetail.vue
Normal file
70
src/views/crm/product/ProductDetail.vue
Normal file
@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<Dialog v-model="dialogVisible" :max-height="500" :scroll="true" title="产品详情">
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="产品名称">
|
||||
{{ detailData.name }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="创建时间">
|
||||
{{ formatDate(detailData.createTime) }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="状态">
|
||||
<dict-tag :type="DICT_TYPE.CRM_PRODUCT_STATUS" :value="detailData.status" />
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="产品分类">
|
||||
{{ productCategoryList?.find((c) => c.id === detailData.categoryId)?.name }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="产品编码">
|
||||
{{ detailData.no }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="产品描述">
|
||||
{{ detailData.description }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="负责人">
|
||||
{{ detailData.ownerUserId }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="单位">
|
||||
<dict-tag :type="DICT_TYPE.PRODUCT_UNIT" :value="detailData.unit" />
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="价格">
|
||||
{{ fenToYuan(detailData.price) }}元
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { DICT_TYPE } from '@/utils/dict'
|
||||
import * as ProductCategoryApi from '@/api/crm/productCategory'
|
||||
import * as ProductApi from '@/api/crm/product'
|
||||
import { formatDate } from '@/utils/formatTime'
|
||||
import { fenToYuan } from '@/utils'
|
||||
import { getSimpleUserList, UserVO } from '@/api/system/user'
|
||||
|
||||
defineOptions({ name: 'CrmProductDetail' })
|
||||
|
||||
const { t } = useI18n() // 国际化
|
||||
|
||||
const dialogVisible = ref(false) // 弹窗的是否展示
|
||||
const detailLoading = ref(false) // 表单的加载中
|
||||
const detailData = ref() // 详情数据
|
||||
|
||||
/** 打开弹窗 */
|
||||
const open = async (data: ProductApi.ProductVO) => {
|
||||
dialogVisible.value = true
|
||||
// 设置数据
|
||||
detailLoading.value = true
|
||||
try {
|
||||
detailData.value = data
|
||||
} finally {
|
||||
detailLoading.value = false
|
||||
}
|
||||
}
|
||||
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
|
||||
|
||||
const productCategoryList = ref([]) // 产品分类树
|
||||
const userList = ref<UserVO[]>([]) // 系统用户
|
||||
|
||||
onMounted(async () => {
|
||||
productCategoryList.value = await ProductCategoryApi.getProductCategoryList({})
|
||||
userList.value = await getSimpleUserList()
|
||||
})
|
||||
</script>
|
185
src/views/crm/product/ProductForm.vue
Normal file
185
src/views/crm/product/ProductForm.vue
Normal file
@ -0,0 +1,185 @@
|
||||
<template>
|
||||
<Dialog :title="dialogTitle" v-model="dialogVisible">
|
||||
<!-- TODO @zange:改成每行两个哈; -->
|
||||
<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="no">
|
||||
<el-input v-model="formData.no" placeholder="请输入产品编码" />
|
||||
</el-form-item>
|
||||
<el-form-item label="单位" prop="unit">
|
||||
<el-select v-model="formData.unit" class="w-1/1" placeholder="请选择单位">
|
||||
<el-option
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.PRODUCT_UNIT)"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="价格" prop="price">
|
||||
<el-input type="number" v-model="formData.price" placeholder="请输入价格" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-select v-model="formData.status" placeholder="请选择状态">
|
||||
<el-option
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.CRM_PRODUCT_STATUS)"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="产品分类" prop="categoryId">
|
||||
<el-cascader
|
||||
v-model="formData.categoryId"
|
||||
:options="productCategoryList"
|
||||
:props="defaultProps"
|
||||
class="w-1/1"
|
||||
clearable
|
||||
placeholder="请选择产品分类"
|
||||
filterable
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="产品描述" prop="description">
|
||||
<el-input v-model="formData.description" placeholder="请输入产品描述" />
|
||||
</el-form-item>
|
||||
<el-form-item label="负责人" prop="ownerUserId">
|
||||
<el-select
|
||||
v-model="formData.ownerUserId"
|
||||
placeholder="请选择负责人"
|
||||
:disabled="formData.id"
|
||||
>
|
||||
<el-option
|
||||
v-for="user in userList"
|
||||
:key="user.id"
|
||||
:label="user.nickname"
|
||||
:value="user.id"
|
||||
/>
|
||||
</el-select>
|
||||
</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 { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
||||
import * as ProductApi from '@/api/crm/product'
|
||||
import * as ProductCategoryApi from '@/api/crm/productCategory'
|
||||
import { defaultProps, handleTree } from '@/utils/tree'
|
||||
import { getSimpleUserList, UserVO } from '@/api/system/user'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
|
||||
defineOptions({ name: 'CrmProductForm' })
|
||||
|
||||
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 userId = useUserStore().getUser.id // 当前登录的编号
|
||||
const formData = ref({
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
no: undefined,
|
||||
unit: undefined,
|
||||
price: undefined,
|
||||
status: undefined,
|
||||
categoryId: undefined,
|
||||
description: undefined,
|
||||
ownerUserId: undefined
|
||||
})
|
||||
const formRules = reactive({
|
||||
name: [{ required: true, message: '产品名称不能为空', trigger: 'blur' }],
|
||||
no: [{ required: true, message: '产品编码不能为空', trigger: 'blur' }],
|
||||
status: [{ required: true, message: '状态不能为空', trigger: 'change' }],
|
||||
categoryId: [{ required: true, message: '产品分类ID不能为空', trigger: 'blur' }],
|
||||
ownerUserId: [{ required: true, message: '负责人不能为空', trigger: 'blur' }],
|
||||
unit: [{ required: true, message: '单位不能为空', trigger: 'blur' }],
|
||||
price: [{ 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()
|
||||
formData.value.ownerUserId = userId
|
||||
// 修改时,设置数据
|
||||
if (id) {
|
||||
formLoading.value = true
|
||||
try {
|
||||
formData.value = await ProductApi.getProduct(id)
|
||||
} 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 data = formData.value as unknown as ProductApi.ProductVO
|
||||
if (formType.value === 'create') {
|
||||
await ProductApi.createProduct(data)
|
||||
message.success(t('common.createSuccess'))
|
||||
} else {
|
||||
await ProductApi.updateProduct(data)
|
||||
message.success(t('common.updateSuccess'))
|
||||
}
|
||||
dialogVisible.value = false
|
||||
// 发送操作成功的事件
|
||||
emit('success')
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 重置表单 */
|
||||
const resetForm = () => {
|
||||
formData.value = {
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
no: undefined,
|
||||
unit: undefined,
|
||||
price: undefined,
|
||||
status: undefined,
|
||||
categoryId: undefined,
|
||||
description: undefined,
|
||||
ownerUserId: undefined
|
||||
}
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
const productCategoryList = ref<any[]>([]) // 产品分类树
|
||||
const userList = ref<UserVO[]>([]) // 系统用户
|
||||
|
||||
onMounted(async () => {
|
||||
const data = await ProductCategoryApi.getProductCategoryList({})
|
||||
productCategoryList.value = handleTree(data, 'id', 'parentId')
|
||||
userList.value = await getSimpleUserList()
|
||||
})
|
||||
</script>
|
269
src/views/crm/product/index.vue
Normal file
269
src/views/crm/product/index.vue
Normal file
@ -0,0 +1,269 @@
|
||||
<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="no">
|
||||
<el-input
|
||||
v-model="queryParams.no"
|
||||
placeholder="请输入产品编码"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
/>
|
||||
</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 getBoolDictOptions(DICT_TYPE.CRM_PRODUCT_STATUS)"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="产品分类" prop="categoryId">
|
||||
<el-input
|
||||
v-model="queryParams.categoryId"
|
||||
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" @click="openForm('create')" v-hasPermi="['crm:product:create']">
|
||||
<Icon icon="ep:plus" class="mr-5px" /> 新增
|
||||
</el-button>
|
||||
<el-button
|
||||
type="success"
|
||||
plain
|
||||
@click="handleExport"
|
||||
:loading="exportLoading"
|
||||
v-hasPermi="['crm:product:export']"
|
||||
>
|
||||
<Icon icon="ep:download" 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="主键id" align="center" prop="id" />-->
|
||||
<el-table-column label="产品名称" align="center" prop="name" />
|
||||
<el-table-column label="产品编码" align="center" prop="no" />
|
||||
<el-table-column label="单位" align="center" prop="unit">
|
||||
<template #default="scope">
|
||||
<dict-tag :type="DICT_TYPE.PRODUCT_UNIT" :value="scope.row.unit" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="价格" align="center" prop="price">
|
||||
<template #default="{ row }">
|
||||
{{ fenToYuan(row.price) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" align="center" prop="status">
|
||||
<template #default="scope">
|
||||
<dict-tag :type="DICT_TYPE.CRM_PRODUCT_STATUS" :value="scope.row.status" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="产品分类" align="center" prop="categoryId">
|
||||
<template #default="{ row }">
|
||||
<span>{{ productCategoryList?.find((c) => c.id === row.categoryId)?.name }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="产品描述" align="center" prop="description" />
|
||||
<el-table-column label="负责人" align="center" prop="ownerUserId">
|
||||
<template #default="{ row }">
|
||||
<span>{{ userList?.find((c) => c.id === row.ownerUserId)?.nickname }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="创建时间"
|
||||
align="center"
|
||||
prop="createTime"
|
||||
:formatter="dateFormatter"
|
||||
width="180px"
|
||||
/>
|
||||
<el-table-column label="操作" align="center" width="160">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
v-hasPermi="['crm:product:query']"
|
||||
link
|
||||
type="primary"
|
||||
@click="openDetail(scope.row)"
|
||||
>
|
||||
详情
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
@click="openForm('update', scope.row.id)"
|
||||
v-hasPermi="['crm:product:update']"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
@click="handleDelete(scope.row.id)"
|
||||
v-hasPermi="['crm:product: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>
|
||||
|
||||
<!-- 表单弹窗:添加/修改 -->
|
||||
<ProductForm ref="formRef" @success="getList" />
|
||||
|
||||
<!-- 表单弹窗:详情 -->
|
||||
<ProductDetail ref="detailRef" @success="getList" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { DICT_TYPE, getBoolDictOptions } from '@/utils/dict'
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
import download from '@/utils/download'
|
||||
import * as ProductApi from '@/api/crm/product'
|
||||
import ProductForm from './ProductForm.vue'
|
||||
import ProductDetail from './ProductDetail.vue'
|
||||
import { fenToYuan } from '@/utils'
|
||||
import * as ProductCategoryApi from '@/api/crm/productCategory'
|
||||
import { getSimpleUserList, UserVO } from '@/api/system/user'
|
||||
|
||||
defineOptions({ name: 'CrmProduct' })
|
||||
|
||||
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,
|
||||
no: null,
|
||||
unit: null,
|
||||
price: null,
|
||||
status: null,
|
||||
categoryId: null,
|
||||
description: null,
|
||||
ownerUserId: null,
|
||||
createTime: []
|
||||
})
|
||||
const queryFormRef = ref() // 搜索的表单
|
||||
const exportLoading = ref(false) // 导出的加载中
|
||||
|
||||
/** 查询列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await ProductApi.getProductPage(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 detailRef = ref()
|
||||
const openDetail = (data: ProductApi.ProductVO) => {
|
||||
detailRef.value.open(data)
|
||||
}
|
||||
|
||||
/** 删除按钮操作 */
|
||||
const handleDelete = async (id: number) => {
|
||||
try {
|
||||
// 删除的二次确认
|
||||
await message.delConfirm()
|
||||
// 发起删除
|
||||
await ProductApi.deleteProduct(id)
|
||||
message.success(t('common.delSuccess'))
|
||||
// 刷新列表
|
||||
await getList()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/** 导出按钮操作 */
|
||||
const handleExport = async () => {
|
||||
try {
|
||||
// 导出的二次确认
|
||||
await message.exportConfirm()
|
||||
// 发起导出
|
||||
exportLoading.value = true
|
||||
const data = await ProductApi.exportProduct(queryParams)
|
||||
download.excel(data, '产品.xls')
|
||||
} catch {
|
||||
} finally {
|
||||
exportLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const productCategoryList = ref([]) // 产品分类树
|
||||
const userList = ref<UserVO[]>([]) // 系统用户
|
||||
|
||||
/** 初始化 **/
|
||||
onMounted(async () => {
|
||||
await getList()
|
||||
productCategoryList.value = await ProductCategoryApi.getProductCategoryList({})
|
||||
userList.value = await getSimpleUserList()
|
||||
})
|
||||
</script>
|
110
src/views/crm/productCategory/ProductCategoryForm.vue
Normal file
110
src/views/crm/productCategory/ProductCategoryForm.vue
Normal file
@ -0,0 +1,110 @@
|
||||
<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="父级id" prop="parentId">
|
||||
<el-select v-model="formData.parentId" placeholder="请选择上级分类">
|
||||
<el-option :key="0" label="顶级分类" :value="0" />
|
||||
<el-option
|
||||
v-for="item in productCategoryList"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="名称" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入名称" />
|
||||
</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 ProductCategoryApi from '@/api/crm/productCategory'
|
||||
|
||||
defineOptions({ name: 'CrmProductCategoryForm' })
|
||||
|
||||
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,
|
||||
parentId: undefined
|
||||
})
|
||||
const formRules = reactive({
|
||||
name: [{ required: true, message: '名称不能为空', trigger: 'blur' }],
|
||||
parentId: [{ required: true, message: '父级分类不能为空', trigger: 'blur' }]
|
||||
})
|
||||
const formRef = ref() // 表单 Ref
|
||||
const productCategoryList = ref<any[]>([]) // 产品分类树
|
||||
|
||||
/** 打开弹窗 */
|
||||
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 {
|
||||
formData.value = await ProductCategoryApi.getProductCategory(id)
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
// 获得分类树
|
||||
productCategoryList.value = await ProductCategoryApi.getProductCategoryList({ parentId: 0 })
|
||||
}
|
||||
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 data = formData.value as unknown as ProductCategoryApi.ProductCategoryVO
|
||||
if (formType.value === 'create') {
|
||||
await ProductCategoryApi.createProductCategory(data)
|
||||
message.success(t('common.createSuccess'))
|
||||
} else {
|
||||
await ProductCategoryApi.updateProductCategory(data)
|
||||
message.success(t('common.updateSuccess'))
|
||||
}
|
||||
dialogVisible.value = false
|
||||
// 发送操作成功的事件
|
||||
emit('success')
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 重置表单 */
|
||||
const resetForm = () => {
|
||||
formData.value = {
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
parentId: undefined
|
||||
}
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
</script>
|
138
src/views/crm/productCategory/index.vue
Normal file
138
src/views/crm/productCategory/index.vue
Normal file
@ -0,0 +1,138 @@
|
||||
<template>
|
||||
<!-- TODO @zange:挪到 product 下,建个 category 包,挪进去哈; -->
|
||||
<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>
|
||||
<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="['crm:product-category:create']"
|
||||
>
|
||||
<Icon icon="ep:plus" class="mr-5px" /> 新增
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 列表 -->
|
||||
<ContentWrap>
|
||||
<el-table v-loading="loading" :data="list" row-key="id" default-expand-all>
|
||||
<el-table-column label="名称" align="center" prop="name" />
|
||||
<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="openForm('update', scope.row.id)"
|
||||
v-hasPermi="['crm:product-category:update']"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
@click="handleDelete(scope.row.id)"
|
||||
v-hasPermi="['crm:product-category:delete']"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 表单弹窗:添加/修改 -->
|
||||
<ProductCategoryForm ref="formRef" @success="getList" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
import download from '@/utils/download'
|
||||
import * as ProductCategoryApi from '@/api/crm/productCategory'
|
||||
import ProductCategoryForm from './ProductCategoryForm.vue'
|
||||
import { handleTree } from '@/utils/tree'
|
||||
|
||||
defineOptions({ name: 'CrmProductCategory' })
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
const { t } = useI18n() // 国际化
|
||||
|
||||
const loading = ref(true) // 列表的加载中
|
||||
const list = ref<any[]>([]) // 列表的数据
|
||||
const queryParams = reactive({
|
||||
name: null
|
||||
})
|
||||
const queryFormRef = ref() // 搜索的表单
|
||||
|
||||
/** 查询列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await ProductCategoryApi.getProductCategoryList(queryParams)
|
||||
list.value = handleTree(data, 'id', 'parentId')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
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 ProductCategoryApi.deleteProductCategory(id)
|
||||
message.success(t('common.delSuccess'))
|
||||
// 刷新列表
|
||||
await getList()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/** 初始化 **/
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
@ -10,14 +10,14 @@
|
||||
<el-form-item label="回款编号" prop="no">
|
||||
<el-input v-model="formData.no" placeholder="请输入回款编号" />
|
||||
</el-form-item>
|
||||
<!--<el-form-item label="回款计划ID" prop="planId">
|
||||
<el-input v-model="formData.planId" placeholder="请输入回款计划ID" />
|
||||
</el-form-item>-->
|
||||
<el-form-item label="客户ID" prop="customerId">
|
||||
<el-input v-model="formData.customerId" placeholder="请输入客户ID" />
|
||||
<el-form-item label="回款计划" prop="planId">
|
||||
<el-input v-model="formData.planId" placeholder="请输入回款计划" />
|
||||
</el-form-item>
|
||||
<el-form-item label="合同ID" prop="contractId">
|
||||
<el-input v-model="formData.contractId" placeholder="请输入合同ID" />
|
||||
<el-form-item label="客户名称" prop="customerId">
|
||||
<el-input v-model="formData.customerId" placeholder="请输入客户名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="合同名称" prop="contractId">
|
||||
<el-input v-model="formData.contractId" placeholder="请输入合同名称" />
|
||||
</el-form-item>
|
||||
<!--<el-form-item label="审批状态" prop="checkStatus">
|
||||
<el-select v-model="formData.checkStatus" placeholder="请选择审批状态">
|
||||
@ -54,15 +54,22 @@
|
||||
<el-input-number v-model="formData.price" placeholder="请输入回款金额" />
|
||||
</el-form-item>
|
||||
<el-form-item label="负责人" prop="ownerUserId">
|
||||
<el-input v-model="formData.ownerUserId" placeholder="请输入负责人" />
|
||||
<el-select v-model="formData.ownerUserId" clearable placeholder="请输入负责人">
|
||||
<el-option
|
||||
v-for="item in userList"
|
||||
:key="item.id"
|
||||
:label="item.nickname"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="批次" prop="batchId">
|
||||
<el-input v-model="formData.batchId" placeholder="请输入批次" />
|
||||
<el-input-number v-model="formData.batchId" placeholder="请输入批次" />
|
||||
</el-form-item>
|
||||
<!--<el-form-item label="显示顺序" prop="sort">
|
||||
<el-input v-model="formData.sort" placeholder="请输入显示顺序" />
|
||||
</el-form-item>-->
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-form-item label="显示排序" prop="sort">
|
||||
<el-input-number v-model="formData.sort" :min="0" controls-position="right" />
|
||||
</el-form-item>
|
||||
<!--<el-form-item label="状态" prop="status">
|
||||
<el-select v-model="formData.status" placeholder="请选择状态">
|
||||
<el-option
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
||||
@ -71,7 +78,7 @@
|
||||
:value="dict.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-form-item>-->
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input type="textarea" :rows="3" v-model="formData.remark" placeholder="请输入备注" />
|
||||
</el-form-item>
|
||||
@ -85,10 +92,11 @@
|
||||
<script setup lang="ts">
|
||||
import { DICT_TYPE, getIntDictOptions, getStrDictOptions } from '@/utils/dict'
|
||||
import * as ReceivableApi from '@/api/crm/receivable'
|
||||
import * as UserApi from '@/api/system/user'
|
||||
|
||||
const { t } = useI18n() // 国际化
|
||||
const message = useMessage() // 消息弹窗
|
||||
|
||||
const userList = ref<UserApi.UserVO[]>([]) // 用户列表
|
||||
const dialogVisible = ref(false) // 弹窗的是否展示
|
||||
const dialogTitle = ref('') // 弹窗的标题
|
||||
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
|
||||
@ -112,9 +120,9 @@ const formData = ref({
|
||||
status: undefined,
|
||||
remark: undefined
|
||||
})
|
||||
const formRules = reactive({
|
||||
status: [{ required: true, message: '状态不能为空', trigger: 'change' }]
|
||||
})
|
||||
// const formRules = reactive({
|
||||
// status: [{ required: true, message: '状态不能为空', trigger: 'change' }]
|
||||
// })
|
||||
const formRef = ref() // 表单 Ref
|
||||
|
||||
/** 打开弹窗 */
|
||||
@ -132,6 +140,8 @@ const open = async (type: string, id?: number) => {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
// 获得用户列表
|
||||
userList.value = await UserApi.getSimpleUserList()
|
||||
}
|
||||
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
|
||||
|
||||
|
@ -26,19 +26,19 @@
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>-->
|
||||
<el-form-item label="客户" prop="customerId">
|
||||
<el-form-item label="客户名称" prop="customerId">
|
||||
<el-input
|
||||
v-model="queryParams.customerId"
|
||||
placeholder="请输入客户"
|
||||
placeholder="请输入客户名称"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="合同" prop="contractId">
|
||||
<el-form-item label="合同名称" prop="contractId">
|
||||
<el-input
|
||||
v-model="queryParams.contractId"
|
||||
placeholder="请输入合同"
|
||||
placeholder="请输入合同名称"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
@ -103,7 +103,7 @@
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>-->
|
||||
<el-form-item label="负责人" prop="ownerUserId">
|
||||
<!--<el-form-item label="负责人" prop="ownerUserId">
|
||||
<el-input
|
||||
v-model="queryParams.ownerUserId"
|
||||
placeholder="请输入负责人"
|
||||
@ -112,7 +112,7 @@
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<!--<el-form-item label="批次" prop="batchId">
|
||||
<el-form-item label="批次" prop="batchId">
|
||||
<el-input
|
||||
v-model="queryParams.batchId"
|
||||
placeholder="请输入批次"
|
||||
@ -227,8 +227,12 @@
|
||||
:formatter="dateFormatter"
|
||||
width="180px"
|
||||
/>
|
||||
<el-table-column label="操作" align="center" width="130px">
|
||||
<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>-->
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
|
@ -7,8 +7,24 @@
|
||||
label-width="100px"
|
||||
v-loading="formLoading"
|
||||
>
|
||||
<el-form-item label="期数" prop="indexNo">
|
||||
<el-input-number v-model="formData.indexNo" placeholder="请输入期数" />
|
||||
<el-form-item label="客户名称" prop="customerId">
|
||||
<el-input v-model="formData.customerId" placeholder="请输入客户名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="合同名称" prop="contractId">
|
||||
<el-input v-model="formData.contractId" placeholder="请输入合同名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="负责人" prop="ownerUserId">
|
||||
<el-select v-model="formData.ownerUserId" clearable placeholder="请输入负责人">
|
||||
<el-option
|
||||
v-for="item in userList"
|
||||
:key="item.id"
|
||||
:label="item.nickname"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="期数" prop="period">
|
||||
<el-input-number v-model="formData.period" placeholder="请输入期数" />
|
||||
</el-form-item>
|
||||
<!--<el-form-item label="回款ID" prop="receivableId">
|
||||
<el-input v-model="formData.receivableId" placeholder="请输入回款ID" />
|
||||
@ -58,18 +74,9 @@
|
||||
placeholder="选择提醒日期"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="客户ID" prop="customerId">
|
||||
<el-input v-model="formData.customerId" placeholder="请输入客户ID" />
|
||||
<el-form-item label="显示排序" prop="sort">
|
||||
<el-input-number v-model="formData.sort" :min="0" controls-position="right" />
|
||||
</el-form-item>
|
||||
<el-form-item label="合同ID" prop="contractId">
|
||||
<el-input v-model="formData.contractId" placeholder="请输入合同ID" />
|
||||
</el-form-item>
|
||||
<el-form-item label="负责人" prop="ownerUserId">
|
||||
<el-input v-model="formData.ownerUserId" placeholder="请输入负责人" />
|
||||
</el-form-item>
|
||||
<!--<el-form-item label="显示顺序" prop="sort">
|
||||
<el-input v-model="formData.sort" placeholder="请输入显示顺序" />
|
||||
</el-form-item>-->
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input type="textarea" :rows="3" v-model="formData.remark" placeholder="请输入备注" />
|
||||
</el-form-item>
|
||||
@ -81,19 +88,18 @@
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { DICT_TYPE, getIntDictOptions, getStrDictOptions } from '@/utils/dict'
|
||||
import * as ReceivablePlanApi from '@/api/crm/receivablePlan'
|
||||
|
||||
import * as UserApi from '@/api/system/user'
|
||||
const { t } = useI18n() // 国际化
|
||||
const message = useMessage() // 消息弹窗
|
||||
|
||||
const userList = ref<UserApi.UserVO[]>([]) // 用户列表
|
||||
const dialogVisible = ref(false) // 弹窗的是否展示
|
||||
const dialogTitle = ref('') // 弹窗的标题
|
||||
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
|
||||
const formType = ref('') // 表单的类型:create - 新增;update - 修改
|
||||
const formData = ref({
|
||||
id: undefined,
|
||||
indexNo: undefined,
|
||||
period: undefined,
|
||||
receivableId: undefined,
|
||||
status: undefined,
|
||||
checkStatus: undefined,
|
||||
@ -128,6 +134,9 @@ const open = async (type: string, id?: number) => {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获得用户列表
|
||||
userList.value = await UserApi.getSimpleUserList()
|
||||
}
|
||||
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
|
||||
|
||||
@ -161,7 +170,7 @@ const submitForm = async () => {
|
||||
const resetForm = () => {
|
||||
formData.value = {
|
||||
id: undefined,
|
||||
indexNo: undefined,
|
||||
period: undefined,
|
||||
receivableId: undefined,
|
||||
status: undefined,
|
||||
checkStatus: undefined,
|
||||
|
@ -8,10 +8,19 @@
|
||||
:inline="true"
|
||||
label-width="68px"
|
||||
>
|
||||
<el-form-item label="期数" prop="indexNo">
|
||||
<el-form-item label="客户" prop="customerId">
|
||||
<el-input
|
||||
v-model="queryParams.indexNo"
|
||||
placeholder="请输入期数"
|
||||
v-model="queryParams.customerId"
|
||||
placeholder="请输入客户"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="合同" prop="contractId">
|
||||
<el-input
|
||||
v-model="queryParams.contractId"
|
||||
placeholder="请输入合同"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
@ -67,7 +76,7 @@
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>-->
|
||||
<el-form-item label="提醒日期" prop="remindTime">
|
||||
<!--<el-form-item label="提醒日期" prop="remindTime">
|
||||
<el-date-picker
|
||||
v-model="queryParams.remindTime"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
@ -77,26 +86,8 @@
|
||||
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="客户" prop="customerId">
|
||||
<el-input
|
||||
v-model="queryParams.customerId"
|
||||
placeholder="请输入客户"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="合同" prop="contractId">
|
||||
<el-input
|
||||
v-model="queryParams.contractId"
|
||||
placeholder="请输入合同"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="负责人" prop="ownerUserId">
|
||||
</el-form-item>-->
|
||||
<!--<el-form-item label="负责人" prop="ownerUserId">
|
||||
<el-input
|
||||
v-model="queryParams.ownerUserId"
|
||||
placeholder="请输入负责人"
|
||||
@ -105,7 +96,7 @@
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<!--<el-form-item label="备注" prop="remark">
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input
|
||||
v-model="queryParams.remark"
|
||||
placeholder="请输入备注"
|
||||
@ -152,8 +143,26 @@
|
||||
<!-- 列表 -->
|
||||
<ContentWrap>
|
||||
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
|
||||
<el-table-column label="ID" align="center" prop="id" />
|
||||
<el-table-column label="期数" align="center" prop="indexNo" />
|
||||
<!--<el-table-column label="ID" align="center" prop="id" />-->
|
||||
<el-table-column label="客户名称" align="center" prop="customerId" width="150px" />
|
||||
<el-table-column label="合同名称" align="center" prop="contractId" width="150px" />
|
||||
<el-table-column label="期数" align="center" prop="period" />
|
||||
<el-table-column label="计划回款" align="center" prop="price" />
|
||||
<el-table-column
|
||||
label="计划回款日期"
|
||||
align="center"
|
||||
prop="returnTime"
|
||||
:formatter="dateFormatter2"
|
||||
width="180px"
|
||||
/>
|
||||
<el-table-column label="提前几天提醒" align="center" prop="remindDays" />
|
||||
<!--<el-table-column
|
||||
label="提醒日期"
|
||||
align="center"
|
||||
prop="remindTime"
|
||||
:formatter="dateFormatter"
|
||||
width="180px"
|
||||
/>-->
|
||||
<!--<el-table-column label="回款ID" align="center" prop="receivableId" />-->
|
||||
<el-table-column label="完成状态" align="center" prop="status">
|
||||
<template #default="scope">
|
||||
@ -166,26 +175,12 @@
|
||||
</template>
|
||||
</el-table-column>
|
||||
<!--<el-table-column label="工作流编号" align="center" prop="processInstanceId" />-->
|
||||
<el-table-column label="回款金额" align="center" prop="price" />
|
||||
<el-table-column
|
||||
label="回款日期"
|
||||
align="center"
|
||||
prop="returnTime"
|
||||
:formatter="dateFormatter2"
|
||||
width="180px"
|
||||
/>
|
||||
<el-table-column label="提前几天提醒" align="center" prop="remindDays" />
|
||||
<el-table-column
|
||||
label="提醒日期"
|
||||
align="center"
|
||||
prop="remindTime"
|
||||
:formatter="dateFormatter"
|
||||
width="180px"
|
||||
/>
|
||||
<el-table-column label="客户ID" align="center" prop="customerId" />
|
||||
<el-table-column label="合同ID" align="center" prop="contractId" />
|
||||
<el-table-column label="负责人" align="center" prop="ownerUserId" />
|
||||
<!--<el-table-column label="显示顺序" align="center" prop="sort" />-->
|
||||
<el-table-column prop="ownerUserId" label="负责人" width="120">
|
||||
<template #default="scope">
|
||||
{{ userList.find((user) => user.id === scope.row.ownerUserId)?.nickname }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="显示顺序" align="center" prop="sort" />
|
||||
<el-table-column label="备注" align="center" prop="remark" />
|
||||
<el-table-column
|
||||
label="创建时间"
|
||||
@ -234,6 +229,7 @@ import { dateFormatter, dateFormatter2 } from '@/utils/formatTime'
|
||||
import download from '@/utils/download'
|
||||
import * as ReceivablePlanApi from '@/api/crm/receivablePlan'
|
||||
import ReceivablePlanForm from './ReceivablePlanForm.vue'
|
||||
import * as UserApi from '@/api/system/user'
|
||||
|
||||
defineOptions({ name: 'ReceivablePlan' })
|
||||
|
||||
@ -243,10 +239,11 @@ const { t } = useI18n() // 国际化
|
||||
const loading = ref(true) // 列表的加载中
|
||||
const total = ref(0) // 列表的总页数
|
||||
const list = ref([]) // 列表的数据
|
||||
const userList = ref<UserApi.UserVO[]>([]) // 用户列表
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
indexNo: null,
|
||||
period: null,
|
||||
status: null,
|
||||
checkStatus: null,
|
||||
returnTime: [],
|
||||
@ -320,7 +317,9 @@ const handleExport = async () => {
|
||||
}
|
||||
|
||||
/** 初始化 **/
|
||||
onMounted(() => {
|
||||
getList()
|
||||
onMounted(async () => {
|
||||
await getList()
|
||||
// 获取用户列表
|
||||
userList.value = await UserApi.getSimpleUserList()
|
||||
})
|
||||
</script>
|
||||
|
@ -3,7 +3,7 @@
|
||||
v-if="formData && !formLoading"
|
||||
v-model="formData.property"
|
||||
:title="formData.name"
|
||||
:libs="componentLibs"
|
||||
:libs="PAGE_LIBS"
|
||||
:show-page-config="true"
|
||||
:show-navigation-bar="true"
|
||||
:show-tab-bar="false"
|
||||
@ -13,35 +13,11 @@
|
||||
<script setup lang="ts">
|
||||
import * as DiyPageApi from '@/api/mall/promotion/diy/page'
|
||||
import { useTagsViewStore } from '@/store/modules/tagsView'
|
||||
import { DiyComponentLibrary } from '@/components/DiyEditor/util'
|
||||
import { PAGE_LIBS } from '@/components/DiyEditor/util'
|
||||
|
||||
/** 装修页面表单 */
|
||||
defineOptions({ name: 'DiyPageDecorate' })
|
||||
|
||||
// 组件库
|
||||
const componentLibs = [
|
||||
{
|
||||
name: '基础组件',
|
||||
extended: true,
|
||||
components: [
|
||||
'SearchBar',
|
||||
'NoticeBar',
|
||||
'GridNavigation',
|
||||
'ListNavigation',
|
||||
'Divider',
|
||||
'TitleBar'
|
||||
]
|
||||
},
|
||||
{ name: '图文组件', extended: true, components: ['Carousel'] },
|
||||
{ name: '商品组件', extended: true, components: ['ProductCard'] },
|
||||
{
|
||||
name: '会员组件',
|
||||
extended: true,
|
||||
components: ['UserCard', 'OrderCard', 'WalletCard', 'CouponCard']
|
||||
},
|
||||
{ name: '营销组件', extended: true, components: ['Combination', 'Seckill', 'Point', 'Coupon'] }
|
||||
] as DiyComponentLibrary[]
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
|
||||
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
|
||||
|
@ -28,7 +28,7 @@
|
||||
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 } from '@/components/DiyEditor/util'
|
||||
import { DiyComponentLibrary, PAGE_LIBS } from '@/components/DiyEditor/util'
|
||||
|
||||
/** 装修模板表单 */
|
||||
defineOptions({ name: 'DiyTemplateDecorate' })
|
||||
@ -62,29 +62,6 @@ const getPageDetail = async (id: any) => {
|
||||
|
||||
// 模板组件库
|
||||
const templateLibs = [] as DiyComponentLibrary[]
|
||||
// 页面组件库
|
||||
const pageLibs = [
|
||||
{
|
||||
name: '基础组件',
|
||||
extended: true,
|
||||
components: [
|
||||
'SearchBar',
|
||||
'NoticeBar',
|
||||
'GridNavigation',
|
||||
'ListNavigation',
|
||||
'Divider',
|
||||
'TitleBar'
|
||||
]
|
||||
},
|
||||
{ name: '图文组件', extended: true, components: ['Carousel'] },
|
||||
{ name: '商品组件', extended: true, components: ['ProductCard'] },
|
||||
{
|
||||
name: '会员组件',
|
||||
extended: true,
|
||||
components: ['UserCard', 'OrderCard', 'WalletCard', 'CouponCard']
|
||||
},
|
||||
{ name: '营销组件', extended: true, components: ['Combination', 'Seckill', 'Point', 'Coupon'] }
|
||||
] as DiyComponentLibrary[]
|
||||
// 当前组件库
|
||||
const libs = ref<DiyComponentLibrary[]>(templateLibs)
|
||||
// 模板选项切换
|
||||
@ -97,7 +74,7 @@ const handleTemplateItemChange = () => {
|
||||
}
|
||||
|
||||
// 编辑页面
|
||||
libs.value = pageLibs
|
||||
libs.value = PAGE_LIBS
|
||||
currentFormData.value = formData.value!.pages.find(
|
||||
(page: DiyPageApi.DiyPageVO) => page.name === templateItems[selectedTemplateItem.value].name
|
||||
)
|
||||
|
196
src/views/mall/promotion/rewardActivity/RewardForm.vue
Normal file
196
src/views/mall/promotion/rewardActivity/RewardForm.vue
Normal file
@ -0,0 +1,196 @@
|
||||
<template>
|
||||
<Dialog :title="dialogTitle" v-model="dialogVisible">
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-width="80px"
|
||||
v-loading="formLoading"
|
||||
>
|
||||
<el-form-item label="活动名称" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入活动名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="活动时间" prop="startAndEndTime">
|
||||
<el-date-picker
|
||||
v-model="formData.startAndEndTime"
|
||||
type="datetimerange"
|
||||
range-separator="-"
|
||||
:start-placeholder="t('common.startTimeText')"
|
||||
:end-placeholder="t('common.endTimeText')"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="条件类型" prop="conditionType">
|
||||
<el-radio-group v-model="formData.conditionType">
|
||||
<el-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.PROMOTION_CONDITION_TYPE)"
|
||||
:key="dict.value"
|
||||
:label="parseInt(dict.value)"
|
||||
>{{ dict.label }}</el-radio
|
||||
>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="优惠设置">
|
||||
<!-- TODO 待实现!这个实现下哈 -->
|
||||
</el-form-item>
|
||||
<el-form-item label="活动商品" prop="productScope">
|
||||
<el-radio-group v-model="formData.productScope">
|
||||
<el-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.PROMOTION_PRODUCT_SCOPE)"
|
||||
:key="dict.value"
|
||||
:label="parseInt(dict.value)"
|
||||
>{{ dict.label }}</el-radio
|
||||
>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<!-- TODO:活动商品的开发,可以参考优惠劵的,已经搞好啦; -->
|
||||
<el-form-item
|
||||
v-if="formData.productScope === PromotionProductScopeEnum.SPU.scope"
|
||||
prop="productSpuIds"
|
||||
>
|
||||
<el-select
|
||||
v-model="formData.productSpuIds"
|
||||
placeholder="请选择活动商品"
|
||||
clearable
|
||||
size="small"
|
||||
multiple
|
||||
filterable
|
||||
style="width: 400px"
|
||||
>
|
||||
<el-option v-for="item in productSpus" :key="item.id" :label="item.name" :value="item.id">
|
||||
<span style="float: left">{{ item.name }}</span>
|
||||
<span style="float: right; font-size: 13px; color: #8492a6"
|
||||
>¥{{ (item.price / 100.0).toFixed(2) }}</span
|
||||
>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注" prop="remark">
|
||||
<el-input v-model="formData.remark" placeholder="请输入备注" />
|
||||
</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 lang="ts" setup>
|
||||
import { getSpuSimpleList } from '@/api/mall/product/spu'
|
||||
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
||||
import { CommonStatusEnum } from '@/utils/constants'
|
||||
import * as ProductBrandApi from '@/api/mall/product/brand'
|
||||
import {
|
||||
PromotionConditionTypeEnum,
|
||||
PromotionProductScopeEnum,
|
||||
PromotionActivityStatusEnum
|
||||
} from '@/utils/constants'
|
||||
// 商品数据
|
||||
const productSpus = ref<any[]>([])
|
||||
|
||||
/** 初始化 **/
|
||||
onMounted(() => {
|
||||
getSpuSimpleList().then((response) => {
|
||||
productSpus.value = response
|
||||
})
|
||||
})
|
||||
defineOptions({ name: 'ProductBrandForm' })
|
||||
|
||||
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,
|
||||
startAndEndTime: undefined,
|
||||
startTime: undefined,
|
||||
endTime: undefined,
|
||||
conditionType: PromotionConditionTypeEnum.PRICE.type,
|
||||
remark: undefined,
|
||||
productScope: PromotionProductScopeEnum.ALL.scope,
|
||||
productSpuIds: undefined,
|
||||
rules: undefined
|
||||
})
|
||||
const formRules = reactive({
|
||||
name: [{ required: true, message: '活动名称不能为空', trigger: 'blur' }],
|
||||
startAndEndTime: [{ required: true, message: '活动时间不能为空', trigger: 'blur' }],
|
||||
conditionType: [{ required: true, message: '条件类型不能为空', trigger: 'change' }],
|
||||
productScope: [{ required: true, message: '商品范围不能为空', trigger: 'blur' }],
|
||||
productSpuIds: [{ 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 {
|
||||
// formData.value = await ProductBrandApi.getBrand(id)
|
||||
formData.value = {
|
||||
conditionType: 10,
|
||||
description: '',
|
||||
id: undefined,
|
||||
name: '测试活动',
|
||||
picUrl: '',
|
||||
productScope: 2,
|
||||
productSpuIds: [634],
|
||||
remark: '测试备注',
|
||||
startAndEndTime: [new Date(), new Date('2023-12-31')],
|
||||
status: 0
|
||||
}
|
||||
} 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
|
||||
console.log(formData.value)
|
||||
message.success('已在控制台打印数据')
|
||||
return
|
||||
// 提交请求
|
||||
formLoading.value = true
|
||||
try {
|
||||
const data = formData.value as ProductBrandApi.BrandVO
|
||||
if (formType.value === 'create') {
|
||||
await ProductBrandApi.createBrand(data)
|
||||
message.success(t('common.createSuccess'))
|
||||
} else {
|
||||
await ProductBrandApi.updateBrand(data)
|
||||
message.success(t('common.updateSuccess'))
|
||||
}
|
||||
dialogVisible.value = false
|
||||
// 发送操作成功的事件
|
||||
emit('success')
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 重置表单 */
|
||||
const resetForm = () => {
|
||||
formData.value = {
|
||||
id: undefined,
|
||||
name: '',
|
||||
picUrl: '',
|
||||
status: CommonStatusEnum.ENABLE,
|
||||
description: ''
|
||||
}
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
</script>
|
213
src/views/mall/promotion/rewardActivity/index.vue
Normal file
213
src/views/mall/promotion/rewardActivity/index.vue
Normal file
@ -0,0 +1,213 @@
|
||||
<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="status">
|
||||
<el-select
|
||||
v-model="queryParams.status"
|
||||
placeholder="请选择活动状态"
|
||||
clearable
|
||||
class="!w-240px"
|
||||
>
|
||||
<el-option
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.PROMOTION_ACTIVITY_STATUS)"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
/>
|
||||
</el-select>
|
||||
</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="['product:brand:create']"
|
||||
>
|
||||
<Icon icon="ep:plus" class="mr-5px" /> 新增
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 列表 -->
|
||||
<ContentWrap>
|
||||
<el-table v-loading="loading" :data="list" row-key="id" default-expand-all>
|
||||
<el-table-column label="活动名称" prop="name" />
|
||||
<el-table-column
|
||||
label="活动开始时间"
|
||||
align="center"
|
||||
prop="sort[0]"
|
||||
:formatter="dateFormatter"
|
||||
/>
|
||||
<el-table-column
|
||||
label="活动结束时间"
|
||||
align="center"
|
||||
prop="sort[1]"
|
||||
:formatter="dateFormatter"
|
||||
/>
|
||||
<el-table-column label="状态" align="center" prop="status">
|
||||
<template #default="scope">
|
||||
<dict-tag :type="DICT_TYPE.PROMOTION_ACTIVITY_STATUS" :value="scope.row.status" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
label="创建时间"
|
||||
align="center"
|
||||
prop="createTime"
|
||||
width="180"
|
||||
:formatter="dateFormatter"
|
||||
/>
|
||||
<el-table-column label="操作" align="center">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
@click="openForm('update', scope.row.id)"
|
||||
v-hasPermi="['product:brand:update']"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
@click="handleDelete(scope.row.id)"
|
||||
v-hasPermi="['product:brand: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>
|
||||
|
||||
<!-- 表单弹窗:添加/修改 -->
|
||||
<RewardForm ref="formRef" @success="getList" />
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
import * as ProductBrandApi from '@/api/mall/product/brand'
|
||||
import RewardForm from './RewardForm.vue'
|
||||
|
||||
defineOptions({ name: 'PromotionRewardActivity' })
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
const { t } = useI18n() // 国际化
|
||||
|
||||
const loading = ref(true) // 列表的加载中
|
||||
const total = ref(0) // 列表的总页数
|
||||
const list = ref<any[]>([]) // 列表的数据
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
name: undefined,
|
||||
status: undefined,
|
||||
createTime: []
|
||||
})
|
||||
const queryFormRef = ref() // 搜索的表单
|
||||
|
||||
/** 查询列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
// const data = await ProductBrandApi.getBrandParam(queryParams)
|
||||
const data = {
|
||||
list: [
|
||||
{
|
||||
createTime: 1693463998000,
|
||||
description: '',
|
||||
id: 3,
|
||||
name: '索尼',
|
||||
picUrl:
|
||||
'http://127.0.0.1:48080/admin-api/infra/file/4/get/f5b7a536306cd1180a42a2211a8212dc23de6b949d30c30d036caa063042f928.png',
|
||||
sort: [+new Date(), +new Date('2023-12-31')],
|
||||
status: 10
|
||||
}
|
||||
],
|
||||
total: 1
|
||||
}
|
||||
list.value = data.list
|
||||
total.value = data.total
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
console.log(queryParams)
|
||||
message.success('已打印搜索参数')
|
||||
return
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
message.success('重置查询表单获取数据')
|
||||
return
|
||||
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()
|
||||
message.success('您以确认删除')
|
||||
return
|
||||
// 发起删除
|
||||
await ProductBrandApi.deleteBrand(id)
|
||||
message.success(t('common.delSuccess'))
|
||||
// 刷新列表
|
||||
await getList()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/** 初始化 **/
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
@ -31,7 +31,7 @@
|
||||
:props="defaultProps2"
|
||||
class="w-1/1"
|
||||
clearable
|
||||
placeholder="请选择商品分类"
|
||||
placeholder="请选择地区"
|
||||
filterable
|
||||
collapse-tags
|
||||
/>
|
||||
|
@ -34,7 +34,7 @@ defineOptions({ name: 'SystemLoginLogDetail' })
|
||||
|
||||
const dialogVisible = ref(false) // 弹窗的是否展示
|
||||
const detailLoading = ref(false) // 表单的加载中
|
||||
const detailData = ref() // 详情数据
|
||||
const detailData = ref({}) // 详情数据
|
||||
|
||||
/** 打开弹窗 */
|
||||
const open = async (data: LoginLogApi.LoginLogVO) => {
|
||||
|
@ -38,7 +38,7 @@
|
||||
<template #label>
|
||||
<Tooltip
|
||||
message="访问的路由地址,如:`user`。如需外网地址时,则以 `http(s)://` 开头"
|
||||
titel="路由地址"
|
||||
title="路由地址"
|
||||
/>
|
||||
</template>
|
||||
<el-input v-model="formData.path" clearable placeholder="请输入路由地址" />
|
||||
@ -53,7 +53,7 @@
|
||||
<template #label>
|
||||
<Tooltip
|
||||
message="Controller 方法上的权限字符,如:@PreAuthorize(`@ss.hasPermission('system:user:list')`)"
|
||||
titel="权限标识"
|
||||
title="权限标识"
|
||||
/>
|
||||
</template>
|
||||
<el-input v-model="formData.permission" clearable placeholder="请输入权限标识" />
|
||||
@ -74,7 +74,7 @@
|
||||
</el-form-item>
|
||||
<el-form-item v-if="formData.type !== 3" label="显示状态" prop="visible">
|
||||
<template #label>
|
||||
<Tooltip message="选择隐藏时,路由将不会出现在侧边栏,但仍然可以访问" titel="显示状态" />
|
||||
<Tooltip message="选择隐藏时,路由将不会出现在侧边栏,但仍然可以访问" title="显示状态" />
|
||||
</template>
|
||||
<el-radio-group v-model="formData.visible">
|
||||
<el-radio key="true" :label="true" border>显示</el-radio>
|
||||
@ -85,7 +85,7 @@
|
||||
<template #label>
|
||||
<Tooltip
|
||||
message="选择不是时,当该菜单只有一个子菜单时,不展示自己,直接展示子菜单"
|
||||
titel="总是显示"
|
||||
title="总是显示"
|
||||
/>
|
||||
</template>
|
||||
<el-radio-group v-model="formData.alwaysShow">
|
||||
@ -97,7 +97,7 @@
|
||||
<template #label>
|
||||
<Tooltip
|
||||
message="选择缓存时,则会被 `keep-alive` 缓存,必须填写「组件名称」字段"
|
||||
titel="缓存状态"
|
||||
title="缓存状态"
|
||||
/>
|
||||
</template>
|
||||
<el-radio-group v-model="formData.keepAlive">
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<doc-alert title="功能权限" url="https://doc.iocoder.cn/resource-permission" />
|
||||
<doc-alert title="菜单路由" url="https://doc.iocoder.cn/vue2/route/" />
|
||||
<doc-alert title="菜单路由" url="https://doc.iocoder.cn/vue3/route/" />
|
||||
|
||||
<!-- 搜索工作栏 -->
|
||||
<ContentWrap>
|
||||
|
154
src/views/system/social/client/SocialClientForm.vue
Normal file
154
src/views/system/social/client/SocialClientForm.vue
Normal file
@ -0,0 +1,154 @@
|
||||
<template>
|
||||
<Dialog v-model="dialogVisible" :title="dialogTitle">
|
||||
<el-form
|
||||
ref="formRef"
|
||||
v-loading="formLoading"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-width="120px"
|
||||
>
|
||||
<el-form-item label="应用名" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入应用名" />
|
||||
</el-form-item>
|
||||
<el-form-item label="社交平台" prop="socialType">
|
||||
<el-radio-group v-model="formData.socialType">
|
||||
<el-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_SOCIAL_TYPE)"
|
||||
:key="dict.value"
|
||||
:label="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="用户类型" prop="userType">
|
||||
<el-radio-group v-model="formData.userType">
|
||||
<el-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.USER_TYPE)"
|
||||
:key="dict.value"
|
||||
:label="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="客户端编号" prop="clientId">
|
||||
<el-input v-model="formData.clientId" placeholder="请输入客户端编号,对应各平台的appKey" />
|
||||
</el-form-item>
|
||||
<el-form-item label="客户端密钥" prop="clientSecret">
|
||||
<el-input
|
||||
v-model="formData.clientSecret"
|
||||
placeholder="请输入客户端密钥,对应各平台的appSecret"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="agentId" prop="agentId" v-if="formData!.socialType === 30">
|
||||
<el-input v-model="formData.agentId" placeholder="授权方的网页应用 ID,有则填" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="formData.status">
|
||||
<el-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
||||
:key="dict.value"
|
||||
:label="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button :disabled="formLoading" type="primary" @click="submitForm">确 定</el-button>
|
||||
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
||||
import * as SocialClientApi from '@/api/system/social/client'
|
||||
|
||||
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,
|
||||
socialType: undefined,
|
||||
userType: undefined,
|
||||
clientId: undefined,
|
||||
clientSecret: undefined,
|
||||
agentId: undefined,
|
||||
status: 0
|
||||
})
|
||||
const formRules = reactive({
|
||||
name: [{ required: true, message: '应用名不能为空', trigger: 'blur' }],
|
||||
socialType: [{ required: true, message: '社交平台不能为空', trigger: 'blur' }],
|
||||
userType: [{ required: true, message: '用户类型不能为空', trigger: 'blur' }],
|
||||
clientId: [{ required: true, message: '客户端编号不能为空', trigger: 'blur' }],
|
||||
clientSecret: [{ required: true, message: '客户端密钥不能为空', trigger: 'blur' }],
|
||||
status: [{ 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 {
|
||||
formData.value = await SocialClientApi.getSocialClient(id)
|
||||
} 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 data = formData.value as unknown as SocialClientApi.SocialClientVO
|
||||
if (formType.value === 'create') {
|
||||
await SocialClientApi.createSocialClient(data)
|
||||
message.success(t('common.createSuccess'))
|
||||
} else {
|
||||
await SocialClientApi.updateSocialClient(data)
|
||||
message.success(t('common.updateSuccess'))
|
||||
}
|
||||
dialogVisible.value = false
|
||||
// 发送操作成功的事件
|
||||
emit('success')
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 重置表单 */
|
||||
const resetForm = () => {
|
||||
formData.value = {
|
||||
id: undefined,
|
||||
name: undefined,
|
||||
socialType: undefined,
|
||||
userType: undefined,
|
||||
clientId: undefined,
|
||||
clientSecret: undefined,
|
||||
agentId: undefined,
|
||||
status: 0
|
||||
}
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
</script>
|
227
src/views/system/social/client/index.vue
Normal file
227
src/views/system/social/client/index.vue
Normal file
@ -0,0 +1,227 @@
|
||||
<template>
|
||||
<doc-alert title="三方登录" url="https://doc.iocoder.cn/social-user/" />
|
||||
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
ref="queryFormRef"
|
||||
:inline="true"
|
||||
:model="queryParams"
|
||||
class="-mb-15px"
|
||||
label-width="130px"
|
||||
>
|
||||
<el-form-item label="应用名" prop="name">
|
||||
<el-input
|
||||
v-model="queryParams.name"
|
||||
class="!w-240px"
|
||||
clearable
|
||||
placeholder="请输入应用名"
|
||||
@keyup.enter="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="社交平台" prop="socialType">
|
||||
<el-select
|
||||
v-model="queryParams.socialType"
|
||||
class="!w-240px"
|
||||
clearable
|
||||
placeholder="请选择社交平台"
|
||||
>
|
||||
<el-option
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_SOCIAL_TYPE)"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="用户类型" prop="userType">
|
||||
<el-select
|
||||
v-model="queryParams.userType"
|
||||
class="!w-240px"
|
||||
clearable
|
||||
placeholder="请选择用户类型"
|
||||
>
|
||||
<el-option
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.USER_TYPE)"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="客户端编号" prop="clientId">
|
||||
<el-input
|
||||
v-model="queryParams.clientId"
|
||||
class="!w-240px"
|
||||
clearable
|
||||
placeholder="请输入客户端编号"
|
||||
@keyup.enter="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-select v-model="queryParams.status" class="!w-240px" clearable placeholder="请选择状态">
|
||||
<el-option
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="handleQuery">
|
||||
<Icon class="mr-5px" icon="ep:search" />
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="resetQuery">
|
||||
<Icon class="mr-5px" icon="ep:refresh" />
|
||||
重置
|
||||
</el-button>
|
||||
<el-button
|
||||
v-hasPermi="['system:social-client:create']"
|
||||
plain
|
||||
type="primary"
|
||||
@click="openForm('create')"
|
||||
>
|
||||
<Icon class="mr-5px" icon="ep:plus" />
|
||||
新增
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 列表 -->
|
||||
<ContentWrap>
|
||||
<el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
|
||||
<el-table-column align="center" label="编号" prop="id" />
|
||||
<el-table-column align="center" label="应用名" prop="name" />
|
||||
<el-table-column align="center" label="社交平台" prop="socialType">
|
||||
<template #default="scope">
|
||||
<dict-tag :type="DICT_TYPE.SYSTEM_SOCIAL_TYPE" :value="scope.row.socialType" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="用户类型" prop="userType">
|
||||
<template #default="scope">
|
||||
<dict-tag :type="DICT_TYPE.USER_TYPE" :value="scope.row.userType" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="客户端编号" prop="clientId" width="180px" />
|
||||
<el-table-column align="center" label="状态" prop="status">
|
||||
<template #default="scope">
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:formatter="dateFormatter"
|
||||
align="center"
|
||||
label="创建时间"
|
||||
prop="createTime"
|
||||
width="180px"
|
||||
/>
|
||||
<el-table-column align="center" label="操作">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
v-hasPermi="['system:social-client:update']"
|
||||
link
|
||||
type="primary"
|
||||
@click="openForm('update', scope.row.id)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
v-hasPermi="['system:social-client:delete']"
|
||||
link
|
||||
type="danger"
|
||||
@click="handleDelete(scope.row.id)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!-- 分页 -->
|
||||
<Pagination
|
||||
v-model:limit="queryParams.pageSize"
|
||||
v-model:page="queryParams.pageNo"
|
||||
:total="total"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 表单弹窗:添加/修改 -->
|
||||
<SocialClientForm ref="formRef" @success="getList" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
import * as SocialClientApi from '@/api/system/social/client'
|
||||
import SocialClientForm from './SocialClientForm.vue'
|
||||
|
||||
defineOptions({ name: 'SocialClient' })
|
||||
|
||||
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,
|
||||
socialType: null,
|
||||
userType: null,
|
||||
clientId: null,
|
||||
status: null
|
||||
})
|
||||
const queryFormRef = ref() // 搜索的表单
|
||||
|
||||
/** 查询列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await SocialClientApi.getSocialClientPage(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 SocialClientApi.deleteSocialClient(id)
|
||||
message.success(t('common.delSuccess'))
|
||||
// 刷新列表
|
||||
await getList()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/** 初始化 **/
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
60
src/views/system/social/user/SocialUserDetail.vue
Normal file
60
src/views/system/social/user/SocialUserDetail.vue
Normal file
@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<Dialog v-model="dialogVisible" title="详情" width="800">
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="社交平台" min-width="160">
|
||||
<dict-tag :type="DICT_TYPE.SYSTEM_SOCIAL_TYPE" :value="detailData.type" />
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="用户昵称" min-width="120">
|
||||
{{ detailData.nickname }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions label="用户头像" min-width="120">
|
||||
<el-image :src="detailData.avatar" class="h-30px w-30px" />
|
||||
</el-descriptions>
|
||||
<el-descriptions-item label="社交 token" min-width="120">
|
||||
{{ detailData.token }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="原始 Token 数据" min-width="120">
|
||||
<el-input
|
||||
v-model="detailData.rawTokenInfo"
|
||||
:autosize="{ maxRows: 20 }"
|
||||
:readonly="true"
|
||||
type="textarea"
|
||||
/>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="原始 User 数据" min-width="120">
|
||||
<el-input
|
||||
v-model="detailData.rawUserInfo"
|
||||
:autosize="{ maxRows: 20 }"
|
||||
:readonly="true"
|
||||
type="textarea"
|
||||
/>
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="最后一次的认证 code" min-width="120">
|
||||
{{ detailData.code }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="最后一次的认证 state" min-width="120">
|
||||
{{ detailData.state }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { DICT_TYPE } from '@/utils/dict'
|
||||
import * as SocialUserApi from '@/api/system/social/user'
|
||||
|
||||
const dialogVisible = ref(false) // 弹窗的是否展示
|
||||
const detailLoading = ref(false) // 表单的加载中
|
||||
const detailData = ref({}) // 详情数据
|
||||
|
||||
/** 打开弹窗 */
|
||||
const open = async (id: number) => {
|
||||
dialogVisible.value = true
|
||||
// 设置数据
|
||||
try {
|
||||
detailData.value = await SocialUserApi.getSocialUser(id)
|
||||
} finally {
|
||||
detailLoading.value = false
|
||||
}
|
||||
}
|
||||
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
|
||||
</script>
|
190
src/views/system/social/user/index.vue
Normal file
190
src/views/system/social/user/index.vue
Normal file
@ -0,0 +1,190 @@
|
||||
<template>
|
||||
<doc-alert title="三方登录" url="https://doc.iocoder.cn/social-user/" />
|
||||
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
ref="queryFormRef"
|
||||
:inline="true"
|
||||
:model="queryParams"
|
||||
class="-mb-15px"
|
||||
label-width="120px"
|
||||
>
|
||||
<el-form-item label="社交平台" prop="type">
|
||||
<el-select
|
||||
v-model="queryParams.type"
|
||||
class="!w-240px"
|
||||
clearable
|
||||
placeholder="请选择社交平台"
|
||||
>
|
||||
<el-option
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_SOCIAL_TYPE)"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="用户昵称" prop="nickname">
|
||||
<el-input
|
||||
v-model="queryParams.nickname"
|
||||
class="!w-240px"
|
||||
clearable
|
||||
placeholder="请输入用户昵称"
|
||||
@keyup.enter="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="社交 openid" prop="openid">
|
||||
<el-input
|
||||
v-model="queryParams.openid"
|
||||
class="!w-240px"
|
||||
clearable
|
||||
placeholder="请输入社交 openid"
|
||||
@keyup.enter="handleQuery"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="创建时间" prop="createTime">
|
||||
<el-date-picker
|
||||
v-model="queryParams.createTime"
|
||||
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
|
||||
class="!w-240px"
|
||||
end-placeholder="结束日期"
|
||||
start-placeholder="开始日期"
|
||||
type="daterange"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="handleQuery">
|
||||
<Icon class="mr-5px" icon="ep:search" />
|
||||
搜索
|
||||
</el-button>
|
||||
<el-button @click="resetQuery">
|
||||
<Icon class="mr-5px" icon="ep:refresh" />
|
||||
重置
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 列表 -->
|
||||
<ContentWrap>
|
||||
<el-table v-loading="loading" :data="list" :show-overflow-tooltip="true" :stripe="true">
|
||||
<el-table-column align="center" label="社交平台" prop="type">
|
||||
<template #default="scope">
|
||||
<dict-tag :type="DICT_TYPE.SYSTEM_SOCIAL_TYPE" :value="scope.row.type" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column align="center" label="社交 openid" prop="openid" />
|
||||
<el-table-column align="center" label="用户昵称" prop="nickname" />
|
||||
<el-table-column align="center" label="用户头像" prop="avatar">
|
||||
<template #default="{ row }">
|
||||
<el-image :src="row.avatar" class="h-30px w-30px" @click="imagePreview(row.avatar)" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:formatter="dateFormatter"
|
||||
align="center"
|
||||
label="创建时间"
|
||||
prop="createTime"
|
||||
width="180px"
|
||||
/>
|
||||
<el-table-column
|
||||
:formatter="dateFormatter"
|
||||
align="center"
|
||||
label="更新时间"
|
||||
prop="updateTime"
|
||||
width="180px"
|
||||
/>
|
||||
<el-table-column align="center" fixed="right" label="操作">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
v-hasPermi="['system:social-user:query']"
|
||||
link
|
||||
type="primary"
|
||||
@click="openDetail(scope.row.id)"
|
||||
>
|
||||
详情
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!-- 分页 -->
|
||||
<Pagination
|
||||
v-model:limit="queryParams.pageSize"
|
||||
v-model:page="queryParams.pageNo"
|
||||
:total="total"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 表单弹窗:详情 -->
|
||||
<SocialUserDetail ref="detailRef" />
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
import * as SocialUserApi from '@/api/system/social/user'
|
||||
import SocialUserDetail from './SocialUserDetail.vue'
|
||||
import { createImageViewer } from '@/components/ImageViewer'
|
||||
|
||||
defineOptions({ name: 'SocialUser' })
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
const { t } = useI18n() // 国际化
|
||||
|
||||
const loading = ref(true) // 列表的加载中
|
||||
const total = ref(0) // 列表的总页数
|
||||
const list = ref([]) // 列表的数据
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
type: undefined,
|
||||
openid: undefined,
|
||||
nickname: undefined,
|
||||
createTime: []
|
||||
})
|
||||
const queryFormRef = ref() // 搜索的表单
|
||||
|
||||
/** 查询列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await SocialUserApi.getSocialUserPage(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 imagePreview = (imgUrl: string) => {
|
||||
createImageViewer({
|
||||
urlList: [imgUrl]
|
||||
})
|
||||
}
|
||||
|
||||
/** 详情操作 */
|
||||
const detailRef = ref()
|
||||
const openDetail = (id: number) => {
|
||||
detailRef.value.open(id)
|
||||
}
|
||||
|
||||
/** 初始化 **/
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
@ -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" />
|
||||
|
@ -27,7 +27,6 @@
|
||||
"@intlify/unplugin-vue-i18n/types",
|
||||
"vite/client",
|
||||
"element-plus/global",
|
||||
"@types/intro.js",
|
||||
"@types/qrcode",
|
||||
"vite-plugin-svg-icons/client"
|
||||
],
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { defineConfig, toEscapedSelector as e, presetUno } from 'unocss'
|
||||
import transformerVariantGroup from '@unocss/transformer-variant-group'
|
||||
// import transformerVariantGroup from '@unocss/transformer-variant-group'
|
||||
|
||||
export default defineConfig({
|
||||
// ...UnoCSS options
|
||||
@ -101,5 +101,8 @@ ${selector}:after {
|
||||
]
|
||||
],
|
||||
presets: [presetUno({ dark: 'class', attributify: false })],
|
||||
transformers: [transformerVariantGroup()]
|
||||
// transformers: [transformerVariantGroup()],
|
||||
shortcuts: {
|
||||
'wh-full': 'w-full h-full'
|
||||
}
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user