【账号中心】 新增# 子账号管理

main
kaeery 2025-02-22 10:53:50 +08:00
parent 6d3cab044e
commit 4221095b2c
19 changed files with 878 additions and 15 deletions

2
.env
View File

@ -1,2 +1,2 @@
VITE_APP_BASE_URL = 'http://192.168.4.117:8082' VITE_APP_BASE_URL = 'http://124.220.209.120:8082'
VITE_MAP_KEY = '2SABZ-S4TWH-AMCDO-W742B-SKEOE-UWBKJ' VITE_MAP_KEY = '2SABZ-S4TWH-AMCDO-W742B-SKEOE-UWBKJ'

View File

@ -0,0 +1,45 @@
<template>
<el-dialog v-model="dialogVisible" :title="parameter.title" :width="parameter.width">
<slot />
<template #footer>
<el-button @click="handleCancel"></el-button>
<el-button type="primary" @click="handleConfirm"></el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
export interface IParams {
title: string
width?: string | number
data: { [key: string]: any }
}
const emit = defineEmits(['handleCancel', 'handleConfirm'])
const dialogVisible = ref(false)
//
const parameter = ref<IParams>({
title: '',
width: '50%',
data: {}
})
const openDialog = (params: IParams) => {
parameter.value = { ...parameter.value, ...params }
dialogVisible.value = true
}
const handleCancel = () => {
emit('handleCancel', closeDialog)
}
const handleConfirm = () => {
emit('handleConfirm', closeDialog)
}
const closeDialog = () => {
dialogVisible.value = false
}
defineExpose({
openDialog,
dialogVisible
})
</script>
<style scoped></style>

View File

@ -0,0 +1,42 @@
<script lang="jsx" >
import { defineComponent, inject, ref } from 'vue';
import { filterEnum, handleRowAccordToProp, formatValue, handleProp } from '@/utils/util'
export default defineComponent({
props: {
column: {
tyep: Object,
default: () => ({})
}
},
setup(props, { emit, slots }) {
const enumMap = inject("enumMap", ref(new Map()))
//
const renderCellData = (item, scope) => {
return enumMap.value.get(item.prop)
? filterEnum(handleRowAccordToProp(scope.row, item.prop), enumMap.value.get(item.prop), item.fieldNames)
: formatValue(handleRowAccordToProp(scope.row, item.prop))
}
const { column } = props
return () => (
<>
<el-table-column {...column} showOverflowTooltip={column?.showOverflowTooltip ?? false } align={column.align ?? 'left'}>
{{
default: (scope) => {
if (column.render) return column.render(scope); //render
if(slots[handleProp(column.prop)]) return slots[handleProp(column.prop)](scope) //
return renderCellData(column, scope)
}
}}
</el-table-column>
</>
)
}
})
</script>
<style scoped></style>

View File

@ -0,0 +1,105 @@
<template>
<el-table
class="pro-table"
ref="tableRef"
v-loading="loading"
:data="tableData"
:row-key="rowKey"
:row-class-name="({ row }) => row[rowKey]"
:max-height="maxHeight"
@selection-change="selectionChange"
>
<slot />
<template v-for="item in tableColumns" :key="item">
<el-table-column v-if="item.type && columnTypes.includes(item.type)" v-bind="item"></el-table-column>
<TableColumn v-if="!item.type && item.prop" :column="item">
<template v-for="slot in Object.keys($slots)" #[slot]="scope">
<slot :name="slot" v-bind="scope" />
</template>
</TableColumn>
</template>
</el-table>
</template>
<script setup name="ProTable">
import { provide, reactive, ref, unref } from 'vue'
import TableColumn from './components/TableColumn.vue'
import useSelection from '@/hooks/useSelection'
const props = defineProps({
columns: {
type: Array,
default: () => []
},
tableData: {
type: Array,
default: () => []
},
loading: {
type: Boolean,
default: false
},
maxHeight: {
type: [Number, String],
default: ''
},
rowKey: {
type: String,
default: 'id'
}
})
const { isSelected, selectedList, selectedListIds, selectionChange } = useSelection(props.rowKey)
const columnTypes = ['selection', 'radio', 'index', 'expand', 'sort']
const tableRef = ref()
const enumMap = ref(new Map())
const tableColumns = reactive(props.columns)
// columns
const flatColumnsFunc = columns => {
columns.forEach(async column => {
// enumMap
await setEnumMap(column)
})
}
// enumMap enum
const setEnumMap = async ({ prop, enum: enumValue }) => {
if (!enumValue) return
// enumMap return
if (enumMap.value.has(prop) && (typeof enumValue === 'function' || enumMap.value.get(prop) === enumValue)) return
// enum enumMap
if (typeof enumValue !== 'function') return enumMap.value.set(prop, unref(enumValue))
// []
enumMap.value.set(prop, [])
// enum enumMap
const { rows } = await enumValue()
enumMap.value.set(prop, rows)
}
flatColumnsFunc(tableColumns)
provide('enumMap', enumMap)
defineExpose({
tableRef,
isSelected,
selectedList,
selectedListIds
})
</script>
<style lang="scss" scoped>
.pro-table {
:deep(.el-table__header) {
width: 100% !important;
}
:deep(.el-table__body) {
width: 100% !important;
}
}
</style>

