feat: 明细表中的选人组件可以将文字部分保存到独立字段,以加快渲染速度

This commit is contained in:
gaoyunqi
2024-05-16 17:13:32 +08:00
parent 92fd90f35d
commit 21eb71d35d
4 changed files with 479 additions and 427 deletions

View File

@ -34,7 +34,7 @@
const emits = defineEmits(['change', 'changeNames', 'close', 'options-change', 'update:value']); const emits = defineEmits(['change', 'changeNames', 'close', 'options-change', 'update:value']);
import { getDepartmentTrees } from '/@/api/system/department'; import { getDepartmentTrees } from '/@/api/system/department';
import { CloseOutlined } from '@ant-design/icons-vue'; import { CloseOutlined } from '@ant-design/icons-vue';
import { camelCaseString } from '/@/utils/event/design'; import { camelCaseString } from '/@/utils/stringUtil';
const visible = ref(false); const visible = ref(false);
const departNames = ref(); const departNames = ref();
@ -76,7 +76,7 @@
(val) => { (val) => {
if (val) { if (val) {
if (props.sepTextField && !valChanged.value) { if (props.sepTextField && !valChanged.value) {
departNames.value = props.row[camelCaseString(props.sepTextField, true)]; departNames.value = props.row[camelCaseString(props.sepTextField)];
} else { } else {
getDefaultList(val); getDefaultList(val);
} }
@ -99,9 +99,14 @@
let nameList = selectedNodes.value.map((item) => item.name); let nameList = selectedNodes.value.map((item) => item.name);
const names = nameList.join(','); const names = nameList.join(',');
departNames.value = names; departNames.value = names;
if (props.sepTextField) { updateSepTextField(names);
props.row[camelCaseString(props.sepTextField, true)] = names; }
function updateSepTextField(v) {
if (!props.sepTextField || !props.row) {
return;
} }
props.row[props.sepTextField] = v;
} }
function resetTreeList(list) { function resetTreeList(list) {
@ -142,9 +147,7 @@
const ids = idList.join(','); const ids = idList.join(',');
emits('update:value', ids); emits('update:value', ids);
departNames.value = names; departNames.value = names;
if (props.sepTextField) { updateSepTextField(names);
props.row[camelCaseString(props.sepTextField, true)] = names;
}
} }
function deleteItem() { function deleteItem() {

View File

@ -1,423 +1,455 @@
<template> <template>
<div :class="{ disabled }" class="form-select-user" @click="show"> <div :class="{ disabled }" class="form-select-user" @click="show">
<a-input v-model:value="userNames" :disabled="disabled" :placeholder="placeholder" :size="size" readonly> <a-input v-model:value="userNames" :disabled="disabled" :placeholder="placeholder" :size="size" readonly>
<template v-if="prefix" #prefix> <template v-if="prefix" #prefix>
<Icon :icon="prefix" /> <Icon :icon="prefix" />
</template> </template>
<template v-if="suffix" #suffix> <template v-if="suffix" #suffix>
<Icon :icon="suffix" /> <Icon :icon="suffix" />
</template> </template>
</a-input> </a-input>
<ModalPanel :visible="visible" :width="900" :title="t('选择人员')" @submit="submit" @close="close" <ModalPanel :title="t('选择人员')" :visible="visible" :width="900" class="select-user-model" @close="close" @submit="submit">
class="select-user-model"> <div class="select-user">
<div class="select-user"> <div class="select-user-left">
<div class="select-user-left"> <a-tabs v-model:activeKey="activeKey" :tabBarGutter="20">
<a-tabs v-model:activeKey="activeKey" :tabBarGutter="20"> <a-tab-pane key="department" tab="组织架构"></a-tab-pane>
<a-tab-pane key="department" tab="组织架构"> <a-tab-pane key="allPerson" tab="所有人"></a-tab-pane>
</a-tab-pane> </a-tabs>
<a-tab-pane key="allPerson" tab="所有人"> <div class="person-search">
</a-tab-pane> <a-form-item label="" name="name">
</a-tabs> <a-input v-model:value="selectPersonNames" :placeholder="searchPlaceholder" :size="size" class="search-input"></a-input>
<div class="person-search"> </a-form-item>
<a-form-item label="" name="name"> </div>
<a-input v-model:value="selectPersonNames" :placeholder="searchPlaceholder" :size="size" <div v-show="activeKey === 'department'" class="select-user-box">
class="search-input"> <div class="department-tree">
</a-input> <a-spin :spinning="treeLoading" class="loading-box" />
</a-form-item> <div class="department-tree-box">
</div> <SelectDepartmentTreeV2 v-if="visible && activeKey === 'department'" class="tree-select" @changeValue="departChange" @queryCompleted="departCompleted"></SelectDepartmentTreeV2>
<div class="select-user-box" v-show="activeKey === 'department'"> </div>
<div class="department-tree"> </div>
<a-spin class="loading-box" :spinning="treeLoading" /> <div class="user-select-box">
<div class="department-tree-box"> <SelectUserListV2 :data="searchDepartMemberList" emptyDescription="暂无人员" @selectId="changeDepMemberSelect"></SelectUserListV2>
<!-- <div class="department-tree-title sub-title"> <div v-if="false" class="user-select-pagination">
组织 <a-pagination v-model:current="searchDepartMemberParams.limit" :pageSize="searchDepartMemberParams.size" :total="searchDepartMemberTotal" />
</div> --> </div>
<SelectDepartmentTreeV2 @changeValue="departChange" class="tree-select" </div>
v-if="visible && activeKey === 'department'" @queryCompleted="departCompleted"> </div>
</SelectDepartmentTreeV2> <div v-show="activeKey === 'allPerson'" class="all-user-select-box">
</div> <SelectUserListV2 :data="searchAllMemberList" @selectId="changeMemberSelect"></SelectUserListV2>
<div v-if="searchAllMemberTotal > 25" class="all-user-select-pagination">
<a-form-item label="" name="pagination">
<a-pagination
v-model:current="searchAllMemberParams.limit"
:pageSize="searchAllMemberParams.size"
:show-size-changer="false"
:show-total="(total) => `${total}`"
:total="searchAllMemberTotal"
hideOnSinglePage
showLessItems
size="small"
@change="changeAllMemberPage"
/>
</a-form-item>
</div>
</div>
</div>
<div class="select-user-right">
<!-- <div class="selected-user-title sub-title">
已选列表
</div> -->
<SelectUserListV2 :data="selectedMemberList" canDel emptyDescription="暂无已选择人员<br> 请从左侧添加人员" viewList @delId="delMember"></SelectUserListV2>
</div>
</div> </div>
<div class="user-select-box"> </ModalPanel>
<!-- <div class="user-select-title sub-title"> </div>
可选列表
</div> -->
<SelectUserListV2 :data="searchDepartMemberList" @selectId="changeDepMemberSelect"
emptyDescription="暂无人员">
</SelectUserListV2>
<div class="user-select-pagination" v-if="false">
<a-pagination v-model:current="searchDepartMemberParams.limit" :pageSize="searchDepartMemberParams.size"
:total="searchDepartMemberTotal" />
</div>
</div>
</div>
<div class="all-user-select-box" v-show="activeKey === 'allPerson'">
<!-- <div class="all-user-select-title sub-title">
可选列表
</div> -->
<SelectUserListV2 :data="searchAllMemberList" @selectId="changeMemberSelect"></SelectUserListV2>
<div class="all-user-select-pagination" v-if="searchAllMemberTotal > 25">
<a-form-item label="" name="pagination">
<a-pagination size="small" showLessItems :total="searchAllMemberTotal" :show-size-changer="false"
v-model:current="searchAllMemberParams.limit" :pageSize="searchAllMemberParams.size"
@change="changeAllMemberPage" hideOnSinglePage :show-total="total => `${total}`" />
</a-form-item>
</div>
</div>
</div>
<div class="select-user-right">
<!-- <div class="selected-user-title sub-title">
已选列表
</div> -->
<SelectUserListV2 :data="selectedMemberList" viewList canDel @delId="delMember"
emptyDescription="暂无已选择人员<br> 请从左侧添加人员">
</SelectUserListV2>
</div>
</div>
</ModalPanel>
</div>
</template> </template>
<script lang="ts" setup> <script setup>
import { watch, ref } from 'vue'; import { watch, ref } from 'vue';
import { cloneDeep } from 'lodash-es'; import { cloneDeep } from 'lodash-es';
import { useI18n } from '/@/hooks/web/useI18n'; import { useI18n } from '/@/hooks/web/useI18n';
import { ModalPanel } from '/@/components/ModalPanel/index'; import { ModalPanel } from '/@/components/ModalPanel/index';
import { getUserPageListNew, getUserMulti } from '/@/api/system/user'; import { getUserPageListNew, getUserMulti } from '/@/api/system/user';
import SelectDepartmentTreeV2 from './SelectDepartmentTreeV2.vue' import SelectDepartmentTreeV2 from './SelectDepartmentTreeV2.vue';
import SelectUserListV2 from './SelectUserListV2.vue'; import SelectUserListV2 from './SelectUserListV2.vue';
const emits = defineEmits(['update:value', 'selectedId', 'change']); import { camelCaseString } from '/@/utils/stringUtil';
const { t } = useI18n();
const userNames = ref('')
const treeLoading = ref(true)
const visible = ref(false)
const activeKey = ref('department')
const selectedMemberList = ref([])
const selectPersonNames = ref('')
const props = defineProps({ const emits = defineEmits(['update:value', 'selectedId', 'change']);
value: { const { t } = useI18n();
type: String, const userNames = ref('');
default: '' const treeLoading = ref(true);
}, const visible = ref(false);
prefix: String, const activeKey = ref('department');
suffix: String, const selectedMemberList = ref([]);
placeholder: String, const selectPersonNames = ref('');
readonly: Boolean, const valChanged = ref(false);
disabled: Boolean,
size: String,
multiple: {
type: Boolean,
default: true
}
});
let timeoutId = null;
let resetMemberList = []
const searchPlaceholder = '请输入姓名或工号搜索'
// 防止输入多次调用搜索接口导致问题,使用防抖
watch(selectPersonNames, (newValue) => {
if (newValue && activeKey.value !== 'allPerson') {
activeKey.value = 'allPerson'
}
if (timeoutId) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(() => { const props = defineProps({
// 调用搜索接口 value: {
console.log('执行搜索API操作, 查询参数为:', newValue); type: String,
searchNameChanged(newValue) default: ''
}, 1000); },
prefix: String,
suffix: String,
placeholder: String,
readonly: Boolean,
disabled: Boolean,
size: String,
multiple: {
type: Boolean,
default: true
},
row: Object, // 行数据,在表格里用的到
sepTextField: String // 将文本表示存储在独立字段
});
let timeoutId = null;
let resetMemberList = [];
const searchPlaceholder = '请输入姓名或工号搜索';
}); function updateSepTextField(v) {
watch(activeKey, (newValue) => { if (!props.sepTextField || !props.row) {
searchAllMemberList.value = [] return;
searchAllMemberTotal.value = 0 }
searchDepartMemberList.value = [] props.row[camelCaseString(props.sepTextField)] = v;
treeLoading.value = true
if (newValue !== 'allPerson') {
selectPersonNames.value = ''
} else {
if (!timeoutId) {
searchNameChanged('')
} }
}
})
watch(props, async () => {
if (props.value && !resetMemberList.length) {
const list = await getUserMulti(props.value)
selectedMemberList.value = list
resetMemberList = cloneDeep(list)
const nameList = resetMemberList.map(item => {
return item.name
})
userNames.value = nameList.join(',')
}
}, {
immediate: true
})
const searchDepartMemberParams = ref({
limit: 1,
size: 10000,
departmentId: ''
})
const searchDepartMemberTotal = ref(0)
const searchDepartMemberList = ref()
const searchAllMemberParams = ref({
limit: 1,
size: 25,
isSearchAll: true,
keyword: ''
})
const searchAllMemberTotal = ref(0)
const searchAllMemberList = ref()
async function departChange(e) {
searchDepartMemberParams.value.departmentId = e[0].id
let res = await getUserList(searchDepartMemberParams.value)
searchDepartMemberTotal.value = res.total
searchDepartMemberList.value = setSelected(res.list)
}
async function searchNameChanged(val) {
searchAllMemberParams.value.limit = 1
searchAllMemberParams.value.keyword = val
getAllMemberList()
}
async function getAllMemberList() {
let res = await getUserList(searchAllMemberParams.value)
searchAllMemberTotal.value = res.total
searchAllMemberList.value = setSelected(res.list)
} // 防止输入多次调用搜索接口导致问题,使用防抖
function setSelected(list) { watch(selectPersonNames, (newValue) => {
const idList = selectedMemberList.value.map(item => item.id) if (newValue && activeKey.value !== 'allPerson') {
const ids = idList.join(',') activeKey.value = 'allPerson';
list.forEach(item => { }
if (ids?.includes(item.id)) { if (timeoutId) {
item.selected = true clearTimeout(timeoutId);
} else { }
item.selected = false
}
})
return list
}
async function getUserList(params) {
return await getUserPageListNew(params)
}
function departCompleted() {
treeLoading.value = false
}
function changeDepMemberSelect(id) {
let list = searchDepartMemberList.value
searchDepartMemberList.value = handleListSelected(list, id)
}
function changeMemberSelect(id) {
let list = searchAllMemberList.value
searchAllMemberList.value = handleListSelected(list, id)
}
function handleListSelected(list, id) {
let selected = false
let selectedItem
list.forEach(item => {
if (item.id === id) {
item.selected = !!!item.selected
selected = item.selected
selectedItem = item
}
})
let memberList = selectedMemberList.value
if (selected) {
memberList.push(selectedItem)
selectedMemberList.value = memberList
} else {
selectedMemberList.value = memberList.filter(m => m.id !== id)
}
return list
}
function changeAllMemberPage(page) { timeoutId = setTimeout(() => {
searchAllMemberParams.value.limit = page // 调用搜索接口
getAllMemberList() console.log('执行搜索API操作, 查询参数为:', newValue);
} searchNameChanged(newValue);
function show() { }, 1000);
visible.value = true });
treeLoading.value = true watch(activeKey, (newValue) => {
} searchAllMemberList.value = [];
function delMember(id) { searchAllMemberTotal.value = 0;
if (activeKey.value === 'department') { searchDepartMemberList.value = [];
changeDepMemberSelect(id) treeLoading.value = true;
} else { if (newValue !== 'allPerson') {
changeMemberSelect(id) selectPersonNames.value = '';
} } else {
} if (!timeoutId) {
function submit() { searchNameChanged('');
let idsList = [] }
let namesList = [] }
selectedMemberList.value.forEach(item => { });
idsList.push(item.id) watch(
namesList.push(item.name) props,
}) async () => {
const ids = idsList.join(',') if (props.value && !valChanged.value && props.sepTextField) {
const names = namesList.join(',') const idArr = props.value.split(',');
resetMemberList = cloneDeep(selectedMemberList.value) const valStr = props.row[camelCaseString(props.sepTextField)];
userNames.value = names const valArr = valStr.split(',');
emits('update:value', ids); userNames.value = valStr;
emits('selectedId', ids); const initValue = idArr.map((id, index) => {
emits('change', ids, selectedMemberList.value); return {
close(); id,
} name: valArr[index]
function close() { };
selectedMemberList.value = cloneDeep(resetMemberList) });
visible.value = false; selectedMemberList.value = initValue;
activeKey.value = 'department' resetMemberList = cloneDeep(initValue);
searchDepartMemberList.value = [] return;
searchAllMemberList.value = [] }
} if (props.value && !resetMemberList.length) {
const list = await getUserMulti(props.value);
selectedMemberList.value = list;
resetMemberList = cloneDeep(list);
const nameList = resetMemberList.map((item) => {
return item.name;
});
userNames.value = nameList.join(',');
}
},
{
immediate: true
}
);
const searchDepartMemberParams = ref({
limit: 1,
size: 10000,
departmentId: ''
});
const searchDepartMemberTotal = ref(0);
const searchDepartMemberList = ref([]);
const searchAllMemberParams = ref({
limit: 1,
size: 25,
isSearchAll: true,
keyword: ''
});
const searchAllMemberTotal = ref(0);
const searchAllMemberList = ref([]);
async function departChange(e) {
searchDepartMemberParams.value.departmentId = e[0].id;
let res = await getUserList(searchDepartMemberParams.value);
searchDepartMemberTotal.value = res.total;
searchDepartMemberList.value = setSelected(res.list);
}
async function searchNameChanged(val) {
searchAllMemberParams.value.limit = 1;
searchAllMemberParams.value.keyword = val;
getAllMemberList();
}
async function getAllMemberList() {
let res = await getUserList(searchAllMemberParams.value);
searchAllMemberTotal.value = res.total;
searchAllMemberList.value = setSelected(res.list);
}
function setSelected(list) {
const idList = selectedMemberList.value.map((item) => item.id);
const ids = idList.join(',');
list.forEach((item) => {
if (ids?.includes(item.id)) {
item.selected = true;
} else {
item.selected = false;
}
});
return list;
}
async function getUserList(params) {
return await getUserPageListNew(params);
}
function departCompleted() {
treeLoading.value = false;
}
function changeDepMemberSelect(id) {
let list = searchDepartMemberList.value;
searchDepartMemberList.value = handleListSelected(list, id);
}
function changeMemberSelect(id) {
let list = searchAllMemberList.value;
searchAllMemberList.value = handleListSelected(list, id);
}
function handleListSelected(list, id) {
let selected = false;
let selectedItem;
list.forEach((item) => {
if (item.id === id) {
item.selected = !!!item.selected;
selected = item.selected;
selectedItem = item;
}
});
let memberList = selectedMemberList.value;
if (selected) {
memberList.push(selectedItem);
selectedMemberList.value = memberList;
} else {
selectedMemberList.value = memberList.filter((m) => m.id !== id);
}
return list;
}
function changeAllMemberPage(page) {
searchAllMemberParams.value.limit = page;
getAllMemberList();
}
function show() {
visible.value = true;
treeLoading.value = true;
}
function delMember(id) {
if (activeKey.value === 'department') {
changeDepMemberSelect(id);
} else {
changeMemberSelect(id);
}
}
function submit() {
let idsList = [];
let namesList = [];
selectedMemberList.value.forEach((item) => {
idsList.push(item.id);
namesList.push(item.name);
});
const ids = idsList.join(',');
const names = namesList.join(',');
resetMemberList = cloneDeep(selectedMemberList.value);
userNames.value = names;
updateSepTextField(names);
emits('update:value', ids);
emits('selectedId', ids);
emits('change', ids, selectedMemberList.value);
valChanged.value = false;
close();
}
function close() {
selectedMemberList.value = cloneDeep(resetMemberList);
visible.value = false;
activeKey.value = 'department';
searchDepartMemberList.value = [];
searchAllMemberList.value = [];
}
</script> </script>
<style lang="less"> <style lang="less">
.select-user-model { .select-user-model {
.content { .content {
margin: 0 !important; margin: 0 !important;
} }
} }
</style> </style>
<style lang="less" scoped> <style lang="less" scoped>
.sub-title { .sub-title {
height: 30px; height: 30px;
padding: 0 8px; padding: 0 8px;
display: flex;
align-items: center;
justify-content: space-between;
color: rgba(144, 147, 153, 0.7);
}
.select-user {
display: flex;
height: 100%;
// border: 1px solid #eaeaea;
.select-user-left {
width: 75%;
height: 100%;
border-right: 1px solid #eaeaea;
display: flex;
flex-direction: column;
:deep(.ant-tabs-nav) {
margin-bottom: 0;
.ant-tabs-nav-wrap {
padding-left: 10px;
}
}
.person-search {
padding: 4px;
:deep(.ant-form-item) {
margin: 0;
}
.search-input {
border: none;
}
}
.select-user-box {
flex: 1;
min-height: 0;
display: flex;
border-top: 1px solid #eaeaea;
.department-tree {
position: relative;
width: 65%;
height: 100%;
border-right: 1px solid #eaeaea;
.loading-box {
position: absolute;
top: 49%;
left: 49%;
}
.department-tree-box {
// overflow: scroll;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
padding: 4px 8px;
.department-tree-title {}
.tree-select {
flex: 1;
min-height: 0;
overflow-y: scroll;
}
}
}
.user-select-box {
flex: 1;
min-height: 0;
height: 100%;
display: flex; display: flex;
flex-direction: column;
.user-select-title {}
.user-select-list {
flex: 1;
min-height: 0;
overflow-y: scroll;
.user-select-item {
height: 30px;
padding: 0 4px;
}
}
.user-select-pagination {
height: 40px;
display: flex;
justify-content: center;
align-items: center;
}
}
}
.all-user-select-box {
flex: 1;
min-height: 0;
height: 100%;
display: flex;
flex-direction: column;
.all-user-select-title {}
.all-user-select-pagination {
height: 40px;
display: flex;
justify-content: center;
align-items: center; align-items: center;
justify-content: space-between;
.ant-form-item { color: rgba(144, 147, 153, 0.7);
margin-bottom: 0;
}
}
} }
.select-user {
display: flex;
height: 100%;
// border: 1px solid #eaeaea;
} .select-user-left {
width: 75%;
height: 100%;
border-right: 1px solid #eaeaea;
display: flex;
flex-direction: column;
.select-user-right { :deep(.ant-tabs-nav) {
width: 25%; margin-bottom: 0;
height: 100%;
display: flex;
flex-direction: column;
.selected-user-title {} .ant-tabs-nav-wrap {
} padding-left: 10px;
} }
}
.person-search {
padding: 4px;
:deep(.ant-form-item) {
margin: 0;
}
.search-input {
border: none;
}
}
.select-user-box {
flex: 1;
min-height: 0;
display: flex;
border-top: 1px solid #eaeaea;
.department-tree {
position: relative;
width: 65%;
height: 100%;
border-right: 1px solid #eaeaea;
.loading-box {
position: absolute;
top: 49%;
left: 49%;
}
.department-tree-box {
// overflow: scroll;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
padding: 4px 8px;
.department-tree-title {
}
.tree-select {
flex: 1;
min-height: 0;
overflow-y: scroll;
}
}
}
.user-select-box {
flex: 1;
min-height: 0;
height: 100%;
display: flex;
flex-direction: column;
.user-select-title {
}
.user-select-list {
flex: 1;
min-height: 0;
overflow-y: scroll;
.user-select-item {
height: 30px;
padding: 0 4px;
}
}
.user-select-pagination {
height: 40px;
display: flex;
justify-content: center;
align-items: center;
}
}
}
.all-user-select-box {
flex: 1;
min-height: 0;
height: 100%;
display: flex;
flex-direction: column;
.all-user-select-title {
}
.all-user-select-pagination {
height: 40px;
display: flex;
justify-content: center;
align-items: center;
.ant-form-item {
margin-bottom: 0;
}
}
}
}
.select-user-right {
width: 25%;
height: 100%;
display: flex;
flex-direction: column;
.selected-user-title {
}
}
}
</style> </style>

View File

@ -27,25 +27,22 @@ export function changeToPinyin(label: string, isUpper?: boolean) {
/* 如果没有下划线,不需要处理 /* 如果没有下划线,不需要处理
如果有下划线,用下划线切割,第一个下划线左边的全部小写,后面的首字母大写,首字母后面的全部小写 */ 如果有下划线,用下划线切割,第一个下划线左边的全部小写,后面的首字母大写,首字母后面的全部小写 */
export function camelCaseString(string: string, skipNoUnderline = false) { export function camelCaseString(string: string) {
if (!string) return; if (!string) return;
if (skipNoUnderline && string.indexOf('_') < 0) { const stringLower = string.toLowerCase();
return string; const len = stringLower.length;
} let str = '';
const stringLower = string.toLowerCase(); for (let i = 0; i < len; i++) {
const len = stringLower.length; const c = stringLower.charAt(i);
let str = ''; if (c === '_') {
for (let i = 0; i < len; i++) { if (++i < len) {
const c = stringLower.charAt(i); str = str.concat(stringLower.charAt(i).toUpperCase());
if (c === '_') { }
if (++i < len) { } else {
str = str.concat(stringLower.charAt(i).toUpperCase()); str = str.concat(c);
} }
} else {
str = str.concat(c);
} }
} return str;
return str;
} }
export async function apiConfigFunc(apiConfig, isCustomForm = false, formModel?, index?) { export async function apiConfigFunc(apiConfig, isCustomForm = false, formModel?, index?) {

20
src/utils/stringUtil.js Normal file
View File

@ -0,0 +1,20 @@
export function camelCaseString(string) {
if (!string) return;
if (string.indexOf('_') < 0) {
return string;
}
const stringLower = string.toLowerCase();
const len = stringLower.length;
let str = '';
for (let i = 0; i < len; i++) {
const c = stringLower.charAt(i);
if (c === '_') {
if (++i < len) {
str = str.concat(stringLower.charAt(i).toUpperCase());
}
} else {
str = str.concat(c);
}
}
return str;
}