董国庆
2026-02-26 e2e87ae046018fbca7fc4c4cb5f7a93736145450
用户-白名单加权限
1个文件已删除
1个文件已添加
7个文件已修改
932 ■■■■■ 已修改文件
src/service/api/appVersion.ts 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/appVersion/appVersionManage/index.vue 177 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/userContent/contentCenter/api.txt 123 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/userContent/contentCenter/index.vue 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/userContent/whitelistManagement/invitationCode/components/GenerateModal.vue 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/userContent/whitelistManagement/invitationCode/index.vue 196 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/userContent/whitelistManagement/whitelist/components/WhitelistModal.vue 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/userContent/whitelistManagement/whitelist/index.vue 179 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/userContent/whitelistManagement/whitelistLog/index.vue 125 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/service/api/appVersion.ts
New file
@@ -0,0 +1,46 @@
import { request } from '@/service/http'
export interface AppVersionItem {
  id: number
  version: string
  updateDescription: string
  createTime: string
  androidFile: string
  iosFile: string
  forceUpdateFlag: number // 0 or 1
  versionCode?: number
  updateBy?: string
  updateTime?: string
  deleted?: boolean
}
export interface AppVersionPageParams {
  pageNum: number
  pageSize: number
}
export interface AppVersionPageResult {
  current: number
  pages: number
  size: number
  total: number
  records: AppVersionItem[]
}
export interface AppVersionReleaseParams {
  version: string
  androidFile: string
  iosFile: string
  updateDescription: string
  forceUpdateFlag: number
}
/** 获取版本管理分页列表 */
export function fetchAppVersionPage(params: AppVersionPageParams) {
  return request.Post<Service.ResponseResult<AppVersionPageResult>>('/operation/api/v1/appVersion/page', params)
}
/** 发布APP版本 */
export function releaseAppVersion(data: AppVersionReleaseParams) {
  return request.Post<Service.ResponseResult<any>>('/operation/api/v1/appVersion/release', data)
}
src/views/system/appVersion/appVersionManage/index.vue
@@ -1,32 +1,49 @@
<script setup lang="tsx">
import { ref, computed } from 'vue'
import { ref, computed, reactive, onMounted } from 'vue'
import type { DataTableColumns, FormInst, FormRules, UploadFileInfo } from 'naive-ui'
import { NButton } from 'naive-ui'
import dayjs from 'dayjs'
import { customUploadRequest, beforeUploadCheck } from '@/utils/upload'
import { fetchAppVersionPage, releaseAppVersion } from '@/service/api/appVersion'
import type { AppVersionItem } from '@/service/api/appVersion'
interface VersionItem {
  id: number
  version: string
  updateNote: string
  createTime: string
  androidFiles: UploadFileInfo[]
  iosFiles: UploadFileInfo[]
}
const listData = ref<VersionItem[]>([])
const listData = ref<AppVersionItem[]>([])
const loading = ref(false)
const showForm = ref(false)
const formMode = ref<'add' | 'edit'>('add')
const editingId = ref<number | null>(null)
const formMode = ref<'add' | 'view'>('add')
const viewingId = ref<number | null>(null)
const formRef = ref<FormInst | null>(null)
const formModel = ref({
  version: '',
  androidFiles: [] as UploadFileInfo[],
  iosFiles: [] as UploadFileInfo[],
  updateNote: ''
  updateNote: '',
  isForceUpdate: false
})
const pagination = reactive({
  page: 1,
  pageSize: 10,
  itemCount: 0,
  showSizePicker: true,
  pageSizes: [10, 20, 50],
  prefix: ({ itemCount }: { itemCount?: number }) => `共 ${itemCount || 0} 条`,
  onChange: (page: number) => {
    pagination.page = page
    getData()
  },
  onUpdatePageSize: (pageSize: number) => {
    pagination.pageSize = pageSize
    pagination.page = 1
    getData()
  }
})
const rules: FormRules = {
  version: [
    { required: true, message: '请输入版本号', trigger: ['blur', 'input'] }
  ],
  androidFiles: [
    { required: true, type: 'array', message: '请上传安卓安装包', trigger: ['blur', 'change'] }
  ],
@@ -38,13 +55,13 @@
  ]
}
const columns = computed<DataTableColumns<VersionItem>>(() => [
const columns = computed<DataTableColumns<AppVersionItem>>(() => [
  {
    title: '序号',
    key: 'index',
    width: 80,
    align: 'center',
    render: (_row, rowIndex) => rowIndex + 1
    render: (_row, rowIndex) => rowIndex + 1 + (pagination.page - 1) * pagination.pageSize
  },
  {
    title: '版本号',
@@ -54,14 +71,15 @@
  },
  {
    title: '更新提示',
    key: 'updateNote',
    key: 'updateDescription',
    minWidth: 220,
    ellipsis: { tooltip: true }
  },
  {
    title: '创建时间',
    key: 'createTime',
    minWidth: 160
    minWidth: 160,
    render: (row) => row.createTime ? dayjs(row.createTime).format('YYYY-MM-DD HH:mm:ss') : '-'
  },
  {
    title: '操作',
@@ -69,37 +87,67 @@
    width: 120,
    render: (row) => {
      return (
        <NButton text type="primary" onClick={() => handleEdit(row)}>
          编辑
        <NButton v-permission="20222" text type="primary" onClick={() => handleView(row)}>
          查看
        </NButton>
      )
    },
  },
])
async function getData() {
  loading.value = true
  try {
    const { data, isSuccess } = await fetchAppVersionPage({
      pageNum: pagination.page,
      pageSize: pagination.pageSize
    })
    if (isSuccess && data) {
      listData.value = data.records || []
      pagination.itemCount = data.total || 0
    }
  } finally {
    loading.value = false
  }
}
function resetForm() {
  formModel.value = {
    version: '',
    androidFiles: [],
    iosFiles: [],
    updateNote: ''
    updateNote: '',
    isForceUpdate: false
  }
  formRef.value?.restoreValidation()
}
function handleAdd() {
  formMode.value = 'add'
  editingId.value = null
  viewingId.value = null
  resetForm()
  showForm.value = true
}
function handleEdit(row: VersionItem) {
  formMode.value = 'edit'
  editingId.value = row.id
function handleView(row: AppVersionItem) {
  formMode.value = 'view'
  viewingId.value = row.id
  formModel.value = {
    androidFiles: row.androidFiles || [],
    iosFiles: row.iosFiles || [],
    updateNote: row.updateNote
    version: row.version,
    androidFiles: row.androidFile ? [{
        id: 'android',
        name: row.androidFile.split('/').pop() || 'android.apk',
        status: 'finished',
        url: row.androidFile
    }] : [],
    iosFiles: row.iosFile ? [{
        id: 'ios',
        name: row.iosFile.split('/').pop() || 'ios.ipa',
        status: 'finished',
        url: row.iosFile
    }] : [],
    updateNote: row.updateDescription,
    isForceUpdate: row.forceUpdateFlag === 1
  }
  showForm.value = true
}
@@ -108,32 +156,27 @@
  await formRef.value?.validate(async (errors) => {
    if (!errors) {
      if (formMode.value === 'add') {
        const newItem: VersionItem = {
          id: Date.now(),
          // 版本号由接口生成,前端新增暂用占位
          version: '-',
          updateNote: formModel.value.updateNote,
          createTime: dayjs().format('YYYY-MM-DD HH:mm:ss'),
          androidFiles: [...formModel.value.androidFiles],
          iosFiles: [...formModel.value.iosFiles]
        }
        listData.value.unshift(newItem)
        window.$message?.success('新增成功')
      } else {
        const idx = listData.value.findIndex(it => it.id === editingId.value)
        if (idx > -1) {
          listData.value[idx] = {
            ...listData.value[idx],
        const params = {
            version: formModel.value.version,
            updateNote: formModel.value.updateNote,
            androidFiles: [...formModel.value.androidFiles],
            iosFiles: [...formModel.value.iosFiles]
          }
          window.$message?.success('保存成功')
            androidFile: formModel.value.androidFiles[0]?.url || '',
            iosFile: formModel.value.iosFiles[0]?.url || '',
            updateDescription: formModel.value.updateNote,
            forceUpdateFlag: formModel.value.isForceUpdate ? 1 : 0
        }
        if (!params.androidFile || !params.iosFile) {
             window.$message?.error('请等待文件上传完成')
             return
        }
        const { isSuccess } = await releaseAppVersion(params)
        if (isSuccess) {
            window.$message?.success('发布成功')
            showForm.value = false
            resetForm()
            getData()
        }
      }
      showForm.value = false
      resetForm()
    } else {
      window.$message?.error('请完善必填信息')
    }
@@ -156,12 +199,16 @@
async function customRequest(options: any) {
  await customUploadRequest(options)
}
onMounted(() => {
  getData()
})
</script>
<template>
  <div class="h-full flex flex-col">
    <n-card style="border-radius: 6px" content-style="padding: 12px">
      <div class="mb-4 flex justify-end">
      <div class="mb-4 flex justify-end" v-permission="20221">
        <n-button style="height: 32px; background-color: #E6F4FF; color: #0068FF; border: 1px solid #7ABDFF" @click="handleAdd">
          新增版本号
        </n-button>
@@ -170,16 +217,11 @@
      <n-data-table
        :columns="columns"
        :data="listData"
        :loading="loading"
        :bordered="false"
        :scroll-x="900"
        :pagination="{
          page: 1,
          pageSize: 10,
          itemCount: listData.length,
          showSizePicker: true,
          pageSizes: [10, 20, 50],
          prefix: ({ itemCount }) => `共 ${itemCount || 0} 条`
        }"
        :pagination="pagination"
        remote
      />
    </n-card>
@@ -187,7 +229,7 @@
      :show="showForm"
      :mask-closable="false"
      preset="card"
      :title="formMode === 'add' ? '新增版本号' : '编辑版本号'"
      :title="formMode === 'add' ? '新增版本号' : '查看版本号'"
      style="width: 640px"
      @close="handleCancel"
    >
@@ -198,8 +240,14 @@
        label-placement="top"
        label-width="120"
        require-mark-placement="left"
        :disabled="formMode === 'view'"
      >
        <n-grid :cols="2" :x-gap="16">
          <n-gi :span="2">
            <n-form-item label="版本号" path="version" required>
              <n-input v-model:value="formModel.version" placeholder="请输入版本号" />
            </n-form-item>
          </n-gi>
          <n-gi>
            <n-form-item label="安卓文件" path="androidFiles" required>
              <n-upload
@@ -227,6 +275,11 @@
            </n-form-item>
          </n-gi>
          <n-gi :span="2">
            <n-form-item label="强制更新" path="isForceUpdate">
              <n-switch v-model:value="formModel.isForceUpdate" />
            </n-form-item>
          </n-gi>
          <n-gi :span="2">
            <n-form-item label="更新说明" path="updateNote" required>
              <n-input
                v-model:value="formModel.updateNote"
@@ -241,7 +294,7 @@
      <template #footer>
        <div class="flex justify-end gap-3">
          <n-button @click="handleCancel">取消</n-button>
          <n-button type="primary" @click="handleConfirm">确定</n-button>
          <n-button v-if="formMode !== 'view'" type="primary" @click="handleConfirm">确定</n-button>
        </div>
      </template>
    </n-modal>
src/views/userContent/contentCenter/api.txt
File was deleted
src/views/userContent/contentCenter/index.vue
@@ -103,7 +103,7 @@
              guideStep: idx + 1,
              guideType: 1,
            }
            if (s.recordId) obj.id = s.recordId
            // if (s.recordId) obj.id = s.recordId
            return obj
          })
          saveRes = await saveGuide({ guideList })
@@ -128,7 +128,7 @@
            guideStep: 1,
            guideType: 2,
          }
          if (data.recordId) obj.id = data.recordId
          // if (data.recordId) obj.id = data.recordId
          const guideList = [obj]
          saveRes = await saveGuide({ guideList })
        }