View File

@ -0,0 +1,5 @@
export type FieldNamesProps = {
label: string
value: string
children?: string
}

View File

@ -0,0 +1,18 @@
import { ElMessageBox } from 'element-plus'
export type messageType = '' | 'success' | 'warning' | 'info' | 'error'
export default function useHandleData(message: string, messageType: messageType = 'warning'): Promise<boolean> {
return new Promise(resolve => {
ElMessageBox.confirm(message, '温馨提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: messageType
})
.then(() => {
resolve(true)
})
.catch(() => {
resolve(false)
})
})
}

View File

@ -0,0 +1,24 @@
import { computed, ref } from 'vue'
export default function useSelection(rowKey = 'id') {
const isSelected = ref(false)
const selectedList = ref([])
// 当前选中的所有id
const selectedListIds = computed(() => {
const ids: number | string[] = []
selectedList.value.forEach(item => ids.push(item[rowKey]))
return ids
})
// 多选操作
const selectionChange = (rowArr = []) => {
isSelected.value = rowArr.length ? true : false
selectedList.value = rowArr
}
return {
isSelected,
selectedList,
selectedListIds,
selectionChange
}
}

View File

@ -7,8 +7,14 @@ import 'virtual:svg-icons-register'
import eventBus from 'vue3-eventbus' import eventBus from 'vue3-eventbus'
import directives from './directives' import directives from './directives'
// 表格组件
import ProTable from '@/components/ProTable/index.vue'
import ProDialog from '@/components/ProDialog/index.vue'
const app = createApp(App) const app = createApp(App)
app.use(install) app.use(install)
app.use(eventBus) app.use(eventBus)
app.use(directives) app.use(directives)
app.component('ProTable', ProTable)
app.component('ProDialog', ProDialog)
app.mount('#app') app.mount('#app')

View File

@ -8,6 +8,7 @@
--navbar-height: 50px; --navbar-height: 50px;
--color-white: #ffffff; --color-white: #ffffff;
--color-red: #d9001b; --color-red: #d9001b;
--color-green: #00b42a;
--table-header-bg-color: #f8f8f8; --table-header-bg-color: #f8f8f8;
--el-font-size-extra-large: 18px; --el-font-size-extra-large: 18px;
--el-menu-base-level-padding: 16px; --el-menu-base-level-padding: 16px;

4
src/utils/is.ts 100644
View File

@ -0,0 +1,4 @@
// 判断是否是数组
export function isArray(value) {
return Array.isArray(value)
}

View File

