Переглянути джерело

Merge branch 'feature/31-carousal' of 1-bright/admin into main

依力 1 рік тому
батько
коміт
8a96ca3bc4

+ 1 - 1
.env.development

@@ -1,4 +1,4 @@
-VITE_ENABLE_MOCK=true
+VITE_ENABLE_MOCK=false
 VITE_API_PREFIX=/api
 VITE_PROXY_ENDPOINT=https://bright.yilibili.com
 

+ 10 - 3
components.d.ts

@@ -8,23 +8,30 @@ export {}
 declare module 'vue' {
   export interface GlobalComponents {
     Copyright: typeof import('./src/components/Copyright.vue')['default']
+    ImageUpload: typeof import('./src/components/ImageUpload.vue')['default']
     RegularPage: typeof import('./src/components/RegularPage.vue')['default']
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']
     TAside: typeof import('tdesign-vue-next')['Aside']
     TAvatar: typeof import('tdesign-vue-next')['Avatar']
     TButton: typeof import('tdesign-vue-next')['Button']
-    TCheckbox: typeof import('tdesign-vue-next')['Checkbox']
-    TCol: typeof import('tdesign-vue-next')['Col']
+    TCard: typeof import('tdesign-vue-next')['Card']
     TDialog: typeof import('tdesign-vue-next')['Dialog']
     TDropdown: typeof import('tdesign-vue-next')['Dropdown']
     THeader: typeof import('tdesign-vue-next')['Header']
     TIcon: typeof import('tdesign-vue-next')['Icon']
+    TImageViewer: typeof import('tdesign-vue-next')['ImageViewer']
     TInput: typeof import('tdesign-vue-next')['Input']
+    TInputNumber: typeof import('tdesign-vue-next')['InputNumber']
     TLayout: typeof import('tdesign-vue-next')['Layout']
-    TLink: typeof import('tdesign-vue-next')['Link']
     TMenu: typeof import('tdesign-vue-next')['Menu']
     TMenuItem: typeof import('tdesign-vue-next')['MenuItem']
+    TPopconfirm: typeof import('tdesign-vue-next')['Popconfirm']
+    TRadioButton: typeof import('tdesign-vue-next')['RadioButton']
+    TRadioGroup: typeof import('tdesign-vue-next')['RadioGroup']
     TSpace: typeof import('tdesign-vue-next')['Space']
+    TTable: typeof import('tdesign-vue-next')['Table']
+    TTag: typeof import('tdesign-vue-next')['Tag']
+    TUpload: typeof import('tdesign-vue-next')['Upload']
   }
 }

+ 29 - 0
src/api/carousal.ts

@@ -0,0 +1,29 @@
+import httpClient from './httpClient'
+import type { CreateCarousalsRequest, GetCarousalsRequest } from '@/model/carousals'
+
+export const createCarousal = (createCarousalsRequest: CreateCarousalsRequest) => {
+  return httpClient.post('/carousals', createCarousalsRequest)
+}
+
+export const getCarousalList = (getCarousalsRequest:GetCarousalsRequest) => {
+  return httpClient.get('/carousals',getCarousalsRequest)
+}
+export const getCarousalItem = (id:string) => {
+  return httpClient.get(`/carousals/${id}`)
+}
+
+export const updateCarousalItem = (id:number,updateCarousalsRequest: CreateCarousalsRequest) => {
+  return httpClient.put(`/carousals/${id}`,updateCarousalsRequest)
+}
+
+export const deleteCarousal = (id:string | number) => {
+  return httpClient.delete(`/carousals/${id}`)
+}
+
+export const activeCarousal = (id: string | number) => {
+  return httpClient.put(`/carousals/${id}/active`)
+}
+
+export const inactiveCarousal = (id: string | number ) => {
+  return httpClient.put(`/carousals/${id}/inactive`)
+}

+ 88 - 0
src/components/ImageUpload.vue

