| New file |
| | |
| | | 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) |
| | | } |
| | |
| | | <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'] } |
| | | ], |
| | |
| | | ] |
| | | } |
| | | |
| | | 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: '版本号', |
| | |
| | | }, |
| | | { |
| | | 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: '操作', |
| | |
| | | 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 |
| | | } |
| | |
| | | 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('请完善必填信息') |
| | | } |
| | |
| | | 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> |
| | |
| | | <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> |
| | | |
| | |
| | | :show="showForm" |
| | | :mask-closable="false" |
| | | preset="card" |
| | | :title="formMode === 'add' ? '新增版本号' : '编辑版本号'" |
| | | :title="formMode === 'add' ? '新增版本号' : '查看版本号'" |
| | | style="width: 640px" |
| | | @close="handleCancel" |
| | | > |
| | |
| | | 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 |
| | |
| | | </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" |
| | |
| | | <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> |
| | |
| | | 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 }) |
| | |
| | | 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 }) |
| | | } |
| | |
| | | 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> |
| | |
| | | </template> |
| | | |
| | | <style scoped lang="scss"> |
| | | :deep(.n-form-item-label) { |
| | | padding-bottom: 8px !important; |
| | | } |
| | | |
| | | :deep(.n-card__action) { |
| | | background-color: transparent !important; |
| | | } |
| | | </style> |
| | |
| | | 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[]>([]) |
| | |
| | | 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: '创建时间', |
| | |
| | | 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() { |
| | |
| | | }) |
| | | } |
| | | |
| | | // 分页 |
| | | function changePage(page: number, size: number) { |
| | | model.value.pageNum = page |
| | | model.value.pageSize = size |
| | | getInvitationCodeList() |
| | | } |
| | | |
| | | // 行选择 |
| | | function handleCheck(rowKeys: number[]) { |
| | | selectedRowKeys.value = rowKeys |
| | |
| | | </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"> |
| | |
| | | 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 |
| | |
| | | /> |
| | | </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> |
| | |
| | | </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> |
| | |
| | | const columns = computed<DataTableColumns<any>>(() => [ |
| | | { |
| | | type: 'selection', |
| | | width: 50, |
| | | }, |
| | | { |
| | | title: '序号', |
| | |
| | | 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> |
| | | ) |
| | |
| | | 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() { |
| | |
| | | }) |
| | | } |
| | | |
| | | // 分页 |
| | | function changePage(page: number, size: number) { |
| | | model.value.pageNum = page |
| | | model.value.pageSize = size |
| | | getWhitelistList() |
| | | } |
| | | |
| | | |
| | | // 选择行变化 |
| | | function handleCheck(rowKeys: number[]) { |
| | |
| | | </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> |
| | |
| | | <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' |
| | |
| | | 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() |
| | |
| | | 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"> |