@ -1,6 +1,8 @@
import { isObject } from '@vue/shared' import { isObject } from '@vue/shared'
import { ElMessage, type messageType } from 'element-plus' import { ElMessage, type messageType } from 'element-plus'
import { cloneDeep } from 'lodash' import { cloneDeep } from 'lodash'
import { isArray } from './is'
import type { FieldNamesProps } from '@/components/ProTable/interface'
/** /**
* @description * @description
@ -261,9 +263,9 @@ export function formatFileSize(bytes: number) {
*/ */
export function retain(value: any, n: any): string { export function retain(value: any, n: any): string {
if (n === 'null' || n === 'undefined' || n === 0) return value if (n === 'null' || n === 'undefined' || n === 0) return value
let tran = Math.round(value * Math.pow(10, n)) / Math.pow(10, n) const tran = Math.round(value * Math.pow(10, n)) / Math.pow(10, n)
let tranV = tran.toString() let tranV = tran.toString()
let newVal = tranV.indexOf('.') const newVal = tranV.indexOf('.')
if (newVal < 0) { if (newVal < 0) {
tranV += '.' tranV += '.'
} }
@ -272,3 +274,55 @@ export function retain(value: any, n: any): string {
} }
return tranV return tranV
} }
/**
* @description ProTable ||
* @param {*} callValue
* @returns {String}
* */
export function formatValue(callValue: any) {
// 如果当前值为数组,使用 / 拼接(根据需求自定义)
if (isArray(callValue)) return callValue.length ? callValue.join(' / ') : '--'
return callValue ?? '--'
}
/**
*
* @param {*} row
* @param {*} prop prop
* @returns
*/
export function handleRowAccordToProp(row: { [key: string]: any }, prop: string) {
if (!prop.includes('.')) return row[prop] ?? '--'
return row
}
/**
* prop
* @param {*} prop prop
*/
export function handleProp(prop: string) {
const propArr = prop.split('.')
if (propArr.length == 1) return prop
return propArr[propArr.length - 1]
}
/**
* label value key
* @param {*} callValue
* @param {*} enumData
*/
export function filterEnum(callValue: any, enumData?: any, fieldNames?: FieldNamesProps) {
const value = fieldNames?.value ?? 'value'
const label = fieldNames?.label ?? 'label'
const children = fieldNames?.children ?? 'children'
let filterData: { [key: string]: any } = {}
// 判断 enumData 是否为数组
if (Array.isArray(enumData)) filterData = findItemNested(enumData, callValue, value, children)
return filterData ? filterData[label] : '--'
}
// 递归查找 callValue 对应的 enum 值
export function findItemNested(enumData: any, callValue: any, value: string, children: string) {
return enumData.reduce((accumulator: any, current: any) => {
if (accumulator) return accumulator
if (current[value] == callValue) return current
if (current[children]) return findItemNested(current[children], callValue, value, children)
}, null)
}

View File

@ -0,0 +1,149 @@
<template>
<ProDialog ref="proDialogRef" @handle-cancel="handleCancel" @handle-confirm="handleConfirm">
<el-form :model="form" label-position="right" label-width="120">
<el-form-item label="所属组织">
<el-tree-select
v-model="form.organization"
:data="data"
:props="defaultProps"
:render-after-expand="false"
default-expand-all
style="width: 100%"
/>
</el-form-item>
<el-form-item label="账号名称">
<el-input v-model="form.accountName" autocomplete="off" placeholder="请输入" maxlength="20" show-word-limit />
</el-form-item>
<el-form-item label="联系电话">
<el-input v-model="form.mobile" autocomplete="off" placeholder="请输入" maxlength="11" show-word-limit />
</el-form-item>
<el-form-item label="岗位">
<el-checkbox-group v-model="form.position">
<el-checkbox v-for="option in positionOptions" :key="option.value" :value="option.value" name="type">
{{ option.label }}
</el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-form-item>
<template #label>
<div class="flex items-center gap-[6px] cursor-pointer">
<span>有效数据流向</span>
<el-tooltip effect="dark" placement="top">
<icon name="el-icon-QuestionFilled" />
<template #content>
<div>电销有效数据流向</div>
<ul>
<li v-for="(tooltip, index) in channelTooltips" :key="index" class="flex items-center gap-[4px]">
<span class="w-[6px] h-[6px] bg-white rounded-[8px]"></span>
{{ tooltip }}
</li>
</ul>
</template>
</el-tooltip>
</div>
</template>
<el-radio-group v-model="form.channel">
<el-radio v-for="option in channelOptions" :key="option.value" :label="option.value">{{ option.label }}</el-radio>
</el-radio-group>
<template v-if="form.channel == 'specific'">
<el-select class="mt-[10px]" v-model="form.teacher" placeholder="请选择指定招生老师">
<el-option label="Zone one" value="shanghai" />
<el-option label="Zone two" value="beijing" />
</el-select>
</template>
</el-form-item>
<el-form-item label="账号状态">
<el-radio-group v-model="form.accountStatus">
<el-radio v-for="option in accountStatusOptions" :key="option.value" :label="option.value">{{ option.label }}</el-radio>
</el-radio-group>
</el-form-item>
</el-form>
</ProDialog>
</template>
<script setup lang="ts">
import ProDialog, { type IParams } from '@/components/ProDialog/index.vue'
const proDialogRef = ref<InstanceType<typeof ProDialog>>()
const data = ref([
{
id: 1,
label: '合创云公司',
disabled: true,
children: [
{
id: 11,
label: '湛江团队',
disabled: true,
children: [
{
id: 111,
label: 'A组'
},
{
id: 112,
label: 'B组'
},
{
id: 113,
label: 'C组'
}
]
},
{
id: 12,
label: '广州团队',
disabled: true,
children: [
{
id: 121,
label: 'A组'
}
]
}
]
}
])
const defaultProps = {
label: 'label', //
value: 'id', //
children: 'children', //
disabled: 'disabled'
}
const positionOptions = ref([
{ label: '电销老师', value: 1 },
{ label: '招生老师', value: 2 }
])
const channelOptions = ref([
{ label: '默认组织', value: 'default' },
{ label: '组织指定', value: 'specific' }
])
const accountStatusOptions = ref([
{ label: '启用', value: 1 },
{ label: '停用', value: 2 }
])
const channelTooltips = ['选择默认组织则数据流向到所属组织下的所有招生老师;', '选择组织指定则可选择将数据流向到所属组织下的指定招生老师;']
const form = ref({
organization: '',
accountName: '',
mobile: '',
position: '',
channel: '',
accountStatus: '',
teacher: ''
})
const openDialog = (params: IParams) => {
proDialogRef.value?.openDialog(params)
}
const handleCancel = (callback: () => void) => {
callback()
}
const handleConfirm = (callback: () => void) => {
callback()
}
defineExpose({
openDialog
})
</script>
<style scoped lang="scss"></style>

