董国庆
2026-03-14 69fd6383123acde42e4d22c911830c21187e6980
修改
4个文件已修改
373 ■■■■■ 已修改文件
src/utils/upload.ts 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/accountPermission/permissionGroup/form.vue 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/system/appVersion/appVersionManage/add.vue 297 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/userContent/contentCenter/components/FaqForm.vue 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/utils/upload.ts
@@ -34,23 +34,21 @@
  onError: () => void
  onProgress: (e: { percent: number }) => void
}) {
  let timer: ReturnType<typeof setInterval> | undefined
  try {
    // 模拟上传进度
    let progress = 0
    const timer = setInterval(() => {
    timer = setInterval(() => {
      progress += 20
      if (progress <= 100) {
        onProgress({ percent: progress })
      }
      else {
      else if (timer) {
        clearInterval(timer)
        timer = undefined
      }
    }, 100)
    // 模拟延迟
    await new Promise(resolve => setTimeout(resolve, 600))
    // TODO: 等待具体上传接口出来后,取消注释以下真实 API 调用逻辑
    const formData = new FormData()
    if (file.file) {
@@ -62,32 +60,30 @@
      file.id = res.data.id
      file.status = 'finished'
      onFinish()
      return {
        id: file.id,
        url: file.url,
        name: file.name,
        status: 'finished',
      }
    }
    else {
      file.status = 'error'
      onError()
      window.$message.error('文件上传失败')
      return undefined
    }
    // --- 模拟返回逻辑开始 ---
    // 生成一个模拟的 URL (如果是图片则使用 base64 预览)
    // file.url = 'https://haitunyingyu.obs.cn-southwest-2.myhuaweicloud.com/admin/8bbba1ccafda4456bf7e3b2054d78541.jpg' // 默认占位图
    // const newId = `mock_${Date.now()}`
    // file.status = 'finished'
    // onFinish()
    // 关键修复:返回上传后的文件信息,供组件内使用
    return {
      id: file.id,
      url: file.url,
      name: file.name,
      status: 'finished',
    }
    // --- 模拟返回逻辑结束 ---
  }
  catch (err) {
    file.status = 'error'
    onError()
    window.$message.error('文件上传出错')
    console.error('Upload Error:', err)
    return undefined
  }
  finally {
    if (timer)
      clearInterval(timer)
  }
}
src/views/system/accountPermission/permissionGroup/form.vue
@@ -68,12 +68,35 @@
})
const userColumns: any = [
  { title: '账号', key: 'userAccount', align: 'center',resizable: true },
  { title: '昵称', key: 'nickName', align: 'center',resizable: true },
  { title: '手机号', key: 'phoneNumber', align: 'center',resizable: true },
  { title: '角色', key: 'roleName', align: 'center',resizable: true },
  { title: '状态', key: 'status', align: 'center', render: (row: any) => (row.status === 1 ? '启用' : '禁用'),resizable: true },
  { title: '创建时间', key: 'createTime', align: 'center',resizable: true },
  {
    title: '序号',
    key: 'index',
    align: 'center',
    width: 80,
    render: (_row: any, index: number) => <span>{index + 1 + (userPagination.page - 1) * userPagination.pageSize}</span>,
  },
  { title: '人员名称', key: 'nickName', align: 'center', resizable: true, ellipsis: true },
  { title: '编号', key: 'uniqueNumber', align: 'center', resizable: true, ellipsis: true },
  { title: '登录账号', key: 'userAccount', align: 'center', resizable: true, ellipsis: true },
  { title: '联系电话', key: 'phoneNumber', align: 'center', resizable: true, ellipsis: true },
  { title: '角色', key: 'roleName', align: 'center', resizable: true, ellipsis: true },
  {
    title: '状态',
    key: 'status',
    align: 'center',
    resizable: true,
    ellipsis: true,
    render: (row: any) => <span>{row.status === 0 ? '启用' : '禁用'}</span>,
  },
  { title: '创建时间', key: 'createTime', align: 'center', resizable: true, ellipsis: true },
  {
    title: '备注',
    key: 'remark',
    align: 'center',
    resizable: true,
    ellipsis: true,
    render: (row: any) => <span>{row.remark ?? row.remarks ?? '-'}</span>,
  },
]
// 获取关联用户列表
src/views/system/appVersion/appVersionManage/add.vue
@@ -1,73 +1,116 @@
<script setup lang="tsx">
import { useMessage } from 'naive-ui'
import type { FormInst, FormRules, UploadFileInfo } from 'naive-ui'
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { useMessage } from 'naive-ui'
import type { UploadFileInfo, FormRules, FormInst } from 'naive-ui'
import { customUploadRequest, beforeUploadCheck } from '@/utils/upload'
import { beforeUploadCheck, customUploadRequest } from '@/utils/upload'
import { releaseAppVersion } from '@/service/api/appVersion'
const router = useRouter()
const message = useMessage()
const formRef = ref<FormInst | null>(null)
const androidUploading = ref(false)
const iosUploading = ref(false)
const formModel = ref({
  version: '',
  androidFiles: [] as UploadFileInfo[],
  iosFiles: [] as UploadFileInfo[],
  updateNote: '',
  isForceUpdate: false
  isForceUpdate: false,
})
const rules: FormRules = {
  version: [
    { required: true, message: '请输入版本号', trigger: ['blur', 'input'] }
    { required: true, message: '请输入版本号', trigger: ['blur', 'input'] },
  ],
  androidFiles: [
    { required: true, type: 'array', message: '请上传安卓安装包', trigger: ['blur', 'change'] }
    { required: true, type: 'array', message: '请上传安卓安装包', trigger: ['blur', 'change'] },
  ],
  iosFiles: [
    { required: true, type: 'array', message: '请上传 iOS 安装包', trigger: ['blur', 'change'] }
    { required: true, type: 'array', message: '请上传 iOS 安装包', trigger: ['blur', 'change'] },
  ],
  updateNote: [
    { required: true, message: '请输入更新说明', trigger: ['blur', 'input'] }
  ]
    { required: true, message: '请输入更新说明', trigger: ['blur', 'input'] },
  ],
}
function beforeUploadApk(data: { file: UploadFileInfo; fileList: UploadFileInfo[] }) {
function beforeUploadApk(data: { file: UploadFileInfo, fileList: UploadFileInfo[] }) {
  return beforeUploadCheck(data, { maxSize: 500, allowedTypes: [] })
}
function beforeUploadIpa(data: { file: UploadFileInfo; fileList: UploadFileInfo[] }) {
function beforeUploadIpa(data: { file: UploadFileInfo, fileList: UploadFileInfo[] }) {
  return beforeUploadCheck(data, { maxSize: 500, allowedTypes: [] })
}
async function customRequest(options: any) {
  await customUploadRequest(options)
async function customRequestAndroid(options: any) {
  androidUploading.value = true
  const originalOnError = options.onError
  options.onError = () => {
    formModel.value.androidFiles = []
    originalOnError?.()
  }
  try {
    let res = await customUploadRequest(options)
    if(!res){
      formModel.value.androidFiles = []
      return
    }
  }
  finally {
    androidUploading.value = false
  }
}
async function customRequestIos(options: any) {
  iosUploading.value = true
  const originalOnError = options.onError
  options.onError = () => {
    formModel.value.iosFiles = []
    originalOnError?.()
  }
  try {
    let res = await customUploadRequest(options)
    console.log(res,'22222222')
    if(!res){
      formModel.value.iosFiles = []
      return
    }
  }
  finally {
    iosUploading.value = false
  }
}
async function handleConfirm() {
  await formRef.value?.validate(async (errors) => {
    if (!errors) {
        const params = {
            version: formModel.value.version,
            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) {
             message.error('请等待文件上传完成')
             return
        }
        const { isSuccess } = await releaseAppVersion(params)
        if (isSuccess) {
            message.success('发布成功')
            router.back()
        }
    } else {
    if (errors) {
      message.error('请完善必填信息')
      return
    }
    const params = {
      version: formModel.value.version,
      androidFile: formModel.value.androidFiles[0]?.url || '',
      iosFile: formModel.value.iosFiles[0]?.url || '',
      updateDescription: formModel.value.updateNote,
      forceUpdateFlag: formModel.value.isForceUpdate ? 1 : 0,
    }
    if (androidUploading.value || iosUploading.value) {
      message.error('请等待文件上传完成')
      return
    }
    if (!params.androidFile || !params.iosFile) {
      message.error('请等待文件上传完成')
      return
    }
    const { isSuccess } = await releaseAppVersion(params)
    if (isSuccess) {
      message.success('发布成功')
      router.back()
    }
  })
}
@@ -79,104 +122,110 @@
<template>
  <div class="h-full flex flex-col bg-white rounded-md">
      <div class="flex items-center px-6 pt-6 mb-6">
        <div class="flex items-center relative pl-3">
          <div class="absolute left-0 top-1/2 -translate-y-1/2 w-1 h-4 bg-[#0068FF] rounded-sm"></div>
          <span class="text-base font-bold text-[#1D2129] ml-2">新增APP版本号</span>
        </div>
    <div class="flex items-center px-6 pt-6 mb-6">
      <div class="flex items-center relative pl-3">
        <div class="absolute left-0 top-1/2 -translate-y-1/2 w-1 h-4 bg-[#0068FF] rounded-sm" />
        <span class="text-base font-bold text-[#1D2129] ml-2">新增APP版本号</span>
      </div>
    </div>
      <div class="flex-1 overflow-y-auto px-6">
        <n-form
          ref="formRef"
          :model="formModel"
          :rules="rules"
          label-placement="left"
          inline
          require-mark-placement="left"
          :show-feedback="false"
          class="max-w-4xl flex flex-col gap-16px"
        >
              <n-form-item label="版本号" path="version">
                <n-input v-model:value="formModel.version" placeholder="请输入版本号" class="!w-[400px]" />
              </n-form-item>
    <div class="flex-1 overflow-y-auto px-6">
      <n-form
        ref="formRef"
        :model="formModel"
        :rules="rules"
        label-placement="left"
        inline
        require-mark-placement="left"
        :show-feedback="false"
        class="max-w-4xl flex flex-col gap-16px"
      >
        <n-form-item label="版本号" path="version">
          <n-input v-model:value="formModel.version" placeholder="请输入版本号" class="!w-[400px]" />
        </n-form-item>
              <n-form-item label="安卓文件" path="androidFiles">
                  <div class="flex flex-col gap-2">
                      <n-upload
                          v-model:file-list="formModel.androidFiles"
                          :max="1"
                          accept=".apk"
                          :on-before-upload="beforeUploadApk"
                          :custom-request="customRequest"
                          :show-file-list="false"
                      >
                          <n-button style="width: 100px">
                             <template #icon>
                               <icon-park-outline-upload />
                             </template>
                             {{ formModel.androidFiles.length > 0 ? '重新上传' : '点击上传' }}
                          </n-button>
                      </n-upload>
                      <div v-if="formModel.androidFiles.length > 0" class="flex items-center bg-[#F5F6F8] px-3 py-2 rounded w-fit gap-3">
                           <svg-icons-android-filled class="text-[#00B42A] text-lg" />
                           <span class="text-sm text-[#1D2129] max-w-[200px] truncate" :title="formModel.androidFiles[0].name">{{ formModel.androidFiles[0].name }}</span>
                           <icon-park-outline-close class="text-gray-400 cursor-pointer hover:text-gray-600" @click="formModel.androidFiles = []" />
                      </div>
                  </div>
              </n-form-item>
        <n-form-item label="安卓文件" path="androidFiles">
          <div class="flex flex-col gap-2">
            <n-upload
              v-model:file-list="formModel.androidFiles"
              :max="1"
              accept=".apk"
              :on-before-upload="beforeUploadApk"
              :custom-request="customRequestAndroid"
              :show-file-list="false"
            >
              <n-button style="width: 100px" :loading="androidUploading" :disabled="androidUploading">
                <template #icon>
                  <icon-park-outline-upload />
                </template>
                {{ formModel.androidFiles.length > 0 ? '重新上传' : '点击上传' }}
              </n-button>
            </n-upload>
               <n-form-item label="iOS文件" path="iosFiles">
                  <div class="flex flex-col gap-2">
                      <n-upload
                          v-model:file-list="formModel.iosFiles"
                          :max="1"
                          accept=".ipa"
                          :on-before-upload="beforeUploadIpa"
                          :custom-request="customRequest"
                           :show-file-list="false"
                      >
                           <n-button style="width: 100px">
                             <template #icon>
                               <icon-park-outline-upload />
                             </template>
                             {{ formModel.iosFiles.length > 0 ? '重新上传' : '点击上传' }}
                          </n-button>
                      </n-upload>
            <div v-if="formModel.androidFiles.length > 0" class="flex items-center bg-[#F5F6F8] px-3 py-2 rounded w-fit gap-3">
              <svg-icons-android-filled class="text-[#00B42A] text-lg" />
              <span class="text-sm text-[#1D2129] max-w-[200px] truncate" :title="formModel.androidFiles[0].name">{{ formModel.androidFiles[0].name }}</span>
              <n-spin v-if="androidUploading" size="small" />
              <icon-park-outline-close v-else class="text-gray-400 cursor-pointer hover:text-gray-600" @click="formModel.androidFiles = []" />
            </div>
          </div>
        </n-form-item>
                      <div v-if="formModel.iosFiles.length > 0" class="flex items-center bg-[#F5F6F8] px-3 py-2 rounded w-fit gap-3">
                           <ri-apple-fill class="text-black text-lg" />
                           <span class="text-sm text-[#1D2129] max-w-[200px] truncate" :title="formModel.iosFiles[0].name">{{ formModel.iosFiles[0].name }}</span>
                           <icon-park-outline-close class="text-gray-400 cursor-pointer hover:text-gray-600" @click="formModel.iosFiles = []" />
                      </div>
                  </div>
              </n-form-item>
        <n-form-item label="iOS文件" path="iosFiles">
          <div class="flex flex-col gap-2">
            <n-upload
              v-model:file-list="formModel.iosFiles"
              :max="1"
              accept=".ipa"
              :on-before-upload="beforeUploadIpa"
              :custom-request="customRequestIos"
              :show-file-list="false"
            >
              <n-button style="width: 100px" :loading="iosUploading" :disabled="iosUploading">
                <template #icon>
                  <icon-park-outline-upload />
                </template>
                {{ formModel.iosFiles.length > 0 ? '重新上传' : '点击上传' }}
              </n-button>
            </n-upload>
              <n-form-item label="更新说明" path="updateNote" label-placement="top">
                <n-input
                  v-model:value="formModel.updateNote"
                  type="textarea"
                  placeholder="请输入"
                  :autosize="{ minRows: 6, maxRows: 10 }"
                  show-count
                  style="width: 750px;"
                  :maxlength="200"
                />
              </n-form-item>
               <n-form-item label="强制更新" path="isForceUpdate" label-placement="left">
                <n-switch v-model:value="formModel.isForceUpdate" />
              </n-form-item>
        </n-form>
      </div>
      <div class="flex justify-end gap-3 mt-auto px-6 py-4 border-t border-gray-100">
          <n-button style="padding: 8px 29px;" @click="handleCancel">返回</n-button>
          <n-button style="padding: 8px 29px;" type="primary" @click="handleConfirm">保存</n-button>
      </div>
            <div v-if="formModel.iosFiles.length > 0" class="flex items-center bg-[#F5F6F8] px-3 py-2 rounded w-fit gap-3">
              <ri-apple-fill class="text-black text-lg" />
              <span class="text-sm text-[#1D2129] max-w-[200px] truncate" :title="formModel.iosFiles[0].name">{{ formModel.iosFiles[0].name }}</span>
              <n-spin v-if="iosUploading" size="small" />
              <icon-park-outline-close v-else class="text-gray-400 cursor-pointer hover:text-gray-600" @click="formModel.iosFiles = []" />
            </div>
          </div>
        </n-form-item>
        <n-form-item label="更新说明" path="updateNote" label-placement="top">
          <n-input
            v-model:value="formModel.updateNote"
            type="textarea"
            placeholder="请输入"
            :autosize="{ minRows: 6, maxRows: 10 }"
            show-count
            style="width: 750px;"
            :maxlength="200"
          />
        </n-form-item>
        <n-form-item label="强制更新" path="isForceUpdate" label-placement="left">
          <n-switch v-model:value="formModel.isForceUpdate" />
        </n-form-item>
      </n-form>
    </div>
    <div class="flex justify-end gap-3 mt-auto px-6 py-4 border-t border-gray-100">
      <n-button style="padding: 8px 29px;" @click="handleCancel">
        返回
      </n-button>
      <n-button style="padding: 8px 29px;" type="primary" @click="handleConfirm">
        保存
      </n-button>
    </div>
  </div>
</template>
<style scoped>
</style>
</style>
src/views/userContent/contentCenter/components/FaqForm.vue
@@ -94,6 +94,7 @@
            <td style="text-align: center;">{{ index + 1 }}</td>
            <td>
              <n-input
              type="textarea"
                v-model:value="item.question"
                placeholder="请输入"
                :bordered="false"