Эх сурвалжийг харах

feat: 🎸 综合实例重构中

chenkl 4 жил өмнө
parent
commit
5142e6e323

+ 49 - 18
mock/example/index.ts

@@ -1,34 +1,28 @@
 import Mock from 'mockjs'
 import { toAnyString } from '@/utils'
 
-const List: any[] = []
+let List: any[] = []
 const count = 100
 
 const baseContent = '<p>I am testing data, I am testing data.</p><p><img src="https://wpimg.wallstcn.com/4c69009c-0fd4-4153-b112-6cb53d1cf943"></p>'
-const image_uri = 'https://wpimg.wallstcn.com/e4558086-631c-425c-9430-56ffb46e70b3'
+// const image_uri = 'https://wpimg.wallstcn.com/e4558086-631c-425c-9430-56ffb46e70b3'
 
 for (let i = 0; i < count; i++) {
   List.push(Mock.mock({
     id: toAnyString(),
-    timestamp: +Mock.Random.date('T'),
+    // timestamp: +Mock.Random.date('T'),
     author: '@first',
-    reviewer: '@first',
     title: '@title(5, 10)',
-    content_short: 'mock data',
     content: baseContent,
-    forecast: '@float(0, 100, 2, 2)',
     importance: '@integer(1, 3)',
-    'type|1': ['CN', 'US', 'JP', 'EU'],
-    'status|1': ['published', 'draft', 'deleted'],
     display_time: '@datetime',
-    comment_disabled: true,
-    pageviews: '@integer(300, 5000)',
-    image_uri,
-    platforms: ['a-platform']
+    pageviews: '@integer(300, 5000)'
+    // image_uri
   }))
 }
 
 export default [
+  // 列表接口
   {
     url: 'http://mockjs.test.cn/example/list',
     type: 'get',
@@ -51,17 +45,33 @@ export default [
     }
   },
 
+  // 删除接口
   {
     url: 'http://mockjs.test.cn/example/delete',
     type: 'post',
     response: (config: any) => {
-      return {
-        code: '0000',
-        data: '删除成功'
+      const ids = config.body.ids
+      if (!ids) {
+        return {
+          code: '500',
+          message: '请选择需要删除的数据'
+        }
+      } else {
+        let i = List.length
+        while (i--) {
+          if (ids.indexOf(List[i].id) !== -1) {
+            List.splice(i, 1)
+          }
+        }
+        return {
+          code: '0000',
+          data: 'success'
+        }
       }
     }
   },
 
+  // 详情接口
   {
     url: 'http://mockjs.test.cn/example/detail',
     type: 'get',
@@ -78,13 +88,34 @@ export default [
     }
   },
 
+  // 保存接口
   {
     url: 'http://mockjs.test.cn/example/save',
     type: 'post',
     response: (config: any) => {
-      return {
-        code: '0000',
-        data: 'success'
+      const data = config.body
+      if (!data.id) {
+        List = [Object.assign(data, { id: toAnyString(), importance: Number(data.importance) })].concat(List)
+        return {
+          code: '0000',
+          data: 'success'
+        }
+      } else {
+        List.map(item => {
+          if (item.id === data.id) {
+            for (const key in item) {
+              if (key === 'importance') {
+                item[key] = Number(data[key])
+              } else {
+                item[key] = data[key]
+              }
+            }
+          }
+        })
+        return {
+          code: '0000',
+          data: 'success'
+        }
       }
     }
   }

+ 45 - 0
src/components/Dialog/index.vue

@@ -0,0 +1,45 @@
+<template>
+  <el-dialog
+    v-bind="getBindValue"
+    destroy-on-close
+    :close-on-click-modal="false"
+    top="10vh"
+  >
+    <template v-if="slots.title" #title>
+      <slot name="title" />
+    </template>
+
+    <!-- 弹窗内容 -->
+    <el-scrollbar class="com-dialog__content">
+      <slot />
+    </el-scrollbar>
+
+    <template v-if="slots.footer" #footer>
+      <slot name="footer" />
+    </template>
+  </el-dialog>
+</template>
+
+<script lang="ts">
+import { defineComponent, computed } from 'vue'
+export default defineComponent({
+  name: 'Dialog',
+  setup(props, { slots, attrs }) {
+    const getBindValue = computed((): any => {
+      const bindValue = { ...attrs, ...props }
+      return bindValue
+    })
+
+    return {
+      getBindValue,
+      slots
+    }
+  }
+})
+</script>
+
+<style lang="less" scoped>
+.com-dialog__content {
+  height: 600px;
+}
+</style>

+ 1 - 1
src/components/Editor/props.ts

@@ -11,7 +11,7 @@ export const editorProps = {
     default: () => {
       return {
         height: 500,
-        zIndex: 500,
+        zIndex: 0,
         placeholder: '请输入文本',
         focus: false,
         onchangeTimeout: 500,

+ 11 - 2
src/components/Table/index.vue

@@ -5,7 +5,8 @@
       <el-table-column
         v-if="selection"
         type="selection"
-        width="55"
+        :reserve-selection="reserveSelection"
+        width="40"
       />
       <template v-for="item in columns">
         <!-- 自定义索引 -->
@@ -85,17 +86,25 @@ export default defineComponent({
     TableColumn
   },
   props: {
+    // 表头
     columns: {
       type: Array as PropType<any[]>,
       default: () => []
     },
+    // 是否多选
     selection: {
       type: Boolean as PropType<boolean>,
       default: false
     },
+    // 是否展示分页
     pagination: {
       type: [Boolean, Object] as PropType<boolean | object>,
       default: false
+    },
+    // 仅对 type=selection 的列有效,类型为 Boolean,为 true 则会在数据更新之后保留之前选中的数据(需指定 row-key)
+    reserveSelection: {
+      type: Boolean as PropType<boolean>,
+      default: false
     }
   },
   setup(props, { attrs, slots }) {
@@ -137,7 +146,7 @@ export default defineComponent({
       }
     })
 
-    function headerDragend(newWidth: number, oldWidth: number, column: any, event: any) {
+    function headerDragend(newWidth: number, oldWidth: number, column: any) {
       // 不懂为啥无法自动计算宽度,只能手动去计算了。。失望ing,到时候看看能不能优化吧。
       const htmlArr = document.getElementsByClassName(column.id)
       for (const v of htmlArr) {

+ 2 - 2
src/components/index.ts

@@ -1,6 +1,6 @@
 import type { App } from 'vue'
-// import Button from '@/components/Button/index.vue'// Button组件
+import Dialog from './Dialog/index.vue'// Dialog组件
 
 export function setupGlobCom(app: App<Element>): void {
-  // app.component('AButton', Button)
+  app.component('ComDialog', Dialog)
 }

+ 55 - 7
src/hooks/useExample.ts

@@ -1,40 +1,88 @@
 // 常用的增删改查 hook
 import { reactive, ref } from 'vue'
+import { ElMessageBox } from 'element-plus'
+import { Message } from '_c/Message'
 
 interface DefalutParams {
-  pageIndex: number
-  pageSize: number
+  pageIndex: number // 页码
+  pageSize: number // 页数
+}
+
+interface DelsParmas {
+  noDataText?: string // 没有选中数据时的提示
+  text?: string // 删除前的提示
+  hiddenVerify?: boolean // 是否隐藏前置判断
 }
 
 export function useExample() {
+  // 请求接口的基本参数
   const defalutParams = reactive<DefalutParams>({
     pageIndex: 1,
     pageSize: 10
   })
-  
+
+  // 多选数据
+  const selectionData = ref<any[]>([])
+
+  // 表格数据
   const tableData = ref<any[]>([])
-  
+
+  // 表格加载状态
   const loading = ref<boolean>(true)
 
+  // 表格总条数
   const total = ref<number>(0)
-  
+
+  // 是否展示弹窗
+  const dialogVisible = ref<boolean>(false)
+
+  // 弹窗标题
+  const title = ref<string>('')
+
+  // 表格展示条目改变时候重置基本参数
   function sizeChange(val: number) {
     loading.value = true
     defalutParams.pageIndex = 1
     defalutParams.pageSize = val
   }
-  
+
+  // 表格分页改变时候重置基本参数
   function currentChange(val: number) {
     loading.value = true
     defalutParams.pageIndex = val
   }
 
+  // 删除多选
+  function delData(callBack: Function, config?: DelsParmas) {
+    if (selectionData.value.length === 0 && !config?.hiddenVerify) {
+      Message.warning(config?.noDataText || '请选择需要删除的数据!')
+      return
+    }
+    ElMessageBox.confirm(config?.text || '此操作将永久删除选中数据, 是否继续?', '提示', {
+      confirmButtonText: '确定',
+      cancelButtonText: '取消',
+      type: 'warning'
+    }).then(async() => {
+      await callBack()
+    })
+  }
+
+  // 多选变化的时候
+  function handleSelectionChange(selection: any[]) {
+    selectionData.value = selection
+  }
+
   return {
     defalutParams,
     tableData,
+    selectionData,
     loading,
     total,
+    dialogVisible,
+    title,
     sizeChange,
-    currentChange
+    currentChange,
+    delData,
+    handleSelectionChange
   }
 }

+ 0 - 18
src/pages/index/api/modules/example.ts

@@ -1,18 +0,0 @@
-import { fetch } from '../../axios-config/axios'
-
-import { AxiosPromise } from 'axios'
-
-import { EmptyObjFun } from '@/types/glob'
-
-interface PropsData {
-  params?: any
-  data?: any
-}
-
-const methods: EmptyObjFun = {
-  getExampleList: function({ params }: PropsData): AxiosPromise {
-    return fetch({ url: '/example/list', method: 'get', params })
-  }
-}
-
-export default methods

+ 8 - 0
src/pages/index/router/index.ts

@@ -170,6 +170,14 @@ export const asyncRouterMap: AppRouteRecordRaw[] = [
         meta: {
           title: 'markdown编辑器'
         }
+      },
+      {
+        path: 'dialog',
+        component: () => import('_p/index/views/components-demo/dialog/index.vue'),
+        name: 'DialogDemo',
+        meta: {
+          title: '弹窗'
+        }
       }
     ]
   },

+ 1 - 1
src/pages/index/store/modules/app.ts

@@ -28,7 +28,7 @@ class App extends VuexModule implements AppState {
   // public fixedTags = true // 是否固定标签栏
   // public fixedNavbar = true // 是否固定navbar
   public fixedHeader = true // 是否固定header
-  public layout = 'Top' // layout布局
+  public layout = 'Classic' // layout布局
   public showBreadcrumb = true // 是否显示面包屑
   public showHamburger = true // 是否显示侧边栏缩收按钮
   public showScreenfull = true // 是否全屏按钮

+ 38 - 0
src/pages/index/views/components-demo/dialog/index.vue

@@ -0,0 +1,38 @@
+<template>
+  <div>
+    <el-alert
+      effect="dark"
+      :closable="false"
+      title="对 Element 的 Dialog 组件进行二次封装,支持所有原生参数。"
+      type="info"
+      style="margin-bottom: 20px;"
+    />
+    <el-button type="primary" @click="visible = true">打开弹窗</el-button>
+
+    <com-dialog v-model="visible" title="提示">
+      <div style="height: 1000px;">
+        我是弹窗内容
+      </div>
+      <template #footer>
+        <el-button @click="visible = false">取消</el-button>
+        <el-button type="primary" @click="visible = false">确定</el-button>
+      </template>
+    </com-dialog>
+  </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, ref } from 'vue'
+export default defineComponent({
+  // name: 'DialogDemo',
+  setup() {
+    const visible = ref<boolean>(false)
+    return {
+      visible
+    }
+  }
+})
+</script>
+
+<style>
+</style>

+ 22 - 0
src/pages/index/views/example-demo/example/api.ts

@@ -0,0 +1,22 @@
+import { fetch } from '_p/index/axios-config/axios'
+
+interface PropsData {
+  params?: any
+  data?: any
+}
+
+export const getExampleListApi = ({ params }: PropsData): any => {
+  return fetch({ url: '/example/list', method: 'get', params })
+}
+
+export const delsExampApi = ({ data }: PropsData): any => {
+  return fetch({ url: '/example/delete', method: 'post', data })
+}
+
+export const saveExampApi = ({ data }: PropsData): any => {
+  return fetch({ url: '/example/save', method: 'post', data })
+}
+
+export const getExampDetApi = ({ params }: PropsData): any => {
+  return fetch({ url: '/example/detail', method: 'get', params })
+}

+ 182 - 0
src/pages/index/views/example-demo/example/components/IfnoWrite.vue

@@ -0,0 +1,182 @@
+<template>
+  <div>
+    <el-form
+      ref="formRef"
+      :model="form"
+      :rules="rules"
+      label-width="100px"
+    >
+      <el-row>
+        <el-col :span="24">
+          <el-form-item prop="title" label="标题">
+            <el-input v-model="form.title" placeholder="请输入标题" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item prop="author" label="作者">
+            <el-input v-model="form.author" placeholder="请输入作者" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item prop="display_time" label="创建时间">
+            <el-date-picker
+              v-model="form.display_time"
+              type="datetime"
+              placeholder="请选择创建时间"
+              style="width: 100%;"
+            />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item prop="importance" label="重要性">
+            <el-select v-model="form.importance" placeholder="请选择重要性" style="width: 100%;">
+              <el-option label="重要" value="3" />
+              <el-option label="良好" value="2" />
+              <el-option label="一般" value="1" />
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item prop="pageviews" label="阅读数">
+            <el-input-number
+              v-model="form.pageviews"
+              :min="0"
+              :max="99999999"
+              style="width: 100%;"
+            />
+          </el-form-item>
+        </el-col>
+        <el-col :span="24">
+          <el-form-item prop="content" label="内容">
+            <editor v-model:value="form.content" />
+          </el-form-item>
+        </el-col>
+      </el-row>
+    </el-form>
+    <div class="dialong__button--wrap">
+      <el-button @click="close">取消</el-button>
+      <el-button :loading="subLoading" type="primary" @click="setListData">保存</el-button>
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, reactive, ref, unref, PropType } from 'vue'
+import Editor from '_c/Editor/index.vue'
+import { Message } from '_c/Message'
+import { formatTime } from '@/utils'
+import { InfoWriteParams, InfoWriteRules } from './types'
+import { saveExampApi, getExampDetApi } from '../api'
+
+const requiredRule = {
+  required: true,
+  message: '该项为必填项'
+}
+
+export default defineComponent({
+  name: 'IfnoWrite',
+  components: {
+    Editor
+  },
+  props: {
+    info: {
+      type: Object as PropType<any>,
+      default: () => null
+    }
+  },
+  emits: ['close', 'success'],
+  setup(props, { emit }) {
+    const formRef = ref<HTMLElement | null>(null)
+    const subLoading = ref<boolean>(false)
+
+    const form = reactive<InfoWriteParams>({
+      id: '', // id
+      author: '', // 作者
+      title: '', // 标题
+      content: '', // 内容
+      importance: '', // 重要性
+      display_time: '', // 创建时间
+      pageviews: 0 // 阅读数
+    })
+
+    const rules = reactive<InfoWriteRules>({
+      title: [requiredRule],
+      author: [requiredRule],
+      content: [requiredRule],
+      importance: [requiredRule],
+      display_time: [requiredRule],
+      pageviews: [requiredRule]
+    })
+
+    async function getDet() {
+      if (props.info) {
+        const id = (props.info as any).id
+        try {
+          const res = await getExampDetApi({
+            params: {
+              id: id
+            }
+          })
+          if (res.code === '0000') {
+            for (const key in form) {
+              if (key === 'importance') {
+                form[key] = res.data[key].toString()
+              } else {
+                form[key] = res.data[key]
+              }
+            }
+          }
+        } catch (e) {
+          console.log(e)
+        }
+      }
+    }
+    getDet()
+
+    // 新增或者编辑
+    function setListData() {
+      const formRefWrap = unref(formRef as any)
+      try {
+        subLoading.value = true
+        formRefWrap.validate(async(valid: boolean) => {
+          if (valid) {
+            const formData = unref(form)
+            formData.display_time = formatTime(formData.display_time, 'yyyy-MM-dd HH:mm:ss')
+            const res = await saveExampApi({
+              data: formData
+            })
+            if (res.code === '0000') {
+              Message.success(form.id ? '编辑成功' : '新增成功')
+              emit('success', form.id ? 'edit' : 'add')
+            }
+          } else {
+            console.log('error submit!!')
+            return false
+          }
+        })
+      } catch (err) {
+        console.log(err)
+      } finally {
+        subLoading.value = false
+      }
+    }
+
+    function close() {
+      emit('close')
+    }
+
+    return {
+      formRef,
+      subLoading,
+      form,
+      rules,
+      setListData,
+      close,
+      getDet
+    }
+  }
+})
+</script>
+
+<style>
+</style>

+ 18 - 0
src/pages/index/views/example-demo/example/components/types.ts

@@ -0,0 +1,18 @@
+export interface InfoWriteParams {
+  title: string
+  id?: string
+  author: string
+  content: string
+  importance: string
+  display_time: string
+  pageviews: number
+}
+
+export interface InfoWriteRules {
+  title?: any[]
+  author?: any[]
+  content?: any[]
+  importance?: any[]
+  display_time?: any[]
+  pageviews?: any[]
+}

+ 159 - 11
src/pages/index/views/example-demo/example/index.vue

@@ -1,7 +1,25 @@
 <template>
   <div>
+    <div class="search__example--wrap">
+      <search
+        :data="searchData"
+        @search-submit="searchSubmit"
+        @reset-submit="resetSubmit"
+      />
+    </div>
+
+    <div class="button__example--wrap">
+      <el-button type="primary" icon="el-icon-circle-plus-outline" @click="open(false)">新增</el-button>
+      <el-button
+        type="danger"
+        icon="el-icon-delete"
+        @click="dels"
+      >删除</el-button>
+    </div>
+
     <com-table
       v-loading="loading"
+      selection
       :columns="columns"
       :data="tableData"
       :pagination="{
@@ -10,17 +28,55 @@
         onSizeChange: handleSizeChange,
         onCurrentChange: handleCurrentChange
       }"
-    />
+      @selection-change="handleSelectionChange"
+    >
+      <template #importance="scope">
+        <el-tag
+          :type="scope.row.importance === 3
+            ? 'success'
+            : (scope.row.importance === 2
+              ? 'warning'
+              : 'danger')"
+        >{{ scope.row.importance === 3
+          ? '重要'
+          : (scope.row.importance === 2
+            ? '良好'
+            : '一般') }}
+        </el-tag>
+      </template>
+      <template #action="scope">
+        <el-button type="primary" size="mini" @click="open(scope.row)">编辑</el-button>
+        <el-button type="danger" size="mini" @click="dels(scope.row)">删除</el-button>
+      </template>
+    </com-table>
+
+    <com-dialog v-model="dialogVisible" :title="title">
+      <ifno-write :info="info" @close="close" @success="success" />
+    </com-dialog>
   </div>
 </template>
 
 <script lang="ts">
-import { defineComponent } from 'vue'
+import { defineComponent, ref } from 'vue'
 import ComTable from '_c/Table/index.vue'
 import Search from '_c/Search/index.vue'
+import IfnoWrite from './components/IfnoWrite.vue'
+
 import { useExample } from '@/hooks/useExample'
+import { Message } from '_c/Message'
+
+import { getExampleListApi, delsExampApi } from './api'
 
-import api from '_p/index/api'
+const searchData = [
+  {
+    label: '标题',
+    value: '',
+    itemType: 'input',
+    field: 'title',
+    placeholder: '请输入标题',
+    clearable: true
+  }
+]
 
 const columns = [
   {
@@ -38,11 +94,22 @@ const columns = [
   },
   {
     key: 'importance',
-    label: '重要性'
+    label: '重要性',
+    slots: {
+      default: 'importance'
+    }
   },
   {
     key: 'pageviews',
     label: '阅读数'
+  },
+  {
+    key: 'action',
+    label: '操作',
+    width: '150px',
+    slots: {
+      default: 'action'
+    }
   }
 ]
 
@@ -50,17 +117,33 @@ export default defineComponent({
   // name: 'Example',
   components: {
     ComTable,
-    Search
+    Search,
+    IfnoWrite
   },
   setup() {
-    const { defalutParams, tableData, loading, total, currentChange, sizeChange } = useExample()
+    const info = ref<any>(null)
 
-    async function getExampleList() {
+    const {
+      defalutParams,
+      tableData,
+      loading,
+      total,
+      dialogVisible,
+      title,
+      currentChange,
+      sizeChange,
+      handleSelectionChange,
+      selectionData,
+      delData
+    } = useExample()
+
+    // 请求数据
+    async function getExampleList(data?: any): Promise<void> {
       try {
-        const res = await api.example.getExampleList({
-          params: defalutParams
+        const res = await getExampleListApi({
+          params: Object.assign(defalutParams, data || {})
         })
-        if (res) {
+        if (res.code === '0000') {
           total.value = res.data.total
           tableData.value = res.data.list
         }
@@ -69,28 +152,93 @@ export default defineComponent({
       }
     }
 
+    // 查询
+    function searchSubmit(data: any) {
+      // 该方法重置了一些默认参数
+      currentChange(1)
+      getExampleList(data)
+    }
+
+    // 重置
+    function resetSubmit(data: any) {
+      // 该方法重置了一些默认参数
+      currentChange(1)
+      getExampleList(data)
+    }
+
+    // 展示多少条
     function handleSizeChange(val: number) {
       // 该方法重置了一些默认参数
       sizeChange(val)
       getExampleList()
     }
 
+    // 展示第几页
     function handleCurrentChange(val: number) {
       // 该方法重置了一些默认参数
       currentChange(val)
       getExampleList()
     }
 
+    // 删除多选
+    function dels(item?: any) {
+      delData(async() => {
+        let ids: string[]
+        if (item.id) {
+          ids = [item.id]
+        } else {
+          ids = selectionData.value.map((v: any) => {
+            return v.id
+          })
+        }
+        const res = await delsExampApi({
+          data: { ids }
+        })
+        if (res.code === '0000') {
+          Message.success('删除成功!')
+          getExampleList()
+        }
+      }, { hiddenVerify: item.id })
+    }
+
+    // 打开弹窗
+    function open(row: any) {
+      title.value = row ? '编辑' : '新增'
+      info.value = row || null
+      dialogVisible.value = true
+    }
+
+    // 弹窗关闭
+    function close() {
+      dialogVisible.value = false
+    }
+
+    // 成功之后的回调
+    function success(type: string) {
+      if (type === 'add') {
+        currentChange(1)
+      }
+      close()
+      getExampleList()
+    }
+
     getExampleList()
 
     return {
+      info, open,
+      searchData, searchSubmit, resetSubmit,
       columns,
       defalutParams,
       loading,
       tableData,
       total,
+      title,
+      dialogVisible,
       handleSizeChange,
-      handleCurrentChange
+      handleCurrentChange,
+      handleSelectionChange,
+      dels,
+      close, success
     }
   }
 })

+ 15 - 1
src/styles/glob.less

@@ -4,4 +4,18 @@
   &:hover {
     background: rgba(0, 0, 0, .025);
   }
-}
+}
+
+// 综合实例样式
+.search__example--wrap {
+  padding: 20px 10px 0 10px;
+  background: #fff;
+  margin-bottom: 15px;
+}
+.button__example--wrap {
+  margin-bottom: 10px;
+}
+.dialong__button--wrap {
+  text-align: center;
+}
+// 综合实例样式

+ 28 - 0
src/utils/index.ts

@@ -83,3 +83,31 @@ export function param2Obj(url: string) {
       '"}'
   )
 }
+
+/**
+ * @param {date} time 需要转换的时间
+ * @param {String} fmt 需要转换的格式 如 yyyy-MM-dd、yyyy-MM-dd HH:mm:ss
+ */
+export function formatTime(time: any, fmt: string) {
+  if (!time) return ''
+  else {
+    const date = new Date(time)
+    const o = {
+      'M+': date.getMonth() + 1,
+      'd+': date.getDate(),
+      'H+': date.getHours(),
+      'm+': date.getMinutes(),
+      's+': date.getSeconds(),
+      'q+': Math.floor((date.getMonth() + 3) / 3),
+      'S': date.getMilliseconds()
+    }
+    if (/(y+)/.test(fmt)) fmt = fmt.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length))
+    for (const k in o) {
+      if (new RegExp('(' + k + ')').test(fmt)) {
+        fmt = fmt.replace(RegExp.$1, (RegExp.$1.length === 1) ? (o[k]) : ((
+          '00' + o[k]).substr(('' + o[k]).length)))
+      }
+    }
+    return fmt
+  }
+}