View File

@ -0,0 +1,125 @@
<template>
<el-dialog v-model="dialogVisible" :title="parameter.title" :width="parameter.width">
<el-space direction="vertical" alignment="normal">
<el-form :model="searchForm" ref="userFormRef" :inline="true" label-width="auto" @submit.native.prevent>
<el-form-item label="账号名称">
<el-input class="ls-input" v-model="searchForm.accountName" placeholder="请输入账号名称" clearable></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary">搜索</el-button>
<el-button>重置</el-button>
</el-form-item>
</el-form>
<ProTable ref="proTableRef" :columns="columns" :tableData="tableData" :loading="loading" :maxHeight="530">
<template #radio="{ row }">
<el-radio v-model="radio" :label="row.id">&nbsp;</el-radio>
</template>
<template #accountName="{ row }">
<el-space>
<span>{{ row.accountName }}</span>
<span
v-if="isGroupLeader(row.groupLeader)"
class="px-[6px] text-green border border-solid border-green rounded-[4px] text-center text-xs"
>
组长
</span>
</el-space>
</template>
<template #accountStatus="{ row }">
<el-switch v-model="row.accountStatus" disabled :active-value="1" :inactive-value="0" />
</template>
</ProTable>
<!-- <pagination v-model="pager" @change="getLists" layout="total, prev, pager, next, jumper" class="mb-[10px]" /> -->
</el-space>
<template #footer>
<el-button @click="handleCancel"></el-button>
<el-button type="primary" @click="handleConfirm"></el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
interface IParams {
title: string
width?: string | number
data: { [key: string]: any }
}
const dialogVisible = ref(false)
//
const parameter = ref<IParams>({
title: '',
width: '50%',
data: {}
})
const openDialog = (params: IParams) => {
parameter.value = { ...parameter.value, ...params }
dialogVisible.value = true
}
const emit = defineEmits(['refreshList'])
defineExpose({
openDialog
})
const searchForm = ref({
accountName: ''
})
const loading = ref(false)
const proTableRef = ref()
const tableData = ref<any[]>([])
const columns = reactive([
{ prop: 'radio', label: '单选', width: 70 },
{ prop: 'accountName', label: '账号名称' },
{ prop: 'mobile', label: '联系电话' },
{ prop: 'organization', label: '所属组织' },
{ prop: 'position', label: '岗位' },
{ prop: 'accountStatus', label: '账号状态' }
])
const radio = ref()
const isGroupLeader = computed(() => (groupLeader: number) => groupLeader == 1)
watch(
() => dialogVisible.value,
val => {
if (val) {
loading.value = true
setTimeout(() => {
tableData.value = [
{
id: 1,
accountName: '张三',
mobile: '18138952909',
organization: '广州团队-A组',
position: '电销老师',
accountStatus: 1,
groupLeader: 1
},
{
id: 2,
accountName: '李四',
mobile: '18138952909',
organization: '广州团队-A组',
position: '电销老师',
accountStatus: 1,
groupLeader: 0
}
]
loading.value = false
//
if (tableData.value.length > 0) {
radio.value = tableData.value.find(item => item.groupLeader == 1)?.id
}
}, 500)
}
}
)
const handleCancel = () => {
dialogVisible.value = false
}
const handleConfirm = () => {
console.log(radio.value)
dialogVisible.value = false
emit('refreshList')
}
</script>
<style scoped></style>