src/views/userContent/whitelistManagement/invitationCode/components/GenerateModal.vue
@@ -122,36 +122,46 @@
    v-model:show="showModal"
    preset="card"
    title="生成邀请码"
    style="width: 600px;"
    style="width: 400px;"
    :mask-closable="false"
    :segmented="{
      content: true,
      action: true,
    }"
    :content-style="{
      padding: '16px 24px 24px 24px',
    }"
  >
    <NForm
      ref="formRef"
      :model="formModel"
      :rules="rules"
      label-placement="left"
      label-width="100"
      label-placement="top"
      require-mark-placement="left"
    >
      <NFormItem label="邀请手机号" path="phoneNumbers">
      <NFormItem label="邀请手机号" path="phoneNumbers" style="margin-bottom: 0;">
        <NInput
          v-model:value="formModel.phoneNumbers"
          type="textarea"
          placeholder="请输入手机号,多个账号请按行输入"
          :rows="8"
          placeholder="请输入手机号,多个账号请换行输入"
          :maxlength="10000"
          show-count
          style="height: 80px;"
          :autosize="{
            minRows: 1,
            maxRows: 8
          }"
        />
      </NFormItem>
    </NForm>
    <template #footer>
        <NSpace justify="end">
          <NButton @click="handleClose">
    <template #action>
        <NSpace justify="end" :size="8">
          <NButton @click="handleClose" style="padding: 7px 29px; border-radius: 4px;">
            取消
          </NButton>
          <NButton type="primary" :loading="loading" @click="handleSave">
            生成
          <NButton type="primary" :loading="loading" @click="handleSave" style="padding: 7px 29px; border-radius: 4px;">
            保存
          </NButton>
        </NSpace>
      </template>