@@ -0,0 +1,88 @@
+<script setup lang="ts">
+import { MessagePlugin, type UploadProps } from 'tdesign-vue-next'
+import { ref, watch } from 'vue'
+import { useAppStore } from '@/stores/app'
+const imageViewerProps = ref<UploadProps['imageViewerProps']>({
+  closeOnEscKeydown: false,
+});
+const sizeLimit = ref<UploadProps['sizeLimit']>({
+  size: 2,
+  unit: 'MB',
+});
+const file1 = ref<UploadProps['value']>([]);
+const disabled = ref(false);
+const autoUpload = ref(true);
+const showImageFileName = ref(true);
+const uploadAllFilesInOneRequest = ref(false);
+const handleFail: UploadProps['onFail'] = ({ file }) => {
+  MessagePlugin.error(`文件 ${file.name} 上传失败`);
+};
+const handleSuccess: UploadProps['onSuccess'] = ({ file }) => {
+  if(file?.url){
+    console.log(file1.value,'file1')
+    emits('on-success',file.url)
+  }
+}
+const appStore = useAppStore()
+
+const uploadHeaders = ref<UploadProps['headers']>({
+  Authorization: `Bearer ${appStore.token}`
+})
+const props = defineProps({
+  imageUrl:{
+    type:Object,
+    default:() => {
+      return { url: '' }
+    }
+  }
+})
+const emits = defineEmits(['on-success'])
+watch(() => props.imageUrl,(newVal) => {
+  if(newVal){
+    if(newVal.url !== ''){
+      file1.value = [{ ...newVal }]
+    }else{
+      file1.value = []
+    }
+  }
+},{
+  deep:true,
+  immediate:true
+})
+</script>
+
+<template>
+  <div>
+    <t-upload
+      ref="uploadRef1"
+      v-model="file1"
+      :image-viewer-props="imageViewerProps"
+      :size-limit="sizeLimit"
+      action="/api/files/upload"
+      :headers="uploadHeaders"
+      theme="image"
+      tips="图片上传限制为2M"
+      accept="image/*"
+      :disabled="disabled"
+      :auto-upload="autoUpload"
+      :show-image-file-name="showImageFileName"
+      :upload-all-files-in-one-request="uploadAllFilesInOneRequest"
+      :locale="{
+          triggerUploadText: {
+            image: '请选择图片',
+          },
+        }"
+      @fail="handleFail"
+      @success="handleSuccess"
+    >
+      <!-- custom UI -->
+      <!-- <template #fileListDisplay="{ files }">
+        <div>{{ JSON.stringify(files) }}</div>
+      </template> -->
+    </t-upload>
+  </div>
+</template>
+
+<style scoped>
+
+</style>

+ 12 - 0
src/model/carousals.ts

@@ -0,0 +1,12 @@
+export interface CreateCarousalsRequest {
+  imageUrl:string,
+  targetType:string,
+  targetUrl?:string,
+  targetId?:string
+}
+export interface GetCarousalsRequest {
+  page:number,
+  size:number,
+  order?:[string]
+}
+

+ 151 - 0
src/pages/carousal/components/CarousalDialog.vue

