Explorar o código

page: payment record 初稿.

peter hai 1 ano
pai
achega
3d81968361

+ 0 - 0
.eslintrc.js → .eslintrc.cjs


+ 2 - 1
.gitignore

@@ -1,7 +1,8 @@
 .DS_Store
 node_modules
 /dist
-
+.eslintcache
+package-lock.json
 
 # local env files
 .env.local

+ 17 - 15
package.json

@@ -1,6 +1,7 @@
 {
   "name": "crm_extend",
   "version": "1.0.0",
+  "type": "module",
   "scripts": {
     "build": "vue-tsc --noEmit && vite build",
     "build-test": "vue-tsc --noEmit && vite build --mode test",
@@ -11,28 +12,29 @@
   "dependencies": {
     "@element-plus/icons-vue": "^2.1.0",
     "axios": "~0.27.2",
-    "dayjs": "^1.11.8",
-    "element-plus": "2.3.9",
+    "dayjs": "^1.11.10",
+    "element-plus": "2.4.3",
     "html2canvas": "^1.4.1",
     "jspdf": "^2.5.1",
     "lodash": "^4.17.21",
-    "vue": "^3.3.4",
-    "vue-router": "^4.2.2"
+    "vue": "^3.3.8",
+    "vue-router": "^4.2.2",
+    "xlsx": "^0.18.5"
   },
   "devDependencies": {
-    "@typescript-eslint/eslint-plugin": "^6.4.1",
-    "@typescript-eslint/parser": "^6.4.1",
-    "@vitejs/plugin-vue": "^4.2.3",
+    "@nabla/vite-plugin-eslint": "^2.0.2",
+    "@typescript-eslint/eslint-plugin": "^ 6.12.0",
+    "@typescript-eslint/parser": "^ 6.12.0",
+    "@vitejs/plugin-vue": "^4.5.0",
     "@vue/eslint-config-prettier": "^8.0.0",
-    "eslint": "^8.40.0",
+    "eslint": "^8.54.0",
     "eslint-config-prettier": "^9.0.0",
-    "eslint-plugin-vue": "^9.12.0",
-    "prettier": "^3.0.2",
-    "sass": "^1.63.2",
+    "eslint-plugin-vue": "^9.18.1",
+    "prettier": "^3.1.0",
+    "sass": "^1.69.5",
     "sass-loader": "^13.3.1",
-    "typescript": "~5.1.6",
-    "vite": "^4.3.9",
-    "vite-plugin-eslint": "^1.8.1",
-    "vue-tsc": "^1.6.5"
+    "typescript": "~5.2.2",
+    "vite": "^5.0.2",
+    "vue-tsc": "^1.8.22"
   }
 }

+ 6 - 1
src/interface.ts

@@ -102,11 +102,16 @@ export interface ICompanyItem {
   taxReimbursement?: boolean
 }
 
+interface IRole {
+  name: string
+  id: number
+}
 export interface IUser {
   id: number
   email: string
   users_id: string
   full_name: string
   Organization: string
-  [x: string]: string | number
+  role: IRole
+  [x: string]: string | number | object
 }

+ 339 - 0
src/pages/payment-record/components/edit.vue

@@ -0,0 +1,339 @@
+<template>
+  <div class="dialog-edit-record-item">
+    <el-dialog
+      v-model="dialogVisible"
+      width="800px"
+      :title="editMode === 1 ? 'New line' : 'Edit'"
+      :close-on-click-modal="false"
+      :close-on-press-escape="false"
+      :before-close="handleClose"
+    >
+      <el-form
+        ref="mainForm"
+        :rules="formRule"
+        :model="form"
+        label-width="140px"
+      >
+        <div class="flex start form-box">
+          <div class="flex-auto">
+            <el-form-item
+              label="PO Number"
+              prop="po_number"
+            >
+              <el-input
+                v-model="form.po_number"
+                placeholder="eg: POXXXX"
+              />
+            </el-form-item>
+
+            <el-form-item
+              label="Unit Price"
+              prop="unit_price"
+            >
+              <el-input
+                v-model="form.unit_price"
+                type="number"
+                min="0"
+              />
+            </el-form-item>
+
+            <el-form-item
+              label="Sample Fee"
+              prop="sample_fee"
+            >
+              <el-input
+                v-model="form.sample_fee"
+                type="number"
+                min="1"
+              />
+            </el-form-item>
+
+            <el-form-item
+              label="Quantity"
+              prop="quantity"
+            >
+              <el-input
+                v-model="form.quantity"
+                type="number"
+                min="1"
+              />
+            </el-form-item>
+
+            <el-form-item
+              label="Total"
+              prop="total"
+            >
+              <el-input
+                v-model="form.total"
+                type="number"
+                min="0"
+              />
+            </el-form-item>
+          </div>
+
+          <div class="flex-auto">
+            <!-- <el-form-item
+              label="PO ID"
+              prop="po_id"
+            >
+              <el-input
+                v-model="form.po_id"
+                placeholder="eg: 4791186000111831822"
+              />
+            </el-form-item> -->
+            <el-form-item
+              label="Currency"
+              prop="currency"
+            >
+              <el-select
+                v-model="form.currency"
+                :disabled="disableFlag"
+                style="width: 100%"
+              >
+                <el-option
+                  v-for="option in currencyList as IOptionItem[]"
+                  :key="option.value"
+                  :label="option.label"
+                  :value="option.value"
+                ></el-option>
+              </el-select>
+            </el-form-item>
+
+            <el-form-item
+              label="Statement Name"
+              prop="statement_name"
+            >
+              <el-select
+                v-model="form.statement_name"
+                style="width: 100%"
+              >
+                <el-option
+                  v-for="option in statementList as IOptionItem[]"
+                  :key="option.value"
+                  :label="option.label"
+                  :value="option.value"
+                ></el-option>
+              </el-select>
+            </el-form-item>
+
+            <el-form-item
+              label="Setup Service Fee"
+              prop="setup_service_fee"
+            >
+              <el-input
+                v-model="form.setup_service_fee"
+                type="number"
+              />
+            </el-form-item>
+
+            <el-form-item label="Description">
+              <el-input
+                v-model="form.description"
+                type="textarea"
+                rows="3"
+              />
+            </el-form-item>
+          </div>
+        </div>
+
+        <br />
+        <div class="flex end">
+          <el-button
+            type="primary"
+            @click="save(mainForm)"
+          >
+            Save
+          </el-button>
+        </div>
+      </el-form>
+    </el-dialog>
+  </div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue'
+export default defineComponent({
+  name: 'EditIPoItem',
+})
+</script>
+
+<script lang="ts" setup>
+import { ref, watchEffect } from 'vue'
+import {
+  ElMessage,
+  ElDialog,
+  ElForm,
+  ElFormItem,
+  ElSelect,
+  ElOption,
+  ElInput,
+  ElButton,
+} from 'element-plus'
+import type { FormInstance, FormRules } from 'element-plus'
+import { IPoItem, IOptionItem } from '../inteface'
+
+const props = defineProps({
+  visible: {
+    type: Boolean,
+    default: false,
+  },
+  disableFlag: {
+    type: Boolean,
+    default: false,
+  },
+  lockedCurrency: {
+    type: String,
+    default: '',
+  },
+  currentEditRow: {
+    type: Object,
+    default: () => {
+      return {}
+    },
+  },
+  currencyList: {
+    type: Array,
+    default: () => {
+      return []
+    },
+  },
+  statementList: {
+    type: Array,
+    default: () => {
+      return [] as string[]
+    },
+  },
+  editMode: {
+    type: Number,
+    default: 1,
+  },
+})
+
+const dialogVisible = ref(false)
+
+const mainForm = ref<FormInstance>()
+const form = ref({} as IPoItem)
+const formRule = ref<FormRules>({
+  po_number: {
+    required: true,
+    message: '必填项',
+    trigger: 'blur',
+  },
+  // po_id: {
+  //   required: true,
+  //   message: '必填项',
+  //   trigger: 'blur',
+  // },
+  statement_name: {
+    required: true,
+    message: '必填项',
+    trigger: 'blur',
+  },
+  total: {
+    required: true,
+    message: '必填项',
+    trigger: 'blur',
+  },
+  unit_price: {
+    required: true,
+    message: '必填项',
+    trigger: 'blur',
+  },
+  quantity: {
+    required: true,
+    message: '必填项',
+    trigger: 'blur',
+  },
+  sample_fee: {
+    required: true,
+    message: '必填项',
+    trigger: 'blur',
+  },
+  setup_service_fee: {
+    required: true,
+    message: '必填项',
+    trigger: 'blur',
+  },
+})
+watchEffect(() => {
+  dialogVisible.value = props.visible
+  form.value = Object.assign(
+    {
+      po_number: '',
+      // po_id: '',
+      unit_price: '',
+      quantity: '',
+      sample_fee: '',
+      setup_service_fee: '',
+      total: '',
+      currency: 'CNY',
+      statement_name: '',
+      description: '',
+    },
+    JSON.parse(JSON.stringify(props.currentEditRow)),
+  )
+  if (props.disableFlag) {
+    form.value.currency = props.lockedCurrency
+  }
+})
+
+const emit = defineEmits(['update:visible', 'edit', 'add'])
+
+const handleClose = function (done: any) {
+  emit('update:visible', false)
+
+  if (typeof done === 'function') {
+    done()
+  }
+}
+const save = function (formEl: FormInstance | undefined) {
+  if (!formEl) return
+  formEl.validate((valid, fields) => {
+    if (valid) {
+      if (props.editMode === 1) {
+        emit('add', form.value)
+      } else if (props.editMode === 2) {
+        emit('edit', form.value)
+      }
+    } else {
+      console.log('check form has not pass!', fields)
+      ElMessage.error('请检查表单必填项')
+    }
+  })
+}
+</script>
+
+<style lang="scss">
+.dialog-edit-record-item {
+  input[type='number'] {
+    -moz-appearance: textfield;
+    appearance: textfield;
+    &:hover {
+      -moz-appearance: textfield;
+      appearance: textfield;
+      &::-webkit-inner-spin-button,
+      &::-webkit-outer-spin-button {
+        -webkit-appearance: none;
+        margin: 0;
+      }
+    }
+    &::-webkit-inner-spin-button,
+    &::-webkit-outer-spin-button {
+      -webkit-appearance: none;
+      margin: 0;
+    }
+  }
+}
+</style>
+<style lang="scss" scoped>
+.form-box {
+  .flex-auto {
+    &:first-of-type {
+      padding-right: 12px;
+    }
+    & + .flex-auto {
+      padding-left: 12px;
+    }
+  }
+}
+</style>

+ 337 - 0
src/pages/payment-record/components/upload.vue

@@ -0,0 +1,337 @@
+<template>
+  <div>
+    <el-dialog
+      v-model="dialogVisible"
+      width="750px"
+      title="Upload Statement"
+      :show-close="false"
+      :close-on-click-modal="false"
+      :close-on-press-escape="false"
+      :before-close="handleClose"
+    >
+      <div
+        v-loading="loading"
+        class="flex start"
+      >
+        <el-form
+          ref="mainForm"
+          :rules="formRule"
+          class="flex-auto"
+          :model="form"
+          label-width="150px"
+        >
+          <el-form-item
+            label="Statement Name"
+            prop="statement_name"
+          >
+            <el-input
+              v-model="form.statement_name"
+              style="width: 190px"
+              type="textarea"
+              rows="3"
+            />
+          </el-form-item>
+
+          <el-form-item label="Currency">
+            <el-select v-model="form.currency">
+              <el-option
+                v-for="option in currencyList as IOptionItem[]"
+                :key="option.value"
+                :label="option.label"
+                :value="option.value"
+              ></el-option>
+            </el-select>
+          </el-form-item>
+
+          <el-form-item label="Payment Type">
+            <el-select v-model="form.paymentType">
+              <el-option
+                v-for="option in paymentOption"
+                :key="option.value"
+                :label="option.label"
+                :value="option.value"
+              ></el-option>
+            </el-select>
+          </el-form-item>
+
+          <el-form-item label="Upload Mode">
+            <el-select v-model="form.mode">
+              <el-option
+                v-for="option in uploadOption"
+                :key="option.value"
+                :label="option.label"
+                :value="option.value"
+              ></el-option>
+            </el-select>
+          </el-form-item>
+        </el-form>
+        <div
+          class="flex-auto drag-area"
+          @dragenter="stop"
+          @dragover="stop"
+          @dragleave="stop"
+          @drop="processExcel"
+        >
+          <label for="fileInput">
+            <div
+              class="flex column stretch"
+              style="text-align: center; padding: 44px 20px; cursor: pointer"
+            >
+              <div>
+                <el-icon
+                  size="60px"
+                  color="#999"
+                >
+                  <upload-filled />
+                </el-icon>
+              </div>
+              <br />
+              <div class="el-upload__text">拖动文件到这或者点击选择</div>
+              <br />
+              <div class="el-upload">
+                单个Excel数据最好控制在100行内, 处理起来会慢
+              </div>
+            </div>
+          </label>
+        </div>
+      </div>
+      <br />
+      <div class="flex end">
+        <el-button
+          :loading="loading"
+          @click="handleClose"
+        >
+          关闭
+        </el-button>
+        <el-button
+          type="primary"
+          :loading="loading"
+          @click="next(mainForm)"
+        >
+          确认
+        </el-button>
+      </div>
+    </el-dialog>
+    <!-- multiple -->
+    <input
+      id="fileInput"
+      type="file"
+      style="display: none"
+      accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-excel"
+      @change="processExcel"
+    />
+  </div>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue'
+export default defineComponent({
+  name: 'DialogUploadExcel',
+})
+</script>
+<script lang="ts" setup>
+import { watchEffect, ref } from 'vue'
+import {
+  ElDialog,
+  ElMessage,
+  ElForm,
+  ElFormItem,
+  ElSelect,
+  ElOption,
+  ElInput,
+  ElButton,
+  ElIcon,
+} from 'element-plus'
+
+import { UploadFilled } from '@element-plus/icons-vue'
+import * as XLSX from 'xlsx'
+import type { FormInstance, FormRules } from 'element-plus'
+import { IPoItem, IOptionItem } from '../inteface'
+import request from '@/utils/axios'
+
+const props = defineProps({
+  visible: {
+    type: Boolean,
+    default: false,
+  },
+  currencyList: {
+    type: Array,
+    default: () => {
+      return []
+    },
+  },
+})
+
+const emit = defineEmits(['update:visible', 'update-table-data'])
+
+const dialogVisible = ref(false)
+
+watchEffect(() => {
+  dialogVisible.value = props.visible
+})
+
+const tableData = ref([] as IPoItem[])
+
+const handleClose = function (done: any) {
+  emit('update:visible', false)
+  tableData.value = []
+  form.value = {
+    statement_name: '',
+    currency: 'CNY',
+    mode: 'Replace',
+    paymentType: '货款',
+  }
+  const target = document.getElementById('fileInput') as HTMLInputElement
+  if (target) {
+    target.value = ''
+  }
+
+  if (typeof done === 'function') {
+    done()
+  }
+}
+const mainForm = ref<FormInstance>()
+const formRule = ref<FormRules>({
+  statement_name: {
+    required: true,
+    message: '必填项',
+    trigger: 'blur',
+  },
+})
+const form = ref({
+  statement_name: '',
+  currency: 'CNY',
+  mode: 'Replace',
+  paymentType: '货款',
+})
+const uploadOption = [
+  {
+    label: '追加(Append)',
+    value: 'Append',
+  },
+  {
+    label: '替换(Replace)',
+    value: 'Replace',
+  },
+]
+
+const paymentOption = [
+  {
+    label: '货款',
+    value: '货款',
+  },
+  {
+    label: '快递款',
+    value: '快递款',
+  },
+]
+
+const stop = (e: any) => {
+  e.preventDefault()
+  e.stopPropagation()
+}
+const processExcel = (event: any) => {
+  const files = event.target.files || event.dataTransfer.files
+  // console.log('files:', files)
+  let str = ''
+  let arr: IPoItem[] = []
+  tableData.value = []
+  try {
+    for (let i = 0; i < files.length; i++) {
+      if (
+        ![
+          'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+          'application/vnd.ms-excel',
+        ].includes(files[i].type)
+      ) {
+        ElMessage.error('读取数据出错, 请确认选择了正确的Excel文件')
+        return
+      }
+      str =
+        str +
+        `${str.length ? ', ' : ''}` +
+        (files[i].name.replace(/\.xlsx?/, '') || 'unNameFile')
+      const fileReader = new FileReader()
+
+      fileReader.onload = (e: any) => {
+        const data = XLSX.read(e.target.result, { type: 'binary' })
+        // 重命名列名
+        data.Sheets[data.SheetNames[0]].A1.w = 'po_number'
+        data.Sheets[data.SheetNames[0]].B1.w = 'sku'
+        data.Sheets[data.SheetNames[0]].C1.w = 'description'
+        data.Sheets[data.SheetNames[0]].D1.w = 'unit_price'
+        data.Sheets[data.SheetNames[0]].E1.w = 'quantity'
+        data.Sheets[data.SheetNames[0]].F1.w = 'sample_fee'
+        data.Sheets[data.SheetNames[0]].G1.w = 'setup_service_fee'
+        data.Sheets[data.SheetNames[0]].H1.w = 'total'
+
+        const jsonData = XLSX.utils.sheet_to_json(
+          data.Sheets[data.SheetNames[0]],
+        ) as IPoItem[]
+        jsonData.forEach((i) => {
+          tableData.value.push(i)
+        })
+        // tableData.value = tableData.value.concat(jsonData)
+      }
+
+      fileReader.readAsBinaryString(files[i])
+    }
+
+    form.value.statement_name = str
+  } catch (error) {
+    console.log('处理文件出错:', error)
+  }
+
+  event.preventDefault()
+  event.stopPropagation()
+}
+const loading = ref(false)
+const next = (formEl: FormInstance | undefined) => {
+  if (!formEl) return
+  formEl.validate((valid, fields) => {
+    if (valid) {
+      loading.value = true
+      request
+        .post('/payment_request/createStatementData', [
+          {
+            Name: form.value.statement_name,
+          },
+        ])
+        .then((response) => {
+          if (response.data.code !== 1) return
+          const res = response.data.result
+
+          let result = {
+            mode: form.value.mode,
+            data: tableData.value.map((i) => {
+              return {
+                ...i,
+                payment_type: form.value.paymentType,
+                statement_name: form.value.statement_name,
+                currency: form.value.currency,
+                statement_id: res.data[0].details.id,
+              }
+            }),
+          }
+          emit('update-table-data', result)
+          handleClose(false)
+        })
+        .finally(() => {
+          loading.value = false
+        })
+    } else {
+      console.log('check form has not pass!', fields)
+      ElMessage.error('请检查表单必填项')
+    }
+  })
+}
+</script>
+
+<style lang="scss" scoped>
+.drag-area {
+  height: 240px;
+  border: 1px solid #ddd;
+  border-radius: 4px;
+  // padding: 24px;
+}
+</style>

+ 551 - 0
src/pages/payment-record/index.vue

@@ -0,0 +1,551 @@
+<template>
+  <div class="page-payment-record">
+    <div
+      v-if="loading"
+      v-loading="true"
+      style="width: 100%; height: 100vh"
+      class=""
+      element-loading-text="Loading..."
+      element-loading-background="rgba(0, 0, 0, 0.3)"
+    ></div>
+    <div
+      v-else
+      class="main-content"
+    >
+      <div class="flex btn-wrap">
+        <el-button
+          :disabled="multipleSelection.length < 1"
+          type="danger"
+          @click="onDelete"
+        >
+          Delete
+        </el-button>
+        <el-button @click="downloadSample">Download XLSX sample</el-button>
+
+        <el-button
+          type="primary"
+          @click="dialogVisible = true"
+        >
+          Upload Statement
+        </el-button>
+        <el-button @click="addRow">Add New Line</el-button>
+        <el-button
+          :disabled="tableData.length < 1"
+          type="primary"
+          @click="save"
+        >
+          Save
+        </el-button>
+      </div>
+      <div class="po-table">
+        <el-table
+          :data="computedTableData"
+          @selection-change="handleSelectionChange"
+        >
+          <el-table-column
+            type="selection"
+            width="55"
+          />
+          <el-table-column
+            prop="payment_type"
+            label="Payment Type"
+            width="120"
+            align="center"
+          />
+          <el-table-column
+            prop="statement_name"
+            label="Statement Name"
+            min-width="130px"
+            align="center"
+          />
+          <el-table-column
+            prop="po_number"
+            label="PO Number"
+            align="center"
+            width="110"
+          />
+          <el-table-column
+            prop="sku"
+            label="sku"
+            align="center"
+            width="80px"
+          />
+          <el-table-column
+            prop="description"
+            label="Description"
+            align="center"
+          />
+          <el-table-column
+            prop="unit_price"
+            label="Unit Price"
+            align="center"
+          />
+          <el-table-column
+            prop="quantity"
+            label="Quantity"
+            align="center"
+            width="100px"
+          />
+          <el-table-column
+            prop="sample_fee"
+            label="Sample Fee"
+            align="center"
+          />
+          <el-table-column
+            prop="setup_service_fee"
+            label="Setup Service Fee"
+            align="center"
+          />
+          <el-table-column
+            prop="total"
+            label="Total"
+            align="center"
+          />
+          <!-- <el-table-column
+                prop="sales_person"
+                label="sales_person"
+                align="center"
+              /> -->
+          <el-table-column
+            label="Action"
+            width="80px"
+          >
+            <template #default="scope">
+              <el-button
+                size="small"
+                type="warning"
+                @click="editRow(scope.row, scope.$index)"
+              >
+                Edit
+              </el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+        <br />
+        <div class="flex end"></div>
+        <br />
+        <div
+          class="flex between"
+          style="align-items: flex-end"
+        >
+          <el-pagination
+            v-model:currentPage="currentPage"
+            v-model:pageSize="pageSize"
+            layout="prev, pager, next, jumper, sizes"
+            :page-sizes="[5, 10, 15, 20, 40, 100]"
+            :total="tableData.length"
+            @current-change="multipleSelection = []"
+            @size-change="multipleSelection = []"
+          />
+          <div class="total-data">
+            <div class="flex">
+              <div>Total line:</div>
+              <div>{{ tableData.length }}</div>
+            </div>
+            <div class="flex">
+              <div class="">Sum Total:</div>
+              <div class="">{{ computedSum }}</div>
+            </div>
+            <div class="flex">
+              <div class="">Currency:</div>
+              <div>{{ tableData.length > 0 ? tableData[0].currency : '' }}</div>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <!-- <el-tabs
+        v-model="currentTab"
+        v-loading="loading"
+        type="card"
+        class=""
+      >
+        <el-tab-pane
+          label="待上传列表"
+          name="upload"
+        >
+        </el-tab-pane>
+        <el-tab-pane
+          label="待确认列表"
+          name="list"
+        >
+          TODO
+        </el-tab-pane>
+      </el-tabs> -->
+    </div>
+    <dialog-upload
+      v-model:visible="dialogVisible"
+      v-model:currencyList="currencyList"
+      @update-table-data="updateTableData"
+    ></dialog-upload>
+    <edit-item
+      v-model:visible="dialogEditRowVisible"
+      v-model:currencyList="currencyList"
+      v-model:currentEditRow="computedCurrentEditRow"
+      v-model:editMode="editMode"
+      v-model:disableFlag="currentDisableFlag"
+      :statement-list="computedStatementList"
+      :locked-currency="tableData.length ? tableData[0].currency : 'CNY'"
+      @edit="onEditRow"
+      @add="onAddRow"
+    ></edit-item>
+  </div>
+</template>
+<script lang="ts">
+export default defineComponent({
+  name: 'PaymentRecord',
+})
+</script>
+
+<script lang="ts" setup>
+import { defineComponent, ref, computed } from 'vue'
+import {
+  ElButton,
+  ElTable,
+  ElTableColumn,
+  // ElTabs,
+  // ElTabPane,
+  ElPagination,
+  ElMessage,
+  ElNotification,
+} from 'element-plus'
+import { useRoute } from 'vue-router'
+import dialogUpload from './components/upload.vue'
+import editItem from './components/edit.vue'
+import { IUser } from '@/interface'
+import { IPoItem } from './inteface'
+import request from '@/utils/axios'
+import utils from '@/utils/index'
+import * as XLSX from 'xlsx'
+
+const loading = ref(false)
+
+const multipleSelection = ref<IPoItem[]>([])
+
+const handleSelectionChange = (val: IPoItem[]) => {
+  multipleSelection.value = val
+}
+const onDelete = function () {
+  const target = multipleSelection.value
+  tableData.value = tableData.value.filter((i) => {
+    return !target.includes(i)
+  })
+}
+
+const sheetData = [
+  {
+    PO_Number: '',
+    SKU: '',
+    Description: '',
+    Unit_Price: '',
+    Quantity: '',
+    Sample_Fee: '',
+    Setup_Service_Fee: '',
+    Total: '',
+  },
+]
+
+const downloadSample = function () {
+  const sheet1 = XLSX.utils.json_to_sheet(sheetData)
+  const wb = XLSX.utils.book_new()
+  sheet1['!cols'] = [
+    { wpx: 80 },
+    { wpx: 80 },
+    { wpx: 80 },
+    { wpx: 80 },
+    { wpx: 80 },
+    { wpx: 80 },
+    { wpx: 120 },
+    { wpx: 80 },
+  ]
+  XLSX.utils.book_append_sheet(wb, sheet1, 'sheet1')
+
+  XLSX.writeFile(wb, '模版.xlsx')
+}
+
+const tableData = ref([] as IPoItem[])
+const computedSum = computed(() => {
+  return tableData.value.reduce((total, current) => {
+    total = total + Number(current.total)
+    return total
+  }, 0)
+})
+const updateTableData = (p: { data: IPoItem[]; mode: string }) => {
+  if (p.mode === 'Append') {
+    tableData.value = tableData.value.concat(p.data)
+  } else {
+    tableData.value = p.data
+  }
+}
+
+const currentDisableFlag = computed(() => {
+  return tableData.value.length > 0
+})
+
+const currentPage = ref(1)
+const pageSize = ref(15)
+// 手动分页. 可能会有很多条数据
+const computedTableData = computed(() => {
+  return tableData.value.length < pageSize.value
+    ? tableData.value
+    : tableData.value.slice(
+        (currentPage.value - 1) * pageSize.value,
+        tableData.value.length > currentPage.value * pageSize.value
+          ? currentPage.value * pageSize.value
+          : tableData.value.length,
+      )
+})
+const computedStatementList = computed(() => {
+  const result: string[] = []
+  tableData.value.forEach((i) => {
+    if (i.statement_name?.length && !result.includes(i.statement_name)) {
+      result.push(i.statement_name)
+    }
+  })
+  return result.map((i) => {
+    return {
+      value: i,
+      label: i,
+    }
+  })
+})
+const dialogEditRowVisible = ref(false)
+const currentEditIndex = ref(-1) // -1新增, 其余则为当前页的行号
+const editMode = ref(1) // 1新增 2编辑
+
+const computedCurrentEditRow = computed(() => {
+  if (currentEditIndex.value > -1) {
+    return tableData.value[
+      (currentPage.value - 1) * pageSize.value + currentEditIndex.value
+    ]
+  }
+  return {}
+})
+const addRow = function () {
+  editMode.value = 1
+  currentEditIndex.value = -1
+  dialogEditRowVisible.value = true
+}
+
+const editRow = function (row: IPoItem, index: number) {
+  // console.log('index', index)
+  // console.log(row)
+  editMode.value = 2
+  currentEditIndex.value = index
+  dialogEditRowVisible.value = true
+}
+
+const onAddRow = function (data: IPoItem) {
+  tableData.value.push(data)
+  dialogEditRowVisible.value = false
+}
+const onEditRow = function (data: IPoItem) {
+  dialogEditRowVisible.value = false
+  tableData.value.splice(
+    (currentPage.value - 1) * pageSize.value + currentEditIndex.value,
+    1,
+    data,
+  )
+}
+
+const save = function () {
+  if (!['Finance Manager', 'CEO'].includes(userInfo.value.role.name)) {
+    ElMessage.error('当前用户没有处理的权限')
+    return
+  }
+  const formData = tableData.value.map((i, index) => {
+    const result: any = {
+      unit_price: i.unit_price,
+      quantity: i.quantity,
+      sample_fee: i.sample_fee,
+      setup_service_fee: i.setup_service_fee,
+      total: i.total,
+      currency: i.currency,
+      description: i.description,
+      PO_id: i.po_number,
+      Statement: { name: i.statement_name, id: i.statement_id },
+      Request_Type: '月结申请',
+      Name: '/',
+      Unit_Price_Non_Currency: i.unit_price.toString(),
+      Owner: '',
+      Payment_Status: 'Pending Verify',
+      Batch_number: new Date().getTime().toString(),
+    }
+    // if (index % 3 !== 0) {
+    //   result.Statement = { name: i.statement_name, id: i.statement_id }
+    // } else {
+    // // 用来测试分批保存的, 这个数据故意写错接口就回出错
+    //   result.Statement = i.statement_name
+    // }
+    return result
+  })
+
+  let size = 100
+  const dataList = utils.splitArray(formData, size)
+  const pool = []
+  for (let i = 0; i < dataList.length; i++) {
+    pool.push(
+      send(
+        dataList[i],
+        i,
+        size,
+        i === dataList.length - 1 ? formData.length : 0,
+      ),
+    )
+  }
+  Promise.all(pool).then(() => {
+    tableData.value = []
+  })
+}
+
+const send = (
+  data: IPoItem[],
+  currentPage = 0,
+  pageSize = 1,
+  finalValue: number,
+) => {
+  return new Promise((resolve, reject) => {
+    request
+      .post('/payment_request/createPaymentRequest', data)
+      .then((response) => {
+        if (response.data.code !== 1) {
+          ElNotification({
+            type: 'error',
+            duration: 0,
+            title: '创建异常',
+            message: `第 ${currentPage * pageSize + 1} ~ ${
+              finalValue || (currentPage + 1) * pageSize
+            } 行数据创建异常`,
+          })
+          reject(0)
+          return
+        }
+        const res = response.data.result
+
+        if (Array.isArray(res.data)) {
+          const temp = res.data.map((i: any, index: number) => {
+            return {
+              status: i.status,
+              index,
+            }
+          })
+
+          const temp2 = temp.filter((i: any) => i.status !== 'success')
+
+          if (temp2.length) {
+            ElNotification({
+              type: 'error',
+              duration: 0,
+              title: '创建异常',
+              message: `第 ${temp2
+                .map((i: any) => i.index + 1 + currentPage * pageSize)
+                .join(',')} 行数据创建异常`,
+            })
+            reject(0)
+          } else {
+            ElNotification({
+              duration: 0,
+              title: '创建成功',
+              type: 'success',
+              message: `第 ${currentPage * pageSize + 1} ~ ${
+                finalValue || (currentPage + 1) * pageSize
+              } 行数据创建成功`,
+            })
+            resolve(1)
+          }
+        }
+      })
+  })
+}
+
+const dialogVisible = ref(false)
+
+// upload list
+// const currentTab = ref('upload')
+
+const currencyList = ref([
+  {
+    label: 'CNY',
+    value: 'CNY',
+  },
+  {
+    label: 'USD',
+    value: 'USD',
+  },
+  {
+    label: 'HKD',
+    value: 'HKD',
+  },
+  {
+    label: 'AUD',
+    value: 'AUD',
+  },
+  {
+    label: 'GBP',
+    value: 'GBP',
+  },
+  {
+    label: 'NZD',
+    value: 'NZD',
+  },
+  {
+    label: 'EUR',
+    value: 'EUR',
+  },
+])
+
+const route = useRoute()
+
+const userInfo = ref({} as IUser)
+loading.value = true
+request
+  .post('/common/getUsersData', { id: route.query.user })
+  .then((response) => {
+    const res = response.data
+    if (res.code !== 1) return
+
+    if (res.result.users && res.result.users.length) {
+      userInfo.value = res.result.users[0]
+    } else if (res.result.id) {
+      userInfo.value = res.result || {}
+    } else if (Array.isArray(res.result) && res.result.length) {
+      userInfo.value = res.result[0] || {}
+    } else {
+      ElMessage.error('获取当前用户身份异常, 请联系管理员')
+    }
+  })
+  .finally(() => {
+    loading.value = false
+  })
+</script>
+
+<style lang="scss" scoped>
+.page-payment-record {
+}
+.main-content {
+  background-color: #fff;
+  padding: 12px 40px;
+  width: 1600px;
+  min-width: 1200px;
+  min-height: 100vh;
+  margin: 0 auto;
+  box-shadow:
+    0 0 0 1px rgba(255, 255, 255, 0.4) inset,
+    0 0.5em 1em rgba(0, 0, 0, 0.6);
+}
+.btn-wrap {
+  width: 1600px;
+  min-width: 1200px;
+  padding: 12px 0;
+  margin: 0 auto;
+}
+.po-table {
+  width: 100%;
+
+  margin: 0 auto;
+}
+.total-data {
+  width: 150px;
+  line-height: 22px;
+}
+</style>

+ 21 - 0
src/pages/payment-record/inteface.ts

@@ -0,0 +1,21 @@
+export interface IPoItem {
+  po_number: string
+  // po_id: string | number
+  sku: string
+  description: string
+  unit_price: number
+  quantity: number
+  sample_fee: number
+  setup_service_fee: number
+  total: number
+  currency: string
+  statement_name: string
+  statement_id?: number | string
+  payment_type?: string
+  sales_person?: string
+}
+
+export interface IOptionItem {
+  label: string
+  value: string
+}

+ 5 - 0
src/route.ts

@@ -20,6 +20,11 @@ const router = createRouter({
         },
       ],
     },