@@ -159,4 +169,11 @@
</template>
<style scoped lang="scss">
:deep(.n-form-item-label) {
 padding-bottom: 8px !important;
}
:deep(.n-card__action) {
  background-color: transparent !important;
}
</style>
src/views/userContent/whitelistManagement/invitationCode/index.vue
@@ -43,11 +43,6 @@
    path: 'bindAccount',
    render: () => <n-input v-model:value={model.value.bindAccount} placeholder="请输入绑定账号" clearable />,
  },
  {
    label: '状态',
    path: 'status',
    render: () => <n-select v-model:value={model.value.status} options={statusOptions} placeholder="请选择状态" clearable />,
  },
])
const listData = ref<any[]>([])
@@ -77,22 +72,13 @@
    title: '邀请码',
    align: 'center',
    key: 'invitationCode',
    render: row => <span style="color: #18a058; font-weight: 500;">{row.invitationCode}</span>,
    render: row => <span>{row.invitationCode}</span>,
  },
  {
    title: '绑定账号',
    align: 'center',
    key: 'bindAccount',
    render: row => <span>{row.bindAccount || '-'}</span>,
  },
  {
    title: '状态',
    align: 'center',
    key: 'status',
    render: (row) => {
      const option = statusOptions.find(opt => opt.value === row.status)
      return <span>{option ? option.label : '-'}</span>
    },
  },
  {
    title: '创建时间',
@@ -112,14 +98,37 @@
    width: 100,
    fixed: 'right',
    render: row => (
      <NSpace justify="center">
        <NButton text type="error" onClick={() => deleteInvitationCode(row.id)}>
      <div class="flex justify-center">
        <span
          style={{ color: '#0068FF', cursor: 'pointer' }}
          v-permission="30115"
          onClick={() => deleteInvitationCode(row.id)}
        >
          删除
        </NButton>
      </NSpace>
        </span>
      </div>
    ),
  },
])
// 分页配置
const pagination = computed(() => ({
  page: model.value.pageNum,
  pageSize: model.value.pageSize,
  itemCount: count.value,
  showSizePicker: true,
  pageSizes: [10, 20, 50, 100],
  prefix: ({ itemCount }) => `共 ${itemCount || 0} 条`,
  onChange: (page: number) => {
    model.value.pageNum = page
    getInvitationCodeList()
  },
  onUpdatePageSize: (pageSize: number) => {
    model.value.pageSize = pageSize
    model.value.pageNum = 1
    getInvitationCodeList()
  }
}))
// 重置搜索
function handleResetSearch() {
@@ -267,13 +276,6 @@
  })
}
// 分页
function changePage(page: number, size: number) {
  model.value.pageNum = page
  model.value.pageSize = size
  getInvitationCodeList()
}
// 行选择
function handleCheck(rowKeys: number[]) {
  selectedRowKeys.value = rowKeys
@@ -285,85 +287,85 @@
</script>
<template>
  <n-flex>
    <NSpace vertical class="flex-1">
      <!-- 搜索区域 -->
      <n-card>
        <n-form
          ref="formRef"
          :model="model"
          label-placement="left"
          inline
          :show-feedback="false"
        >
          <SearchPanel>
            <n-gi v-for="item in filterItems" :key="item.path">
              <n-form-item :label="item.label" :path="item.path">
                <component :is="item.render" />
              </n-form-item>
            </n-gi>
            <template #action>
              <NButton strong secondary @click="handleResetSearch">
                <template #icon>
                  <icon-park-outline-redo />
                </template>
                {{ $t("common.reset") }}
              </NButton>
              <NButton type="primary" @click="getInvitationCodeList">
                <template #icon>
                  <icon-park-outline-search />
                </template>
                {{ $t("common.search") }}
              </NButton>
            </template>
          </SearchPanel>
        </n-form>
      </n-card>
  <div class="h-full flex flex-col">
    <!-- 搜索区域 -->
    <n-card style="border-radius: 6px" content-style="padding: 12px">
      <n-form
        ref="formRef"
        :model="model"
        label-placement="left"
        inline
        :show-feedback="false"
      >
        <SearchPanel>
          <n-gi v-for="item in filterItems" :key="item.path">
            <n-form-item :label="item.label" :path="item.path">
              <component :is="item.render" />
            </n-form-item>
          </n-gi>
          <template #action>
            <NButton style="width: 84px; height: 32px" @click="handleResetSearch">
              {{ $t("common.reset") }}
            </NButton>
            <NButton type="primary" style="width: 84px; height: 32px" @click="getInvitationCodeList">
              {{ $t("common.search") }}
            </NButton>
          </template>
        </SearchPanel>
      </n-form>
    </n-card>
      <!-- 表格区域 -->
      <n-card class="flex-1">
        <template #header>
          <NSpace>
            <NButton type="primary" @click="handleGenerate">
              生成邀请码
            </NButton>
            <NButton
              :disabled="selectedRowKeys.length === 0"
              @click="handleBatchDelete"
            >
              批量删除
            </NButton>
            <NButton @click="handleImport"> 导入 </NButton>
            <NButton @click="handleExport"> 导出 </NButton>
            <NButton @click="handleDownloadTemplate"> 下载模板 </NButton>
          </NSpace>
        </template>
        <NSpace vertical>
          <n-data-table
            :columns="columns"
            :data="listData"
            :loading="loading"
            :row-key="(row: any) => row.id"
            @update:checked-row-keys="handleCheck"
          />
          <Pagination :count="count" @change="changePage" />
        </NSpace>
      </n-card>
      <input
        ref="fileInputRef"
        type="file"
        style="display: none"
        accept=".xlsx, .xls"
        @change="handleFileChange"
    <!-- 表格区域 -->
    <n-card class="flex-1" style="border-radius: 6px; margin-top: 12px;" content-style="padding: 12px">
      <div class="mb-4 flex justify-between items-center">
        <div class="flex gap-2">
          <NButton
            v-permission="30111"
            :disabled="selectedRowKeys.length === 0"
            @click="handleBatchDelete"
            style="width: 84px; height: 32px"
          >
            批量删除
          </NButton>
        </div>
        <div class="flex gap-2">
          <NButton v-permission="30112" @click="handleDownloadTemplate" style="width: 84px; height: 32px"> 下载模板 </NButton>
          <NButton v-permission="30113" @click="handleExport" style="width: 84px; height: 32px"> 导出 </NButton>
          <NButton v-permission="30112" @click="handleImport" style="width: 84px; height: 32px"> 导入 </NButton>
          <NButton
          v-permission="30114"
            @click="handleGenerate"
            style="width: 110px; height: 32px; background-color: #E6F4FF; color: #0068FF; border: 1px solid #7ABDFF;"
          >
            生成邀请码
          </NButton>
        </div>
      </div>
      <n-data-table
        :columns="columns"
        :data="listData"
        :loading="loading"
        :row-key="(row: any) => row.id"
        @update:checked-row-keys="handleCheck"
        :pagination="pagination"
      />
    </NSpace>
    </n-card>
    <input
      ref="fileInputRef"
      type="file"
      style="display: none"
      accept=".xlsx, .xls"
      @change="handleFileChange"
    />
    <!-- 生成邀请码弹窗 -->
    <GenerateModal
      v-model:visible="showGenerateModal"
      @success="handleGenerateSuccess"
    />
  </n-flex>
  </div>
</template>
<style scoped lang="scss">
src/views/userContent/whitelistManagement/whitelist/components/WhitelistModal.vue
@@ -137,7 +137,14 @@
    preset="card"
    :title="mode === 'add' ? '新增白名单' : '编辑白名单'"
    class="whitelist-modal"
    :style="{ width: '600px' }"
    :style="{ width: '400px' }"
    :segmented="{
      content: true,
      action: true,
    }"
    :content-style="{
      padding: '16px 24px 0px 24px',
    }"
    @close="handleClose"
  >
    <NForm