@@ -0,0 +1,151 @@
+<template>
+  <t-dialog
+    width="70%"
+    :header="header"
+    :confirmBtn="confirmBtn"
+    :closeOnOverlayClick="false"
+    @close="handleCloseDialog"
+    @confirm="fetchSaveCarousalData"
+  >
+    <t-space direction="vertical" style="width: 100%">
+      <TForm ref="form" :data="carousalData" :rules="rules" resetType="initial" label-width="100">
+        <TFormItem label="轮播图:" name="imageUrl">
+          <ImageUpload @on-success="handleSuccessImg" :image-url="imgUrl"/>
+        </TFormItem>
+        <TFormItem label="跳转类型:" name="targetType" >
+          <t-radio-group :default-value="carousalData.targetType" v-model="carousalData.targetType">
+            <t-radio-button v-for="(item,index) in typeOption" :key="index" :value="item.value">{{item.label}}</t-radio-button>
+          </t-radio-group>
+        </TFormItem>
+        <TFormItem label="跳转文章路径:" name="targetId" v-if="carousalData.targetType === 'article' ">
+          <t-input v-model.trim="carousalData.targetId" clearable placeholder="请输入文章id" />
+        </TFormItem>
+        <TFormItem label="跳转网址路径:" name="targetUrl" v-else-if="carousalData.targetType === 'url' ">
+          <t-input v-model.trim="carousalData.targetUrl" clearable placeholder="请输入网址路径" />
+        </TFormItem>
+      </TForm>
+    </t-space>
+  </t-dialog>
+</template>
+<script lang="ts" setup>
+import { computed, ref, reactive, watch } from 'vue'
+import type { FormInstanceFunctions, FormProps } from 'tdesign-vue-next'
+import ImageUpload from '@/components/ImageUpload.vue'
+import { createCarousal, updateCarousalItem } from '@/api/carousal'
+const props = defineProps<{
+  isEdit?: Boolean | null
+  headerTitle?: String | null
+  carousal: {} | number
+}>()
+const emit = defineEmits(['success'])
+const form = ref<FormInstanceFunctions | null>(null)
+const header = computed(() => `${props.isEdit ? '编辑' : '创建'}${props.headerTitle || ''}`)
+const formType = computed(() => `${props.isEdit ? 'update' : 'add'}`)
+const confirmBtn = computed(() => (props.isEdit ? '保存' : '确定'))
+const carousalData: FormProps['data'] = ref({})
+const imgUrl = ref({})
+const rules = computed(() => {
+  const commonRules = {
+    imageUrl: [
+      {
+        required: true,
+        message: '轮播图必须是上传',
+        type: 'error',
+        trigger: 'change',
+      },
+    ],
+    targetType: [
+      {
+        required: true,
+        message: '请选择跳转类型',
+        type: 'error',
+        trigger: 'blur',
+      },
+      {
+        required: true,
+        message: '请选择跳转类型',
+        type: 'error',
+        trigger: 'change',
+      },
+    ],
+  };
+  return carousalData.value.targetType === 'article'
+    ? {...commonRules, targetId: [{ required: true, message: '文章 id 不能为空', type: 'error', trigger: 'blur' }, { required: true, message: '文章 id 不能为空', type: 'error', trigger: 'change' }, { whitespace: true, message: '网址路径不能为空' }] }
+    : {...commonRules, targetUrl: [{ required: true, message: '网址路径不能为空', type: 'error', trigger: 'blur' }, { required: true, message: '网址路径不能为空', type: 'error', trigger: 'change' }, { whitespace: true, message: '网址路径不能为空' }] };
+});
+const handleSuccessImg = (res:string) => {
+  console.log(res,'图片地址')
+  carousalData.value.imageUrl = res
+}
+const typeOption = reactive([
+  { value: 'article', label: '文章' },
+  { value: 'url', label: '地址' },
+])
+const handleFormTypeData = () => {
+  if (carousalData.value.targetType === 'article') {
+    delete carousalData.value.targetUrl
+  } else if (carousalData.value.targetType === 'url') {
+    delete carousalData.value.targetId
+  }
+}
+
+watch(
+  () => props.carousal,
+  (newClassify) => {
+    carousalData.value = Object.assign({}, newClassify)
+    const carousalObj = Object.assign({},newClassify)
+    // imgUrl.value.url = carousalObj.imageUrl || ''
+    imgUrl.value = { url: carousalObj.imageUrl || '' }
+  },
+  {
+    deep: true,
+    immediate: true
+  },
+)
+const fetchSaveCarousalData = async () => {
+  /* TODO: 保存分类数据 */
+  if (form.value) {
+    const valid = await form.value.validate()
+    // console.log(valid,'valid')
+    if (valid && typeof valid === 'boolean') {
+      /* TODO: 校验通过保存分类数据 */
+      handleFormTypeData()
+      // console.log(formType.value,'formType')
+      let res
+      switch (formType.value){
+        case 'create':
+          res = await createCarousal(carousalData.value)
+          emit('success')
+          break;
+        case 'update':
+          const fieldsToKeep = ["imageUrl", "targetType", "targetUrl", "targetId"];
+          const requestObj = {};
+
+          for (const field of fieldsToKeep) {
+            if (carousalData.value[field]!== null && carousalData.value[field]!== undefined) {
+              requestObj[field] = carousalData.value[field];
+            }
+          }
+          handleFormTypeData()
+          res = await updateCarousalItem(carousalData.value.id,requestObj)
+          emit('success')
+          break;
+        default:
+          res = await createCarousal(carousalData.value)
+          emit('success')
+          break;
+      }
+      // const createCarousalApi = createCarousal(carousalData.value)
+      // console.log(createCarousalApi,'createCarousalApi')
+      return
+    }
+  }
+}
+const handleCloseDialog = () => {
+  // 数据&&规则校验结果重置
+  if (form.value) {
+    form.value.reset()
+    form.value.clearValidate()
+  }
+}
+</script>

