Prechádzať zdrojové kódy

Merge branch 'dev' into xinyan

xinyan 6 mesiacov pred
rodič
commit
69c55639cf

+ 24 - 0
src/views/sku-manage/Columns.ts

@@ -37,3 +37,27 @@ export const ProductBrandColumns = [
     slots: { default: 'operate' }
   }
 ];
+
+export const ProductCategoryColumns = [
+  { type: 'seq', title: 'No.', width: 60, align: 'center', fixed: 'left' },
+  { field: 'name', title: '种类名称',  align: 'center', fixed: 'left',
+    slots: { default: 'name' }
+  },
+  {
+    field: 'main', title: '主属性', align: 'center', width: 480,
+    slots: { default: 'main' }
+  },
+  {
+    field: 'status', title: '状 态', align: 'center',
+    slots: { default: 'status' }
+  },
+  {
+    field: 'update_datetime', title: '更新时间', align: 'center',
+    slots: { default: 'update_datetime' }
+  },
+  { field: 'create_datetime', title: '创建时间',  align: 'center', slots: { default: 'create_datetime' } },
+  {
+    field: 'operate', title: '操 作', align: 'center', fixed: 'right',
+    slots: { default: 'operate' }
+  }
+];

+ 1 - 1
src/views/sku-manage/product-brand/component/DataTable.vue