@@ -173,23 +180,25 @@
        />
      </NFormItem>
      <!-- 状态 -->
      <NFormItem label="状态">
      <!-- 状态 (仅编辑模式显示) -->
      <div v-if="mode === 'edit'" style="display: flex; align-items: center; margin-bottom: 16px;">
        <span style="margin-right: 12px; font-weight: 500;">状态</span>
        <NSwitch v-model:value="formModel.status" />
      </NFormItem>
      </div>
      <!-- 是否优先派单 -->
      <NFormItem label="是否优先派单">
      <div style="display: flex; align-items: center; margin-bottom: 16px;">
        <span style="margin-right: 12px; font-weight: 500;">是否优先派单</span>
        <NSwitch v-model:value="formModel.isPriorityDispatch" />
      </NFormItem>
      </div>
    </NForm>
    <template #footer>
      <NSpace justify="end">
        <NButton @click="handleCancel">
    <template #action>
      <NSpace justify="end" :size="8">
        <NButton @click="handleCancel" style="padding: 7px 29px; border-radius: 4px;">
          取消
        </NButton>
        <NButton type="primary" @click="handleSave">
        <NButton type="primary" @click="handleSave" style="padding: 7px 29px; border-radius: 4px;">
          保存
        </NButton>
      </NSpace>
