diff --git a/src/components/business/CodeGeneratorButton.vue b/src/components/business/CodeGeneratorButton.vue new file mode 100644 index 0000000..e70e6c5 --- /dev/null +++ b/src/components/business/CodeGeneratorButton.vue @@ -0,0 +1,59 @@ + + + + + diff --git a/src/components/core/forms/art-search-bar/index.vue b/src/components/core/forms/art-search-bar/index.vue index ac40537..773acbd 100644 --- a/src/components/core/forms/art-search-bar/index.vue +++ b/src/components/core/forms/art-search-bar/index.vue @@ -75,6 +75,7 @@ import ArtSearchSelect from './widget/art-search-select/index.vue' import ArtSearchRadio from './widget/art-search-radio/index.vue' import ArtSearchDate from './widget/art-search-date/index.vue' + import ArtSearchTreeSelect from './widget/art-search-tree-select/index.vue' import { SearchComponentType, SearchFormItem } from '@/types' const { width } = useWindowSize() @@ -131,6 +132,7 @@ const componentsMap: Record = { input: ArtSearchInput, select: ArtSearchSelect, + 'tree-select': ArtSearchTreeSelect, radio: ArtSearchRadio, datetime: ArtSearchDate, date: ArtSearchDate, diff --git a/src/components/core/forms/art-search-bar/widget/art-search-tree-select/index.vue b/src/components/core/forms/art-search-bar/widget/art-search-tree-select/index.vue new file mode 100644 index 0000000..64be683 --- /dev/null +++ b/src/components/core/forms/art-search-bar/widget/art-search-tree-select/index.vue @@ -0,0 +1,46 @@ + + + diff --git a/src/composables/useCheckedColumns.ts b/src/composables/useCheckedColumns.ts index 1bf40b6..1d11c28 100644 --- a/src/composables/useCheckedColumns.ts +++ b/src/composables/useCheckedColumns.ts @@ -2,6 +2,23 @@ import { ref, computed } from 'vue' +/** + * 格式化空值为 "-" + * @param value 要格式化的值 + * @returns 格式化后的值 + */ +const formatEmptyValue = (value: any): any => { + // 判断值是否为空 + if (value === null || value === undefined || value === '') { + return '-' + } + // 如果是数字0,返回0 + if (value === 0) { + return '0' + } + return value +} + // 定义列配置接口 export interface ColumnOption { type?: 'selection' | 'expand' | 'index' @@ -76,6 +93,27 @@ export function useCheckedColumns(columnsFactory: () => ColumnOption[]) { const columnMap = new Map() cols.forEach((column) => { + // 为普通列添加空值处理 + if (!column.type || (column.type !== 'selection' && column.type !== 'expand' && column.type !== 'index')) { + const originalFormatter = column.formatter + + // 包装原有的 formatter,在其基础上添加空值处理 + column.formatter = (row: any, ...args: any[]) => { + let value + + // 如果有自定义 formatter,先执行 + if (originalFormatter) { + value = originalFormatter(row, ...args) + } else { + // 没有自定义 formatter,直接取字段值 + value = row[column.prop as string] + } + + // 对结果进行空值处理 + return formatEmptyValue(value) + } + } + if (column.type === 'selection') { columnMap.set(SELECTION_KEY, column) } else if (column.type === 'expand') { diff --git a/src/types/component/index.ts b/src/types/component/index.ts index 40a2bec..fa47eff 100644 --- a/src/types/component/index.ts +++ b/src/types/component/index.ts @@ -8,6 +8,7 @@ import { Option } from '../common' export type SearchComponentType = | 'input' | 'select' + | 'tree-select' | 'radio' | 'checkbox' | 'date' diff --git a/src/types/components.d.ts b/src/types/components.d.ts index 7390a14..0b260cb 100644 --- a/src/types/components.d.ts +++ b/src/types/components.d.ts @@ -56,10 +56,12 @@ declare module 'vue' { ArtSearchInput: typeof import('./../components/core/forms/art-search-bar/widget/art-search-input/index.vue')['default'] ArtSearchRadio: typeof import('./../components/core/forms/art-search-bar/widget/art-search-radio/index.vue')['default'] ArtSearchSelect: typeof import('./../components/core/forms/art-search-bar/widget/art-search-select/index.vue')['default'] + ArtSearchTreeSelect: typeof import('./../components/core/forms/art-search-bar/widget/art-search-tree-select/index.vue')['default'] ArtSettingsPanel: typeof import('./../components/core/layouts/art-settings-panel/index.vue')['default'] ArtSidebarMenu: typeof import('./../components/core/layouts/art-menus/art-sidebar-menu/index.vue')['default'] ArtStatsCard: typeof import('./../components/core/cards/ArtStatsCard.vue')['default'] ArtTable: typeof import('./../components/core/tables/ArtTable.vue')['default'] + ArtTableColumn: typeof import('./../components/core/tables/ArtTableColumn.vue')['default'] ArtTableFullScreen: typeof import('./../components/core/tables/ArtTableFullScreen.vue')['default'] ArtTableHeader: typeof import('./../components/core/tables/ArtTableHeader.vue')['default'] ArtTextScroll: typeof import('./../components/core/text-effect/ArtTextScroll.vue')['default'] @@ -73,6 +75,7 @@ declare module 'vue' { BoxStyleSettings: typeof import('./../components/core/layouts/art-settings-panel/widget/BoxStyleSettings.vue')['default'] CardOperationDialog: typeof import('./../components/business/CardOperationDialog.vue')['default'] CardStatusTag: typeof import('./../components/business/CardStatusTag.vue')['default'] + CodeGeneratorButton: typeof import('./../components/business/CodeGeneratorButton.vue')['default'] ColorSettings: typeof import('./../components/core/layouts/art-settings-panel/widget/ColorSettings.vue')['default'] CommentItem: typeof import('./../components/custom/comment-widget/widget/CommentItem.vue')['default'] CommentWidget: typeof import('./../components/custom/comment-widget/index.vue')['default'] @@ -86,6 +89,7 @@ declare module 'vue' { ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup'] ElCalendar: typeof import('element-plus/es')['ElCalendar'] ElCard: typeof import('element-plus/es')['ElCard'] + ElCascader: typeof import('element-plus/es')['ElCascader'] ElCheckbox: typeof import('element-plus/es')['ElCheckbox'] ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup'] ElCol: typeof import('element-plus/es')['ElCol'] diff --git a/src/utils/codeGenerator.ts b/src/utils/codeGenerator.ts index 3921030..7fd4487 100644 --- a/src/utils/codeGenerator.ts +++ b/src/utils/codeGenerator.ts @@ -45,3 +45,25 @@ export function generatePackageCode(): string { return `PKG${year}${month}${day}${hours}${minutes}${seconds}${randomChars}` } + +/** + * 生成店铺编码 + * 规则: SHOP + 年月日时分秒 + 4位随机数 + * 示例: SHOP20260314143025ABCD + */ +export function generateShopCode(): string { + const now = new Date() + const year = now.getFullYear() + const month = String(now.getMonth() + 1).padStart(2, '0') + const day = String(now.getDate()).padStart(2, '0') + const hours = String(now.getHours()).padStart(2, '0') + const minutes = String(now.getMinutes()).padStart(2, '0') + const seconds = String(now.getSeconds()).padStart(2, '0') + + // 生成4位随机大写字母 + const randomChars = Array.from({ length: 4 }, () => + String.fromCharCode(65 + Math.floor(Math.random() * 26)) + ).join('') + + return `SHOP${year}${month}${day}${hours}${minutes}${seconds}${randomChars}` +} diff --git a/src/utils/constants/regionData.ts b/src/utils/constants/regionData.ts new file mode 100644 index 0000000..05322ab --- /dev/null +++ b/src/utils/constants/regionData.ts @@ -0,0 +1,220 @@ +/** + * 省市区三级联动数据 + * 简化版数据,包含主要省市区 + */ + +export interface RegionItem { + value: string + label: string + children?: RegionItem[] +} + +export const regionData: RegionItem[] = [ + { + value: '北京市', + label: '北京市', + children: [ + { + value: '北京市', + label: '北京市', + children: [ + { value: '东城区', label: '东城区' }, + { value: '西城区', label: '西城区' }, + { value: '朝阳区', label: '朝阳区' }, + { value: '丰台区', label: '丰台区' }, + { value: '石景山区', label: '石景山区' }, + { value: '海淀区', label: '海淀区' }, + { value: '门头沟区', label: '门头沟区' }, + { value: '房山区', label: '房山区' }, + { value: '通州区', label: '通州区' }, + { value: '顺义区', label: '顺义区' }, + { value: '昌平区', label: '昌平区' }, + { value: '大兴区', label: '大兴区' }, + { value: '怀柔区', label: '怀柔区' }, + { value: '平谷区', label: '平谷区' }, + { value: '密云区', label: '密云区' }, + { value: '延庆区', label: '延庆区' } + ] + } + ] + }, + { + value: '上海市', + label: '上海市', + children: [ + { + value: '上海市', + label: '上海市', + children: [ + { value: '黄浦区', label: '黄浦区' }, + { value: '徐汇区', label: '徐汇区' }, + { value: '长宁区', label: '长宁区' }, + { value: '静安区', label: '静安区' }, + { value: '普陀区', label: '普陀区' }, + { value: '虹口区', label: '虹口区' }, + { value: '杨浦区', label: '杨浦区' }, + { value: '闵行区', label: '闵行区' }, + { value: '宝山区', label: '宝山区' }, + { value: '嘉定区', label: '嘉定区' }, + { value: '浦东新区', label: '浦东新区' }, + { value: '金山区', label: '金山区' }, + { value: '松江区', label: '松江区' }, + { value: '青浦区', label: '青浦区' }, + { value: '奉贤区', label: '奉贤区' }, + { value: '崇明区', label: '崇明区' } + ] + } + ] + }, + { + value: '广东省', + label: '广东省', + children: [ + { + value: '广州市', + label: '广州市', + children: [ + { value: '荔湾区', label: '荔湾区' }, + { value: '越秀区', label: '越秀区' }, + { value: '海珠区', label: '海珠区' }, + { value: '天河区', label: '天河区' }, + { value: '白云区', label: '白云区' }, + { value: '黄埔区', label: '黄埔区' }, + { value: '番禺区', label: '番禺区' }, + { value: '花都区', label: '花都区' }, + { value: '南沙区', label: '南沙区' }, + { value: '从化区', label: '从化区' }, + { value: '增城区', label: '增城区' } + ] + }, + { + value: '深圳市', + label: '深圳市', + children: [ + { value: '罗湖区', label: '罗湖区' }, + { value: '福田区', label: '福田区' }, + { value: '南山区', label: '南山区' }, + { value: '宝安区', label: '宝安区' }, + { value: '龙岗区', label: '龙岗区' }, + { value: '盐田区', label: '盐田区' }, + { value: '龙华区', label: '龙华区' }, + { value: '坪山区', label: '坪山区' }, + { value: '光明区', label: '光明区' } + ] + }, + { + value: '东莞市', + label: '东莞市', + children: [ + { value: '莞城区', label: '莞城区' }, + { value: '南城区', label: '南城区' }, + { value: '东城区', label: '东城区' }, + { value: '万江区', label: '万江区' } + ] + }, + { + value: '佛山市', + label: '佛山市', + children: [ + { value: '禅城区', label: '禅城区' }, + { value: '南海区', label: '南海区' }, + { value: '顺德区', label: '顺德区' }, + { value: '三水区', label: '三水区' }, + { value: '高明区', label: '高明区' } + ] + } + ] + }, + { + value: '浙江省', + label: '浙江省', + children: [ + { + value: '杭州市', + label: '杭州市', + children: [ + { value: '上城区', label: '上城区' }, + { value: '拱墅区', label: '拱墅区' }, + { value: '西湖区', label: '西湖区' }, + { value: '滨江区', label: '滨江区' }, + { value: '萧山区', label: '萧山区' }, + { value: '余杭区', label: '余杭区' }, + { value: '临平区', label: '临平区' }, + { value: '钱塘区', label: '钱塘区' }, + { value: '富阳区', label: '富阳区' }, + { value: '临安区', label: '临安区' } + ] + }, + { + value: '宁波市', + label: '宁波市', + children: [ + { value: '海曙区', label: '海曙区' }, + { value: '江北区', label: '江北区' }, + { value: '北仑区', label: '北仑区' }, + { value: '镇海区', label: '镇海区' }, + { value: '鄞州区', label: '鄞州区' }, + { value: '奉化区', label: '奉化区' } + ] + } + ] + }, + { + value: '江苏省', + label: '江苏省', + children: [ + { + value: '南京市', + label: '南京市', + children: [ + { value: '玄武区', label: '玄武区' }, + { value: '秦淮区', label: '秦淮区' }, + { value: '建邺区', label: '建邺区' }, + { value: '鼓楼区', label: '鼓楼区' }, + { value: '浦口区', label: '浦口区' }, + { value: '栖霞区', label: '栖霞区' }, + { value: '雨花台区', label: '雨花台区' }, + { value: '江宁区', label: '江宁区' }, + { value: '六合区', label: '六合区' }, + { value: '溧水区', label: '溧水区' }, + { value: '高淳区', label: '高淳区' } + ] + }, + { + value: '苏州市', + label: '苏州市', + children: [ + { value: '姑苏区', label: '姑苏区' }, + { value: '虎丘区', label: '虎丘区' }, + { value: '吴中区', label: '吴中区' }, + { value: '相城区', label: '相城区' }, + { value: '吴江区', label: '吴江区' } + ] + } + ] + }, + { + value: '四川省', + label: '四川省', + children: [ + { + value: '成都市', + label: '成都市', + children: [ + { value: '锦江区', label: '锦江区' }, + { value: '青羊区', label: '青羊区' }, + { value: '金牛区', label: '金牛区' }, + { value: '武侯区', label: '武侯区' }, + { value: '成华区', label: '成华区' }, + { value: '龙泉驿区', label: '龙泉驿区' }, + { value: '青白江区', label: '青白江区' }, + { value: '新都区', label: '新都区' }, + { value: '温江区', label: '温江区' }, + { value: '双流区', label: '双流区' }, + { value: '郫都区', label: '郫都区' }, + { value: '新津区', label: '新津区' } + ] + } + ] + } +] diff --git a/src/views/my-simcard/single-card/index.vue b/src/views/my-simcard/single-card/index.vue index 2515db9..a818d67 100644 --- a/src/views/my-simcard/single-card/index.vue +++ b/src/views/my-simcard/single-card/index.vue @@ -154,11 +154,11 @@
- 已使用流量(真) + 总流量(虚) {{ cardInfo.realUsedFlow || '0.00MB' }}
- 实际流量 + 已使用量(虚) {{ cardInfo.actualFlow || '0.00MB' }}
@@ -280,9 +280,9 @@ -
+ -
+ diff --git a/src/views/package-management/package-series/index.vue b/src/views/package-management/package-series/index.vue index 5eefaa0..b040d09 100644 --- a/src/views/package-management/package-series/index.vue +++ b/src/views/package-management/package-series/index.vue @@ -75,9 +75,11 @@ clearable style="flex: 1" /> - - 生成编码 - + @@ -407,6 +409,7 @@ import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue' import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue' import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue' + import CodeGeneratorButton from '@/components/business/CodeGeneratorButton.vue' import { formatDateTime } from '@/utils/business/format' import { CommonStatus, @@ -414,7 +417,6 @@ frontendStatusToApi, apiStatusToFrontend } from '@/config/constants' - import { generateSeriesCode } from '@/utils/codeGenerator' import { useRouter } from 'vue-router' defineOptions({ name: 'PackageSeries' }) @@ -938,14 +940,13 @@ }) } - // 生成系列编码 - const handleGenerateSeriesCode = () => { - form.series_code = generateSeriesCode() + // 处理编码生成 + const handleCodeGenerated = (code: string) => { + form.series_code = code // 清除该字段的验证错误提示 nextTick(() => { formRef.value?.clearValidate('series_code') }) - ElMessage.success('编码生成成功') } // 删除套餐系列 diff --git a/src/views/product/shop/index.vue b/src/views/product/shop/index.vue index 9246892..6051ffb 100644 --- a/src/views/product/shop/index.vue +++ b/src/views/product/shop/index.vue @@ -65,7 +65,18 @@ - +
+ + +
@@ -73,44 +84,37 @@ - - - + /> - - - - - - - - - - - - - - - + + @@ -298,7 +302,9 @@ ElTag, ElSwitch, ElSelect, - ElOption + ElOption, + ElTreeSelect, + ElCascader } from 'element-plus' import type { FormRules } from 'element-plus' import { useCheckedColumns } from '@/composables/useCheckedColumns' @@ -308,6 +314,7 @@ import ArtMenuRight from '@/components/core/others/ArtMenuRight.vue' import TableContextMenuHint from '@/components/core/others/TableContextMenuHint.vue' import type { MenuItemType } from '@/components/core/others/ArtMenuRight.vue' + import CodeGeneratorButton from '@/components/business/CodeGeneratorButton.vue' import { ShopService, RoleService } from '@/api/modules' import type { SearchFormItem } from '@/types' import type { ShopResponse, ShopRoleResponse } from '@/types/api' @@ -315,6 +322,7 @@ import { formatDateTime } from '@/utils/business/format' import { CommonStatus, getStatusText, STATUS_SELECT_OPTIONS } from '@/config/constants' import { RoutesAlias } from '@/router/routesAlias' + import { regionData } from '@/utils/constants/regionData' defineOptions({ name: 'Shop' }) @@ -336,7 +344,7 @@ const submitLoading = ref(false) const parentShopLoading = ref(false) const parentShopList = ref([]) - const searchParentShopList = ref([]) + const parentShopTreeData = ref([]) const defaultRoleLoading = ref(false) const defaultRoleList = ref([]) @@ -415,36 +423,19 @@ { label: '上级店铺', prop: 'parent_id', - type: 'select', - options: searchParentShopList.value.map((shop) => ({ - label: shop.shop_name, - value: shop.id - })), + type: 'tree-select', config: { + data: parentShopTreeData.value, clearable: true, filterable: true, - remote: true, - remoteMethod: handleSearchParentShop, - loading: parentShopLoading.value, - placeholder: '请选择或搜索上级店铺' - } - }, - { - label: '店铺层级', - prop: 'level', - type: 'select', - options: [ - { label: '1级', value: 1 }, - { label: '2级', value: 2 }, - { label: '3级', value: 3 }, - { label: '4级', value: 4 }, - { label: '5级', value: 5 }, - { label: '6级', value: 6 }, - { label: '7级', value: 7 } - ], - config: { - clearable: true, - placeholder: '请选择店铺层级' + checkStrictly: true, + renderAfterExpand: false, + props: { + label: 'shop_name', + value: 'id', + children: 'children' + }, + placeholder: '请选择上级店铺' } }, { @@ -463,7 +454,6 @@ const columnOptions = [ { label: '店铺名称', prop: 'shop_name' }, { label: '店铺编号', prop: 'shop_code' }, - { label: '层级', prop: 'level' }, { label: '所在地区', prop: 'region' }, { label: '联系人', prop: 'contact_name' }, { label: '联系电话', prop: 'contact_phone' }, @@ -487,6 +477,12 @@ formData.province = row.province || '' formData.city = row.city || '' formData.district = row.district || '' + // 编辑时回显地区 + if (row.province && row.city && row.district) { + formData.region = [row.province, row.city, row.district] + } else { + formData.region = [] + } formData.address = row.address || '' formData.contact_name = row.contact_name || '' formData.contact_phone = row.contact_phone || '' @@ -500,6 +496,7 @@ formData.shop_name = '' formData.shop_code = '' formData.parent_id = undefined + formData.region = [] formData.province = '' formData.city = '' formData.district = '' @@ -556,14 +553,6 @@ width: 140, showOverflowTooltip: true }, - { - prop: 'level', - label: '层级', - width: 80, - formatter: (row: ShopResponse) => { - return h(ElTag, { type: 'info', size: 'small' }, () => `${row.level}级`) - } - }, { prop: 'region', label: '所在地区', @@ -643,6 +632,7 @@ shop_name: '', shop_code: '', parent_id: undefined as number | undefined, + region: [] as string[], // 省市区级联数据 province: '', city: '', district: '', @@ -656,6 +646,28 @@ default_role_id: undefined as number | undefined }) + // 处理编码生成 + const handleCodeGenerated = (code: string) => { + formData.shop_code = code + // 清除该字段的验证错误提示 + nextTick(() => { + formRef.value?.clearValidate('shop_code') + }) + } + + // 处理地区选择变化 + const handleRegionChange = (value: string[]) => { + if (value && value.length === 3) { + formData.province = value[0] + formData.city = value[1] + formData.district = value[2] + } else { + formData.province = '' + formData.city = '' + formData.district = '' + } + } + // 搜索默认角色(仅加载客户角色) const searchDefaultRoles = async (query: string) => { defaultRoleLoading.value = true @@ -683,24 +695,26 @@ onMounted(() => { getShopList() loadParentShopList() - loadSearchParentShopList() searchDefaultRoles('') // 加载初始默认角色列表 }) - // 加载上级店铺列表(用于新增对话框,默认加载20条) + // 加载上级店铺列表(用于新增对话框和搜索栏,获取所有店铺构建树形结构) const loadParentShopList = async (shopName?: string) => { parentShopLoading.value = true try { const params: any = { page: 1, - pageSize: 20 + page_size: 9999 // 获取所有数据用于构建树形结构 } if (shopName) { params.shop_name = shopName } const res = await ShopService.getShops(params) if (res.code === 0) { - parentShopList.value = res.data.items || [] + const items = res.data.items || [] + parentShopList.value = items + // 构建树形数据 + parentShopTreeData.value = buildTreeData(items) } } catch (error) { console.error('获取上级店铺列表失败:', error) @@ -709,43 +723,6 @@ } } - // 加载搜索栏上级店铺列表(默认加载20条) - const loadSearchParentShopList = async (shopName?: string) => { - try { - const params: any = { - page: 1, - pageSize: 20 - } - if (shopName) { - params.shop_name = shopName - } - const res = await ShopService.getShops(params) - if (res.code === 0) { - searchParentShopList.value = res.data.items || [] - } - } catch (error) { - console.error('获取上级店铺列表失败:', error) - } - } - - // 搜索上级店铺(用于新增对话框) - const searchParentShops = (query: string) => { - if (query) { - loadParentShopList(query) - } else { - loadParentShopList() - } - } - - // 搜索上级店铺(用于搜索栏) - const handleSearchParentShop = (query: string) => { - if (query) { - loadSearchParentShopList(query) - } else { - loadSearchParentShopList() - } - } - // 获取店铺列表 const getShopList = async () => { loading.value = true