@@ -51,7 +51,7 @@ const gridOptions: any = reactive({
     isHover: true
   },
   columnConfig: {
-    resizable: true
+    // resizable: true
   },
   pagerConfig: {
     total: tableOptions.value.total,

+ 29 - 0
src/views/sku-manage/product-category/api.ts

@@ -0,0 +1,29 @@
+import { request } from '/@/utils/service';
+
+
+const apiPrefix = '/api/cms/sku_kind/';
+
+export function getTableData(query: any) {
+  return request({
+    url: apiPrefix,
+    method: 'GET',
+    params: query
+  });
+}
+
+export function deleteRow(query: any) {
+  return request({
+    url: apiPrefix + query.id + '/',
+    method: 'DELETE',
+    params: query
+  });
+}
+
+export function updateRow(query: any) {
+  return request({
+    url: apiPrefix + query.id + '/',
+    method: 'PUT',
+    data: query
+  });
+}
+

+ 189 - 0
src/views/sku-manage/product-category/component/DataTable.vue

@@ -0,0 +1,189 @@
+<script lang="ts" setup>
+/**
+ * @Name: Table.vue
+ * @Description: 产品属性表格
+ * @Author: Cheney
+ */
+
+import { ElMessage } from 'element-plus';
+import { Download, InfoFilled, Refresh, Plus } from '@element-plus/icons-vue';
+import { usePagination } from '/@/utils/usePagination';
+import { useTableData } from '/@/utils/useTableData';
+import { useResponse } from '/@/utils/useResponse';
+import { ProductCategoryColumns } from '/@/views/sku-manage/Columns';
+import PermissionButton from '/@/components/PermissionButton/index.vue';
+import DataTableSlot from './DataTableSlot.vue';
+import EditDrawer from './EditDrawer.vue';
+import NoticeDialog from '/src/views/product-manage/product-list/component/NoticeDialog.vue';
+import * as api from '../api';
+
+
+interface Parameter {
+  name: string,
+}
+
+const queryParameter: Parameter | undefined = inject('query-parameter');
+const { tableOptions, handlePageChange } = usePagination(fetchList);
+
+const gridRef = ref();
+const gridOptions: any = reactive({
+  id: 'product-attribute-table',
+  keepSource: true,
+  size: 'small',
+  border: false,
+  round: true,
+  stripe: true,
+  currentRowHighLight: true,
+  height: '100%',
+  customConfig: {
+    storage: true,
+  },
+  toolbarConfig: {
+    size: 'large',
+    custom: true,
+    slots: {
+      tools: 'toolbar_tools',
+      buttons: 'toolbar_buttons'
+    }
+  },
+  rowConfig: {
+    isHover: true
+  },
+  columnConfig: {
+    resizable: true
+  },
+  pagerConfig: {
+    total: tableOptions.value.total,
+    page: tableOptions.value.page,
+    limit: tableOptions.value.limit
+  },
+  loading: false,
+  loadingConfig: {
+    icon: 'vxe-icon-indicator roll',
+    text: '正在拼命加载中...'
+  },
+  columns: '',
+  data: ''
+});
+
+const editOpen = ref(false);
+const rowData = ref({});
+
+const dialogVisible = ref(false);
+
+onMounted(() => {
+  fetchList();
+});
+
+async function fetchList() {
+  gridOptions.data = [];
+  gridOptions.columns = [];
+
+  const query = {
+    brand_name: queryParameter?.name,
+  };
+
+  await useTableData(api.getTableData, query, gridOptions);
+  await gridRef.value.loadColumn(ProductCategoryColumns);
+  gridOptions.showHeader = Boolean(gridOptions.data?.length);
+}
+
+function handleRefresh() {
+  fetchList();
+}
+
+function handleEdit(row: any) {
+  editOpen.value = true;
+  rowData.value = row;
+}
+
+async function singleDelete(row: any) {
+  const res = await useResponse(api.deleteRow, row);
+  if (res.code === 2000) {
+    ElMessage.success({ message: '删除成功', plain: true });
+    handleRefresh();
+  }
+}
+
+async function handleDownload() {
+}
+
+const gridEvents = {
+  custom ({ type }: any) {
+    // console.log(`点击 ${type}`)
+    if (type == 'confirm') {
+      fetchList();
+    }
+  }
+}
+
+defineExpose({ fetchList });
+</script>
+
+<template>
+  <vxe-grid ref="gridRef" v-bind="gridOptions" v-on="gridEvents">
+    <template #toolbar_buttons>
+      <PermissionButton type="primary" :icon="Plus" plain round>新 增</PermissionButton>
+    </template>
+    <!-- 工具栏右侧 -->
+    <template #toolbar_tools>
+      <el-button circle class="toolbar-btn" @click="handleRefresh">
+        <el-icon>
+          <Refresh />
+        </el-icon>
+      </el-button>
+      <el-popconfirm
+          :icon="InfoFilled"
+          icon-color="#626AEF"
+          title="是否确认导出当前时间内所有数据项?"
+          width="220"
+          @confirm="handleDownload"
+      >
+        <template #reference>
+          <el-button circle class="mr-3 toolbar-btn">
+            <el-icon>
+              <Download />
+            </el-icon>
+          </el-button>
+        </template>
+        <template #actions="{ confirm, cancel }">
+          <el-button size="small" @click="cancel">No!</el-button>
+          <el-button
+              size="small"
+              type="danger"
+              @click="confirm"
+          >
+            Yes?
+          </el-button>
+        </template>
+      </el-popconfirm>
+    </template>
+    <template #pager>
+      <vxe-pager
+          v-model:currentPage="gridOptions.pagerConfig.page"
+          v-model:pageSize="gridOptions.pagerConfig.limit"
+          :total="gridOptions.pagerConfig.total"
+          class="mt-1.5"
+          @page-change="handlePageChange"
+      />
+    </template>
+    <!-- 自定义列插槽 -->
+    <template v-for="col in ProductCategoryColumns" #[`${col.field}`]="{ row }">
+      <DataTableSlot :key="row.id" :field="col.field" :row="row" @edit-row="handleEdit" @handle-delete="singleDelete" />
+    </template>
+  </vxe-grid>
+  <EditDrawer v-if="editOpen" v-model="editOpen" :row-data="rowData" @refresh="handleRefresh" />
+  <NoticeDialog v-if="dialogVisible" v-model="dialogVisible" :row-data="rowData" />
+</template>
+
+<style scoped>
+.toolbar-btn {
+  width: 34px;
+  height: 34px;
+  font-size: 18px
+}
+
+:deep(.custom-el-input .el-select__wrapper) {
+  border-radius: 20px;
+}
+</style>

+ 82 - 0
src/views/sku-manage/product-category/component/DataTableSlot.vue

@@ -0,0 +1,82 @@
+<script lang="ts" setup>
+/**
+ * @Name: DataTableSlot.vue
+ * @Description: 产品属性-单元格插槽
+ * @Author: Cheney
+ */
+
+import { Delete, InfoFilled, Operation } from '@element-plus/icons-vue';
+import PermissionButton from '/@/components/PermissionButton/index.vue';
+import MainAttr from '/@/views/sku-manage/product-category/component/MainAttr.vue';
+
+
+const props = defineProps<{
+  row: any,
+  field: any
+}>();
+const { row, field } = props;
+
+const emit = defineEmits([ 'edit-row', 'handle-delete' ]);
+
+function handleEdit() {
+  emit('edit-row', row);
+}
+
+function onConfirm() {
+  emit('handle-delete', row);
+}
+</script>
+
+<template>
+  <div class="font-medium">
+    <div v-if="field === 'operate'">
+      <div class="flex justify-center gap-2">
+        <PermissionButton circle plain type="warning" @click="handleEdit">
+          <el-icon>
+            <Operation />
+          </el-icon>
+        </PermissionButton>
+        <el-popconfirm
+            :icon="InfoFilled"
+            icon-color="#626AEF"
+            title="你确定要删除此项吗?"
+            width="220"
+            @confirm="onConfirm"
+        >
+          <template #reference>
+            <PermissionButton circle plain type="danger">
+              <el-icon>
+                <Delete />
+              </el-icon>
+            </PermissionButton>
+          </template>
+          <template #actions="{ confirm, cancel }">
+            <el-button size="small" @click="cancel">No!</el-button>
+            <el-button
+                size="small"
+                type="danger"
+                @click="confirm"
+            >
+              Yes?
+            </el-button>
+          </template>
+        </el-popconfirm>
+      </div>
+    </div>
+    <div v-else-if="field === 'main'">
+     <MainAttr :attrs="row.RelatedAttrs" />
+    </div>
+    <div v-else-if="field === 'status'">
+      <el-tag :type="row.status === 3 ? 'primary' : row.status === 1 ? 'warning' : row.status === 2 ? 'info' : ''">
+        {{ row.status === 3 ? '已发布' : row.status === 1 ? '草稿' : row.status === 2 ? '待审批' : row.status }}
+      </el-tag>
+    </div>
+    <div v-else>
+      {{ row[field] }}
+    </div>
+  </div>
+</template>
+
+<style scoped>
+
+</style>

+ 107 - 0
src/views/sku-manage/product-category/component/EditDrawer.vue

@@ -0,0 +1,107 @@
+<script lang="ts" setup>
+/**
+ * @Name: EditDrawer.vue
+ * @Description: 产品属性-行编辑
+ * @Author: Cheney
+ */
+
+import { ElMessage, FormInstance, FormRules } from 'element-plus';
+import { Close, Finished } from '@element-plus/icons-vue';
+import { useResponse } from '/@/utils/useResponse';
+import * as api from '../api';
+
+
+const btnLoading = ref(false);
+
+const editOpen = defineModel({ default: false });
+
+const editDrawer = <Ref>useTemplateRef('editDrawer');
+
+const props = defineProps({
+  rowData: Object
+});
+const { rowData } = props;
+const emit = defineEmits([ 'refresh' ]);
+
+interface RuleForm {
+  name: any,
+}
+
+const ruleFormRef = ref<FormInstance>();
+const ruleForm = reactive<RuleForm>({
+  name: rowData?.name
+});
+
+const rules = reactive<FormRules<RuleForm>>({
+  // shop_name: [ { required: true, message: '请输入店铺', trigger: 'blur' } ],
+});
+
+const submitForm = async (formEl: FormInstance | undefined) => {
+  if (!formEl) return;
+  await formEl.validate(async (valid, fields) => {
+    if (valid) {
+      try {
+        const res = await useResponse(api.updateRow, { id: rowData?.id, ...ruleForm }, btnLoading);
+        if (res && res.code == 2000) {
+          editOpen.value = false;
+          ElMessage.success('编辑成功');
+          emit('refresh');
+        }
+      } catch (error) {
+        console.error('Error==>', error);
+      }
+    } else {
+      console.log('error submit!', fields);
+    }
+  });
+};
+
+function closeDrawer() {
+  editDrawer.value.handleClose();
+}
+
+</script>
+
+<template>
+  <div class="drawer-container">
+    <el-drawer ref="editDrawer"
+               v-model="editOpen"
+               :close-on-click-modal="false"
+               :close-on-press-escape="false"
+               :title="`产品种类- 编辑 `"
+               size="25%">
+      <el-form
+          ref="ruleFormRef"
+          :model="ruleForm"
+          :rules="rules"
+          class="mx-2.5 mt-7"
+          label-position="top"
+          label-width="auto"
+          status-icon>
+        <el-form-item class="font-medium" label="种类名称" prop="name">
+          <el-input v-model="ruleForm.name" />
+        </el-form-item>
+        <el-form-item>
+          <el-divider />
+          <div class="flex flex-1 justify-end">
+            <el-button :icon="Close" @click="closeDrawer">取 消</el-button>
+            <el-button :icon="Finished" :loading="btnLoading" type="primary" @click="submitForm(ruleFormRef)">
+              确 定
+            </el-button>
+          </div>
+        </el-form-item>
+      </el-form>
+    </el-drawer>
+  </div>
+</template>
+
+<style scoped>
+.drawer-container :deep(.el-drawer__header) {
+  border-bottom: none;
+  font-weight: 500;
+}
+
+.drawer-container :deep(.el-drawer__title) {
+  font-size: 18px;
+}
+</style>

+ 63 - 0
src/views/sku-manage/product-category/component/MainAttr.vue

@@ -0,0 +1,63 @@
+<script lang="ts" setup>
+/**
+ * @Name: MainAttr.vue
+ * @Description: 产品种类-主属性插槽
+ * @Author: Cheney
+ */
+
+import XEUtils from 'xe-utils';
+
+
+const props = defineProps({
+  attrs: {
+    required: true,
+    type: Array
+  }
+});
+
+const groupedAttrs = computed(() => {
+  const retData: Array<any> = [];
+  if (props.attrs.length === 0) {
+    return retData;
+  }
+  const info = XEUtils.groupBy(props.attrs, 'section');  // 使用 XEUtils 的 groupBy 方法将 props.attrs 中的元素根据 'section' 属性进行分组
+  const sortedKey = XEUtils.sortBy(Object.keys(info), 'asc'); // 或 'desc'
+  for (const section of sortedKey) {
+    const attrItems = XEUtils.sortBy(info[section], 'order');
+    retData.push({
+      section: section,
+      items: attrItems
+    });
+  }
+  return retData;
+});
+</script>
+
+<template>
+  <div>
+    <template v-for="info of groupedAttrs">
+      <div v-for="item of info.items" :key="item.id" style="display: inline;">
+        <!--<el-tooltip effect="dark" :content="item.description" placement="top-start" :disabled="!item.description">-->
+        <el-tag
+            :key="item.id"
+            :type="item.description? 'warning': ''"
+            disable-transitions
+            effect="dark"
+            plain
+            size="small"
+        >
+          {{ item.attr.name }}
+        </el-tag>
+        <!--</el-tooltip>-->
+      </div>
+      <span class="line-separator">——</span>
+    </template>
+  </div>
+
+</template>
+
+<style scoped>
+.line-separator:last-of-type {
+  display: none;
+}
+</style>

+ 78 - 0
src/views/sku-manage/product-category/index.vue

@@ -0,0 +1,78 @@
+<script lang="ts" setup>
+/**
+ * @Name: index.vue
+ * @Description: 产品种类
+ * @Author: Cheney
+ */
+
+import { RefreshRight, Search } from '@element-plus/icons-vue';
+import DataTable from './component/DataTable.vue';
+import VerticalDivider from '/@/components/VerticalDivider/index.vue';
+import { useTableHeight } from '/@/utils/useTableHeight';
+
+
+const titleContainer: Ref<HTMLElement | null> = useTemplateRef('titleContainer');
+const queryContainer: Ref<HTMLElement | null> = useTemplateRef('queryContainer');
+const { tableHeight } = useTableHeight(titleContainer, queryContainer);
+
+const tableRef: Ref<any> = useTemplateRef('table');
+
+const btnLoading = ref(false);
+
+const formInline = reactive<any>({
+  name: '',
+});
+provide('query-parameter', formInline);
+
+async function handleQuery() {
+  btnLoading.value = true;
+  await tableRef.value?.fetchList();
+  btnLoading.value = false;
+}
+
+function resetParameter() {
+  for (const key in formInline) {
+    formInline[key] = '';
+  }
+}
+</script>
+
+<template>
+  <div class="p-5 flex-grow">
+    <el-card class="h-full" style="color: rgba(0, 0, 0, 0.88);">
+      <div ref="titleContainer" class="text-xl font-semibold pb-5">产品属性</div>
+      <!-- 查询条件 -->
+      <div ref="queryContainer" class="flex justify-between">
+        <div class="flex flex-1">
+          <div class="w-full whitespace-nowrap">
+            <el-row :gutter="20" style="margin-bottom: 5px;">
+              <el-col :span="5">
+                <div class="flex items-center">
+                  <span class="mr-2">品牌名称</span>
+                  <el-input v-model="formInline.name" clearable placeholder="请输入品牌名称" />
+                </div>
+              </el-col>
+            </el-row>
+          </div>
+        </div>
+        <VerticalDivider />
+        <div class="flex gap-1.5 ml-5">
+          <el-button :icon="Search" :loading="btnLoading" type="primary" @click="handleQuery">
+            查 询
+          </el-button>
+          <el-button :icon="RefreshRight" color="#ECECF1C9" style="width: 88px; color: #3c3c3c;" @click="resetParameter">
+            重 置
+          </el-button>
+        </div>
+      </div>
+      <el-divider ref="dividerContainer" style="margin: 20px 0 12px 0;" />
+      <div :style="{ height: tableHeight + 'px' }">
+        <DataTable ref="table" />
+      </div>
+    </el-card>
+  </div>
+</template>
+
+<style scoped>
+
+</style>

+ 21 - 16
vite.config.ts

@@ -3,10 +3,10 @@ import { resolve } from 'path';
 import { ConfigEnv, defineConfig, loadEnv } from 'vite';
 import vueJsx from '@vitejs/plugin-vue-jsx';
 import compression from 'vite-plugin-compression';
-import AutoImport from 'unplugin-auto-import/vite'
-import Components from 'unplugin-vue-components/vite'
-import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
-import { lazyImport, VxeResolver } from 'vite-plugin-lazy-import'
+import AutoImport from 'unplugin-auto-import/vite';
+import Components from 'unplugin-vue-components/vite';
+import { ElementPlusResolver } from 'unplugin-vue-components/resolvers';
+import { lazyImport, VxeResolver } from 'vite-plugin-lazy-import';
 
 
 const pathResolve = (dir: string) => {
@@ -35,8 +35,8 @@ const viteConfig = defineConfig((mode: ConfigEnv) => {
           {
             'dayjs': [
               // 默认导入
-              ['default', 'dayjs']
-            ],
+              [ 'default', 'dayjs' ]
+            ]
             // '/@/utils/useTableData': [
             //   'useTableData'
             // ],
@@ -46,10 +46,10 @@ const viteConfig = defineConfig((mode: ConfigEnv) => {
             // '/@/utils/useResponse': [
             //   'useResponse'
             // ]
-          },
+          }
         ],
-        resolvers: [ElementPlusResolver()],
-        dts: 'src/auto-imports.d.ts', // 生成 TypeScript 声明文件
+        resolvers: [ ElementPlusResolver() ],
+        dts: 'src/auto-imports.d.ts' // 生成 TypeScript 声明文件
       }),
       Components({
         // resolvers: [ElementPlusResolver()],
@@ -58,7 +58,7 @@ const viteConfig = defineConfig((mode: ConfigEnv) => {
         algorithm: 'gzip', // 使用 gzip 压缩
         ext: '.gz', // 输出的文件扩展名
         threshold: 10240, // 只有大小大于该值的资源会被压缩(默认 10KB)
-        deleteOriginFile: false, // 是否删除原始未压缩的文件
+        deleteOriginFile: false // 是否删除原始未压缩的文件
       }),
       lazyImport({
         resolvers: [
@@ -75,7 +75,12 @@ const viteConfig = defineConfig((mode: ConfigEnv) => {
     resolve: { alias },
     base: mode.command === 'serve' ? './' : env.VITE_PUBLIC_PATH,
     optimizeDeps: {
-      include: [ 'element-plus/es/locale/lang/zh-cn', 'element-plus/es/locale/lang/en', 'element-plus/es/locale/lang/zh-tw' ]
+      include: [ 
+          'element-plus/es/locale/lang/zh-cn', 
+          'element-plus/es/locale/lang/en', 
+          'element-plus/es/locale/lang/zh-tw', 
+          '@fast-crud/fast-crud'
+      ]
     },
     server: {
       host: '0.0.0.0',
@@ -103,7 +108,7 @@ const viteConfig = defineConfig((mode: ConfigEnv) => {
           manualChunks: {
             vue: [ 'vue', 'vue-router', 'pinia' ],
             echarts: [ 'echarts' ],
-            elementPlus: ['element-plus'],
+            elementPlus: [ 'element-plus' ]
           }
           // manualChunks(id) {
           //   if (id.includes('node_modules')) {
@@ -113,11 +118,11 @@ const viteConfig = defineConfig((mode: ConfigEnv) => {
         }
       }
     },
-    css: { 
-      preprocessorOptions: { 
+    css: {
+      preprocessorOptions: {
         css: { charset: false },
-        scss: { api: 'modern-compiler' },
-      } 
+        scss: { api: 'modern-compiler' }
+      }
     },
     define: {
       __VUE_I18N_LEGACY_API__: JSON.stringify(false),