@@ -198,9 +207,15 @@
</template>
<style scoped lang="scss">
.whitelist-modal {
  :deep(.n-card__content) {
    padding-top: 20px;
  }
:deep(.n-form-item-label) {
 padding-bottom: 8px !important;
}
:deep(.n-card__action) {
  background-color: transparent !important;
}
:deep(.n-form-item) {
  margin-bottom: 8px !important;
}
</style>
src/views/userContent/whitelistManagement/whitelist/index.vue
@@ -73,6 +73,7 @@
const columns = computed<DataTableColumns<any>>(() => [
  {
    type: 'selection',
    width: 50,
  },
  {
    title: '序号',
@@ -101,7 +102,7 @@
    render: (row) => {
      const isNormal = row.status === 1 || row.status === true
      return (
        <NTag type={isNormal ? 'success' : 'warning'} round>
        <NTag type={isNormal ? 'primary' : 'warning'} bordered={false} style={{ color: isNormal ? '#0068FF' : undefined, backgroundColor: isNormal ? '#E6F4FF' : undefined }}>
          {isNormal ? '正常' : '冻结'}
        </NTag>
      )
@@ -139,29 +140,53 @@
    render: row => {
      const isNormal = row.status === 1 || row.status === true
      return (
        <NSpace justify="center">
          <NButton text type="primary" onClick={() => handleEdit(row)}>
        <div class="flex justify-center gap-3">
          <span v-permission="30125" style={{ color: '#0068FF', cursor: 'pointer' }} onClick={() => handleEdit(row)}>
            编辑
          </NButton>
          </span>
          {isNormal
            ? (
                <NButton text type="warning" onClick={() => handleFreeze(row)}>
                <span v-permission="30126" style={{ color: '#0068FF', cursor: 'pointer' }} onClick={() => handleFreeze(row)}>
                  冻结
                </NButton>
                </span>
              )
            : (
                <NButton text type="success" onClick={() => handleUnfreeze(row)}>
                <span v-permission="30126" style={{ color: '#0068FF', cursor: 'pointer' }} onClick={() => handleUnfreeze(row)}>
                  解冻
                </NButton>
                </span>
              )}
          <NButton text type="error" disabled={isNormal} onClick={() => handleDelete(row)}>
          <span
                v-permission="30127"
            style={{ color: isNormal ? '#ccc' : '#0068FF', cursor: isNormal ? 'not-allowed' : 'pointer' }}
            onClick={() => !isNormal && handleDelete(row)}
          >
            删除
          </NButton>
        </NSpace>
          </span>
        </div>
      )
    },
  },
])
// 分页配置
const pagination = computed(() => ({
  page: model.value.pageNum,
  pageSize: model.value.pageSize,
  itemCount: totalCount.value,
  showSizePicker: true,
  pageSizes: [10, 20, 50, 100],
  prefix: ({ itemCount }) => `共 ${itemCount || 0} 条`,
  onChange: (page: number) => {
    model.value.pageNum = page
    getWhitelistList()
  },
  onUpdatePageSize: (pageSize: number) => {
    model.value.pageSize = pageSize
    model.value.pageNum = 1
    getWhitelistList()
  }
}))
// 重置搜索
function handleResetSearch() {
@@ -318,12 +343,7 @@
  })
}
// 分页
function changePage(page: number, size: number) {
  model.value.pageNum = page
  model.value.pageSize = size
  getWhitelistList()
}
// 选择行变化
function handleCheck(rowKeys: number[]) {
@@ -336,73 +356,64 @@
</script>
<template>
  <n-flex>
    <NSpace vertical class="flex-1">
      <n-card>
        <n-form ref="formRef" :model="model" label-placement="left" inline :show-feedback="false">
          <SearchPanel>
            <n-gi v-for="item in filterItems" :key="item.path">
              <n-form-item :label="item.label" :path="item.path">
                <component :is="item.render" />
              </n-form-item>
            </n-gi>
            <template #action>
              <NButton strong secondary @click="handleResetSearch">
                <template #icon>
                  <icon-park-outline-redo />
                </template>
                {{ $t('common.reset') }}
              </NButton>
              <NButton type="primary" @click="getWhitelistList">
                <template #icon>
                  <icon-park-outline-search />
                </template>
                {{ $t('common.search') }}
              </NButton>
            </template>
          </SearchPanel>
        </n-form>
      </n-card>
  <div class="h-full flex flex-col">
    <!-- 搜索区域 -->
    <n-card style="border-radius: 6px" content-style="padding: 12px">
      <n-form ref="formRef" :model="model" label-placement="left" inline :show-feedback="false">
        <SearchPanel>
          <n-gi v-for="item in filterItems" :key="item.path">
            <n-form-item :label="item.label" :path="item.path">
              <component :is="item.render" />
            </n-form-item>
          </n-gi>
          <template #action>
            <NButton style="padding: 7px 16px; height: 32px" @click="handleResetSearch">
              {{ $t("common.reset") }}
            </NButton>
            <NButton type="primary" style="padding: 7px 16px; height: 32px" @click="getWhitelistList">
              {{ $t("common.search") }}
            </NButton>
          </template>
        </SearchPanel>
      </n-form>
    </n-card>
      <n-card class="flex-1">
        <template #header>
          <NSpace>
            <NButton type="primary" @click="handleAdd">
              新增白名单
            </NButton>
            <NButton @click="handleImport">
              导入白名单
            </NButton>
            <NButton @click="handleDownloadTemplate">
              下载模板
            </NButton>
            <NButton @click="handleExport">
              导出
            </NButton>
            <NButton type="warning" @click="handleBatchFreeze">
              批量冻结
            </NButton>
          </NSpace>
        </template>
        <NSpace vertical>
          <n-data-table
            :columns="columns"
            :data="whitelistData"
            :loading="loading"
            :row-key="(row: any) => row.id"
            @update:checked-row-keys="handleCheck"
          />
          <Pagination :count="totalCount" @change="changePage" />
        </NSpace>
      </n-card>
    <!-- 表格区域 -->
    <n-card class="flex-1" style="border-radius: 6px; margin-top: 12px" content-style="padding: 12px">
      <div class="mb-4 flex justify-between items-center">
        <div class="flex gap-2">
          <NButton v-permission="30121" :disabled="selectedRowKeys.length === 0" @click="handleBatchFreeze"
            style="padding: 7px 16px; height: 32px">
            批量冻结
          </NButton>
        </div>
        <div class="flex gap-2">
          <NButton v-permission="30123" @click="handleDownloadTemplate" style="padding: 7px 16px; height: 32px">
            下载模板
          </NButton>
          <NButton v-permission="30122" @click="handleExport" style="padding: 7px 16px; height: 32px">
            导出
          </NButton>
          <NButton v-permission="30123" @click="handleImport" style="padding: 7px 16px; height: 32px">
            导入
          </NButton>
          <NButton v-permission="30124" @click="handleAdd" style="
              padding: 7px 16px;
              height: 32px;
              background-color: #e6f4ff;
              color: #0068ff;
              border: 1px solid #7abdff;
            ">
            新增白名单
          </NButton>
        </div>
      </div>
      <n-data-table :columns="columns" :data="whitelistData" :loading="loading" :row-key="(row: any) => row.id"
        @update:checked-row-keys="handleCheck" :pagination="pagination" />
    </n-card>
      <!-- 白名单弹窗 -->
      <WhitelistModal
        v-model:visible="showModal"
        :mode="modalMode"
        :row-data="currentRow"
        @success="handleModalSuccess"
      />
    </NSpace>
  </n-flex>
    <!-- 白名单弹窗 -->
    <WhitelistModal v-model:visible="showModal" :mode="modalMode" :row-data="currentRow"
      @success="handleModalSuccess" />
  </div>
</template>
src/views/userContent/whitelistManagement/whitelistLog/index.vue
@@ -1,6 +1,6 @@
<script setup lang="tsx">
import type { DataTableColumns, FormInst } from 'naive-ui'
import { NButton, NSpace, NTag, useMessage } from 'naive-ui'
import { NButton, NCard, NDataTable, NForm, NFormItem, NGi, NInput, NSelect, NDatePicker, useMessage } from 'naive-ui'
import dayjs from 'dayjs'
import { useBoolean } from '@/hooks'
import { downloadFile } from '@/utils'
@@ -64,61 +64,75 @@
const columns = computed<DataTableColumns<any>>(() => [
  {
    title: '序号',
    align: 'center',
    key: 'index',
    width: 80,
    render: (row, index) => <span>{(model.value.pageNum - 1) * model.value.pageSize + index + 1}</span>,
  },
  {
    title: '操作人',
    align: 'center',
    key: 'operationName',
    width: 150,
    ellipsis: true,
  },
  {
    title: '账号',
    align: 'center',
    key: 'operationAccount',
    width: 200,
    ellipsis: true,
  },
  {
    title: '操作类型',
    align: 'center',
    key: 'operationType',
    width: 100,
    width: 120,
    render: (row) => {
      const typeMap: Record<number, { type: 'success' | 'info' | 'warning' | 'error', text: string }> = {
        1: { type: 'success', text: '新增' },
        2: { type: 'info', text: '编辑' },
        3: { type: 'error', text: '删除' },
        4: { type: 'success', text: '导入' },
        5: { type: 'info', text: '导出' },
        6: { type: 'warning', text: '批量冻结' },
      const typeMap: Record<number, string> = {
        1: '新增',
        2: '编辑',
        3: '删除',
        4: '导入',
        5: '导出',
        6: '批量冻结',
      }
      const type = typeMap[row.operationType as number] || { type: 'info', text: row.operationType }
      return <NTag type={type.type} round>{type.text}</NTag>
      return <span>{typeMap[row.operationType as number] || row.operationType}</span>
    },
  },
  {
    title: '操作内容',
    align: 'center',
    key: 'operationContent',
    ellipsis: true,
  },
  {
    title: '操作IP',
    align: 'center',
    key: 'operationIp',
    width: 150,
    ellipsis: true,
  },
  {
    title: '操作时间',
    align: 'center',
    key: 'createTime',
    width: 260,
    ellipsis: true,
  },
])
// 分页配置
const pagination = computed(() => ({
  page: model.value.pageNum,
  pageSize: model.value.pageSize,
  itemCount: count.value,
  showSizePicker: true,
  pageSizes: [10, 20, 50, 100],
  prefix: ({ itemCount }) => `共 ${itemCount || 0} 条`,
  onChange: (page: number) => {
    model.value.pageNum = page
    getLogList()
  },
  onUpdatePageSize: (pageSize: number) => {
    model.value.pageSize = pageSize
    model.value.pageNum = 1
    getLogList()
  }
}))
async function getLogList() {
  startLoading()
@@ -163,56 +177,39 @@
onMounted(() => {
  getLogList()
})
function changePage(page: number, size: number) {
  model.value.pageNum = page
  model.value.pageSize = size
  getLogList()
}
</script>
<template>
  <n-flex>
    <NSpace vertical class="flex-1">
      <n-card>
        <n-form ref="formRef" :model="model" label-placement="left" inline :show-feedback="false">
          <SearchPanel>
            <n-gi v-for="item in filterItems" :key="item.path">
              <n-form-item :label="item.label" :path="item.path">
                <component :is="item.render" />
              </n-form-item>
            </n-gi>
            <template #action>
              <NButton strong secondary @click="handleResetSearch">
                <template #icon>
                  <icon-park-outline-redo />
                </template>
                {{ $t('common.reset') }}
              </NButton>
              <NButton type="primary" @click="getLogList">
                <template #icon>
                  <icon-park-outline-search />
                </template>
                {{ $t('common.search') }}
              </NButton>
            </template>
          </SearchPanel>
        </n-form>
      </n-card>
  <div class="h-full flex flex-col">
    <n-card style="border-radius: 6px" content-style="padding: 12px">
      <n-form ref="formRef" :model="model" label-placement="left" inline :show-feedback="false">
        <SearchPanel>
          <n-gi v-for="item in filterItems" :key="item.path">
            <n-form-item :label="item.label" :path="item.path">
              <component :is="item.render" />
            </n-form-item>
          </n-gi>
          <template #action>
            <NButton @click="handleResetSearch" style="padding: 7px 16px; height: 32px">
              {{ $t('common.reset') }}
            </NButton>
            <NButton type="primary" @click="getLogList" style="padding: 7px 16px; height: 32px">
              {{ $t('common.search') }}
            </NButton>
          </template>
        </SearchPanel>
      </n-form>
    </n-card>
      <n-card class="flex-1">
        <template #header>
          <NButton @click="handleExport">
            导出
          </NButton>
        </template>
        <NSpace vertical>
          <n-data-table :columns="columns" :data="listData" :loading="loading" />
          <Pagination :count="count" @change="changePage" />
        </NSpace>
      </n-card>
    </NSpace>
  </n-flex>
    <n-card class="flex-1" style="border-radius: 6px; margin-top: 12px" content-style="padding: 12px">
      <div class="mb-4 flex justify-end items-center" v-permission="30131">
        <NButton @click="handleExport" style="padding: 7px 16px; height: 32px">
          导出
        </NButton>
      </div>
      <n-data-table :columns="columns" :data="listData" :loading="loading" :pagination="pagination" />
    </n-card>
  </div>
</template>
<style scoped lang="scss">