View File

@ -0,0 +1,109 @@
<template>
<ProDialog ref="proDialogRef" @handle-cancel="handleCancel" @handle-confirm="handleConfirm">
<el-space direction="vertical" alignment="normal">
<el-form :model="searchForm" ref="userFormRef" :inline="true" label-width="auto" @submit.native.prevent>
<el-form-item label="账号名称">
<el-input class="ls-input" v-model="searchForm.accountName" placeholder="请输入账号名称" clearable></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary">搜索</el-button>
<el-button>重置</el-button>
</el-form-item>
</el-form>
<ProTable ref="proTableRef" :columns="columns" :tableData="tableData" :loading="loading" :maxHeight="530">
<template #radio="{ row }">
<el-radio v-model="radio" :label="row.id">&nbsp;</el-radio>
</template>
<template #accountName="{ row }">
<el-space>
<span>{{ row.accountName }}</span>
<span
v-if="isGroupLeader(row.groupLeader)"
class="px-[6px] text-green border border-solid border-green rounded-[4px] text-center text-xs"
>
组长
</span>
</el-space>
</template>
<template #accountStatus="{ row }">
<el-switch v-model="row.accountStatus" disabled :active-value="1" :inactive-value="0" />
</template>
</ProTable>
<!-- <pagination v-model="pager" @change="getLists" layout="total, prev, pager, next, jumper" class="mb-[10px]" /> -->
</el-space>
</ProDialog>
</template>
<script setup lang="ts">
import ProDialog, { type IParams } from '@/components/ProDialog/index.vue'
const proDialogRef = ref<InstanceType<typeof ProDialog>>()
const emit = defineEmits(['refreshList'])
const searchForm = ref({
accountName: ''
})
const loading = ref(false)
const proTableRef = ref()
const tableData = ref<any[]>([])
const columns = reactive([
{ prop: 'radio', label: '单选', width: 70 },
{ prop: 'accountName', label: '账号名称' },
{ prop: 'mobile', label: '联系电话' },
{ prop: 'organization', label: '所属组织' },
{ prop: 'position', label: '岗位' },
{ prop: 'accountStatus', label: '账号状态' }
])
const radio = ref()
const isGroupLeader = computed(() => (groupLeader: number) => groupLeader == 1)
watch(
() => proDialogRef.value?.dialogVisible,
val => {
if (val) {
loading.value = true
setTimeout(() => {
tableData.value = [
{
id: 1,
accountName: '张三',
mobile: '18138952909',
organization: '广州团队-A组',
position: '电销老师',
accountStatus: 1,
groupLeader: 1
},
{
id: 2,
accountName: '李四',
mobile: '18138952909',
organization: '广州团队-A组',
position: '电销老师',
accountStatus: 1,
groupLeader: 0
}
]
loading.value = false
//
if (tableData.value.length > 0) {
radio.value = tableData.value.find(item => item.groupLeader == 1)?.id
}
}, 500)
}
}
)
const openDialog = (params: IParams) => {
proDialogRef.value?.openDialog(params)
}
const handleCancel = (callback: () => void) => {
callback()
}
const handleConfirm = (callback: () => void) => {
callback()
emit('refreshList')
}
defineExpose({
openDialog
})
</script>
<style scoped></style>

View File