+ 207 - 0
src/pages/carousal/index.vue

@@ -0,0 +1,207 @@
+<script setup lang="ts">
+import RegularPage from '@/components/RegularPage.vue'
+import { onMounted, ref, reactive } from 'vue'
+import type { BaseTableColumns } from 'tdesign-vue-next'
+import CarousalDialog from '@/pages/carousal/components/CarousalDialog.vue'
+import { activeCarousal, deleteCarousal, getCarousalList, inactiveCarousal } from '@/api/carousal'
+import { BrowseIcon } from 'tdesign-icons-vue-next'
+
+const columns: BaseTableColumns = [
+  {
+    title: 'ID',
+    colKey: 'id',
+    width: 100,
+  },
+  {
+    title: '轮播图',
+    colKey: 'imageUrl',
+    width: 100,
+  },
+  {
+    title: '显示状态',
+    colKey: 'status',
+    width: 100,
+  },
+  {
+    title: '操作',
+    colKey: 'operation',
+    width: 100,
+  }
+]
+const loading = ref(false)
+const data = ref([])
+const pagination = reactive({
+  page: 1,
+  size: 10,
+  total:0
+})
+const query = reactive({
+  page:1,
+  size:10,
+})
+const carousalDialogVisible = ref(false)
+const isEdit = ref(false)
+const currentTableData = ref<{}>({})
+const fetchSaveCarousalData = async () => {
+  /* TODO: 获取轮播图表格数据 */
+  query.page = pagination.page
+  query.size = pagination.size
+  const getCarousalListApi = await getCarousalList(query)
+  data.value = getCarousalListApi.data
+  pagination.total = getCarousalListApi.pagination.total
+}
+onMounted(fetchSaveCarousalData)
+const editClick = (row:object) => {
+  isEdit.value = true
+  currentTableData.value = Object.assign({}, row)
+  carousalDialogVisible.value = true
+}
+const showClick = async (res:object) => {
+  if (res.status === 'active'){
+    await inactiveCarousal(res.id)
+  } else {
+    await activeCarousal(res.id)
+  }
+  await fetchSaveCarousalData()
+}
+const delClick = async (row:object) => {
+  console.log(row,'删除')
+  await deleteCarousal(row.id)
+  await fetchSaveCarousalData()
+}
+const handleCreateCarousal = () => {
+  isEdit.value = false
+  carousalDialogVisible.value = true
+  currentTableData.value = {}
+}
+const onPageChange = () => {
+  /* TODO: 分页切换 */
+}
+const handleSuccessDialog = async () => {
+  await fetchSaveCarousalData()
+  carousalDialogVisible.value = false
+}
+</script>
+
+<template>
+  <RegularPage title="轮播图管理">
+    <template #right-area>
+      <TButton @click="handleCreateCarousal">创建轮播图</TButton>
+    </template>
+    <TTable
+      class="w-full h-full"
+      row-key="id"
+      height="92%"
+      table-layout="auto"
+      :data="data"
+      :loading="loading"
+      :columns="columns"
+      cell-empty-content="-"
+      :paginationAffixedBottom="true"
+      :pagination="{
+        total: pagination.total,
+        current: pagination.page,
+        pageSize: pagination.size,
+        onChange: onPageChange
+      }"
+    >
+      <template #imageUrl="{row}">
+        <div class="tdesign-demo-image-viewer__base">
+          <t-image-viewer :images="[row.imageUrl]">
+            <template #trigger="{ open }">
+              <div class="tdesign-demo-image-viewer__ui-image">
+                <img alt="test" :src="row.imageUrl" class="tdesign-demo-image-viewer__ui-image--img" />
+                <div class="tdesign-demo-image-viewer__ui-image--hover" @click="open">
+                  <span><BrowseIcon size="1.4em" /> 预览</span>
+                </div>
+              </div>
+            </template>
+          </t-image-viewer>
+        </div>
+      </template>
+      <template #status="{row}">
+        <TSpace>
+          <TTag :theme="row.status === 'active' ? 'success' : 'danger'" variant="light">{{row.status === 'active' ? '显示中' : '已关闭'}}</TTag>
+        </TSpace>
+      </template>
+      <template #roles="{ row }">
+        <TSpace>
+          <TTag v-for="role in row.roles" :key="role.id" theme="success" variant="light">{{
+              role.label
+            }}</TTag>
+        </TSpace>
+      </template>
+      <template #operation="{ row }">
+        <TButton variant="text" size="small" theme="primary" @click="editClick(row)">编辑</TButton>
+        <TButton variant="text" size="small" theme="primary" @click="showClick(row)">{{ row.status === 'active' ? '关闭显示' : '开启显示'}}</TButton>
+        <TButton variant="text" size="small" theme="danger" @click="delClick(row)">删除</TButton>
+      </template>
+    </TTable>
+    <CarousalDialog
+      :carousal="currentTableData"
+      v-model:visible="carousalDialogVisible"
+      :isEdit="isEdit"
+      @success="handleSuccessDialog"
+      headerTitle="轮播图"
+    ></CarousalDialog>
+  </RegularPage>
+</template>
+
+<style scoped>
+:deep(.t-card__body) {
+  flex: 1;
+  min-height: 0;
+}
+.tdesign-demo-image-viewer__ui-image {
+  width: 100%;
+  height: 100%;
+  display: inline-flex;
+  position: relative;
+  justify-content: center;
+  align-items: center;
+  border-radius: var(--td-radius-small);
+  overflow: hidden;
+}
+
+.tdesign-demo-image-viewer__ui-image--hover {
+  width: 100%;
+  height: 100%;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  position: absolute;
+  left: 0;
+  top: 0;
+  opacity: 0;
+  background-color: rgba(0, 0, 0, 0.6);
+  color: var(--td-text-color-anti);
+  line-height: 22px;
+  transition: 0.2s;
+}
+
+.tdesign-demo-image-viewer__ui-image:hover .tdesign-demo-image-viewer__ui-image--hover {
+  opacity: 1;
+  cursor: pointer;
+}
+
+.tdesign-demo-image-viewer__ui-image--img {
+  width: auto;
+  height: auto;
+  max-width: 100%;
+  max-height: 100%;
+  cursor: pointer;
+  position: absolute;
+}
+
+
+.tdesign-demo-image-viewer__ui-image--icons .tdesign-demo-icon {
+  cursor: pointer;
+}
+
+.tdesign-demo-image-viewer__base {
+  width: 100px;
+  height: 100px;
+  border: 4px solid var(--td-bg-color-secondarycontainer);
+  border-radius: var(--td-radius-medium);
+}
+</style>

+ 9 - 0
src/router/index.ts

@@ -14,6 +14,15 @@ export const asyncRoutes: RouteRecordRaw[] = [
       icon: 'system-3'
     }
   },
+  {
+    name: 'carousal',
+    path: 'carousal',
+    component: () => import('@/pages/carousal/index.vue'),
+    meta: {
+      title: '轮播图管理',
+      icon: 'system-3'
+    }
+  },
 ]
 
 const router = createRouter({