diff --git a/.vscode/settings.json b/.vscode/settings.json index 1d3f0aa8..34310929 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -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", diff --git a/package.json b/package.json index 21a731a8..fb0f23ad 100644 --- a/package.json +++ b/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": { diff --git a/src/api/crm/product/index.ts b/src/api/crm/product/index.ts new file mode 100644 index 00000000..cb1ddcda --- /dev/null +++ b/src/api/crm/product/index.ts @@ -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 }) +} diff --git a/src/api/crm/productCategory/index.ts b/src/api/crm/productCategory/index.ts new file mode 100644 index 00000000..6341d1bc --- /dev/null +++ b/src/api/crm/productCategory/index.ts @@ -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 }) +} diff --git a/src/api/crm/receivablePlan/index.ts b/src/api/crm/receivablePlan/index.ts index 178b21b5..f80f0572 100644 --- a/src/api/crm/receivablePlan/index.ts +++ b/src/api/crm/receivablePlan/index.ts @@ -2,7 +2,7 @@ import request from '@/config/axios' export interface ReceivablePlanVO { id: number - indexNo: number + period: number receivableId: number status: number checkStatus: string diff --git a/src/api/login/index.ts b/src/api/login/index.ts index 1ffb38d6..ef86563b 100644 --- a/src/api/login/index.ts +++ b/src/api/login/index.ts @@ -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' }) diff --git a/src/api/system/social/client/index.ts b/src/api/system/social/client/index.ts new file mode 100644 index 00000000..bf13ab49 --- /dev/null +++ b/src/api/system/social/client/index.ts @@ -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 }) +} diff --git a/src/api/system/social/user/index.ts b/src/api/system/social/user/index.ts new file mode 100644 index 00000000..f11231b7 --- /dev/null +++ b/src/api/system/social/user/index.ts @@ -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 }) +} diff --git a/src/components/Card/src/CardTitle.vue b/src/components/Card/src/CardTitle.vue index 5b122f49..76a83564 100644 --- a/src/components/Card/src/CardTitle.vue +++ b/src/components/Card/src/CardTitle.vue @@ -3,7 +3,7 @@ defineComponent({ name: 'CardTitle' }) -const { title } = defineProps({ +defineProps({ title: { type: String, required: true diff --git a/src/components/ConfigGlobal/src/ConfigGlobal.vue b/src/components/ConfigGlobal/src/ConfigGlobal.vue index a0873967..5bd90cf1 100644 --- a/src/components/ConfigGlobal/src/ConfigGlobal.vue +++ b/src/components/ConfigGlobal/src/ConfigGlobal.vue @@ -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> diff --git a/src/components/DiyEditor/components/ComponentContainer.vue b/src/components/DiyEditor/components/ComponentContainer.vue new file mode 100644 index 00000000..c013362f --- /dev/null +++ b/src/components/DiyEditor/components/ComponentContainer.vue @@ -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> diff --git a/src/components/DiyEditor/components/ComponentContainerProperty.vue b/src/components/DiyEditor/components/ComponentContainerProperty.vue new file mode 100644 index 00000000..5c5ed9a0 --- /dev/null +++ b/src/components/DiyEditor/components/ComponentContainerProperty.vue @@ -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> diff --git a/src/components/DiyEditor/components/ComponentLibrary.vue b/src/components/DiyEditor/components/ComponentLibrary.vue index 8e918fa9..2bcd81fd 100644 --- a/src/components/DiyEditor/components/ComponentLibrary.vue +++ b/src/components/DiyEditor/components/ComponentLibrary.vue @@ -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 }" diff --git a/src/components/DiyEditor/components/mobile/Carousel/config.ts b/src/components/DiyEditor/components/mobile/Carousel/config.ts index 6c790186..3e74a511 100644 --- a/src/components/DiyEditor/components/mobile/Carousel/config.ts +++ b/src/components/DiyEditor/components/mobile/Carousel/config.ts @@ -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> diff --git a/src/components/DiyEditor/components/mobile/Carousel/index.vue b/src/components/DiyEditor/components/mobile/Carousel/index.vue index e9a0ab39..940a9a56 100644 --- a/src/components/DiyEditor/components/mobile/Carousel/index.vue +++ b/src/components/DiyEditor/components/mobile/Carousel/index.vue @@ -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> diff --git a/src/components/DiyEditor/components/mobile/Carousel/property.vue b/src/components/DiyEditor/components/mobile/Carousel/property.vue index acaee35f..700e0005 100644 --- a/src/components/DiyEditor/components/mobile/Carousel/property.vue +++ b/src/components/DiyEditor/components/mobile/Carousel/property.vue @@ -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> diff --git a/src/components/DiyEditor/components/mobile/ImageBar/config.ts b/src/components/DiyEditor/components/mobile/ImageBar/config.ts new file mode 100644 index 00000000..68edf728 --- /dev/null +++ b/src/components/DiyEditor/components/mobile/ImageBar/config.ts @@ -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> diff --git a/src/components/DiyEditor/components/mobile/ImageBar/index.vue b/src/components/DiyEditor/components/mobile/ImageBar/index.vue new file mode 100644 index 00000000..6f70c52d --- /dev/null +++ b/src/components/DiyEditor/components/mobile/ImageBar/index.vue @@ -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> diff --git a/src/components/DiyEditor/components/mobile/ImageBar/property.vue b/src/components/DiyEditor/components/mobile/ImageBar/property.vue new file mode 100644 index 00000000..58af1bc8 --- /dev/null +++ b/src/components/DiyEditor/components/mobile/ImageBar/property.vue @@ -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> diff --git a/src/components/DiyEditor/components/mobile/NavigationBar/config.ts b/src/components/DiyEditor/components/mobile/NavigationBar/config.ts index b250f5f1..f722d525 100644 --- a/src/components/DiyEditor/components/mobile/NavigationBar/config.ts +++ b/src/components/DiyEditor/components/mobile/NavigationBar/config.ts @@ -29,7 +29,7 @@ export const component = { title: '页面标题', description: '', navBarHeight: 35, - backgroundColor: '#f5f5f5', + backgroundColor: '#fff', backgroundImage: '', styleType: 'default', alwaysShow: true, diff --git a/src/components/DiyEditor/components/mobile/SearchBar/config.ts b/src/components/DiyEditor/components/mobile/SearchBar/config.ts index 1241748d..ef47b27c 100644 --- a/src/components/DiyEditor/components/mobile/SearchBar/config.ts +++ b/src/components/DiyEditor/components/mobile/SearchBar/config.ts @@ -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> diff --git a/src/components/DiyEditor/components/mobile/SearchBar/index.vue b/src/components/DiyEditor/components/mobile/SearchBar/index.vue index e120405a..618c918b 100644 --- a/src/components/DiyEditor/components/mobile/SearchBar/index.vue +++ b/src/components/DiyEditor/components/mobile/SearchBar/index.vue @@ -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; diff --git a/src/components/DiyEditor/components/mobile/SearchBar/property.vue b/src/components/DiyEditor/components/mobile/SearchBar/property.vue index 9123ebe5..d121a1e3 100644 --- a/src/components/DiyEditor/components/mobile/SearchBar/property.vue +++ b/src/components/DiyEditor/components/mobile/SearchBar/property.vue @@ -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"> diff --git a/src/components/DiyEditor/components/mobile/VideoPlayer/config.ts b/src/components/DiyEditor/components/mobile/VideoPlayer/config.ts new file mode 100644 index 00000000..30501cb8 --- /dev/null +++ b/src/components/DiyEditor/components/mobile/VideoPlayer/config.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> diff --git a/src/components/DiyEditor/components/mobile/VideoPlayer/index.vue b/src/components/DiyEditor/components/mobile/VideoPlayer/index.vue new file mode 100644 index 00000000..a62dea08 --- /dev/null +++ b/src/components/DiyEditor/components/mobile/VideoPlayer/index.vue @@ -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> diff --git a/src/components/DiyEditor/components/mobile/VideoPlayer/property.vue b/src/components/DiyEditor/components/mobile/VideoPlayer/property.vue new file mode 100644 index 00000000..96f317d6 --- /dev/null +++ b/src/components/DiyEditor/components/mobile/VideoPlayer/property.vue @@ -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> diff --git a/src/components/DiyEditor/index.vue b/src/components/DiyEditor/index.vue index d0d49812..fbb7e103 100644 --- a/src/components/DiyEditor/index.vue +++ b/src/components/DiyEditor/index.vue @@ -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%; } } } diff --git a/src/components/DiyEditor/util.ts b/src/components/DiyEditor/util.ts index 407efa30..29b44cf6 100644 --- a/src/components/DiyEditor/util.ts +++ b/src/components/DiyEditor/util.ts @@ -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[] diff --git a/src/components/ShortcutDateRangePicker/index.vue b/src/components/ShortcutDateRangePicker/index.vue index d7fa90cb..9f268a3f 100644 --- a/src/components/ShortcutDateRangePicker/index.vue +++ b/src/components/ShortcutDateRangePicker/index.vue @@ -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 = [ diff --git a/src/components/Sticky/src/Sticky.vue b/src/components/Sticky/src/Sticky.vue index b958544a..28ecbcb8 100644 --- a/src/components/Sticky/src/Sticky.vue +++ b/src/components/Sticky/src/Sticky.vue @@ -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' } diff --git a/src/components/Tooltip/src/Tooltip.vue b/src/components/Tooltip/src/Tooltip.vue index 7490bd70..1a2e09cc 100644 --- a/src/components/Tooltip/src/Tooltip.vue +++ b/src/components/Tooltip/src/Tooltip.vue @@ -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> diff --git a/src/components/UploadFile/src/UploadFile.vue b/src/components/UploadFile/src/UploadFile.vue index c8a3b972..6895440b 100644 --- a/src/components/UploadFile/src/UploadFile.vue +++ b/src/components/UploadFile/src/UploadFile.vue @@ -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"> diff --git a/src/components/VerticalButtonGroup/index.vue b/src/components/VerticalButtonGroup/index.vue new file mode 100644 index 00000000..479ed559 --- /dev/null +++ b/src/components/VerticalButtonGroup/index.vue @@ -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> diff --git a/src/hooks/web/useGuide.ts b/src/hooks/web/useGuide.ts new file mode 100644 index 00000000..7fd2fb09 --- /dev/null +++ b/src/hooks/web/useGuide.ts @@ -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 + } +} diff --git a/src/hooks/web/useIntro.ts b/src/hooks/web/useIntro.ts deleted file mode 100644 index 7fe00845..00000000 --- a/src/hooks/web/useIntro.ts +++ /dev/null @@ -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 - } -} diff --git a/src/hooks/web/useNetwork.ts b/src/hooks/web/useNetwork.ts new file mode 100644 index 00000000..66fa4464 --- /dev/null +++ b/src/hooks/web/useNetwork.ts @@ -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 } diff --git a/src/hooks/web/useNow.ts b/src/hooks/web/useNow.ts new file mode 100644 index 00000000..09d3176b --- /dev/null +++ b/src/hooks/web/useNow.ts @@ -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 + } +} diff --git a/src/hooks/web/useTagsView.ts b/src/hooks/web/useTagsView.ts new file mode 100644 index 00000000..31eadb02 --- /dev/null +++ b/src/hooks/web/useTagsView.ts @@ -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 + } +} diff --git a/src/hooks/web/useValidator.ts b/src/hooks/web/useValidator.ts index 0c16fa31..151e35b2 100644 --- a/src/hooks/web/useValidator.ts +++ b/src/hooks/web/useValidator.ts @@ -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 } } diff --git a/src/layout/components/Footer/src/Footer.vue b/src/layout/components/Footer/src/Footer.vue index c350e38a..3eede386 100644 --- a/src/layout/components/Footer/src/Footer.vue +++ b/src/layout/components/Footer/src/Footer.vue @@ -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> diff --git a/src/layout/components/Message/src/Message.vue b/src/layout/components/Message/src/Message.vue index 28f796b3..3019df29 100644 --- a/src/layout/components/Message/src/Message.vue +++ b/src/layout/components/Message/src/Message.vue @@ -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; diff --git a/src/utils/dict.ts b/src/utils/dict.ts index 5a818fa9..9fe429e9 100644 --- a/src/utils/dict.ts +++ b/src/utils/dict.ts @@ -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' } diff --git a/src/utils/formatTime.ts b/src/utils/formatTime.ts index 53ccda11..e2ffcadd 100644 --- a/src/utils/formatTime.ts +++ b/src/utils/formatTime.ts @@ -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()] } diff --git a/src/utils/index.ts b/src/utils/index.ts index d5301ddb..10f57567 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -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') { diff --git a/src/views/Login/components/LoginForm.vue b/src/views/Login/components/LoginForm.vue index 9bee2523..ef212505 100644 --- a/src/views/Login/components/LoginForm.vue +++ b/src/views/Login/components/LoginForm.vue @@ -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> diff --git a/src/views/Profile/Index.vue b/src/views/Profile/Index.vue index b05f93cc..8e1695b5 100644 --- a/src/views/Profile/Index.vue +++ b/src/views/Profile/Index.vue @@ -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> diff --git a/src/views/Profile/components/UserSocial.vue b/src/views/Profile/components/UserSocial.vue index 2f021abf..7746f510 100644 --- a/src/views/Profile/components/UserSocial.vue +++ b/src/views/Profile/components/UserSocial.vue @@ -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 diff --git a/src/views/crm/product/ProductDetail.vue b/src/views/crm/product/ProductDetail.vue new file mode 100644 index 00000000..f14e571a --- /dev/null +++ b/src/views/crm/product/ProductDetail.vue @@ -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> diff --git a/src/views/crm/product/ProductForm.vue b/src/views/crm/product/ProductForm.vue new file mode 100644 index 00000000..13d0e402 --- /dev/null +++ b/src/views/crm/product/ProductForm.vue @@ -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> diff --git a/src/views/crm/product/index.vue b/src/views/crm/product/index.vue new file mode 100644 index 00000000..122f964d --- /dev/null +++ b/src/views/crm/product/index.vue @@ -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> diff --git a/src/views/crm/productCategory/ProductCategoryForm.vue b/src/views/crm/productCategory/ProductCategoryForm.vue new file mode 100644 index 00000000..22cd09a8 --- /dev/null +++ b/src/views/crm/productCategory/ProductCategoryForm.vue @@ -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> diff --git a/src/views/crm/productCategory/index.vue b/src/views/crm/productCategory/index.vue new file mode 100644 index 00000000..897a08c0 --- /dev/null +++ b/src/views/crm/productCategory/index.vue @@ -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> diff --git a/src/views/crm/receivable/ReceivableForm.vue b/src/views/crm/receivable/ReceivableForm.vue index 2b9278fe..c338fe97 100644 --- a/src/views/crm/receivable/ReceivableForm.vue +++ b/src/views/crm/receivable/ReceivableForm.vue @@ -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 方法,用于打开弹窗 diff --git a/src/views/crm/receivable/index.vue b/src/views/crm/receivable/index.vue index 8a07fdda..ac1def09 100644 --- a/src/views/crm/receivable/index.vue +++ b/src/views/crm/receivable/index.vue @@ -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" diff --git a/src/views/crm/receivablePlan/ReceivablePlanForm.vue b/src/views/crm/receivablePlan/ReceivablePlanForm.vue index 22022ea0..bbc0379c 100644 --- a/src/views/crm/receivablePlan/ReceivablePlanForm.vue +++ b/src/views/crm/receivablePlan/ReceivablePlanForm.vue @@ -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, diff --git a/src/views/crm/receivablePlan/index.vue b/src/views/crm/receivablePlan/index.vue index 46857dcd..2a18b931 100644 --- a/src/views/crm/receivablePlan/index.vue +++ b/src/views/crm/receivablePlan/index.vue @@ -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> diff --git a/src/views/mall/promotion/diy/page/decorate.vue b/src/views/mall/promotion/diy/page/decorate.vue index 54e7206c..c5e90311 100644 --- a/src/views/mall/promotion/diy/page/decorate.vue +++ b/src/views/mall/promotion/diy/page/decorate.vue @@ -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)提交的按钮禁用 diff --git a/src/views/mall/promotion/diy/template/decorate.vue b/src/views/mall/promotion/diy/template/decorate.vue index 7a3cff2f..8304088a 100644 --- a/src/views/mall/promotion/diy/template/decorate.vue +++ b/src/views/mall/promotion/diy/template/decorate.vue @@ -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 ) diff --git a/src/views/mall/promotion/rewardActivity/RewardForm.vue b/src/views/mall/promotion/rewardActivity/RewardForm.vue new file mode 100644 index 00000000..18827014 --- /dev/null +++ b/src/views/mall/promotion/rewardActivity/RewardForm.vue @@ -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> diff --git a/src/views/mall/promotion/rewardActivity/index.vue b/src/views/mall/promotion/rewardActivity/index.vue new file mode 100644 index 00000000..7a05c9fc --- /dev/null +++ b/src/views/mall/promotion/rewardActivity/index.vue @@ -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> diff --git a/src/views/mall/trade/delivery/expressTemplate/ExpressTemplateForm.vue b/src/views/mall/trade/delivery/expressTemplate/ExpressTemplateForm.vue index 09bbc76e..edbcbc3a 100644 --- a/src/views/mall/trade/delivery/expressTemplate/ExpressTemplateForm.vue +++ b/src/views/mall/trade/delivery/expressTemplate/ExpressTemplateForm.vue @@ -31,7 +31,7 @@ :props="defaultProps2" class="w-1/1" clearable - placeholder="请选择商品分类" + placeholder="请选择地区" filterable collapse-tags /> diff --git a/src/views/system/loginlog/LoginLogDetail.vue b/src/views/system/loginlog/LoginLogDetail.vue index 54db0259..3679fa98 100644 --- a/src/views/system/loginlog/LoginLogDetail.vue +++ b/src/views/system/loginlog/LoginLogDetail.vue @@ -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) => { diff --git a/src/views/system/menu/MenuForm.vue b/src/views/system/menu/MenuForm.vue index 78debc55..7eaf6ab6 100644 --- a/src/views/system/menu/MenuForm.vue +++ b/src/views/system/menu/MenuForm.vue @@ -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"> diff --git a/src/views/system/menu/index.vue b/src/views/system/menu/index.vue index 2f564dd9..bf64a807 100644 --- a/src/views/system/menu/index.vue +++ b/src/views/system/menu/index.vue @@ -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> diff --git a/src/views/system/social/client/SocialClientForm.vue b/src/views/system/social/client/SocialClientForm.vue new file mode 100644 index 00000000..e6f92bd6 --- /dev/null +++ b/src/views/system/social/client/SocialClientForm.vue @@ -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> diff --git a/src/views/system/social/client/index.vue b/src/views/system/social/client/index.vue new file mode 100644 index 00000000..82f66297 --- /dev/null +++ b/src/views/system/social/client/index.vue @@ -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> diff --git a/src/views/system/social/user/SocialUserDetail.vue b/src/views/system/social/user/SocialUserDetail.vue new file mode 100644 index 00000000..1fa8b480 --- /dev/null +++ b/src/views/system/social/user/SocialUserDetail.vue @@ -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> diff --git a/src/views/system/social/user/index.vue b/src/views/system/social/user/index.vue new file mode 100644 index 00000000..c4b4f7f7 --- /dev/null +++ b/src/views/system/social/user/index.vue @@ -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> diff --git a/src/views/system/tenant/TenantForm.vue b/src/views/system/tenant/TenantForm.vue index a6085363..f6cdd075 100644 --- a/src/views/system/tenant/TenantForm.vue +++ b/src/views/system/tenant/TenantForm.vue @@ -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() diff --git a/src/views/system/tenant/index.vue b/src/views/system/tenant/index.vue index e357b6b4..703e01e4 100644 --- a/src/views/system/tenant/index.vue +++ b/src/views/system/tenant/index.vue @@ -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" /> diff --git a/tsconfig.json b/tsconfig.json index 1ee23774..182852ac 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -27,7 +27,6 @@ "@intlify/unplugin-vue-i18n/types", "vite/client", "element-plus/global", - "@types/intro.js", "@types/qrcode", "vite-plugin-svg-icons/client" ], diff --git a/uno.config.ts b/uno.config.ts index 0645fe68..d1467319 100644 --- a/uno.config.ts +++ b/uno.config.ts @@ -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' + } })