@ -2,7 +2,6 @@
<div class="flex-1 p-[16px]" v-loading="loading"> <div class="flex-1 p-[16px]" v-loading="loading">
<div class="mb-[10px]"> <div class="mb-[10px]">
<el-input v-model="filterText" placeholder="请输入" :suffix-icon="Search" clearable /> <el-input v-model="filterText" placeholder="请输入" :suffix-icon="Search" clearable />
{{ currentNodeKey }}
</div> </div>
<el-tree <el-tree
ref="treeRef" ref="treeRef"
@ -12,13 +11,14 @@
highlight-current highlight-current
:current-node-key="currentNodeKey" :current-node-key="currentNodeKey"
:default-expanded-keys="[primaryAccountId]" :default-expanded-keys="[primaryAccountId]"
:expand-on-click-node="false"
:filter-node-method="filterNode" :filter-node-method="filterNode"
@node-click="handleNodeClick" @node-click="handleNodeClick"
> >
<template #default="{ node, data }"> <template #default="{ node, data }">
<span class="flex-1 flex-row-between-center"> <span class="flex-1 flex-row-between-center">
<span>{{ node.label }}</span> <span>{{ node.label }}</span>
<el-dropdown placement="bottom" @command="handleCommand"> <el-dropdown placement="bottom" @command="(...args: any[]) => handleCommand(args, data)">
<span class="more mr-[12px]" v-if="data.id !== 1"> <span class="more mr-[12px]" v-if="data.id !== 1">
<icon name="el-icon-MoreFilled" :size="16" color="999" /> <icon name="el-icon-MoreFilled" :size="16" color="999" />
</span> </span>
@ -26,7 +26,7 @@
<el-dropdown-menu> <el-dropdown-menu>
<el-dropdown-item command="editOrganization">编辑组织信息</el-dropdown-item> <el-dropdown-item command="editOrganization">编辑组织信息</el-dropdown-item>
<el-dropdown-item command="deleteOrganization">删除组织</el-dropdown-item> <el-dropdown-item command="deleteOrganization">删除组织</el-dropdown-item>
<el-dropdown-item command="setGroupLeader">设置组长</el-dropdown-item> <el-dropdown-item command="setGroupLeader" v-if="!data?.children"></el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>
</el-dropdown> </el-dropdown>
@ -60,6 +60,7 @@ const props = defineProps({
type: [String, Number] type: [String, Number]
} }
}) })
const emit = defineEmits(['setGroupLeader'])
const defaultProps = { const defaultProps = {
children: 'children', children: 'children',
label: 'label', label: 'label',
@ -80,15 +81,19 @@ watch(filterText, val => {
const handleNodeClick = (data: Tree) => { const handleNodeClick = (data: Tree) => {
parent?.setSelectedNode(data) parent?.setSelectedNode(data)
} }
//
watch( watch(
() => props.currentNodeKey, () => props.currentNodeKey,
val => { val => {
if (val) { if (val) {
treeRef.value?.setCurrentKey(val) nextTick(() => {
treeRef.value?.setCurrentKey(val)
})
} }
} }
) )
const handleCommand = (command: string) => { const handleCommand = (args: any[], data: Tree) => {
const command = args[0]
switch (command) { switch (command) {
case 'editOrganization': case 'editOrganization':
parent?.fetchOrganizationList() parent?.fetchOrganizationList()
@ -97,6 +102,7 @@ const handleCommand = (command: string) => {
parent?.fetchOrganizationList() parent?.fetchOrganizationList()
break break
case 'setGroupLeader': case 'setGroupLeader':
emit('setGroupLeader', data)
break break
} }
} }

View File

@ -1,9 +1,9 @@
<template> <template>
<div class="flex flex-col h-full"> <div class="flex flex-col h-full">
<account-number /> <account-number />
<div class="flex flex-1 bg-white"> <div class="flex flex-1 bg-white overflow-x-auto">
<organization @set-selected-node="setSelectedNode" :curSelectedNode="selectedNode" /> <organization @set-selected-node="setSelectedNode" :curSelectedNode="selectedNode" @fetch-table-list="fetchTableList" />
<account-list :curOrganization="selectedNode" /> <account-list ref="accountListRef" :curOrganization="selectedNode" />
</div> </div>
</div> </div>
</template> </template>
@ -18,5 +18,17 @@ const selectedNode = ref()
const setSelectedNode = (data: Tree) => { const setSelectedNode = (data: Tree) => {
selectedNode.value = data selectedNode.value = data
} }
watch(
() => selectedNode.value,
val => {
if (val) {
fetchTableList()
}
}
)
const accountListRef = ref<InstanceType<typeof accountList>>()
const fetchTableList = () => {
accountListRef.value?.fetchTableList()
}
</script> </script>
<style scoped></style> <style scoped></style>

View File

@ -1,15 +1,162 @@
<template> <template>
<div class="flex flex-col flex-1"> <div class="flex flex-col flex-1">
<div class="h-[50px] flex items-center border-b-solid-light2 px-[20px] text-[20px] font-bold">{{ curOrganization.label }}</div> <div class="h-[50px] flex items-center border-b-solid-light2 px-[20px] text-[20px] font-bold">
{{ curOrganization.label }}
</div>
<div class="px-[20px] py-[16px] flex justify-between">
<el-space>
<el-button type="primary" :icon="Plus" @click="showAccountDialog"></el-button>
<el-button type="danger" plain :icon="Delete" :disabled="isDisabled" @click="handleBatchDelete"></el-button>
</el-space>
<el-space>
<el-input :placeholder="`请输入${placeholder}`">
<template #prepend>
<el-select v-model="selectKey" :placeholder="placeholder" class="w-[100px]">
<el-option v-for="option in searchOptions" :key="option.field" :label="option.label" :value="option.field" />
</el-select>
</template>
</el-input>
<el-select placeholder="请选择账号状态">
<el-option label="Restaurant" value="1" />
</el-select>
</el-space>
</div>
<ProTable ref="proTableRef" :columns="columns" :tableData="tableData" :loading="loading" :maxHeight="530">
<template #accountName="{ row }">
<el-space>
<div class="w-[50px] h-[50px] bg-primary rounded-[6px] text-white flex-row-center-center">{{ row.accountName }}</div>
<span>{{ row.accountName }}</span>
<span
v-if="isGroupLeader(row.groupLeader)"
class="px-[6px] text-green border border-solid border-green rounded-[4px] text-center text-xs"
>
组长
</span>
</el-space>
</template>
<template #accountStatus="{ row }">
<el-switch v-model="row.accountStatus" :active-value="1" :inactive-value="0" :before-change="() => handleStatusChange(row)" />
</template>
<template #operation="{ row }">
<el-button link type="primary">详情</el-button>
<el-button link type="primary" @click="setGroupLeader(row)">{{ btnText(row.groupLeader) }}</el-button>
<el-button link type="primary" @click="handleDelete(row)"></el-button>
</template>
</ProTable>
</div> </div>
<account-dialog ref="accountDialogRef" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import useHandleData from '@/hooks/useHandleData'
import feedback from '@/utils/feedback'
import { Plus, Delete } from '@element-plus/icons-vue'
import accountDialog from '../components/account-list/account-dialog.vue'
defineProps({ defineProps({
curOrganization: { curOrganization: {
type: Object, type: Object,
default: () => ({}) default: () => ({})
} }
}) })
const selectKey = ref('accountName')
const searchOptions = shallowRef([
{ field: 'accountName', label: '账号名称' },
{ field: 'mobile', label: '联系电话' },
{ field: 'positionName', label: '岗位名称' }
])
const placeholder = computed(() => searchOptions.value.find(item => item.field == selectKey.value)?.label)
const loading = ref(false)
const proTableRef = ref()
const tableData = ref<any[]>([])
const columns = reactive([
{ type: 'selection', fixed: 'left', width: 70, reserveSelection: true },
{ prop: 'accountName', label: '账号名称', width: 180 },
{ prop: 'mobile', label: '联系电话', width: 180 },
{ prop: 'organization', label: '所属组织', width: 180 },
{ prop: 'position', label: '岗位', width: 180 },
{ prop: 'accountStatus', label: '账号状态', width: 180 },
{ prop: 'operation', label: '操作', fixed: 'right', width: 250 }
])
const isGroupLeader = computed(() => (groupLeader: number) => groupLeader == 1)
const btnText = computed(() => (groupLeader: number) => isGroupLeader.value(groupLeader) ? '取消设为组长' : '设置组长')
const fetchTableList = () => {
loading.value = true
setTimeout(() => {
tableData.value = [
{
id: 1,
accountName: '张三',
mobile: '18138952909',
organization: '广州团队-A组',
position: '电销老师',
accountStatus: 1,
groupLeader: 1
},
{
id: 2,
accountName: '李四',
mobile: '18138952909',
organization: '广州团队-A组',
position: '电销老师',
accountStatus: 1,
groupLeader: 0
},
{
id: 3,
accountName: '王五',
mobile: '18138952909',
organization: '广州团队-A组',
position: '招生老师',
accountStatus: 0,
groupLeader: 0
}
]
loading.value = false
}, 500)
}
const handleStatusChange = row => {
console.log(row)
return new Promise(resolve => {
resolve(true)
})
}
const setGroupLeader = async row => {
const { accountName, id, groupLeader } = row
const message = isGroupLeader.value(groupLeader) ? `确定要取消【${accountName}】的组长身份吗?` : `确定将【${accountName}】设置为组长?`
const flag = await useHandleData(message)
if (!flag) return
feedback.msgSuccess('设置成功')
}
const isDisabled = computed(() => proTableRef?.value?.selectedListIds?.length == 0)
const handleBatchDelete = () => {
deleteCommon()
}
const handleDelete = async row => {
deleteCommon(row)
}
const deleteCommon = async (row?: any) => {
if (row) proTableRef.value.tableRef.toggleRowSelection(row, true)
const ids = proTableRef.value.selectedListIds
const message = '是否要删除选中的员工?'
const flag = await useHandleData(message)
if (flag) {
console.log(ids)
}
proTableRef.value.tableRef.clearSelection()
}
defineExpose({
fetchTableList
})
const accountDialogRef = ref<InstanceType<typeof accountDialog>>()
const showAccountDialog = () => {
accountDialogRef.value?.openDialog({
title: '新建账号',
width: 400,
data: {}
})
}
</script> </script>
<style scoped></style> <style scoped></style>

View File

@ -1,13 +1,15 @@
<template> <template>
<div class="flex flex-col w-[240px] border-r-solid-light2"> <div class="flex flex-col w-[240px] border-r-solid-light2">
<structure /> <structure />
<organization-tree :data="data" :loading="loading" :currentNodeKey="curSelectedNode?.id" /> <organization-tree :data="data" :loading="loading" :currentNodeKey="curSelectedNode?.id" @set-group-leader="setGroupLeader" />
</div> </div>
<group-leader-dialog ref="groupLeaderDialogRef" @refresh-list="$emit('fetchTableList')" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import structure from '../components/organization/structure.vue' import structure from '../components/organization/structure.vue'
import organizationTree, { type Tree } from '../components/organization/organization-tree.vue' import organizationTree, { type Tree } from '../components/organization/organization-tree.vue'
import groupLeaderDialog from '../components/organization/groupLeader-dialog.vue'
import { ORGANIZATION_INJECTION_KEY } from '@/config/symbol' import { ORGANIZATION_INJECTION_KEY } from '@/config/symbol'
import type { PropType } from 'vue' import type { PropType } from 'vue'
@ -17,7 +19,7 @@ defineProps({
default: () => ({}) default: () => ({})
} }
}) })
const emit = defineEmits(['setSelectedNode']) const emit = defineEmits(['setSelectedNode', 'fetchTableList'])
const data = ref<Tree[]>([]) const data = ref<Tree[]>([])
const loading = ref(false) const loading = ref(false)
const setSelectedNode = (data: Tree) => { const setSelectedNode = (data: Tree) => {
@ -72,6 +74,14 @@ provide(ORGANIZATION_INJECTION_KEY, {
setSelectedNode setSelectedNode
}) })
fetchOrganizationList() fetchOrganizationList()
const groupLeaderDialogRef = ref<InstanceType<typeof groupLeaderDialog>>()
const setGroupLeader = (data: Tree) => {
groupLeaderDialogRef.value?.openDialog({
title: '设置组长',
data
})
}
</script> </script>
<style scoped lang="scss"></style> <style scoped lang="scss"></style>

View File

@ -34,7 +34,8 @@ module.exports = {
fill: 'var(--el-fill-color)', fill: 'var(--el-fill-color)',
'fill-light': 'var(--el-fill-color-light)', 'fill-light': 'var(--el-fill-color-light)',
'fill-lighter': 'var(--el-fill-color-lighter)', 'fill-lighter': 'var(--el-fill-color-lighter)',
mask: 'var(--el-mask-color)' mask: 'var(--el-mask-color)',
green: 'var(--color-green)'
}, },
fontFamily: { fontFamily: {
sans: ['PingFang SC', 'Arial', 'Hiragino Sans GB', 'Microsoft YaHei', 'sans-serif'] sans: ['PingFang SC', 'Arial', 'Hiragino Sans GB', 'Microsoft YaHei', 'sans-serif']