+    {
+      name: 'paymentRecord',
+      path: '/payment-record',
+      component: () => import('@/pages/payment-record/index.vue'),
+    },
     {
       path: '/:pathMatch(.*)*',
       name: 'pageNotFound',

+ 16 - 0
src/utils/index.ts

@@ -7,8 +7,24 @@ const toFixed = function (number: number, ratio = 100) {
   return Math.round(multiply(number, ratio)) / ratio
 }
 
+// 按长度分割数组
+const splitArray = function (target: any[], length = 100) {
+  const result = []
+  if (target.length <= length) {
+    result.push(target)
+  } else {
+    let index = 0
+    while (index < target.length) {
+      result.push(target.slice(index, index + length))
+      index = index + length
+    }
+  }
+  return result
+}
+
 export default {
   debounce,
   multiply,
   toFixed,
+  splitArray,
 }

+ 6 - 3
vite.config.ts

@@ -1,6 +1,6 @@
 import { defineConfig } from 'vite'
 import vue from '@vitejs/plugin-vue'
-import eslintPlugin from 'vite-plugin-eslint'
+import eslintPlugin from '@nabla/vite-plugin-eslint'
 
 // https://vitejs.dev/config/
 export default defineConfig(({ mode }) => {
@@ -11,7 +11,7 @@ export default defineConfig(({ mode }) => {
       port: 9527,
       proxy: {
         '/api': {
-          target: 'http://zohocrm.promocollection.com.au:9008/',
+          target: 'http://zohocrm.promocollection.com.au:9007/',
           changeOrigin: true,
           rewrite: (path) => path.replace(/^\/api/, ''),
         },
@@ -30,7 +30,10 @@ export default defineConfig(({ mode }) => {
         },
       },
     },
-    plugins: [vue(), eslintPlugin()],
+    plugins: [
+      vue({ template: { compilerOptions: { hoistStatic: false } } }),
+      eslintPlugin(),
+    ],
     build: {
       // 设置最终构建的浏览器兼容目标
       target: 'es2015',