fix: 自定义标签
This commit is contained in:
@@ -1,99 +0,0 @@
|
|||||||
import { resolve } from 'path'
|
|
||||||
import Vue from '@vitejs/plugin-vue'
|
|
||||||
import VueJsx from '@vitejs/plugin-vue-jsx'
|
|
||||||
import progress from 'vite-plugin-progress'
|
|
||||||
import EslintPlugin from 'vite-plugin-eslint'
|
|
||||||
import PurgeIcons from 'vite-plugin-purge-icons'
|
|
||||||
import { ViteEjsPlugin } from 'vite-plugin-ejs'
|
|
||||||
// @ts-ignore
|
|
||||||
import ElementPlus from 'unplugin-element-plus/vite'
|
|
||||||
import AutoImport from 'unplugin-auto-import/vite'
|
|
||||||
import Components from 'unplugin-vue-components/vite'
|
|
||||||
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
|
|
||||||
import viteCompression from 'vite-plugin-compression'
|
|
||||||
import topLevelAwait from 'vite-plugin-top-level-await'
|
|
||||||
import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite'
|
|
||||||
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons-ng'
|
|
||||||
import UnoCSS from 'unocss/vite'
|
|
||||||
|
|
||||||
export function createVitePlugins() {
|
|
||||||
const root = process.cwd()
|
|
||||||
|
|
||||||
// 路径查找
|
|
||||||
function pathResolve(dir: string) {
|
|
||||||
return resolve(root, '.', dir)
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
Vue(),
|
|
||||||
VueJsx(),
|
|
||||||
UnoCSS(),
|
|
||||||
progress(),
|
|
||||||
PurgeIcons(),
|
|
||||||
ElementPlus({}),
|
|
||||||
AutoImport({
|
|
||||||
include: [
|
|
||||||
/\.[tj]sx?$/, // .ts, .tsx, .js, .jsx
|
|
||||||
/\.vue$/,
|
|
||||||
/\.vue\?vue/, // .vue
|
|
||||||
/\.md$/ // .md
|
|
||||||
],
|
|
||||||
imports: [
|
|
||||||
'vue',
|
|
||||||
'vue-router',
|
|
||||||
// 可额外添加需要 autoImport 的组件
|
|
||||||
{
|
|
||||||
'@/hooks/web/useI18n': ['useI18n'],
|
|
||||||
'@/hooks/web/useMessage': ['useMessage'],
|
|
||||||
'@/hooks/web/useTable': ['useTable'],
|
|
||||||
'@/hooks/web/useCrudSchemas': ['useCrudSchemas'],
|
|
||||||
'@/utils/formRules': ['required'],
|
|
||||||
'@/utils/dict': ['DICT_TYPE']
|
|
||||||
}
|
|
||||||
],
|
|
||||||
dts: 'src/types/auto-imports.d.ts',
|
|
||||||
resolvers: [ElementPlusResolver()],
|
|
||||||
eslintrc: {
|
|
||||||
enabled: false, // Default `false`
|
|
||||||
filepath: './.eslintrc-auto-import.json', // Default `./.eslintrc-auto-import.json`
|
|
||||||
globalsPropValue: true // Default `true`, (true | false | 'readonly' | 'readable' | 'writable' | 'writeable')
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
Components({
|
|
||||||
// 生成自定义 `auto-components.d.ts` 全局声明
|
|
||||||
dts: 'src/types/auto-components.d.ts',
|
|
||||||
// 自定义组件的解析器
|
|
||||||
resolvers: [ElementPlusResolver()],
|
|
||||||
globs: ["src/components/**/**.{vue, md}", '!src/components/DiyEditor/components/mobile/**']
|
|
||||||
}),
|
|
||||||
EslintPlugin({
|
|
||||||
cache: false,
|
|
||||||
include: ['src/**/*.vue', 'src/**/*.ts', 'src/**/*.tsx'] // 检查的文件
|
|
||||||
}),
|
|
||||||
VueI18nPlugin({
|
|
||||||
runtimeOnly: true,
|
|
||||||
compositionOnly: true,
|
|
||||||
include: [resolve(__dirname, 'src/locales/**')]
|
|
||||||
}),
|
|
||||||
createSvgIconsPlugin({
|
|
||||||
iconDirs: [pathResolve('src/assets/svgs')],
|
|
||||||
symbolId: 'icon-[dir]-[name]',
|
|
||||||
}),
|
|
||||||
viteCompression({
|
|
||||||
verbose: true, // 是否在控制台输出压缩结果
|
|
||||||
disable: false, // 是否禁用
|
|
||||||
threshold: 10240, // 体积大于 threshold 才会被压缩,单位 b
|
|
||||||
algorithm: 'gzip', // 压缩算法,可选 [ 'gzip' , 'brotliCompress' ,'deflate' , 'deflateRaw']
|
|
||||||
ext: '.gz', // 生成的压缩包后缀
|
|
||||||
deleteOriginFile: false //压缩后是否删除源文件
|
|
||||||
}),
|
|
||||||
ViteEjsPlugin(),
|
|
||||||
topLevelAwait({
|
|
||||||
// https://juejin.cn/post/7152191742513512485
|
|
||||||
// The export name of top-level await promise for each chunk module
|
|
||||||
promiseExportName: '__tla',
|
|
||||||
// The function to generate import names of top-level await promise in each chunk module
|
|
||||||
promiseImportName: (i) => `__tla_${i}`
|
|
||||||
})
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
const include = [
|
|
||||||
'qs',
|
|
||||||
'url',
|
|
||||||
'vue',
|
|
||||||
'sass',
|
|
||||||
'mitt',
|
|
||||||
'axios',
|
|
||||||
'pinia',
|
|
||||||
'dayjs',
|
|
||||||
'qrcode',
|
|
||||||
'unocss',
|
|
||||||
'vue-router',
|
|
||||||
'vue-types',
|
|
||||||
'vue-i18n',
|
|
||||||
'crypto-js',
|
|
||||||
'cropperjs',
|
|
||||||
'lodash-es',
|
|
||||||
'nprogress',
|
|
||||||
'web-storage-cache',
|
|
||||||
'@iconify/iconify',
|
|
||||||
'@vueuse/core',
|
|
||||||
'@zxcvbn-ts/core',
|
|
||||||
'echarts/core',
|
|
||||||
'echarts/charts',
|
|
||||||
'echarts/components',
|
|
||||||
'echarts/renderers',
|
|
||||||
'echarts-wordcloud',
|
|
||||||
'@wangeditor-next/editor',
|
|
||||||
'@wangeditor-next/editor-for-vue',
|
|
||||||
'@microsoft/fetch-event-source',
|
|
||||||
'markdown-it',
|
|
||||||
'markmap-view',
|
|
||||||
'markmap-lib',
|
|
||||||
'markmap-toolbar',
|
|
||||||
'highlight.js',
|
|
||||||
'element-plus',
|
|
||||||
'element-plus/es',
|
|
||||||
'element-plus/es/locale/lang/zh-cn',
|
|
||||||
'element-plus/es/locale/lang/en',
|
|
||||||
'element-plus/es/components/avatar/style/css',
|
|
||||||
'element-plus/es/components/space/style/css',
|
|
||||||
'element-plus/es/components/backtop/style/css',
|
|
||||||
'element-plus/es/components/form/style/css',
|
|
||||||
'element-plus/es/components/radio-group/style/css',
|
|
||||||
'element-plus/es/components/radio/style/css',
|
|
||||||
'element-plus/es/components/checkbox/style/css',
|
|
||||||
'element-plus/es/components/checkbox-group/style/css',
|
|
||||||
'element-plus/es/components/switch/style/css',
|
|
||||||
'element-plus/es/components/time-picker/style/css',
|
|
||||||
'element-plus/es/components/date-picker/style/css',
|
|
||||||
'element-plus/es/components/descriptions/style/css',
|
|
||||||
'element-plus/es/components/descriptions-item/style/css',
|
|
||||||
'element-plus/es/components/link/style/css',
|
|
||||||
'element-plus/es/components/tooltip/style/css',
|
|
||||||
'element-plus/es/components/drawer/style/css',
|
|
||||||
'element-plus/es/components/dialog/style/css',
|
|
||||||
'element-plus/es/components/checkbox-button/style/css',
|
|
||||||
'element-plus/es/components/option-group/style/css',
|
|
||||||
'element-plus/es/components/radio-button/style/css',
|
|
||||||
'element-plus/es/components/cascader/style/css',
|
|
||||||
'element-plus/es/components/color-picker/style/css',
|
|
||||||
'element-plus/es/components/input-number/style/css',
|
|
||||||
'element-plus/es/components/rate/style/css',
|
|
||||||
'element-plus/es/components/select-v2/style/css',
|
|
||||||
'element-plus/es/components/tree-select/style/css',
|
|
||||||
'element-plus/es/components/slider/style/css',
|
|
||||||
'element-plus/es/components/time-select/style/css',
|
|
||||||
'element-plus/es/components/autocomplete/style/css',
|
|
||||||
'element-plus/es/components/image-viewer/style/css',
|
|
||||||
'element-plus/es/components/upload/style/css',
|
|
||||||
'element-plus/es/components/col/style/css',
|
|
||||||
'element-plus/es/components/form-item/style/css',
|
|
||||||
'element-plus/es/components/alert/style/css',
|
|
||||||
'element-plus/es/components/breadcrumb/style/css',
|
|
||||||
'element-plus/es/components/select/style/css',
|
|
||||||
'element-plus/es/components/input/style/css',
|
|
||||||
'element-plus/es/components/breadcrumb-item/style/css',
|
|
||||||
'element-plus/es/components/tag/style/css',
|
|
||||||
'element-plus/es/components/pagination/style/css',
|
|
||||||
'element-plus/es/components/table/style/css',
|
|
||||||
'element-plus/es/components/table-v2/style/css',
|
|
||||||
'element-plus/es/components/table-column/style/css',
|
|
||||||
'element-plus/es/components/card/style/css',
|
|
||||||
'element-plus/es/components/row/style/css',
|
|
||||||
'element-plus/es/components/button/style/css',
|
|
||||||
'element-plus/es/components/menu/style/css',
|
|
||||||
'element-plus/es/components/sub-menu/style/css',
|
|
||||||
'element-plus/es/components/menu-item/style/css',
|
|
||||||
'element-plus/es/components/option/style/css',
|
|
||||||
'element-plus/es/components/dropdown/style/css',
|
|
||||||
'element-plus/es/components/dropdown-menu/style/css',
|
|
||||||
'element-plus/es/components/dropdown-item/style/css',
|
|
||||||
'element-plus/es/components/skeleton/style/css',
|
|
||||||
'element-plus/es/components/skeleton/style/css',
|
|
||||||
'element-plus/es/components/backtop/style/css',
|
|
||||||
'element-plus/es/components/menu/style/css',
|
|
||||||
'element-plus/es/components/sub-menu/style/css',
|
|
||||||
'element-plus/es/components/menu-item/style/css',
|
|
||||||
'element-plus/es/components/dropdown/style/css',
|
|
||||||
'element-plus/es/components/tree/style/css',
|
|
||||||
'element-plus/es/components/dropdown-menu/style/css',
|
|
||||||
'element-plus/es/components/dropdown-item/style/css',
|
|
||||||
'element-plus/es/components/badge/style/css',
|
|
||||||
'element-plus/es/components/breadcrumb/style/css',
|
|
||||||
'element-plus/es/components/breadcrumb-item/style/css',
|
|
||||||
'element-plus/es/components/image/style/css',
|
|
||||||
'element-plus/es/components/collapse-transition/style/css',
|
|
||||||
'element-plus/es/components/timeline/style/css',
|
|
||||||
'element-plus/es/components/timeline-item/style/css',
|
|
||||||
'element-plus/es/components/collapse/style/css',
|
|
||||||
'element-plus/es/components/collapse-item/style/css',
|
|
||||||
'element-plus/es/components/button-group/style/css',
|
|
||||||
'element-plus/es/components/text/style/css',
|
|
||||||
'element-plus/es/components/segmented/style/css',
|
|
||||||
'@element-plus/icons-vue',
|
|
||||||
'element-plus/es/components/footer/style/css',
|
|
||||||
'element-plus/es/components/empty/style/css',
|
|
||||||
'element-plus/es/components/mention/style/css',
|
|
||||||
'element-plus/es/components/progress/style/css'
|
|
||||||
]
|
|
||||||
|
|
||||||
const exclude = ['@iconify/json']
|
|
||||||
|
|
||||||
export { include, exclude }
|
|
||||||
3531
package-lock.json
generated
3531
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -137,6 +137,7 @@
|
|||||||
"vite-plugin-eslint": "^1.8.1",
|
"vite-plugin-eslint": "^1.8.1",
|
||||||
"vite-plugin-progress": "^0.0.7",
|
"vite-plugin-progress": "^0.0.7",
|
||||||
"vite-plugin-purge-icons": "^0.10.0",
|
"vite-plugin-purge-icons": "^0.10.0",
|
||||||
|
"vite-plugin-svg-icons": "^2.0.1",
|
||||||
"vite-plugin-svg-icons-ng": "^1.3.1",
|
"vite-plugin-svg-icons-ng": "^1.3.1",
|
||||||
"vite-plugin-top-level-await": "^1.4.4",
|
"vite-plugin-top-level-await": "^1.4.4",
|
||||||
"vue-eslint-parser": "^9.3.2",
|
"vue-eslint-parser": "^9.3.2",
|
||||||
|
|||||||
1951
pnpm-lock.yaml
generated
1951
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
55
src/api/ydoyun/customtag/index.ts
Normal file
55
src/api/ydoyun/customtag/index.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import request from '@/config/axios'
|
||||||
|
import type { Dayjs } from 'dayjs';
|
||||||
|
|
||||||
|
/** 自定义标签类型:product-产品, store-店铺, supplier-供货商 */
|
||||||
|
export type CustomTagType = 'product' | 'store' | 'supplier'
|
||||||
|
|
||||||
|
/** 自定义标签信息 */
|
||||||
|
export interface CustomTag {
|
||||||
|
id?: number
|
||||||
|
type?: CustomTagType // 类型,用于区分产品/店铺/供货商
|
||||||
|
name?: string
|
||||||
|
expression?: string
|
||||||
|
color?: string
|
||||||
|
sqlScript?: string
|
||||||
|
useParams?: boolean | number
|
||||||
|
params?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自定义标签 API
|
||||||
|
export const CustomTagApi = {
|
||||||
|
// 查询自定义标签分页
|
||||||
|
getCustomTagPage: async (params: any) => {
|
||||||
|
return await request.get({ url: `/ydoyun/custom-tag/page`, params })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 查询自定义标签详情
|
||||||
|
getCustomTag: async (id: number) => {
|
||||||
|
return await request.get({ url: `/ydoyun/custom-tag/get?id=` + id })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 新增自定义标签
|
||||||
|
createCustomTag: async (data: CustomTag) => {
|
||||||
|
return await request.post({ url: `/ydoyun/custom-tag/create`, data })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 修改自定义标签
|
||||||
|
updateCustomTag: async (data: CustomTag) => {
|
||||||
|
return await request.put({ url: `/ydoyun/custom-tag/update`, data })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 删除自定义标签
|
||||||
|
deleteCustomTag: async (id: number) => {
|
||||||
|
return await request.delete({ url: `/ydoyun/custom-tag/delete?id=` + id })
|
||||||
|
},
|
||||||
|
|
||||||
|
/** 批量删除自定义标签 */
|
||||||
|
deleteCustomTagList: async (ids: number[]) => {
|
||||||
|
return await request.delete({ url: `/ydoyun/custom-tag/delete-list?ids=${ids.join(',')}` })
|
||||||
|
},
|
||||||
|
|
||||||
|
// 导出自定义标签 Excel
|
||||||
|
exportCustomTag: async (params) => {
|
||||||
|
return await request.download({ url: `/ydoyun/custom-tag/export-excel`, params })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -822,6 +822,46 @@ const remainingRouter: AppRouteRecordRaw[] = [
|
|||||||
component: () => import('@/views/ydoyun/report/lijun/reportpage6/detail.vue')
|
component: () => import('@/views/ydoyun/report/lijun/reportpage6/detail.vue')
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/ydoyuntag',
|
||||||
|
component: Layout,
|
||||||
|
name: 'YdoyunTag',
|
||||||
|
meta: {
|
||||||
|
hidden: true
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: 'product-custom-tag',
|
||||||
|
name: 'ProductCustomTag',
|
||||||
|
meta: {
|
||||||
|
title: '产品标签',
|
||||||
|
noCache: true,
|
||||||
|
canTo: true
|
||||||
|
},
|
||||||
|
component: () => import(/* webpackChunkName: "product-custom-tag" */ '@/views/ydoyun/productcustomtag/index.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'store-custom-tag',
|
||||||
|
name: 'StoreCustomTag',
|
||||||
|
meta: {
|
||||||
|
title: '店铺标签',
|
||||||
|
noCache: true,
|
||||||
|
canTo: true
|
||||||
|
},
|
||||||
|
component: () => import(/* webpackChunkName: "store-custom-tag" */ '@/views/ydoyun/storecustomtag/index.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'supplier-custom-tag',
|
||||||
|
name: 'SupplierCustomTag',
|
||||||
|
meta: {
|
||||||
|
title: '供货商标签',
|
||||||
|
noCache: true,
|
||||||
|
canTo: true
|
||||||
|
},
|
||||||
|
component: () => import(/* webpackChunkName: "supplier-custom-tag" */ '@/views/ydoyun/suppliercustomtag/index.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
203
src/views/ydoyun/customtag/CustomTagForm.vue
Normal file
203
src/views/ydoyun/customtag/CustomTagForm.vue
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
<template>
|
||||||
|
<Dialog :title="dialogTitle" v-model="dialogVisible" width="560px">
|
||||||
|
<el-form
|
||||||
|
ref="formRef"
|
||||||
|
:model="formData"
|
||||||
|
:rules="formRules"
|
||||||
|
label-width="110px"
|
||||||
|
v-loading="formLoading"
|
||||||
|
>
|
||||||
|
<el-form-item label="标签名称" prop="name">
|
||||||
|
<el-input v-model="formData.name" placeholder="请输入标签名称,如:销售区域" maxlength="50" show-word-limit />
|
||||||
|
<div class="form-tip">用于报表中展示的标签名称</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="公式描述" prop="expression">
|
||||||
|
<el-input
|
||||||
|
v-model="formData.expression"
|
||||||
|
type="textarea"
|
||||||
|
:rows="2"
|
||||||
|
placeholder="请输入公式描述,如:${region_name}"
|
||||||
|
/>
|
||||||
|
<div class="form-tip">报表中引用的表达式,支持 ${字段名} 格式</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="标签颜色" prop="color">
|
||||||
|
<div class="color-field">
|
||||||
|
<div class="color-presets">
|
||||||
|
<span
|
||||||
|
v-for="c in presetColors"
|
||||||
|
:key="c"
|
||||||
|
class="color-chip"
|
||||||
|
:class="{ active: formData.color === c }"
|
||||||
|
:style="{ backgroundColor: c }"
|
||||||
|
@click="formData.color = c"
|
||||||
|
></span>
|
||||||
|
</div>
|
||||||
|
<div class="color-picker-wrap">
|
||||||
|
<el-color-picker v-model="formData.color" :predefine="presetColors" />
|
||||||
|
<el-input
|
||||||
|
v-model="formData.color"
|
||||||
|
placeholder="或输入颜色值"
|
||||||
|
class="color-input"
|
||||||
|
maxlength="20"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-tip">用于报表展示时的颜色标识,可点击预设色或使用取色盘</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="SQL 脚本" prop="sqlScript">
|
||||||
|
<el-input
|
||||||
|
v-model="formData.sqlScript"
|
||||||
|
type="textarea"
|
||||||
|
:rows="4"
|
||||||
|
placeholder="请输入 SQL 脚本,用于获取标签数据"
|
||||||
|
/>
|
||||||
|
<div class="form-tip">执行 SQL 获取标签选项,支持多列,第一列作为显示值</div>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
|
||||||
|
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { CustomTagApi, CustomTag } from '@/api/ydoyun/customtag'
|
||||||
|
|
||||||
|
defineOptions({ name: 'CustomTagForm' })
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const message = useMessage()
|
||||||
|
|
||||||
|
const dialogVisible = ref(false)
|
||||||
|
const dialogTitle = ref('')
|
||||||
|
const formLoading = ref(false)
|
||||||
|
const formType = ref('')
|
||||||
|
|
||||||
|
const formData = ref({
|
||||||
|
id: undefined,
|
||||||
|
name: undefined,
|
||||||
|
expression: undefined,
|
||||||
|
color: undefined,
|
||||||
|
sqlScript: undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
const formRules = reactive({
|
||||||
|
name: [{ required: true, message: '标签名称不能为空', trigger: 'blur' }]
|
||||||
|
})
|
||||||
|
|
||||||
|
const formRef = ref()
|
||||||
|
|
||||||
|
/** 预设颜色选项 */
|
||||||
|
const presetColors = [
|
||||||
|
'#1890ff',
|
||||||
|
'#52c41a',
|
||||||
|
'#faad14',
|
||||||
|
'#f5222d',
|
||||||
|
'#722ed1',
|
||||||
|
'#eb2f96',
|
||||||
|
'#13c2c2',
|
||||||
|
'#fa8c16'
|
||||||
|
]
|
||||||
|
|
||||||
|
/** 打开弹窗 */
|
||||||
|
const open = async (type: string, id?: number) => {
|
||||||
|
dialogVisible.value = true
|
||||||
|
dialogTitle.value = t('action.' + type)
|
||||||
|
formType.value = type
|
||||||
|
resetForm()
|
||||||
|
|
||||||
|
if (id) {
|
||||||
|
formLoading.value = true
|
||||||
|
try {
|
||||||
|
const data = await CustomTagApi.getCustomTag(id)
|
||||||
|
formData.value = { ...data }
|
||||||
|
} finally {
|
||||||
|
formLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
defineExpose({ open })
|
||||||
|
|
||||||
|
const emit = defineEmits(['success'])
|
||||||
|
const submitForm = async () => {
|
||||||
|
await formRef.value.validate()
|
||||||
|
formLoading.value = true
|
||||||
|
try {
|
||||||
|
const data = formData.value as unknown as CustomTag
|
||||||
|
if (formType.value === 'create') {
|
||||||
|
await CustomTagApi.createCustomTag(data)
|
||||||
|
message.success(t('common.createSuccess'))
|
||||||
|
} else {
|
||||||
|
await CustomTagApi.updateCustomTag(data)
|
||||||
|
message.success(t('common.updateSuccess'))
|
||||||
|
}
|
||||||
|
dialogVisible.value = false
|
||||||
|
emit('success')
|
||||||
|
} finally {
|
||||||
|
formLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 重置表单 */
|
||||||
|
const resetForm = () => {
|
||||||
|
formData.value = {
|
||||||
|
id: undefined,
|
||||||
|
name: undefined,
|
||||||
|
expression: undefined,
|
||||||
|
color: undefined,
|
||||||
|
sqlScript: undefined
|
||||||
|
}
|
||||||
|
formRef.value?.resetFields()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.form-tip {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
line-height: 1.4;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-field {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-presets {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-chip {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-chip:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-chip.active {
|
||||||
|
border-color: var(--el-color-primary);
|
||||||
|
box-shadow: 0 0 0 2px var(--el-color-primary-light-7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-picker-wrap {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.color-picker-wrap .color-input {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
232
src/views/ydoyun/customtag/index.vue
Normal file
232
src/views/ydoyun/customtag/index.vue
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
<template>
|
||||||
|
<ContentWrap>
|
||||||
|
<!-- 搜索 -->
|
||||||
|
<el-form
|
||||||
|
class="-mb-15px"
|
||||||
|
:model="queryParams"
|
||||||
|
ref="queryFormRef"
|
||||||
|
:inline="true"
|
||||||
|
label-width="90px"
|
||||||
|
>
|
||||||
|
<el-form-item label="标签名称" prop="name">
|
||||||
|
<el-input
|
||||||
|
v-model="queryParams.name"
|
||||||
|
placeholder="请输入标签名称"
|
||||||
|
clearable
|
||||||
|
@keyup.enter="handleQuery"
|
||||||
|
class="!w-240px"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="公式描述" prop="expression">
|
||||||
|
<el-input
|
||||||
|
v-model="queryParams.expression"
|
||||||
|
placeholder="请输入公式描述"
|
||||||
|
clearable
|
||||||
|
@keyup.enter="handleQuery"
|
||||||
|
class="!w-240px"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="创建时间" prop="createTime">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="queryParams.createTime"
|
||||||
|
value-format="YYYY-MM-DD HH:mm:ss"
|
||||||
|
type="daterange"
|
||||||
|
start-placeholder="开始日期"
|
||||||
|
end-placeholder="结束日期"
|
||||||
|
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
|
||||||
|
class="!w-220px"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button @click="handleQuery"><Icon icon="ep:search" />搜索</el-button>
|
||||||
|
<el-button @click="resetQuery"><Icon icon="ep:refresh" />重置</el-button>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
plain
|
||||||
|
@click="openForm('create')"
|
||||||
|
v-hasPermi="['ydoyun:custom-tag:create']"
|
||||||
|
>
|
||||||
|
<Icon icon="ep:plus" /> 新增
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="success"
|
||||||
|
plain
|
||||||
|
@click="handleExport"
|
||||||
|
:loading="exportLoading"
|
||||||
|
v-hasPermi="['ydoyun:custom-tag:export']"
|
||||||
|
>
|
||||||
|
<Icon icon="ep:download" /> 导出
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
type="danger"
|
||||||
|
plain
|
||||||
|
:disabled="isEmpty(checkedIds)"
|
||||||
|
@click="handleDeleteBatch"
|
||||||
|
v-hasPermi="['ydoyun:custom-tag:delete']"
|
||||||
|
>
|
||||||
|
<Icon icon="ep:delete" /> 批量删除
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</ContentWrap>
|
||||||
|
|
||||||
|
<!-- 列表 -->
|
||||||
|
<ContentWrap>
|
||||||
|
<el-table
|
||||||
|
row-key="id"
|
||||||
|
v-loading="loading"
|
||||||
|
:data="list"
|
||||||
|
:stripe="true"
|
||||||
|
:show-overflow-tooltip="true"
|
||||||
|
@selection-change="handleRowCheckboxChange"
|
||||||
|
>
|
||||||
|
<el-table-column type="selection" width="55" />
|
||||||
|
<el-table-column label="标签名称" align="center" prop="name" min-width="120" :show-overflow-tooltip="true" />
|
||||||
|
<el-table-column label="公式描述" align="center" prop="expression" min-width="150" :show-overflow-tooltip="true" />
|
||||||
|
<el-table-column label="标签颜色" align="center" prop="color" width="100">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-tag v-if="scope.row.color" :color="scope.row.color" effect="dark" size="small">
|
||||||
|
{{ scope.row.color }}
|
||||||
|
</el-tag>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column
|
||||||
|
label="创建时间"
|
||||||
|
align="center"
|
||||||
|
prop="createTime"
|
||||||
|
:formatter="dateFormatter"
|
||||||
|
width="180"
|
||||||
|
/>
|
||||||
|
<el-table-column label="操作" align="center" width="140" fixed="right">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-button
|
||||||
|
link
|
||||||
|
type="primary"
|
||||||
|
@click="openForm('update', scope.row.id)"
|
||||||
|
v-hasPermi="['ydoyun:custom-tag:update']"
|
||||||
|
>
|
||||||
|
<Icon icon="ep:edit" /> 编辑
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
link
|
||||||
|
type="danger"
|
||||||
|
@click="handleDelete(scope.row.id)"
|
||||||
|
v-hasPermi="['ydoyun:custom-tag:delete']"
|
||||||
|
>
|
||||||
|
<Icon icon="ep:delete" /> 删除
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
<Pagination
|
||||||
|
:total="total"
|
||||||
|
v-model:page="queryParams.pageNo"
|
||||||
|
v-model:limit="queryParams.pageSize"
|
||||||
|
@pagination="getList"
|
||||||
|
/>
|
||||||
|
</ContentWrap>
|
||||||
|
|
||||||
|
<CustomTagForm ref="formRef" @success="getList" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { isEmpty } from '@/utils/is'
|
||||||
|
import { dateFormatter } from '@/utils/formatTime'
|
||||||
|
import download from '@/utils/download'
|
||||||
|
import { CustomTagApi, CustomTag } from '@/api/ydoyun/customtag'
|
||||||
|
import CustomTagForm from './CustomTagForm.vue'
|
||||||
|
|
||||||
|
defineOptions({ name: 'CustomTag' })
|
||||||
|
|
||||||
|
const message = useMessage()
|
||||||
|
const { t } = useI18n()
|
||||||
|
|
||||||
|
const loading = ref(true)
|
||||||
|
const list = ref<CustomTag[]>([])
|
||||||
|
const total = ref(0)
|
||||||
|
|
||||||
|
const queryParams = reactive({
|
||||||
|
pageNo: 1,
|
||||||
|
pageSize: 10,
|
||||||
|
name: undefined,
|
||||||
|
expression: undefined,
|
||||||
|
createTime: [] as string[]
|
||||||
|
})
|
||||||
|
|
||||||
|
const queryFormRef = ref()
|
||||||
|
const exportLoading = ref(false)
|
||||||
|
|
||||||
|
/** 查询列表 */
|
||||||
|
const getList = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const data = await CustomTagApi.getCustomTagPage(queryParams)
|
||||||
|
list.value = data.list
|
||||||
|
total.value = data.total
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 搜索 */
|
||||||
|
const handleQuery = () => {
|
||||||
|
queryParams.pageNo = 1
|
||||||
|
getList()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 重置 */
|
||||||
|
const resetQuery = () => {
|
||||||
|
queryFormRef.value?.resetFields()
|
||||||
|
handleQuery()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 添加/修改 */
|
||||||
|
const formRef = ref()
|
||||||
|
const openForm = (type: string, id?: number) => {
|
||||||
|
formRef.value.open(type, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 删除 */
|
||||||
|
const handleDelete = async (id: number) => {
|
||||||
|
try {
|
||||||
|
await message.delConfirm()
|
||||||
|
await CustomTagApi.deleteCustomTag(id)
|
||||||
|
message.success(t('common.delSuccess'))
|
||||||
|
await getList()
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 批量删除 */
|
||||||
|
const checkedIds = ref<number[]>([])
|
||||||
|
const handleRowCheckboxChange = (records: CustomTag[]) => {
|
||||||
|
checkedIds.value = records.map((item) => item.id!).filter(Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDeleteBatch = async () => {
|
||||||
|
try {
|
||||||
|
await message.delConfirm()
|
||||||
|
await CustomTagApi.deleteCustomTagList(checkedIds.value)
|
||||||
|
checkedIds.value = []
|
||||||
|
message.success(t('common.delSuccess'))
|
||||||
|
await getList()
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 导出 */
|
||||||
|
const handleExport = async () => {
|
||||||
|
try {
|
||||||
|
await message.exportConfirm()
|
||||||
|
exportLoading.value = true
|
||||||
|
const data = await CustomTagApi.exportCustomTag(queryParams)
|
||||||
|
download.excel(data, '自定义标签.xls')
|
||||||
|
} catch {
|
||||||
|
} finally {
|
||||||
|
exportLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getList()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -2,12 +2,6 @@
|
|||||||
<div class="product-cards-page">
|
<div class="product-cards-page">
|
||||||
<!-- 查询条件区域:与主页/详情页一致,首行时间+快捷+查询/重置/更多条件,其余折叠 -->
|
<!-- 查询条件区域:与主页/详情页一致,首行时间+快捷+查询/重置/更多条件,其余折叠 -->
|
||||||
<el-card class="query-card" shadow="never">
|
<el-card class="query-card" shadow="never">
|
||||||
<div class="query-header">
|
|
||||||
<h1 class="page-title">查看更多产品卡片</h1>
|
|
||||||
<span v-if="currentProductCode" class="current-product-tip">
|
|
||||||
当前查看商品:<strong>{{ currentProductName || currentProductCode }}</strong>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="query-form">
|
<div class="query-form">
|
||||||
<div class="query-item">
|
<div class="query-item">
|
||||||
<span class="query-label">时间区间</span>
|
<span class="query-label">时间区间</span>
|
||||||
@@ -193,44 +187,42 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<!-- 展示形式切换(放在最上面) -->
|
<!-- 内容卡片:展示形式切换 + 标签筛选 + 卡片/列表内容 -->
|
||||||
<div class="view-toolbar">
|
|
||||||
<div class="view-switch">
|
|
||||||
<span class="toolbar-label">展示形式:</span>
|
|
||||||
<el-radio-group v-model="displayMode" size="default">
|
|
||||||
<el-radio-button label="card">
|
|
||||||
<el-icon><Grid /></el-icon>
|
|
||||||
卡片
|
|
||||||
</el-radio-button>
|
|
||||||
<el-radio-button label="table">
|
|
||||||
<el-icon><List /></el-icon>
|
|
||||||
列表
|
|
||||||
</el-radio-button>
|
|
||||||
</el-radio-group>
|
|
||||||
</div>
|
|
||||||
<div v-if="displayMode === 'table'" class="table-search">
|
|
||||||
<el-input
|
|
||||||
v-model="searchKeyword"
|
|
||||||
placeholder="搜索款号/名称"
|
|
||||||
style="width: 200px"
|
|
||||||
clearable
|
|
||||||
>
|
|
||||||
<template #prefix>
|
|
||||||
<el-icon><Search /></el-icon>
|
|
||||||
</template>
|
|
||||||
</el-input>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 内容卡片:标签筛选 + 卡片/列表内容 -->
|
|
||||||
<el-card class="content-card" shadow="never">
|
<el-card class="content-card" shadow="never">
|
||||||
|
<!-- 展示形式切换:移入内容卡片内部,置于标签上方,仅保留切换按钮 -->
|
||||||
|
<div class="view-toolbar">
|
||||||
|
<div class="view-switch">
|
||||||
|
<el-radio-group v-model="displayMode" size="default">
|
||||||
|
<el-radio-button label="card">
|
||||||
|
<el-icon><Grid /></el-icon>
|
||||||
|
卡片
|
||||||
|
</el-radio-button>
|
||||||
|
<el-radio-button label="table">
|
||||||
|
<el-icon><List /></el-icon>
|
||||||
|
列表
|
||||||
|
</el-radio-button>
|
||||||
|
</el-radio-group>
|
||||||
|
</div>
|
||||||
|
<div v-if="displayMode === 'table'" class="table-search">
|
||||||
|
<el-input
|
||||||
|
v-model="searchKeyword"
|
||||||
|
placeholder="搜索款号/名称"
|
||||||
|
style="width: 200px"
|
||||||
|
clearable
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<el-icon><Search /></el-icon>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 标签分类筛选(放在卡片内部) -->
|
<!-- 标签分类筛选(放在卡片内部) -->
|
||||||
<div v-if="labelOpts.length > 0" class="filter-bar">
|
<div v-if="labelOpts.length > 0" class="filter-bar">
|
||||||
<div
|
<div
|
||||||
v-for="opt in labelOpts"
|
v-for="opt in labelOpts"
|
||||||
:key="opt.value"
|
:key="opt.value"
|
||||||
class="pill"
|
:class="['pill', productTagClass(opt.label), { active: labelFilter === opt.value }]"
|
||||||
:class="{ active: labelFilter === opt.value }"
|
|
||||||
@click="labelFilter = opt.value"
|
@click="labelFilter = opt.value"
|
||||||
>
|
>
|
||||||
{{ opt.label }}
|
{{ opt.label }}
|
||||||
@@ -261,39 +253,83 @@
|
|||||||
<!-- 媒体区:缩略图 + 名称/标签/价格 -->
|
<!-- 媒体区:缩略图 + 名称/标签/价格 -->
|
||||||
<div class="kb22-media-row">
|
<div class="kb22-media-row">
|
||||||
<div class="kb22-thumb-box">
|
<div class="kb22-thumb-box">
|
||||||
<img v-if="item.imageUrl" :src="item.imageUrl" :alt="item.name" class="kb22-thumb-img" />
|
<img
|
||||||
|
v-if="item.imageUrl || (item as any).pic"
|
||||||
|
:src="item.imageUrl || (item as any).pic"
|
||||||
|
:alt="item.name"
|
||||||
|
class="kb22-thumb-img"
|
||||||
|
/>
|
||||||
<div v-else class="kb22-no-img"><el-icon :size="32"><Picture /></el-icon></div>
|
<div v-else class="kb22-no-img"><el-icon :size="32"><Picture /></el-icon></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="kb22-info-col">
|
<div class="kb22-info-col">
|
||||||
<div class="kb22-prod-title">{{ item.name }}</div>
|
<div class="kb22-prod-title">{{ item.name }}</div>
|
||||||
<div class="kb22-tags-container">
|
<div class="kb22-tags-container">
|
||||||
<span class="kb22-tag-pill kb22-tag-blue">{{ item.season }}</span>
|
<span
|
||||||
<span class="kb22-tag-pill kb22-tag-purple">{{ item.discount }}</span>
|
v-if="item.type"
|
||||||
|
class="kb22-tag-pill"
|
||||||
|
:class="productTagClass(item.type)"
|
||||||
|
>
|
||||||
|
{{ item.type }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="item.lifecycle"
|
||||||
|
class="kb22-tag-pill"
|
||||||
|
:class="productTagClass(item.lifecycle)"
|
||||||
|
>
|
||||||
|
{{ item.lifecycle }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="kb22-price-section">
|
<div class="kb22-price-section">
|
||||||
<span class="kb22-current-price">¥{{ item.sellingPrice ?? 0 }}</span>
|
<span class="kb22-current-price">
|
||||||
<span class="kb22-cost-price">¥{{ item.purchasePrice ?? 0 }}</span>
|
<template v-if="item.sellingPrice != null">¥{{ formatNumber(item.sellingPrice) }}</template>
|
||||||
<span class="kb22-margin-text">毛利 {{ formatGrossMarginPct(item.grossMargin, item.purchasePrice != null && item.sellingPrice != null ? (1 - item.purchasePrice / item.sellingPrice) * 100 : undefined) }}%</span>
|
<template v-else>-</template>
|
||||||
|
</span>
|
||||||
|
<span class="kb22-cost-price">
|
||||||
|
<template v-if="item.purchasePrice != null">¥{{ formatNumber(item.purchasePrice) }}</template>
|
||||||
|
<template v-else>-</template>
|
||||||
|
</span>
|
||||||
|
<span class="kb22-margin-text">
|
||||||
|
<template v-if="item.grossMargin != null">
|
||||||
|
毛利 {{ formatGrossMarginPct(item.grossMargin, item.purchasePrice != null && item.sellingPrice != null ? (1 - item.purchasePrice / item.sellingPrice) * 100 : undefined) }}%
|
||||||
|
</template>
|
||||||
|
<template v-else>-</template>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 重点数据区:总销量 + 当前库存 -->
|
<!-- 重点数据区:总销量 + 当前库存 -->
|
||||||
<div class="kb22-stats-grid">
|
<div class="kb22-stats-grid">
|
||||||
<div class="kb22-stat-card kb22-sales">
|
<div class="kb22-stat-card kb22-sales">
|
||||||
<div class="kb22-stat-label">总销量</div>
|
<div class="kb22-stat-label">总销量</div>
|
||||||
<div class="kb22-stat-value">{{ item.salesCount ?? 0 }}<span class="kb22-stat-sub">件</span></div>
|
<div class="kb22-stat-value">
|
||||||
<div class="kb22-sales-footer">
|
<template v-if="item.salesCount != null">
|
||||||
<div class="kb22-mini-progress">
|
{{ item.salesCount }}<span class="kb22-stat-sub">件</span>
|
||||||
<div class="kb22-mini-fill" :style="{ width: Math.min(100, item.selloutRate ?? 0) + '%' }"></div>
|
</template>
|
||||||
|
<template v-else>-</template>
|
||||||
|
</div>
|
||||||
|
<div class="kb22-sales-footer">
|
||||||
|
<div class="kb22-mini-progress" v-if="item.selloutRate != null">
|
||||||
|
<div class="kb22-mini-fill" :style="{ width: Math.min(100, item.selloutRate) + '%' }"></div>
|
||||||
|
</div>
|
||||||
|
<div class="kb22-rate-text">
|
||||||
|
<template v-if="item.selloutRate != null">售罄率 {{ item.selloutRate }}%</template>
|
||||||
|
<template v-else>-</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="kb22-stat-card kb22-stock">
|
||||||
|
<div class="kb22-stat-label">当前库存</div>
|
||||||
|
<div class="kb22-stat-value">
|
||||||
|
<template v-if="item.inventoryCount != null">
|
||||||
|
{{ item.inventoryCount }}<span class="kb22-stat-sub">件</span>
|
||||||
|
</template>
|
||||||
|
<template v-else>-</template>
|
||||||
|
</div>
|
||||||
|
<div class="kb22-turnover-text" :class="uiTextClass(item.turnoverStatus)">
|
||||||
|
<template v-if="item.turnoverText">{{ '周转: ' + item.turnoverText }}</template>
|
||||||
|
<template v-else>-</template>
|
||||||
</div>
|
</div>
|
||||||
<div class="kb22-rate-text">售罄率 {{ item.selloutRate ?? 0 }}%</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="kb22-stat-card kb22-stock">
|
|
||||||
<div class="kb22-stat-label">当前库存</div>
|
|
||||||
<div class="kb22-stat-value">{{ item.inventoryCount ?? 0 }}<span class="kb22-stat-sub">件</span></div>
|
|
||||||
<div class="kb22-turnover-text" :class="uiTextClass(item.turnoverStatus)">周转: {{ item.turnoverText ?? '-' }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<!-- SKU 明细:尺码 + 库存 + 销量 -->
|
<!-- SKU 明细:尺码 + 库存 + 销量 -->
|
||||||
<div v-if="item.sizes && item.sizes.length" class="kb22-stock-footer">
|
<div v-if="item.sizes && item.sizes.length" class="kb22-stock-footer">
|
||||||
@@ -329,36 +365,88 @@
|
|||||||
<!-- 列表形式:与 ProductDashboard 商品明细列表一致的表格(放在同一张内容卡片内) -->
|
<!-- 列表形式:与 ProductDashboard 商品明细列表一致的表格(放在同一张内容卡片内) -->
|
||||||
<div v-show="displayMode === 'table'" class="detail-table-wrap">
|
<div v-show="displayMode === 'table'" class="detail-table-wrap">
|
||||||
<el-table
|
<el-table
|
||||||
:data="filteredTableList"
|
:data="filteredTableList"
|
||||||
v-loading="loading"
|
v-loading="loading"
|
||||||
border
|
border
|
||||||
stripe
|
stripe
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
row-class-name="product-list-row-clickable"
|
row-class-name="product-list-row-clickable"
|
||||||
@row-click="handleProductRowClick"
|
@row-click="handleProductRowClick"
|
||||||
>
|
>
|
||||||
<el-table-column prop="productInfo" label="商品信息" width="280">
|
<el-table-column prop="imageUrl" label="商品图片" width="110" align="center">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<div class="product-info">
|
|
||||||
<div class="product-image">
|
<div class="product-image">
|
||||||
<img v-if="row.imageUrl" :src="row.imageUrl" alt="商品图片" class="product-img" />
|
<img
|
||||||
|
v-if="row.imageUrl || row.pic"
|
||||||
|
:src="row.imageUrl || row.pic"
|
||||||
|
alt="商品图片"
|
||||||
|
class="product-img"
|
||||||
|
/>
|
||||||
<el-icon v-else><Picture /></el-icon>
|
<el-icon v-else><Picture /></el-icon>
|
||||||
</div>
|
</div>
|
||||||
<div class="product-details">
|
</template>
|
||||||
<div class="product-code">款号: {{ row.code || '-' }}</div>
|
</el-table-column>
|
||||||
<div class="product-code">条码: {{ row.barcode || '-' }}</div>
|
<el-table-column prop="name" label="商品名称" min-width="200" show-overflow-tooltip>
|
||||||
<div class="product-code">颜色: {{ row.color }}</div>
|
<template #default="{ row }">
|
||||||
<div class="product-code">进价: ¥{{ formatNumber(row.purchasePrice || 0) }}</div>
|
<span>{{ row.name || '-' }}</span>
|
||||||
<div class="product-code">售价: ¥{{ formatNumber(row.sellingPrice || 0) }}</div>
|
</template>
|
||||||
</div>
|
</el-table-column>
|
||||||
</div>
|
<el-table-column prop="code" label="款号" width="140">
|
||||||
</template>
|
<template #default="{ row }">
|
||||||
</el-table-column>
|
<span class="product-code-link" @click.stop="copyCellText(row.code)">
|
||||||
|
{{ row.code || '-' }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="barcode" label="条码" width="160" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span class="product-code-link" @click.stop="copyCellText(row.barcode)">
|
||||||
|
{{ row.barcode || '-' }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="color" label="颜色" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span>{{ row.color || '-' }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="purchasePrice" label="进价" align="right" width="110">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span v-if="row.purchasePrice != null">¥{{ formatNumber(row.purchasePrice) }}</span>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="sellingPrice" label="售价" align="right" width="110">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<span v-if="row.sellingPrice != null">¥{{ formatNumber(row.sellingPrice) }}</span>
|
||||||
|
<span v-else>-</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column prop="category" label="商品属性" width="200" show-overflow-tooltip>
|
<el-table-column prop="category" label="商品属性" width="200" show-overflow-tooltip>
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<div>{{ row.category }}</div>
|
<div>{{ row.category }}</div>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
<el-table-column prop="tags" label="标签" width="200" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="product-tags">
|
||||||
|
<span
|
||||||
|
v-if="row.type"
|
||||||
|
class="kb22-tag-pill"
|
||||||
|
:class="productTagClass(row.type)"
|
||||||
|
>
|
||||||
|
{{ row.type }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="row.lifecycle"
|
||||||
|
class="kb22-tag-pill"
|
||||||
|
:class="productTagClass(row.lifecycle)"
|
||||||
|
>
|
||||||
|
{{ row.lifecycle }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column prop="ls" label="销售数据" align="right" width="150">
|
<el-table-column prop="ls" label="销售数据" align="right" width="150">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<div class="font-bold">{{ row.lsRaw || '-' }}</div>
|
<div class="font-bold">{{ row.lsRaw || '-' }}</div>
|
||||||
@@ -438,6 +526,7 @@ import dayjs from 'dayjs'
|
|||||||
import { Picture, Grid, List, Search } from '@element-plus/icons-vue'
|
import { Picture, Grid, List, Search } from '@element-plus/icons-vue'
|
||||||
import { ReportApi } from '@/api/ydoyun/report/reportpage'
|
import { ReportApi } from '@/api/ydoyun/report/reportpage'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { useUserStore } from '@/store/modules/user'
|
||||||
|
|
||||||
const REPORT_ID = 6
|
const REPORT_ID = 6
|
||||||
|
|
||||||
@@ -485,11 +574,20 @@ interface ProductCardItem {
|
|||||||
grossMargin?: number
|
grossMargin?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function productTagClass(tag: string): string {
|
||||||
|
if (!tag) return ''
|
||||||
|
if (tag.includes('慢') || tag.includes('低') || tag.includes('预警')) return 'kb22-tag-warn'
|
||||||
|
if (tag.includes('滞后') || tag.includes('高')) return 'kb22-tag-danger'
|
||||||
|
return 'kb22-tag-success'
|
||||||
|
}
|
||||||
|
|
||||||
defineOptions({ name: 'ProductCardsPage' })
|
defineOptions({ name: 'ProductCardsPage' })
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
const userStore = useUserStore()
|
||||||
|
const username = computed(() => userStore.user?.username || '')
|
||||||
|
|
||||||
/** 展示形式:卡片 | 列表(与 ProductDashboard 商品明细表格一致) */
|
/** 展示形式:卡片 | 列表(与 ProductDashboard 商品明细表格一致) */
|
||||||
const displayMode = ref<'card' | 'table'>('card')
|
const displayMode = ref<'card' | 'table'>('card')
|
||||||
@@ -589,233 +687,7 @@ function hasOutOfStockSize(sizes: ProductSizeStatus[]): boolean {
|
|||||||
return Array.isArray(sizes) && sizes.some((s) => s.status === 'out' || s.stock === 0)
|
return Array.isArray(sizes) && sizes.some((s) => s.status === 'out' || s.stock === 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mock:与列表页一致的全量数据(7 条)
|
const productList = ref<ProductCardItem[]>([])
|
||||||
const productList = ref<ProductCardItem[]>([
|
|
||||||
{
|
|
||||||
name: 'GZ6596Z 法式收腰连衣裙',
|
|
||||||
code: '54000008',
|
|
||||||
color: '黑色',
|
|
||||||
season: '25春 一波段',
|
|
||||||
category: '女装 / 连衣裙',
|
|
||||||
daysOnMarket: 35,
|
|
||||||
salesAmount: 85200,
|
|
||||||
salesCount: 320,
|
|
||||||
inventoryCount: 120,
|
|
||||||
turnoverText: '周转: 15天 (极快)',
|
|
||||||
turnoverStatus: 'success',
|
|
||||||
selloutRate: 78,
|
|
||||||
selloutRateStatus: 'danger',
|
|
||||||
discount: '9.5折',
|
|
||||||
discountStatus: 'info',
|
|
||||||
sizes: [
|
|
||||||
{ label: 'S', status: 'out', title: 'S码 缺货', stock: 0 },
|
|
||||||
{ label: 'M', status: 'out', title: 'M码 缺货', stock: 0 },
|
|
||||||
{ label: 'L', status: 'low', title: 'L码 紧张', stock: 8 },
|
|
||||||
{ label: 'XL', status: 'ok', title: 'XL码 充足', stock: 45 }
|
|
||||||
],
|
|
||||||
sizeStatusText: '缺核心码',
|
|
||||||
sizeStatusStatus: 'danger',
|
|
||||||
lifecycle: '爆发成长期',
|
|
||||||
lifecycleType: 'success',
|
|
||||||
barcode: '6901234567890',
|
|
||||||
purchasePrice: 168,
|
|
||||||
sellingPrice: 399,
|
|
||||||
imageUrl: 'https://img.alicdn.com/imgextra/i1/2215397650053/O1CN01kQKs0S1COq3OWK7Wb_!!2215397650053.jpg',
|
|
||||||
supplierName: '杭州女装供应链有限公司'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '7537X 双面呢大衣',
|
|
||||||
code: '54000321',
|
|
||||||
color: '驼色',
|
|
||||||
season: '24冬 三波段',
|
|
||||||
category: '女装 / 外套',
|
|
||||||
daysOnMarket: 120,
|
|
||||||
salesAmount: 22000,
|
|
||||||
salesCount: 18,
|
|
||||||
inventoryCount: 850,
|
|
||||||
turnoverText: '周转: 280天 (滞销)',
|
|
||||||
turnoverStatus: 'info',
|
|
||||||
selloutRate: 15,
|
|
||||||
selloutRateStatus: 'warning',
|
|
||||||
discount: '6.0折',
|
|
||||||
discountStatus: 'danger',
|
|
||||||
sizes: [
|
|
||||||
{ label: 'S', status: 'ok', title: 'S码 充足', stock: 210 },
|
|
||||||
{ label: 'M', status: 'ok', title: 'M码 充足', stock: 220 },
|
|
||||||
{ label: 'L', status: 'ok', title: 'L码 充足', stock: 215 },
|
|
||||||
{ label: 'XL', status: 'ok', title: 'XL码 充足', stock: 205 }
|
|
||||||
],
|
|
||||||
sizeStatusText: '库存齐色齐码',
|
|
||||||
sizeStatusStatus: 'info',
|
|
||||||
lifecycle: '严重滞销',
|
|
||||||
lifecycleType: 'danger',
|
|
||||||
barcode: '6901234567891',
|
|
||||||
purchasePrice: 580,
|
|
||||||
sellingPrice: 1299,
|
|
||||||
imageUrl: 'https://img.alicdn.com/imgextra/i2/2215397650053/O1CN01kQKs0S1COq3OWK7Wb_!!2215397650053.jpg',
|
|
||||||
supplierName: '上海大衣制造厂'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'C01J 圆领针织衫',
|
|
||||||
code: '54000159',
|
|
||||||
color: '米白',
|
|
||||||
season: '25春 二波段',
|
|
||||||
category: '女装 / 毛衫',
|
|
||||||
daysOnMarket: 25,
|
|
||||||
salesAmount: 45000,
|
|
||||||
salesCount: 120,
|
|
||||||
inventoryCount: 450,
|
|
||||||
turnoverText: '周转: 45天 (健康)',
|
|
||||||
turnoverStatus: 'success',
|
|
||||||
selloutRate: 35,
|
|
||||||
selloutRateStatus: 'info',
|
|
||||||
discount: '9.0折',
|
|
||||||
discountStatus: 'info',
|
|
||||||
sizes: [
|
|
||||||
{ label: 'S', status: 'ok', title: 'S码 充足', stock: 120 },
|
|
||||||
{ label: 'M', status: 'warn', title: 'M码 紧张', stock: 25 },
|
|
||||||
{ label: 'L', status: 'ok', title: 'L码 充足', stock: 150 },
|
|
||||||
{ label: 'XL', status: 'ok', title: 'XL码 充足', stock: 155 }
|
|
||||||
],
|
|
||||||
sizeStatusText: '',
|
|
||||||
sizeStatusStatus: 'info',
|
|
||||||
lifecycle: '正常销售',
|
|
||||||
lifecycleType: 'info',
|
|
||||||
barcode: '6901234567892',
|
|
||||||
purchasePrice: 128,
|
|
||||||
sellingPrice: 299,
|
|
||||||
imageUrl: 'https://img.alicdn.com/imgextra/i3/2215397650053/O1CN01kQKs0S1COq3OWK7Wb_!!2215397650053.jpg',
|
|
||||||
supplierName: '广州针织品有限公司'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'LK2025 春季休闲裤',
|
|
||||||
code: '54000245',
|
|
||||||
color: '卡其色',
|
|
||||||
season: '25春 一波段',
|
|
||||||
category: '女装 / 休闲裤',
|
|
||||||
daysOnMarket: 28,
|
|
||||||
salesAmount: 125000,
|
|
||||||
salesCount: 450,
|
|
||||||
inventoryCount: 280,
|
|
||||||
turnoverText: '周转: 38天 (健康)',
|
|
||||||
turnoverStatus: 'success',
|
|
||||||
selloutRate: 62,
|
|
||||||
selloutRateStatus: 'success',
|
|
||||||
discount: '8.5折',
|
|
||||||
discountStatus: 'info',
|
|
||||||
sizes: [
|
|
||||||
{ label: 'S', status: 'ok', title: 'S码 充足', stock: 45 },
|
|
||||||
{ label: 'M', status: 'warn', title: 'M码 紧张', stock: 8 },
|
|
||||||
{ label: 'L', status: 'ok', title: 'L码 充足', stock: 52 },
|
|
||||||
{ label: 'XL', status: 'ok', title: 'XL码 充足', stock: 38 }
|
|
||||||
],
|
|
||||||
sizeStatusText: 'M码需补货',
|
|
||||||
sizeStatusStatus: 'warning',
|
|
||||||
lifecycle: '稳定销售期',
|
|
||||||
lifecycleType: 'info',
|
|
||||||
barcode: '6901234567893',
|
|
||||||
purchasePrice: 158,
|
|
||||||
sellingPrice: 369,
|
|
||||||
imageUrl: 'https://img.alicdn.com/imgextra/i4/2215397650053/O1CN01kQKs0S1COq3OWK7Wb_!!2215397650053.jpg',
|
|
||||||
supplierName: '深圳休闲服饰有限公司'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'FW2024 冬季羽绒服',
|
|
||||||
code: '54000188',
|
|
||||||
color: '黑色',
|
|
||||||
season: '24冬 二波段',
|
|
||||||
category: '女装 / 外套',
|
|
||||||
daysOnMarket: 95,
|
|
||||||
salesAmount: 185000,
|
|
||||||
salesCount: 95,
|
|
||||||
inventoryCount: 520,
|
|
||||||
turnoverText: '周转: 210天 (滞销)',
|
|
||||||
turnoverStatus: 'danger',
|
|
||||||
selloutRate: 18,
|
|
||||||
selloutRateStatus: 'danger',
|
|
||||||
discount: '5.5折',
|
|
||||||
discountStatus: 'danger',
|
|
||||||
sizes: [
|
|
||||||
{ label: 'S', status: 'ok', title: 'S码 充足', stock: 120 },
|
|
||||||
{ label: 'M', status: 'ok', title: 'M码 充足', stock: 135 },
|
|
||||||
{ label: 'L', status: 'ok', title: 'L码 充足', stock: 145 },
|
|
||||||
{ label: 'XL', status: 'ok', title: 'XL码 充足', stock: 120 }
|
|
||||||
],
|
|
||||||
sizeStatusText: '库存积压严重',
|
|
||||||
sizeStatusStatus: 'danger',
|
|
||||||
lifecycle: '衰退期',
|
|
||||||
lifecycleType: 'danger',
|
|
||||||
barcode: '6901234567894',
|
|
||||||
purchasePrice: 680,
|
|
||||||
sellingPrice: 1599,
|
|
||||||
imageUrl: 'https://img.alicdn.com/imgextra/i1/2215397650053/O1CN01kQKs0S1COq3OWK7Wb_!!2215397650053.jpg',
|
|
||||||
supplierName: '江苏羽绒制品厂'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'TS2025 春季新品T恤',
|
|
||||||
code: '54000356',
|
|
||||||
color: '白色',
|
|
||||||
season: '25春 一波段',
|
|
||||||
category: '女装 / T恤',
|
|
||||||
daysOnMarket: 15,
|
|
||||||
salesAmount: 98000,
|
|
||||||
salesCount: 680,
|
|
||||||
inventoryCount: 320,
|
|
||||||
turnoverText: '周转: 12天 (极快)',
|
|
||||||
turnoverStatus: 'success',
|
|
||||||
selloutRate: 68,
|
|
||||||
selloutRateStatus: 'success',
|
|
||||||
discount: '9.8折',
|
|
||||||
discountStatus: 'info',
|
|
||||||
sizes: [
|
|
||||||
{ label: 'S', status: 'out', title: 'S码 缺货', stock: 0 },
|
|
||||||
{ label: 'M', status: 'out', title: 'M码 缺货', stock: 2 },
|
|
||||||
{ label: 'L', status: 'low', title: 'L码 紧张', stock: 15 },
|
|
||||||
{ label: 'XL', status: 'ok', title: 'XL码 充足', stock: 48 }
|
|
||||||
],
|
|
||||||
sizeStatusText: '核心码缺货,需紧急补货',
|
|
||||||
sizeStatusStatus: 'danger',
|
|
||||||
lifecycle: '爆发成长期',
|
|
||||||
lifecycleType: 'success',
|
|
||||||
barcode: '6901234567895',
|
|
||||||
purchasePrice: 68,
|
|
||||||
sellingPrice: 159,
|
|
||||||
imageUrl: 'https://img.alicdn.com/imgextra/i2/2215397650053/O1CN01kQKs0S1COq3OWK7Wb_!!2215397650053.jpg',
|
|
||||||
supplierName: '浙江T恤代工厂'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'CS2025 春季衬衫',
|
|
||||||
code: '54000378',
|
|
||||||
color: '蓝色',
|
|
||||||
season: '25春 二波段',
|
|
||||||
category: '女装 / 衬衫',
|
|
||||||
daysOnMarket: 20,
|
|
||||||
salesAmount: 156000,
|
|
||||||
salesCount: 520,
|
|
||||||
inventoryCount: 480,
|
|
||||||
turnoverText: '周转: 25天 (快速)',
|
|
||||||
turnoverStatus: 'success',
|
|
||||||
selloutRate: 52,
|
|
||||||
selloutRateStatus: 'success',
|
|
||||||
discount: '9.2折',
|
|
||||||
discountStatus: 'info',
|
|
||||||
sizes: [
|
|
||||||
{ label: 'S', status: 'ok', title: 'S码 充足', stock: 95 },
|
|
||||||
{ label: 'M', status: 'warn', title: 'M码 紧张', stock: 12 },
|
|
||||||
{ label: 'L', status: 'ok', title: 'L码 充足', stock: 88 },
|
|
||||||
{ label: 'XL', status: 'ok', title: 'XL码 充足', stock: 76 }
|
|
||||||
],
|
|
||||||
sizeStatusText: 'M码需补货',
|
|
||||||
sizeStatusStatus: 'warning',
|
|
||||||
lifecycle: '成长期',
|
|
||||||
lifecycleType: 'success',
|
|
||||||
barcode: '6901234567896',
|
|
||||||
purchasePrice: 138,
|
|
||||||
sellingPrice: 329,
|
|
||||||
imageUrl: 'https://img.alicdn.com/imgextra/i3/2215397650053/O1CN01kQKs0S1COq3OWK7Wb_!!2215397650053.jpg',
|
|
||||||
supplierName: '杭州衬衫供应链'
|
|
||||||
}
|
|
||||||
])
|
|
||||||
|
|
||||||
/** 表格行:在 ProductCardItem 基础上补充列表列所需字段(与 index 商品明细一致) */
|
/** 表格行:在 ProductCardItem 基础上补充列表列所需字段(与 index 商品明细一致) */
|
||||||
type TableRow = ProductCardItem & {
|
type TableRow = ProductCardItem & {
|
||||||
@@ -853,16 +725,16 @@ const visibleProductList = computed(() => {
|
|||||||
return productList.value.filter((item) => productMatchesLabelFilter(item, filter))
|
return productList.value.filter((item) => productMatchesLabelFilter(item, filter))
|
||||||
})
|
})
|
||||||
|
|
||||||
/** 将 visibleProductList 转为表格行(缺的 Raw 用派生值或 '-') */
|
/** 将 visibleProductList 转为表格行(Raw 字段直接用于展示,缺失时用 '-') */
|
||||||
const tableList = computed<TableRow[]>(() =>
|
const tableList = computed<TableRow[]>(() =>
|
||||||
visibleProductList.value.map((item) => ({
|
visibleProductList.value.map((item) => ({
|
||||||
...item,
|
...item,
|
||||||
lsRaw: item.salesCount != null || item.salesAmount != null ? `${item.salesCount ?? 0}件/${item.salesAmount ?? 0}元` : '-',
|
lsRaw: item.salesAmount != null || item.salesCount != null ? `${item.salesCount ?? ''}${item.salesCount != null ? '件/' : ''}${item.salesAmount ?? ''}${item.salesAmount != null ? '元' : ''}` : undefined,
|
||||||
j7slRaw: '-',
|
j7slRaw: undefined,
|
||||||
kcslRaw: item.inventoryCount != null ? `${item.inventoryCount}件` : '-',
|
kcslRaw: item.inventoryCount != null ? `${item.inventoryCount}件` : undefined,
|
||||||
zksqlRaw: item.discount != null && item.selloutRate != null ? `${item.discount},${item.selloutRate}%` : '-',
|
zksqlRaw: item.discount != null || item.selloutRate != null ? `${item.discount ?? ''}${item.discount && item.selloutRate != null ? ',' : ''}${item.selloutRate != null ? item.selloutRate + '%' : ''}` : undefined,
|
||||||
shdxdRaw: '-',
|
shdxdRaw: undefined,
|
||||||
wsdpRaw: '-',
|
wsdpRaw: undefined,
|
||||||
actionText: '分析',
|
actionText: '分析',
|
||||||
actionType: 'default' as const
|
actionType: 'default' as const
|
||||||
}))
|
}))
|
||||||
@@ -880,18 +752,47 @@ function handleProductRowClick(row: TableRow) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleProductCodeClick(row: TableRow) {
|
function handleProductCodeClick(row: TableRow) {
|
||||||
router.push({
|
const spdm = row.code || ''
|
||||||
path: '/reports/lijun/reportpage6/detail',
|
router
|
||||||
query: { spdm: row.code || '' }
|
.push({
|
||||||
}).catch(() => {
|
path: '/reports/lijun/reportpage6/detail',
|
||||||
ElMessage.warning('路由未配置或详情页不存在')
|
query: {
|
||||||
})
|
// 关键商品编码
|
||||||
|
spdm,
|
||||||
|
// 透传当前大盘的所有查询条件,便于明细页还原筛选环境
|
||||||
|
rq: queryParams.rq,
|
||||||
|
rq2: queryParams.rq2,
|
||||||
|
ckdm: queryParams.ckdm.join(','),
|
||||||
|
pp: queryParams.pp.join(','),
|
||||||
|
season: queryParams.season.join(','),
|
||||||
|
zgj: queryParams.zgj.join(','),
|
||||||
|
category: queryParams.category.join(','),
|
||||||
|
line: queryParams.line.join(',')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
ElMessage.warning('路由未配置或详情页不存在')
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleTableAction(row: TableRow) {
|
function handleTableAction(row: TableRow) {
|
||||||
ElMessage.info(`执行操作: ${row.actionText}`)
|
ElMessage.info(`执行操作: ${row.actionText}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function copyCellText(val?: string) {
|
||||||
|
const text = (val ?? '').toString().trim()
|
||||||
|
if (!text) {
|
||||||
|
ElMessage.warning('无可复制内容')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(text)
|
||||||
|
ElMessage.success('已复制到剪贴板')
|
||||||
|
} catch {
|
||||||
|
ElMessage.error('复制失败,请手动选择复制')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function arrToArray(val: unknown): string[] {
|
function arrToArray(val: unknown): string[] {
|
||||||
if (Array.isArray(val)) return val.filter((v) => typeof v === 'string')
|
if (Array.isArray(val)) return val.filter((v) => typeof v === 'string')
|
||||||
if (val === undefined || val === null) return []
|
if (val === undefined || val === null) return []
|
||||||
@@ -913,6 +814,46 @@ function applyQueryFromRoute() {
|
|||||||
currentProductCode.value = (q.productCode && typeof q.productCode === 'string') ? q.productCode : ''
|
currentProductCode.value = (q.productCode && typeof q.productCode === 'string') ? q.productCode : ''
|
||||||
currentProductName.value = (q.productName && typeof q.productName === 'string') ? q.productName : ''
|
currentProductName.value = (q.productName && typeof q.productName === 'string') ? q.productName : ''
|
||||||
dateRange.value = [queryParams.rq, queryParams.rq2]
|
dateRange.value = [queryParams.rq, queryParams.rq2]
|
||||||
|
|
||||||
|
// 根据路由传入的时间区间反推当前快捷按钮(与详情页/供应商/品类诊断一致)
|
||||||
|
try {
|
||||||
|
const rq = queryParams.rq
|
||||||
|
const rq2 = queryParams.rq2
|
||||||
|
const today = dayjs().format('YYYY-MM-DD')
|
||||||
|
|
||||||
|
// 仅在 rq2 为今天时才尝试匹配 week7/day15/day30 或年份
|
||||||
|
if (rq && rq2 && rq2 === today) {
|
||||||
|
const diff = dayjs(today).diff(dayjs(rq), 'day')
|
||||||
|
if (diff === 6) {
|
||||||
|
activeTimeRange.value = 'week7'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (diff === 14) {
|
||||||
|
activeTimeRange.value = 'day15'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (diff === 29) {
|
||||||
|
activeTimeRange.value = 'day30'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试匹配全年快捷:从某年1月1日到该年末或今天
|
||||||
|
if (rq && rq2) {
|
||||||
|
const startYear = dayjs(rq).year()
|
||||||
|
const yearStart = dayjs(`${startYear}-01-01`).format('YYYY-MM-DD')
|
||||||
|
const yearEnd = startYear === dayjs().year() ? today : dayjs(`${startYear}-12-31`).format('YYYY-MM-DD')
|
||||||
|
if (rq === yearStart && rq2 === yearEnd) {
|
||||||
|
activeTimeRange.value = `year${startYear}`
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 其余情况,不高亮任何快捷按钮
|
||||||
|
activeTimeRange.value = ''
|
||||||
|
} catch {
|
||||||
|
activeTimeRange.value = ''
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function disabledDate(time: Date) {
|
function disabledDate(time: Date) {
|
||||||
@@ -975,12 +916,158 @@ function handleReset() {
|
|||||||
handleQuery()
|
handleQuery()
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleQuery() {
|
function arrToQuery(arr: string[]): string {
|
||||||
|
return Array.isArray(arr) && arr.length > 0 ? arr.join(',') : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleQuery() {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
setTimeout(() => {
|
try {
|
||||||
loading.value = false
|
const baseParams = {
|
||||||
|
reportId: REPORT_ID,
|
||||||
|
rq_s: queryParams.rq,
|
||||||
|
rq_e: queryParams.rq2,
|
||||||
|
ckdm: arrToQuery(queryParams.ckdm),
|
||||||
|
pp: arrToQuery(queryParams.pp),
|
||||||
|
dalei: arrToQuery(queryParams.category),
|
||||||
|
jj: arrToQuery(queryParams.season),
|
||||||
|
p: '123',
|
||||||
|
username: username.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const res: any = await ReportApi.executeProcedureWithData({
|
||||||
|
...baseParams,
|
||||||
|
name: 'YDY_AI_GET_SPXQ'
|
||||||
|
} as any)
|
||||||
|
|
||||||
|
// 兼容多种返回结构:数组 / { data: [] } / { code, data: [] }
|
||||||
|
let data: any[] | null = null
|
||||||
|
if (Array.isArray(res)) {
|
||||||
|
data = res
|
||||||
|
} else if (res && Array.isArray(res.data)) {
|
||||||
|
data = res.data
|
||||||
|
} else if (res && res.code != null && Array.isArray(res.data)) {
|
||||||
|
data = res.data
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data && data.length > 0) {
|
||||||
|
productList.value = data.map((row: any) => {
|
||||||
|
// 映射存储过程字段 → 页面字段
|
||||||
|
// jj: 季节代码,例如 "20"
|
||||||
|
// jijie: 季节名称,例如 "春"
|
||||||
|
const seasonName = row.jijie ?? ''
|
||||||
|
const seasonCode = row.jj ?? ''
|
||||||
|
const season = seasonName || seasonCode ? `${seasonName || ''}${seasonCode ? `(${seasonCode})` : ''}` : '-'
|
||||||
|
|
||||||
|
// kcsl: 库存数量(字符串),直接转数值
|
||||||
|
const inventoryCount = Number(row.kcsl ?? 0) || 0
|
||||||
|
|
||||||
|
// lssl: 零售数量,如 "2726件/" → 2726
|
||||||
|
const salesCount = (() => {
|
||||||
|
const raw = String(row.lssl ?? '').split('件')[0].trim()
|
||||||
|
const n = Number(raw)
|
||||||
|
return isNaN(n) ? 0 : n
|
||||||
|
})()
|
||||||
|
|
||||||
|
// lsje: 零售金额,如 "79051元" → 79051
|
||||||
|
const salesAmount = (() => {
|
||||||
|
const raw = String(row.lsje ?? '').replace('元', '').trim()
|
||||||
|
const n = Number(raw)
|
||||||
|
return isNaN(n) ? 0 : n
|
||||||
|
})()
|
||||||
|
|
||||||
|
// zksql: 售罄率,如 "60.96%" → 60.96
|
||||||
|
const selloutRate = (() => {
|
||||||
|
const raw = String(row.zksql ?? '').replace('%', '').trim()
|
||||||
|
const n = Number(raw)
|
||||||
|
return isNaN(n) ? 0 : n
|
||||||
|
})()
|
||||||
|
|
||||||
|
// pjz: 平均折,例如 "1.00" → "1.00折"
|
||||||
|
const discount = row.pjz != null ? `${row.pjz}折` : row.discount ?? '-'
|
||||||
|
|
||||||
|
// 进售价:sj=售价,jj=进价
|
||||||
|
const purchasePrice = Number(row.jj ?? row.purchasePrice ?? 0) || 0
|
||||||
|
const sellingPrice = Number(row.sj ?? row.sellingPrice ?? 0) || 0
|
||||||
|
const grossMargin =
|
||||||
|
row.grossMargin != null
|
||||||
|
? Number(row.grossMargin)
|
||||||
|
: sellingPrice > 0
|
||||||
|
? (sellingPrice - purchasePrice) / sellingPrice
|
||||||
|
: 0
|
||||||
|
|
||||||
|
return {
|
||||||
|
// 商品名称:spmc
|
||||||
|
name: row.spmc ?? row.name ?? row.spdm ?? '-',
|
||||||
|
// 款号:spdm
|
||||||
|
code: String(row.spdm ?? row.code ?? ''),
|
||||||
|
// 颜色:ysmc
|
||||||
|
color: row.ysmc ?? row.color ?? '-',
|
||||||
|
// 季节:jijie + jj
|
||||||
|
season,
|
||||||
|
// 类目:大类/中类/小类 dl / zl / xl
|
||||||
|
category: row.dl || row.zl || row.xl || '-',
|
||||||
|
// 上市天数:根据上市日期 ssrq 粗略计算
|
||||||
|
daysOnMarket: row.ssrq ? dayjs().diff(dayjs(row.ssrq), 'day') : Number(row.daysOnMarket ?? 0),
|
||||||
|
salesAmount,
|
||||||
|
salesCount,
|
||||||
|
inventoryCount,
|
||||||
|
turnoverText: row.turnoverText ?? '',
|
||||||
|
turnoverStatus: (row.turnoverStatus as UiStatus) ?? 'info',
|
||||||
|
// 售罄率:zksql
|
||||||
|
selloutRate,
|
||||||
|
selloutRateStatus: (row.selloutRateStatus as UiStatus) ?? 'info',
|
||||||
|
// 平均折:pjz
|
||||||
|
discount,
|
||||||
|
discountStatus: (row.discountStatus as UiStatus) ?? 'info',
|
||||||
|
// 尺码明细:cmjs "M,488,red;L,718,red;XL,540,yellow"
|
||||||
|
sizes: Array.isArray(row.sizes)
|
||||||
|
? (row.sizes as ProductSizeStatus[])
|
||||||
|
: String(row.cmjs ?? '')
|
||||||
|
.split(';')
|
||||||
|
.map((seg: string) => seg.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((seg: string) => {
|
||||||
|
const [label, stockStr, color] = seg.split(',').map((s) => s.trim())
|
||||||
|
const stock = Number(stockStr ?? 0) || 0
|
||||||
|
let status: ProductSizeStatus['status'] = 'ok'
|
||||||
|
if (color === 'red') status = 'low'
|
||||||
|
else if (color === 'yellow') status = 'warn'
|
||||||
|
return {
|
||||||
|
label: label || '',
|
||||||
|
status,
|
||||||
|
title: `${label || ''}码`,
|
||||||
|
stock,
|
||||||
|
sales: undefined
|
||||||
|
} as ProductSizeStatus
|
||||||
|
}),
|
||||||
|
sizeStatusText: row.sizeStatusText ?? '',
|
||||||
|
sizeStatusStatus: (row.sizeStatusStatus as UiStatus) ?? 'info',
|
||||||
|
// 类型/生命周期标签:优先 type,其次 cz
|
||||||
|
lifecycle: row.lifecycle ?? '',
|
||||||
|
lifecycleType: (row.lifecycleType as any) ?? 'info',
|
||||||
|
imageUrl: row.imageUrl ?? row.pic ?? undefined,
|
||||||
|
type: row.type ?? row.cz ?? row.lx ?? undefined,
|
||||||
|
supplierName: row.supplierName ?? row.ghs ?? undefined,
|
||||||
|
productId: row.productId ?? undefined,
|
||||||
|
barcode: row.barcode ?? row.sptm ?? undefined,
|
||||||
|
purchasePrice,
|
||||||
|
sellingPrice,
|
||||||
|
grossMargin
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
productList.value = []
|
||||||
|
}
|
||||||
|
|
||||||
ElMessage.success('查询成功')
|
ElMessage.success('查询成功')
|
||||||
}, 300)
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
ElMessage.error('查询失败,请稍后重试')
|
||||||
|
productList.value = []
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchBrandOptions() {
|
async function fetchBrandOptions() {
|
||||||
@@ -1045,6 +1132,8 @@ onMounted(async () => {
|
|||||||
if (queryParams.ckdm.length === 0 && storeOptions.value.length > 0) {
|
if (queryParams.ckdm.length === 0 && storeOptions.value.length > 0) {
|
||||||
queryParams.ckdm = storeOptions.value.map((o) => o.value)
|
queryParams.ckdm = storeOptions.value.map((o) => o.value)
|
||||||
}
|
}
|
||||||
|
// 进入页面后直接按当前查询条件拉取商品数据
|
||||||
|
await handleQuery()
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(() => route.query, () => applyQueryFromRoute(), { deep: true })
|
watch(() => route.query, () => applyQueryFromRoute(), { deep: true })
|
||||||
@@ -1052,7 +1141,6 @@ watch(() => route.query, () => applyQueryFromRoute(), { deep: true })
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.product-cards-page {
|
.product-cards-page {
|
||||||
padding: 16px;
|
|
||||||
background: var(--el-bg-color-page);
|
background: var(--el-bg-color-page);
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
@@ -1175,18 +1263,17 @@ watch(() => route.query, () => applyQueryFromRoute(), { deep: true })
|
|||||||
margin-bottom: 16px;
|
margin-bottom: 16px;
|
||||||
|
|
||||||
.pill {
|
.pill {
|
||||||
padding: 6px 14px;
|
padding: 4px 10px;
|
||||||
border-radius: 20px;
|
border-radius: 4px;
|
||||||
font-size: 13px;
|
font-size: 12px;
|
||||||
border: 1px solid var(--el-border-color);
|
border: 1px solid var(--el-border-color-lighter);
|
||||||
background: var(--el-bg-color);
|
background: var(--el-fill-color-light);
|
||||||
color: var(--el-text-color-regular);
|
color: var(--el-text-color-secondary);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
border-color: var(--el-color-primary-light-5);
|
background: var(--el-fill-color);
|
||||||
color: var(--el-color-primary);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
@@ -1194,6 +1281,24 @@ watch(() => route.query, () => applyQueryFromRoute(), { deep: true })
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
border-color: var(--el-color-primary);
|
border-color: var(--el-color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.kb22-tag-danger {
|
||||||
|
background: #fee2e2;
|
||||||
|
color: #ef4444;
|
||||||
|
border-color: #fecaca;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.kb22-tag-warn {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #d97706;
|
||||||
|
border-color: #fde68a;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.kb22-tag-success {
|
||||||
|
background: #dcfce7;
|
||||||
|
color: #16a34a;
|
||||||
|
border-color: #bbf7d0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1213,14 +1318,9 @@ watch(() => route.query, () => applyQueryFromRoute(), { deep: true })
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-info {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
align-items: flex-start;
|
|
||||||
|
|
||||||
.product-image {
|
.product-image {
|
||||||
width: 48px;
|
width: 48px;
|
||||||
height: 56px;
|
height: 64px;
|
||||||
background-color: var(--el-fill-color);
|
background-color: var(--el-fill-color);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -1230,6 +1330,7 @@ watch(() => route.query, () => applyQueryFromRoute(), { deep: true })
|
|||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
||||||
.product-img {
|
.product-img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -1237,6 +1338,12 @@ watch(() => route.query, () => applyQueryFromRoute(), { deep: true })
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.product-info {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
.product-details {
|
.product-details {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@@ -1298,12 +1405,11 @@ watch(() => route.query, () => applyQueryFromRoute(), { deep: true })
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.product-cards-grid {
|
.product-cards-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
gap: 20px;
|
gap: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1381,8 +1487,9 @@ $kb22-border: #e2e8f0;
|
|||||||
}
|
}
|
||||||
.kb22-tags-container { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; }
|
.kb22-tags-container { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; }
|
||||||
.kb22-tag-pill { font-size: 10px; padding: 2px 8px; border-radius: 4px; font-weight: 600; }
|
.kb22-tag-pill { font-size: 10px; padding: 2px 8px; border-radius: 4px; font-weight: 600; }
|
||||||
.kb22-tag-blue { background: #eff6ff; color: $kb22-accent; border: 1px solid #dbeafe; }
|
.kb22-tag-danger { background: #fee2e2; color: #ef4444; border: 1px solid #fecaca; }
|
||||||
.kb22-tag-purple { background: #f5f3ff; color: #7c3aed; border: 1px solid #ede9fe; }
|
.kb22-tag-warn { background: #fef3c7; color: #d97706; border: 1px solid #fde68a; }
|
||||||
|
.kb22-tag-success { background: #dcfce7; color: #16a34a; border: 1px solid #bbf7d0; }
|
||||||
.kb22-price-section { margin-top: auto; }
|
.kb22-price-section { margin-top: auto; }
|
||||||
.kb22-current-price { font-size: 22px; font-weight: 800; color: $kb22-primary; letter-spacing: -0.5px; }
|
.kb22-current-price { font-size: 22px; font-weight: 800; color: $kb22-primary; letter-spacing: -0.5px; }
|
||||||
.kb22-cost-price { font-size: 12px; color: #94a3b8; text-decoration: line-through; margin-left: 6px; }
|
.kb22-cost-price { font-size: 12px; color: #94a3b8; text-decoration: line-through; margin-left: 6px; }
|
||||||
@@ -1487,7 +1594,7 @@ $kb22-border: #e2e8f0;
|
|||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
.kb22-legend-item { display: flex; align-items: center; gap: 4px; font-size: 9px; color: #94a3b8; }
|
.kb22-legend-item { display: flex; align-items: center; gap: 4px; font-size: 12px; color: #94a3b8; }
|
||||||
.kb22-dot { width: 6px; height: 6px; border-radius: 50%; }
|
.kb22-dot { width: 6px; height: 6px; border-radius: 50%; }
|
||||||
.kb22-dot-stock { background: #0f172a; }
|
.kb22-dot-stock { background: #0f172a; }
|
||||||
.kb22-dot-sales { background: #3b82f6; }
|
.kb22-dot-sales { background: #3b82f6; }
|
||||||
|
|||||||
@@ -196,92 +196,170 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<!-- 标签分类筛选:与主页供应商表现一致 -->
|
<!-- 内容卡片:展示形式切换 + 标签筛选 + 卡片/列表(标签放在卡片内部更统一) -->
|
||||||
<div v-if="labelOpts.length > 0" class="filter-bar">
|
<el-card class="content-card" shadow="never">
|
||||||
<div
|
<!-- 展示形式切换:移动到内容卡片内部,置于标签上方,仅保留切换按钮 -->
|
||||||
v-for="opt in labelOpts"
|
<div class="view-toolbar">
|
||||||
:key="opt.value"
|
<div class="view-switch">
|
||||||
class="pill"
|
<el-radio-group v-model="displayMode" size="default">
|
||||||
:class="{ active: labelFilter === opt.value }"
|
<el-radio-button label="card">
|
||||||
@click="labelFilter = opt.value"
|
<el-icon><Grid /></el-icon>
|
||||||
>
|
卡片
|
||||||
{{ opt.label }}
|
</el-radio-button>
|
||||||
|
<el-radio-button label="table">
|
||||||
|
<el-icon><List /></el-icon>
|
||||||
|
列表
|
||||||
|
</el-radio-button>
|
||||||
|
</el-radio-group>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="cards-wrap" v-loading="loading" element-loading-text="加载中...">
|
<!-- 标签分类筛选:与主页供应商表现一致 -->
|
||||||
<div v-if="visibleRows.length === 0 && !loading" class="empty">暂无数据</div>
|
<div v-if="labelOpts.length > 0" class="filter-bar">
|
||||||
<div v-else class="cards-grid">
|
|
||||||
<div
|
<div
|
||||||
v-for="(row, idx) in visibleRows"
|
v-for="opt in labelOpts"
|
||||||
:key="rowKey(row, idx)"
|
:key="opt.value"
|
||||||
class="supplier-card"
|
:class="['pill', getTagClass(opt.label), { active: labelFilter === opt.value }]"
|
||||||
:class="getCardBorderClass(row)"
|
@click="labelFilter = opt.value"
|
||||||
>
|
>
|
||||||
<!-- 卡片头部:标题+标签在左上,右上显示 titleName / titleValue -->
|
{{ opt.label }}
|
||||||
<div class="card-header">
|
</div>
|
||||||
<div class="header-left">
|
</div>
|
||||||
<h2 class="card-title-main">
|
|
||||||
{{ String((row as any)?.title ?? (row as any)?.name ?? '-') }}
|
<!-- 卡片形式 -->
|
||||||
</h2>
|
<div v-show="displayMode === 'card'" class="cards-wrap" v-loading="loading" element-loading-text="加载中...">
|
||||||
<div v-if="getRowTags(row).length > 0" class="card-tags">
|
<div v-if="visibleRows.length === 0 && !loading" class="empty">暂无数据</div>
|
||||||
<span
|
<div v-else class="cards-grid">
|
||||||
v-for="(tag, ti) in getRowTags(row)"
|
<div
|
||||||
:key="ti"
|
v-for="(row, idx) in visibleRows"
|
||||||
class="badge"
|
:key="rowKey(row, idx)"
|
||||||
:class="getTagClass(tag)"
|
class="supplier-card"
|
||||||
|
:class="getCardBorderClass(row)"
|
||||||
|
>
|
||||||
|
<!-- 卡片头部:标题+标签在左上,右上显示 titleName / titleValue -->
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="header-left">
|
||||||
|
<h2 class="card-title-main">
|
||||||
|
{{ String((row as any)?.title ?? (row as any)?.name ?? '-') }}
|
||||||
|
</h2>
|
||||||
|
<div v-if="getRowTags(row).length > 0" class="card-tags">
|
||||||
|
<span
|
||||||
|
v-for="(tag, ti) in getRowTags(row)"
|
||||||
|
:key="ti"
|
||||||
|
class="badge"
|
||||||
|
:class="getTagClass(tag)"
|
||||||
|
>
|
||||||
|
{{ tag }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="getTitleName(row) || getTitleValue(row)" class="header-right">
|
||||||
|
<span class="rate-label">{{ getTitleName(row) }}</span>
|
||||||
|
<span class="rate-value">{{ getTitleValue(row) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 指标表格:三列网格布局 -->
|
||||||
|
<div v-if="getColumnsWithDate(row).length > 0" class="metric-section">
|
||||||
|
<div class="grid-layout metric-header">
|
||||||
|
<span>检测指标</span>
|
||||||
|
<span>实际结果</span>
|
||||||
|
<span>基准参考</span>
|
||||||
|
</div>
|
||||||
|
<div class="metric-rows">
|
||||||
|
<div
|
||||||
|
v-for="(item, mi) in getTableData(row)"
|
||||||
|
:key="mi"
|
||||||
|
class="grid-layout metric-row"
|
||||||
>
|
>
|
||||||
{{ tag }}
|
<span class="metric-label">{{ item.metricName }}</span>
|
||||||
</span>
|
<span class="metric-actual" :class="item.valueColorClass">
|
||||||
|
{{ item.actualValue }}
|
||||||
|
<span v-if="item.arrow" class="trend-arrow" :class="item.arrowClass">{{ item.arrow }}</span>
|
||||||
|
<template v-if="item.labelPairs && item.labelPairs.length > 0">
|
||||||
|
<span
|
||||||
|
v-for="(pair, pi) in item.labelPairs"
|
||||||
|
:key="pi"
|
||||||
|
class="badge-small"
|
||||||
|
:class="getTrendClassFromColor(pair.color)"
|
||||||
|
>
|
||||||
|
{{ pair.label }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</span>
|
||||||
|
<span class="metric-ref">{{ item.paramValue }}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="getTitleName(row) || getTitleValue(row)" class="header-right">
|
|
||||||
<span class="rate-label">{{ getTitleName(row) }}</span>
|
|
||||||
<span class="rate-value">{{ getTitleValue(row) }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 指标表格:三列网格布局 -->
|
<!-- 底部 -->
|
||||||
<div v-if="getColumnsWithDate(row).length > 0" class="metric-section">
|
<div class="card-footer">
|
||||||
<div class="grid-layout metric-header">
|
<span class="footer-label"> </span>
|
||||||
<span>检测指标</span>
|
<span class="footer-link">详情 →</span>
|
||||||
<span>实际结果</span>
|
|
||||||
<span>基准参考</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="metric-rows">
|
|
||||||
<div
|
|
||||||
v-for="(item, mi) in getTableData(row)"
|
|
||||||
:key="mi"
|
|
||||||
class="grid-layout metric-row"
|
|
||||||
>
|
|
||||||
<span class="metric-label">{{ item.metricName }}</span>
|
|
||||||
<span class="metric-actual" :class="item.valueColorClass">
|
|
||||||
{{ item.actualValue }}
|
|
||||||
<span v-if="item.arrow" class="trend-arrow" :class="item.arrowClass">{{ item.arrow }}</span>
|
|
||||||
<template v-if="item.labelPairs && item.labelPairs.length > 0">
|
|
||||||
<span
|
|
||||||
v-for="(pair, pi) in item.labelPairs"
|
|
||||||
:key="pi"
|
|
||||||
class="badge-small"
|
|
||||||
:class="getTrendClassFromColor(pair.color)"
|
|
||||||
>
|
|
||||||
{{ pair.label }}
|
|
||||||
</span>
|
|
||||||
</template>
|
|
||||||
</span>
|
|
||||||
<span class="metric-ref">{{ item.paramValue }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 底部 -->
|
|
||||||
<div class="card-footer">
|
|
||||||
<span class="footer-label"> </span>
|
|
||||||
<span class="footer-link">详情 →</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
<!-- 列表形式(不需要悬浮卡片) -->
|
||||||
|
<div v-show="displayMode === 'table'" class="table-wrap">
|
||||||
|
<el-table :data="visibleRows" v-loading="loading" border stripe style="width: 100%">
|
||||||
|
<el-table-column label="供应商" min-width="220" show-overflow-tooltip>
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="table-title">
|
||||||
|
<div class="table-title-main">{{ String((row as any)?.title ?? (row as any)?.name ?? '-') }}</div>
|
||||||
|
<div v-if="getRowTags(row).length > 0" class="table-tags">
|
||||||
|
<span
|
||||||
|
v-for="(tag, ti) in getRowTags(row)"
|
||||||
|
:key="ti"
|
||||||
|
class="badge"
|
||||||
|
:class="getTagClass(tag)"
|
||||||
|
>
|
||||||
|
{{ tag }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="核心指标" width="160" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="table-core">
|
||||||
|
<div class="core-name">{{ getTitleName(row) || '-' }}</div>
|
||||||
|
<div class="core-value">{{ getTitleValue(row) || '-' }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column label="检测指标明细" min-width="420">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<div class="metric-cell">
|
||||||
|
<div
|
||||||
|
v-for="(item, mi) in getTableData(row)"
|
||||||
|
:key="mi"
|
||||||
|
class="metric-cell-row"
|
||||||
|
>
|
||||||
|
<div class="metric-cell-name">{{ item.metricName }}</div>
|
||||||
|
<div class="metric-cell-actual" :class="item.valueColorClass">
|
||||||
|
{{ item.actualValue }}
|
||||||
|
<span v-if="item.arrow" class="trend-arrow" :class="item.arrowClass">{{ item.arrow }}</span>
|
||||||
|
<template v-if="item.labelPairs && item.labelPairs.length > 0">
|
||||||
|
<span
|
||||||
|
v-for="(pair, pi) in item.labelPairs"
|
||||||
|
:key="pi"
|
||||||
|
class="badge-small"
|
||||||
|
:class="getTrendClassFromColor(pair.color)"
|
||||||
|
>
|
||||||
|
{{ pair.label }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class="metric-cell-ref">{{ item.paramValue }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -290,6 +368,7 @@ import { computed, nextTick, onMounted, reactive, ref } from 'vue'
|
|||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
|
import { Grid, List } from '@element-plus/icons-vue'
|
||||||
import { ReportApi } from '@/api/ydoyun/report/reportpage'
|
import { ReportApi } from '@/api/ydoyun/report/reportpage'
|
||||||
import { useUserStore } from '@/store/modules/user'
|
import { useUserStore } from '@/store/modules/user'
|
||||||
|
|
||||||
@@ -305,6 +384,9 @@ const REPORT_ID = 6
|
|||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|
||||||
|
/** 展示形式:卡片 | 列表 */
|
||||||
|
const displayMode = ref<'card' | 'table'>('card')
|
||||||
const columns = ref<ColumnCfg[]>([])
|
const columns = ref<ColumnCfg[]>([])
|
||||||
const rows = ref<any[]>([])
|
const rows = ref<any[]>([])
|
||||||
/** 标签分类筛选(与主页供应商表现一致) */
|
/** 标签分类筛选(与主页供应商表现一致) */
|
||||||
@@ -341,6 +423,46 @@ function initQueryParamsFromRoute() {
|
|||||||
if (q.line) queryParams.line = String(q.line).split(',').filter(Boolean)
|
if (q.line) queryParams.line = String(q.line).split(',').filter(Boolean)
|
||||||
if (q.code) supplierCode.value = String(q.code)
|
if (q.code) supplierCode.value = String(q.code)
|
||||||
dateRange.value = [queryParams.rq, queryParams.rq2]
|
dateRange.value = [queryParams.rq, queryParams.rq2]
|
||||||
|
|
||||||
|
// 根据路由传入的时间区间反推当前快捷按钮
|
||||||
|
try {
|
||||||
|
const rq = queryParams.rq
|
||||||
|
const rq2 = queryParams.rq2
|
||||||
|
const today = dayjs().format('YYYY-MM-DD')
|
||||||
|
|
||||||
|
// 仅在 rq2 为今天时才尝试匹配 week7/day15/day30 或年份
|
||||||
|
if (rq && rq2 && rq2 === today) {
|
||||||
|
const diff = dayjs(today).diff(dayjs(rq), 'day')
|
||||||
|
if (diff === 6) {
|
||||||
|
activeTimeRange.value = 'week7'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (diff === 14) {
|
||||||
|
activeTimeRange.value = 'day15'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (diff === 29) {
|
||||||
|
activeTimeRange.value = 'day30'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试匹配全年快捷:从某年1月1日到该年末或今天
|
||||||
|
if (rq && rq2) {
|
||||||
|
const startYear = dayjs(rq).year()
|
||||||
|
const yearStart = dayjs(`${startYear}-01-01`).format('YYYY-MM-DD')
|
||||||
|
const yearEnd = startYear === dayjs().year() ? today : dayjs(`${startYear}-12-31`).format('YYYY-MM-DD')
|
||||||
|
if (rq === yearStart && rq2 === yearEnd) {
|
||||||
|
activeTimeRange.value = `year${startYear}`
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 其余情况,不高亮任何快捷按钮
|
||||||
|
activeTimeRange.value = ''
|
||||||
|
} catch {
|
||||||
|
activeTimeRange.value = ''
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 下拉选项(门店通过 executeTable tableName=kehu 获取)
|
// 下拉选项(门店通过 executeTable tableName=kehu 获取)
|
||||||
@@ -822,6 +944,36 @@ onMounted(async () => {
|
|||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.view-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
.view-switch {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
.toolbar-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-card {
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid var(--el-border-color-lighter);
|
||||||
|
|
||||||
|
:deep(.el-card__body) {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.query-form {
|
.query-form {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -921,12 +1073,176 @@ onMounted(async () => {
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
border-color: var(--el-color-primary);
|
border-color: var(--el-color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.badge-danger {
|
||||||
|
background: #fee2e2;
|
||||||
|
color: #ef4444;
|
||||||
|
border-color: #fecaca;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.badge-warn {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #d97706;
|
||||||
|
border-color: #fde68a;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.badge-success {
|
||||||
|
background: #dcfce7;
|
||||||
|
color: #22c55e;
|
||||||
|
border-color: #bbf7d0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.cards-wrap {
|
.cards-wrap {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.table-wrap {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-title {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
.table-title-main {
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-danger {
|
||||||
|
background: #fee2e2;
|
||||||
|
color: #ef4444;
|
||||||
|
border: 1px solid #fecaca;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-warn {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #d97706;
|
||||||
|
border: 1px solid #fde68a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-success {
|
||||||
|
background: #dcfce7;
|
||||||
|
color: #22c55e;
|
||||||
|
border: 1px solid #bbf7d0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-core {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
.core-name {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.core-value {
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-cell {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-cell-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 160px 1fr 120px;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-cell-name {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-cell-actual {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
line-height: 1.4;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
.trend-arrow {
|
||||||
|
font-weight: 700;
|
||||||
|
margin-left: 2px;
|
||||||
|
|
||||||
|
&.arrow-up {
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.arrow-down {
|
||||||
|
color: #22c55e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-small {
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
&.badge-danger,
|
||||||
|
&.label-trend-up {
|
||||||
|
background: #fee2e2;
|
||||||
|
color: #ef4444;
|
||||||
|
border: 1px solid #fecaca;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.badge-warn,
|
||||||
|
&.label-trend-flat {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #d97706;
|
||||||
|
border: 1px solid #fde68a;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.badge-success,
|
||||||
|
&.label-trend-down {
|
||||||
|
background: #dcfce7;
|
||||||
|
color: #22c55e;
|
||||||
|
border: 1px solid #bbf7d0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-cell-ref {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
text-align: right;
|
||||||
|
line-height: 1.4;
|
||||||
|
white-space: pre-line;
|
||||||
|
}
|
||||||
|
|
||||||
.empty {
|
.empty {
|
||||||
padding: 40px 0;
|
padding: 40px 0;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
@@ -206,90 +206,167 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<!-- 标签分类筛选 -->
|
<!-- 内容卡片:展示形式切换 + 标签筛选 + 卡片/列表 -->
|
||||||
<div v-if="labelOpts.length > 0" class="filter-bar">
|
<el-card class="content-card" shadow="never">
|
||||||
<div
|
<!-- 展示形式切换(移动到卡片内部,置于标签上方,仅显示切换按钮) -->
|
||||||
v-for="opt in labelOpts"
|
<div class="view-toolbar">
|
||||||
:key="opt.value"
|
<div class="view-switch">
|
||||||
class="pill"
|
<el-radio-group v-model="displayMode" size="default">
|
||||||
:class="{ active: labelFilter === opt.value }"
|
<el-radio-button label="card">
|
||||||
@click="labelFilter = opt.value"
|
<el-icon><Grid /></el-icon>
|
||||||
>
|
卡片
|
||||||
{{ opt.label }}
|
</el-radio-button>
|
||||||
|
<el-radio-button label="table">
|
||||||
|
<el-icon><List /></el-icon>
|
||||||
|
列表
|
||||||
|
</el-radio-button>
|
||||||
|
</el-radio-group>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 品类卡片列表 -->
|
<!-- 标签分类筛选 -->
|
||||||
<div class="category-cards-grid" v-loading="loading">
|
<div v-if="labelOpts.length > 0" class="filter-bar">
|
||||||
<div v-if="!loading && visibleRows.length === 0" class="empty-tip">暂无数据</div>
|
|
||||||
<transition-group v-else name="card-list" tag="div" class="cards-container">
|
|
||||||
<div
|
<div
|
||||||
v-for="(row, index) in visibleRows"
|
v-for="opt in labelOpts"
|
||||||
:key="rowKey(row, index)"
|
:key="opt.value"
|
||||||
class="category-card slide-in-up"
|
:class="['pill', getTagClass(opt.label), { active: labelFilter === opt.value }]"
|
||||||
:class="getCardBorderClass(row)"
|
@click="labelFilter = opt.value"
|
||||||
:style="{ animationDelay: `${index * 0.1}s` }"
|
|
||||||
>
|
>
|
||||||
<!-- 卡片头部:与供应商详情页一致,标题+标签在左上,右上 titleName/titleValue -->
|
{{ opt.label }}
|
||||||
<div class="card-header">
|
</div>
|
||||||
<div class="header-left">
|
</div>
|
||||||
<h2 class="category-name">{{ String(row?.title ?? row?.name ?? '-') }}</h2>
|
|
||||||
<div class="category-tags">
|
<!-- 卡片形式:品类卡片列表 -->
|
||||||
<span
|
<div v-show="displayMode === 'card'" class="category-cards-grid" v-loading="loading">
|
||||||
v-for="(tag, ti) in getRowTags(row)"
|
<div v-if="!loading && visibleRows.length === 0" class="empty-tip">暂无数据</div>
|
||||||
:key="ti"
|
<transition-group v-else name="card-list" tag="div" class="cards-container">
|
||||||
class="badge"
|
<div
|
||||||
:class="getTagClass(tag)"
|
v-for="(row, index) in visibleRows"
|
||||||
>
|
:key="rowKey(row, index)"
|
||||||
{{ tag }}
|
class="category-card slide-in-up"
|
||||||
</span>
|
:class="getCardBorderClass(row)"
|
||||||
|
:style="{ animationDelay: `${index * 0.1}s` }"
|
||||||
|
>
|
||||||
|
<!-- 卡片头部:与供应商详情页一致,标题+标签在左上,右上 titleName/titleValue -->
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="header-left">
|
||||||
|
<h2 class="category-name">{{ String(row?.title ?? row?.name ?? '-') }}</h2>
|
||||||
|
<div class="category-tags">
|
||||||
|
<span
|
||||||
|
v-for="(tag, ti) in getRowTags(row)"
|
||||||
|
:key="ti"
|
||||||
|
class="badge"
|
||||||
|
:class="getTagClass(tag)"
|
||||||
|
>
|
||||||
|
{{ tag }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="getTitleName(row) || getTitleValue(row)" class="header-right">
|
||||||
|
<span class="rate-label">{{ getTitleName(row) }}</span>
|
||||||
|
<span class="rate-value">{{ getTitleValue(row) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="getTitleName(row) || getTitleValue(row)" class="header-right">
|
|
||||||
<span class="rate-label">{{ getTitleName(row) }}</span>
|
<!-- 指标表格:与供应商详情页一致,由接口列配置 + 行数据动态生成 -->
|
||||||
<span class="rate-value">{{ getTitleValue(row) }}</span>
|
<div v-if="getColumnsWithDate(row).length > 0" class="metric-section">
|
||||||
|
<div class="metric-header">
|
||||||
|
<span>检测指标</span>
|
||||||
|
<span>实际结果</span>
|
||||||
|
<span>基准参考</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-for="(item, mi) in getTableData(row)"
|
||||||
|
:key="mi"
|
||||||
|
class="grid-layout metric-row"
|
||||||
|
>
|
||||||
|
<span class="metric-label">{{ item.metricName }}</span>
|
||||||
|
<span class="metric-actual" :class="item.valueColorClass">
|
||||||
|
{{ item.actualValue }}
|
||||||
|
<span v-if="item.arrow" class="trend-arrow" :class="item.arrowClass">{{ item.arrow }}</span>
|
||||||
|
<template v-if="item.labelPairs && item.labelPairs.length > 0">
|
||||||
|
<span
|
||||||
|
v-for="(pair, pi) in item.labelPairs"
|
||||||
|
:key="pi"
|
||||||
|
class="badge-small"
|
||||||
|
:class="getTrendClassFromColor(pair.color)"
|
||||||
|
>
|
||||||
|
{{ pair.label }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</span>
|
||||||
|
<span class="metric-ref">{{ item.paramValue }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 卡片底部 -->
|
||||||
|
<div class="card-footer">
|
||||||
|
<span class="footer-label"> </span>
|
||||||
|
<span class="footer-link" @click="handleViewDetail(row)">详情 →</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</transition-group>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 指标表格:与供应商详情页一致,由接口列配置 + 行数据动态生成 -->
|
<!-- 列表形式(不需要悬浮卡片) -->
|
||||||
<div v-if="getColumnsWithDate(row).length > 0" class="metric-section">
|
<div v-show="displayMode === 'table'" class="table-wrap">
|
||||||
<div class="metric-header">
|
<el-table :data="visibleRows" v-loading="loading" border stripe style="width: 100%">
|
||||||
<span>检测指标</span>
|
<el-table-column label="中类" min-width="220" show-overflow-tooltip>
|
||||||
<span>实际结果</span>
|
<template #default="{ row }">
|
||||||
<span>基准参考</span>
|
<div class="table-title">
|
||||||
</div>
|
<div class="table-title-main">{{ String(row?.title ?? row?.name ?? '-') }}</div>
|
||||||
<div
|
<div v-if="getRowTags(row).length > 0" class="table-tags">
|
||||||
v-for="(item, mi) in getTableData(row)"
|
|
||||||
:key="mi"
|
|
||||||
class="grid-layout metric-row"
|
|
||||||
>
|
|
||||||
<span class="metric-label">{{ item.metricName }}</span>
|
|
||||||
<span class="metric-actual" :class="item.valueColorClass">
|
|
||||||
{{ item.actualValue }}
|
|
||||||
<span v-if="item.arrow" class="trend-arrow" :class="item.arrowClass">{{ item.arrow }}</span>
|
|
||||||
<template v-if="item.labelPairs && item.labelPairs.length > 0">
|
|
||||||
<span
|
<span
|
||||||
v-for="(pair, pi) in item.labelPairs"
|
v-for="(tag, ti) in getRowTags(row)"
|
||||||
:key="pi"
|
:key="ti"
|
||||||
class="badge-small"
|
class="badge"
|
||||||
:class="getTrendClassFromColor(pair.color)"
|
:class="getTagClass(tag)"
|
||||||
>
|
>
|
||||||
{{ pair.label }}
|
{{ tag }}
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</div>
|
||||||
</span>
|
</div>
|
||||||
<span class="metric-ref">{{ item.paramValue }}</span>
|
</template>
|
||||||
</div>
|
</el-table-column>
|
||||||
</div>
|
<el-table-column label="核心指标" width="160" align="center">
|
||||||
|
<template #default="{ row }">
|
||||||
<!-- 卡片底部 -->
|
<div class="table-core">
|
||||||
<div class="card-footer">
|
<div class="core-name">{{ getTitleName(row) || '-' }}</div>
|
||||||
<span class="footer-label"> </span>
|
<div class="core-value">{{ getTitleValue(row) || '-' }}</div>
|
||||||
<span class="footer-link" @click="handleViewDetail(row)">详情 →</span>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
</div>
|
</el-table-column>
|
||||||
</transition-group>
|
<el-table-column label="检测指标明细" min-width="420">
|
||||||
</div>
|
<template #default="{ row }">
|
||||||
|
<div class="metric-cell">
|
||||||
|
<div
|
||||||
|
v-for="(item, mi) in getTableData(row)"
|
||||||
|
:key="mi"
|
||||||
|
class="metric-cell-row"
|
||||||
|
>
|
||||||
|
<div class="metric-cell-name">{{ item.metricName }}</div>
|
||||||
|
<div class="metric-cell-actual" :class="item.valueColorClass">
|
||||||
|
{{ item.actualValue }}
|
||||||
|
<span v-if="item.arrow" class="metric-trend-arrow" :class="item.arrowClass">{{ item.arrow }}</span>
|
||||||
|
<template v-if="item.labelPairs && item.labelPairs.length > 0">
|
||||||
|
<span
|
||||||
|
v-for="(pair, pi) in item.labelPairs"
|
||||||
|
:key="pi"
|
||||||
|
class="metric-badge-small"
|
||||||
|
:class="getTrendClassFromColor(pair.color)"
|
||||||
|
>
|
||||||
|
{{ pair.label }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class="metric-cell-ref">{{ item.paramValue }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -297,7 +374,7 @@
|
|||||||
import { ref, reactive, computed, onMounted, nextTick } from 'vue'
|
import { ref, reactive, computed, onMounted, nextTick } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import { Search } from '@element-plus/icons-vue'
|
import { Search, Grid, List } from '@element-plus/icons-vue'
|
||||||
import { ReportApi } from '@/api/ydoyun/report/reportpage'
|
import { ReportApi } from '@/api/ydoyun/report/reportpage'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { useUserStore } from '@/store/modules/user'
|
import { useUserStore } from '@/store/modules/user'
|
||||||
@@ -396,6 +473,8 @@ const getTimeRange = (range: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
/** 展示形式:卡片 | 列表 */
|
||||||
|
const displayMode = ref<'card' | 'table'>('card')
|
||||||
const columns = ref<{ title: string; key: string; order?: number; labelKey?: string; colorKey?: string }[]>([])
|
const columns = ref<{ title: string; key: string; order?: number; labelKey?: string; colorKey?: string }[]>([])
|
||||||
const categoryList = ref<any[]>([])
|
const categoryList = ref<any[]>([])
|
||||||
/** 标签分类筛选 */
|
/** 标签分类筛选 */
|
||||||
@@ -414,11 +493,44 @@ function initQueryParamsFromRoute() {
|
|||||||
if (q.line) queryParams.line = String(q.line).split(',').filter(Boolean)
|
if (q.line) queryParams.line = String(q.line).split(',').filter(Boolean)
|
||||||
if (q.zhonglei !== undefined) queryParams.zhonglei = String(q.zhonglei)
|
if (q.zhonglei !== undefined) queryParams.zhonglei = String(q.zhonglei)
|
||||||
dateRange.value = [queryParams.rq, queryParams.rq2]
|
dateRange.value = [queryParams.rq, queryParams.rq2]
|
||||||
// 如果日期范围匹配默认的"近一周",设置 activeTimeRange
|
|
||||||
const defaultWeek7 = getTimeRange('week7')
|
// 根据路由传入的时间区间反推当前快捷按钮
|
||||||
if (queryParams.rq === defaultWeek7.rq && queryParams.rq2 === defaultWeek7.rq2) {
|
try {
|
||||||
activeTimeRange.value = 'week7'
|
const rq = queryParams.rq
|
||||||
} else {
|
const rq2 = queryParams.rq2
|
||||||
|
const today = dayjs().format('YYYY-MM-DD')
|
||||||
|
|
||||||
|
// 仅在 rq2 为今天时才尝试匹配 week7/day15/day30 或年份
|
||||||
|
if (rq && rq2 && rq2 === today) {
|
||||||
|
const diff = dayjs(today).diff(dayjs(rq), 'day')
|
||||||
|
if (diff === 6) {
|
||||||
|
activeTimeRange.value = 'week7'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (diff === 14) {
|
||||||
|
activeTimeRange.value = 'day15'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (diff === 29) {
|
||||||
|
activeTimeRange.value = 'day30'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试匹配全年快捷:从某年1月1日到该年末或今天
|
||||||
|
if (rq && rq2) {
|
||||||
|
const startYear = dayjs(rq).year()
|
||||||
|
const yearStart = dayjs(`${startYear}-01-01`).format('YYYY-MM-DD')
|
||||||
|
const yearEnd = startYear === dayjs().year() ? today : dayjs(`${startYear}-12-31`).format('YYYY-MM-DD')
|
||||||
|
if (rq === yearStart && rq2 === yearEnd) {
|
||||||
|
activeTimeRange.value = `year${startYear}`
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 其余情况,不高亮任何快捷按钮
|
||||||
|
activeTimeRange.value = ''
|
||||||
|
} catch {
|
||||||
activeTimeRange.value = ''
|
activeTimeRange.value = ''
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -843,6 +955,179 @@ onMounted(async () => {
|
|||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.view-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
|
||||||
|
.view-switch {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
|
.toolbar-label {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-card {
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid var(--el-border-color-lighter);
|
||||||
|
|
||||||
|
:deep(.el-card__body) {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-wrap {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-title {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
.table-title-main {
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-danger {
|
||||||
|
background: #fee2e2;
|
||||||
|
color: #ef4444;
|
||||||
|
border: 1px solid #fecaca;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-warn {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #d97706;
|
||||||
|
border: 1px solid #fde68a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-success {
|
||||||
|
background: #dcfce7;
|
||||||
|
color: #22c55e;
|
||||||
|
border: 1px solid #bbf7d0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-core {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
.core-name {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.core-value {
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-cell {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-cell-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 160px 1fr 120px;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-cell-name {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-cell-actual {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
line-height: 1.4;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
.metric-trend-arrow {
|
||||||
|
font-weight: 700;
|
||||||
|
margin-left: 2px;
|
||||||
|
|
||||||
|
&.arrow-up {
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.arrow-down {
|
||||||
|
color: #22c55e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-badge-small {
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
&.badge-danger {
|
||||||
|
background: #fee2e2;
|
||||||
|
color: #ef4444;
|
||||||
|
border: 1px solid #fecaca;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.badge-warn {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #d97706;
|
||||||
|
border: 1px solid #fde68a;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.badge-success {
|
||||||
|
background: #dcfce7;
|
||||||
|
color: #22c55e;
|
||||||
|
border: 1px solid #bbf7d0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-cell-ref {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
text-align: right;
|
||||||
|
line-height: 1.4;
|
||||||
|
white-space: pre-line;
|
||||||
|
}
|
||||||
|
|
||||||
.query-form {
|
.query-form {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -918,6 +1203,149 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.table-wrap {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-title {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
.table-title-main {
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-tags {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-danger {
|
||||||
|
background: #fee2e2;
|
||||||
|
color: #ef4444;
|
||||||
|
border: 1px solid #fecaca;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-warn {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #d97706;
|
||||||
|
border: 1px solid #fde68a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-success {
|
||||||
|
background: #dcfce7;
|
||||||
|
color: #22c55e;
|
||||||
|
border: 1px solid #bbf7d0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-core {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
.core-name {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.core-value {
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-cell {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-cell-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 160px 1fr 120px;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-cell-name {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-text-color-regular);
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-cell-actual {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-text-color-primary);
|
||||||
|
line-height: 1.4;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
.metric-trend-arrow {
|
||||||
|
font-weight: 700;
|
||||||
|
margin-left: 2px;
|
||||||
|
|
||||||
|
&.arrow-up {
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.arrow-down {
|
||||||
|
color: #22c55e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-badge-small {
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
&.badge-danger {
|
||||||
|
background: #fee2e2;
|
||||||
|
color: #ef4444;
|
||||||
|
border: 1px solid #fecaca;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.badge-warn {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #d97706;
|
||||||
|
border: 1px solid #fde68a;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.badge-success {
|
||||||
|
background: #dcfce7;
|
||||||
|
color: #22c55e;
|
||||||
|
border: 1px solid #bbf7d0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-cell-ref {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--el-text-color-secondary);
|
||||||
|
text-align: right;
|
||||||
|
line-height: 1.4;
|
||||||
|
white-space: pre-line;
|
||||||
|
}
|
||||||
|
|
||||||
.pill {
|
.pill {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
padding: 4px 10px;
|
padding: 4px 10px;
|
||||||
@@ -938,6 +1366,24 @@ onMounted(async () => {
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
border-color: var(--el-color-primary);
|
border-color: var(--el-color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.badge-danger {
|
||||||
|
background: #fee2e2;
|
||||||
|
color: #ef4444;
|
||||||
|
border-color: #fecaca;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.badge-warn {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #d97706;
|
||||||
|
border-color: #fde68a;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.badge-success {
|
||||||
|
background: #dcfce7;
|
||||||
|
color: #22c55e;
|
||||||
|
border-color: #bbf7d0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 卡片网格布局 ====================
|
// ==================== 卡片网格布局 ====================
|
||||||
|
|||||||
@@ -197,43 +197,42 @@
|
|||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<!-- 展示形式切换(放在最上面) -->
|
<!-- 展示形式切换(放在最上面) -->
|
||||||
<div class="view-toolbar">
|
<!-- 内容卡片:展示形式切换 + 标签 + 卡片/列表内容(标签放在卡片内,不突兀) -->
|
||||||
<div class="view-switch">
|
|
||||||
<span class="toolbar-label">展示形式:</span>
|
|
||||||
<el-radio-group v-model="displayMode" size="default">
|
|
||||||
<el-radio-button label="card">
|
|
||||||
<el-icon><Grid /></el-icon>
|
|
||||||
卡片
|
|
||||||
</el-radio-button>
|
|
||||||
<el-radio-button label="table">
|
|
||||||
<el-icon><List /></el-icon>
|
|
||||||
列表
|
|
||||||
</el-radio-button>
|
|
||||||
</el-radio-group>
|
|
||||||
</div>
|
|
||||||
<div v-if="displayMode === 'table'" class="table-search">
|
|
||||||
<el-input
|
|
||||||
v-model="tableSearchKeyword"
|
|
||||||
placeholder="搜索款号/名称"
|
|
||||||
style="width: 200px"
|
|
||||||
clearable
|
|
||||||
>
|
|
||||||
<template #prefix>
|
|
||||||
<el-icon><Search /></el-icon>
|
|
||||||
</template>
|
|
||||||
</el-input>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 内容卡片:标签 + 卡片/列表内容(标签放在卡片内,不突兀) -->
|
|
||||||
<el-card class="content-card" shadow="never">
|
<el-card class="content-card" shadow="never">
|
||||||
|
<!-- 展示形式切换:移入内容卡片内部,置于标签上方,仅保留切换按钮 -->
|
||||||
|
<div class="view-toolbar">
|
||||||
|
<div class="view-switch">
|
||||||
|
<el-radio-group v-model="displayMode" size="default">
|
||||||
|
<el-radio-button label="card">
|
||||||
|
<el-icon><Grid /></el-icon>
|
||||||
|
卡片
|
||||||
|
</el-radio-button>
|
||||||
|
<el-radio-button label="table">
|
||||||
|
<el-icon><List /></el-icon>
|
||||||
|
列表
|
||||||
|
</el-radio-button>
|
||||||
|
</el-radio-group>
|
||||||
|
</div>
|
||||||
|
<div v-if="displayMode === 'table'" class="table-search">
|
||||||
|
<el-input
|
||||||
|
v-model="tableSearchKeyword"
|
||||||
|
placeholder="搜索款号/名称"
|
||||||
|
style="width: 200px"
|
||||||
|
clearable
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<el-icon><Search /></el-icon>
|
||||||
|
</template>
|
||||||
|
</el-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- 标签分类筛选:放在卡片内部 -->
|
<!-- 标签分类筛选:放在卡片内部 -->
|
||||||
<div v-if="labelOpts.length > 0" class="filter-bar">
|
<div v-if="labelOpts.length > 0" class="filter-bar">
|
||||||
<div
|
<div
|
||||||
v-for="opt in labelOpts"
|
v-for="opt in labelOpts"
|
||||||
:key="opt.value"
|
:key="opt.value"
|
||||||
class="pill"
|
:class="['pill', { active: labelFilter === opt.value }]"
|
||||||
:class="{ active: labelFilter === opt.value }"
|
|
||||||
@click="labelFilter = opt.value"
|
@click="labelFilter = opt.value"
|
||||||
>
|
>
|
||||||
{{ opt.label }}
|
{{ opt.label }}
|
||||||
@@ -495,6 +494,46 @@ function initQueryParamsFromRoute() {
|
|||||||
if (q.line) queryParams.line = String(q.line).split(',').filter(Boolean)
|
if (q.line) queryParams.line = String(q.line).split(',').filter(Boolean)
|
||||||
if (q.spdm) queryParams.spdm = String(q.spdm)
|
if (q.spdm) queryParams.spdm = String(q.spdm)
|
||||||
dateRange.value = [queryParams.rq, queryParams.rq2]
|
dateRange.value = [queryParams.rq, queryParams.rq2]
|
||||||
|
|
||||||
|
// 根据路由传入的时间区间反推当前快捷按钮
|
||||||
|
try {
|
||||||
|
const rq = queryParams.rq
|
||||||
|
const rq2 = queryParams.rq2
|
||||||
|
const today = dayjs().format('YYYY-MM-DD')
|
||||||
|
|
||||||
|
// 仅在 rq2 为今天时才尝试匹配 week7/day15/day30 或年份
|
||||||
|
if (rq && rq2 && rq2 === today) {
|
||||||
|
const diff = dayjs(today).diff(dayjs(rq), 'day')
|
||||||
|
if (diff === 6) {
|
||||||
|
activeTimeRange.value = 'week7'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (diff === 14) {
|
||||||
|
activeTimeRange.value = 'day15'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (diff === 29) {
|
||||||
|
activeTimeRange.value = 'day30'
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试匹配全年快捷:从某年1月1日到该年末或今天
|
||||||
|
if (rq && rq2) {
|
||||||
|
const startYear = dayjs(rq).year()
|
||||||
|
const yearStart = dayjs(`${startYear}-01-01`).format('YYYY-MM-DD')
|
||||||
|
const yearEnd = startYear === dayjs().year() ? today : dayjs(`${startYear}-12-31`).format('YYYY-MM-DD')
|
||||||
|
if (rq === yearStart && rq2 === yearEnd) {
|
||||||
|
activeTimeRange.value = `year${startYear}`
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 其余情况,不高亮任何快捷按钮
|
||||||
|
activeTimeRange.value = ''
|
||||||
|
} catch {
|
||||||
|
activeTimeRange.value = ''
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 下拉选项(门店通过 executeTable tableName=kehu 获取)
|
// 下拉选项(门店通过 executeTable tableName=kehu 获取)
|
||||||
@@ -1275,6 +1314,24 @@ onMounted(async () => {
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
border-color: var(--el-color-primary);
|
border-color: var(--el-color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.badge-danger {
|
||||||
|
background: #fee2e2;
|
||||||
|
color: #ef4444;
|
||||||
|
border-color: #fecaca;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.badge-warn {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #d97706;
|
||||||
|
border-color: #fde68a;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.badge-success {
|
||||||
|
background: #dcfce7;
|
||||||
|
color: #22c55e;
|
||||||
|
border-color: #bbf7d0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.view-toolbar {
|
.view-toolbar {
|
||||||
|
|||||||
@@ -546,10 +546,11 @@
|
|||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-popover
|
<el-popover
|
||||||
placement="right"
|
placement="right"
|
||||||
:width="340"
|
:width="360"
|
||||||
trigger="hover"
|
trigger="hover"
|
||||||
popper-class="product-detail-popover"
|
popper-class="product-detail-popover"
|
||||||
:popper-options="{ modifiers: [{ name: 'offset', options: { offset: [0, 12] } }] }"
|
:popper-options="{ modifiers: [{ name: 'offset', options: { offset: [0, 12] } }] }"
|
||||||
|
@show="handleProductHoverShow(row)"
|
||||||
>
|
>
|
||||||
<template #reference>
|
<template #reference>
|
||||||
<div class="product-info">
|
<div class="product-info">
|
||||||
@@ -574,59 +575,116 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<!-- 悬浮详情卡片(KB22 样式) -->
|
<!-- 悬浮详情卡片(KB22 样式,与 product-cards 保持一致,仅展示当前单个商品) -->
|
||||||
<div class="product-detail-card kb22-card">
|
<div class="product-detail-card kb22-card">
|
||||||
<div class="kb22-header-bar">
|
<div class="kb22-header-bar">
|
||||||
<div>款号: <span class="kb22-code">{{ row.code }}</span></div>
|
<div>款号: <span class="kb22-code">{{ hoverDetail?.code || row.code }}</span></div>
|
||||||
<div class="kb22-color-badge">
|
<div class="kb22-color-badge">
|
||||||
<span class="kb22-color-dot" :style="{ background: getColorCode(row.color) }"></span>
|
<span class="kb22-color-dot" :style="{ background: getColorCode(hoverDetail?.color || row.color) }"></span>
|
||||||
{{ row.color }}
|
{{ hoverDetail?.color || row.color }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="kb22-card-body">
|
<div class="kb22-card-body">
|
||||||
<div class="kb22-media-row">
|
<div class="kb22-media-row">
|
||||||
<div class="kb22-thumb-box">
|
<div class="kb22-thumb-box">
|
||||||
<img v-if="row.imageUrl" :src="row.imageUrl" alt="" class="kb22-thumb-img" />
|
<img v-if="hoverDetail?.imageUrl || row.imageUrl" :src="hoverDetail?.imageUrl || row.imageUrl" alt="" class="kb22-thumb-img" />
|
||||||
<div v-else class="kb22-no-img"><el-icon :size="32"><Picture /></el-icon></div>
|
<div v-else class="kb22-no-img"><el-icon :size="32"><Picture /></el-icon></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="kb22-info-col">
|
<div class="kb22-info-col">
|
||||||
<div class="kb22-prod-title">{{ row.name }}</div>
|
<div class="kb22-prod-title">{{ hoverDetail?.name || row.name }}</div>
|
||||||
<div class="kb22-tags-container">
|
<div class="kb22-tags-container">
|
||||||
<span class="kb22-tag-pill kb22-tag-blue">{{ row.season }}</span>
|
<span
|
||||||
<span class="kb22-tag-pill kb22-tag-purple">{{ row.discount }}</span>
|
v-if="hoverDetail?.type"
|
||||||
|
class="kb22-tag-pill"
|
||||||
|
:class="productTagClass(hoverDetail.type)"
|
||||||
|
>
|
||||||
|
{{ hoverDetail.type }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-if="hoverDetail?.lifecycle"
|
||||||
|
class="kb22-tag-pill"
|
||||||
|
:class="productTagClass(hoverDetail.lifecycle)"
|
||||||
|
>
|
||||||
|
{{ hoverDetail.lifecycle }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="kb22-price-section">
|
<div class="kb22-price-section">
|
||||||
<span class="kb22-current-price">¥{{ row.sellingPrice ?? 0 }}</span>
|
<span class="kb22-current-price">
|
||||||
<span class="kb22-cost-price">¥{{ row.purchasePrice ?? 0 }}</span>
|
<template v-if="(hoverDetail?.sellingPrice ?? row.sellingPrice) != null">
|
||||||
<span class="kb22-margin-text">毛利 {{ formatGrossMarginPct(row.grossMargin, row.purchasePrice != null && row.sellingPrice != null ? (1 - row.purchasePrice / row.sellingPrice) * 100 : undefined) }}%</span>
|
¥{{ formatNumber(hoverDetail?.sellingPrice ?? row.sellingPrice) }}
|
||||||
|
</template>
|
||||||
|
<template v-else>-</template>
|
||||||
|
</span>
|
||||||
|
<span class="kb22-cost-price">
|
||||||
|
<template v-if="(hoverDetail?.purchasePrice ?? row.purchasePrice) != null">
|
||||||
|
¥{{ formatNumber(hoverDetail?.purchasePrice ?? row.purchasePrice) }}
|
||||||
|
</template>
|
||||||
|
<template v-else>-</template>
|
||||||
|
</span>
|
||||||
|
<span class="kb22-margin-text">
|
||||||
|
<template v-if="(hoverDetail?.grossMargin ?? row.grossMargin) != null">
|
||||||
|
毛利
|
||||||
|
{{
|
||||||
|
formatGrossMarginPct(
|
||||||
|
hoverDetail?.grossMargin ?? row.grossMargin,
|
||||||
|
(hoverDetail?.purchasePrice ?? row.purchasePrice) != null &&
|
||||||
|
(hoverDetail?.sellingPrice ?? row.sellingPrice) != null
|
||||||
|
? (1 - (hoverDetail?.purchasePrice ?? row.purchasePrice) / (hoverDetail?.sellingPrice ?? row.sellingPrice)) * 100
|
||||||
|
: undefined
|
||||||
|
)
|
||||||
|
}}%
|
||||||
|
</template>
|
||||||
|
<template v-else>-</template>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="kb22-stats-grid">
|
<div class="kb22-stats-grid">
|
||||||
<div class="kb22-stat-card kb22-sales">
|
<div class="kb22-stat-card kb22-sales">
|
||||||
<div class="kb22-stat-label">总销量</div>
|
<div class="kb22-stat-label">总销量</div>
|
||||||
<div class="kb22-stat-value">{{ row.salesCount ?? 0 }}<span class="kb22-stat-sub">件</span></div>
|
<div class="kb22-stat-value">
|
||||||
|
<template v-if="(hoverDetail?.salesCount ?? row.salesCount) != null">
|
||||||
|
{{ hoverDetail?.salesCount ?? row.salesCount }}<span class="kb22-stat-sub">件</span>
|
||||||
|
</template>
|
||||||
|
<template v-else>-</template>
|
||||||
|
</div>
|
||||||
<div class="kb22-sales-footer">
|
<div class="kb22-sales-footer">
|
||||||
<div class="kb22-mini-progress">
|
<div class="kb22-mini-progress" v-if="(hoverDetail?.selloutRate ?? row.selloutRate) != null">
|
||||||
<div class="kb22-mini-fill" :style="{ width: Math.min(100, row.selloutRate ?? 0) + '%' }"></div>
|
<div class="kb22-mini-fill" :style="{ width: Math.min(100, hoverDetail?.selloutRate ?? row.selloutRate) + '%' }"></div>
|
||||||
|
</div>
|
||||||
|
<div class="kb22-rate-text">
|
||||||
|
<template v-if="(hoverDetail?.selloutRate ?? row.selloutRate) != null">
|
||||||
|
售罄率 {{ hoverDetail?.selloutRate ?? row.selloutRate }}%
|
||||||
|
</template>
|
||||||
|
<template v-else>-</template>
|
||||||
</div>
|
</div>
|
||||||
<div class="kb22-rate-text">售罄率 {{ row.selloutRate ?? 0 }}%</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="kb22-stat-card kb22-stock">
|
<div class="kb22-stat-card kb22-stock">
|
||||||
<div class="kb22-stat-label">当前库存</div>
|
<div class="kb22-stat-label">当前库存</div>
|
||||||
<div class="kb22-stat-value kcsl-display">{{ row.kcslRaw || (row.inventoryCount != null ? row.inventoryCount + '件' : '0件') }}</div>
|
<div class="kb22-stat-value kcsl-display">
|
||||||
<div v-if="row.turnoverText" class="kb22-turnover-text" :class="uiTextClass(row.turnoverStatus)">周转: {{ row.turnoverText }}</div>
|
<template v-if="(hoverDetail?.inventoryCount ?? row.inventoryCount) != null">
|
||||||
|
{{ hoverDetail?.inventoryCount ?? row.inventoryCount }}件
|
||||||
|
</template>
|
||||||
|
<template v-else>-</template>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="hoverDetail?.turnoverText || row.turnoverText"
|
||||||
|
class="kb22-turnover-text"
|
||||||
|
:class="uiTextClass((hoverDetail?.turnoverStatus as any) || row.turnoverStatus)"
|
||||||
|
>
|
||||||
|
周转: {{ hoverDetail?.turnoverText || row.turnoverText }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="row.sizes && row.sizes.length" class="kb22-stock-footer">
|
<div v-if="(hoverDetail?.sizes && hoverDetail.sizes.length) || (row.sizes && row.sizes.length)" class="kb22-stock-footer">
|
||||||
<div class="kb22-section-header">
|
<div class="kb22-section-header">
|
||||||
<span>SKU明细 ({{ row.sizes.length }}码)</span>
|
<span>SKU明细 ({{ (hoverDetail?.sizes || row.sizes).length }}码)</span>
|
||||||
<span v-if="hasOutOfStockSize(row.sizes)" class="kb22-alert-tip">⚠️ 有断货</span>
|
<span v-if="hasOutOfStockSize(hoverDetail?.sizes || row.sizes)" class="kb22-alert-tip">⚠️ 有断货</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="kb22-size-grid">
|
<div class="kb22-size-grid">
|
||||||
<div
|
<div
|
||||||
v-for="(size, i) in row.sizes"
|
v-for="(size, i) in (hoverDetail?.sizes || row.sizes)"
|
||||||
:key="i"
|
:key="i"
|
||||||
class="kb22-size-cell"
|
class="kb22-size-cell"
|
||||||
:class="{ 'kb22-alert': size.status === 'out' || size.stock === 0 }"
|
:class="{ 'kb22-alert': size.status === 'out' || size.stock === 0 }"
|
||||||
@@ -1176,6 +1234,7 @@ const loadingPie = ref(false)
|
|||||||
/** 商品明细列表加载状态 */
|
/** 商品明细列表加载状态 */
|
||||||
const loadingProductList = ref(false)
|
const loadingProductList = ref(false)
|
||||||
const searchKeyword = ref('')
|
const searchKeyword = ref('')
|
||||||
|
const hoverDetail = ref<ProductDetailData | null>(null)
|
||||||
|
|
||||||
// 计算时间区间(近一周、近15天、近30天、年份)
|
// 计算时间区间(近一周、近15天、近30天、年份)
|
||||||
const getTimeRange = (range: string) => {
|
const getTimeRange = (range: string) => {
|
||||||
@@ -1211,6 +1270,44 @@ const getTimeRange = (range: string) => {
|
|||||||
return { rq: start.format('YYYY-MM-DD'), rq2: end.format('YYYY-MM-DD') }
|
return { rq: start.format('YYYY-MM-DD'), rq2: end.format('YYYY-MM-DD') }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 商品明细 hover 时,按当前查询条件 + spdm 取最新单品详情(使用与 product-cards 相同的存储过程)
|
||||||
|
const handleProductHoverShow = async (row: ProductDetailData) => {
|
||||||
|
try {
|
||||||
|
const params = {
|
||||||
|
reportId: REPORT_ID,
|
||||||
|
rq_s: queryParams.rq,
|
||||||
|
rq_e: queryParams.rq2,
|
||||||
|
ckdm: arrToQuery(queryParams.ckdm),
|
||||||
|
pp: arrToQuery(queryParams.pp),
|
||||||
|
dalei: arrToQuery(queryParams.category),
|
||||||
|
jj: arrToQuery(queryParams.season),
|
||||||
|
p: '123',
|
||||||
|
username: username.value,
|
||||||
|
spdm: String(row.spdm || '')
|
||||||
|
}
|
||||||
|
|
||||||
|
const res: any = await ReportApi.executeProcedureWithData({
|
||||||
|
...params,
|
||||||
|
name: 'YDY_AI_GET_SPXQ'
|
||||||
|
} as any)
|
||||||
|
|
||||||
|
let data: any[] | null = null
|
||||||
|
if (Array.isArray(res)) data = res
|
||||||
|
else if (res && Array.isArray(res.data)) data = res.data
|
||||||
|
else if (res && res.code != null && Array.isArray(res.data)) data = res.data
|
||||||
|
|
||||||
|
if (data && data.length > 0) {
|
||||||
|
// 复用现有映射逻辑,将第一条记录映射为 ProductDetailData
|
||||||
|
hoverDetail.value = mapApiRowToProductDetail(data[0])
|
||||||
|
} else {
|
||||||
|
hoverDetail.value = null
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('加载商品 hover 详情失败:', e)
|
||||||
|
hoverDetail.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 处理快捷时间按钮点击
|
// 处理快捷时间按钮点击
|
||||||
const handleTimeRangeClick = (range: string) => {
|
const handleTimeRangeClick = (range: string) => {
|
||||||
activeTimeRange.value = range
|
activeTimeRange.value = range
|
||||||
@@ -1920,13 +2017,23 @@ const getProductCode = (row: ProductDetailData): string => {
|
|||||||
return row.code || '-'
|
return row.code || '-'
|
||||||
}
|
}
|
||||||
|
|
||||||
// 点击款号跳转到详情页面
|
// 点击款号跳转到详情页面(携带当前大盘的全部查询条件)
|
||||||
const handleProductCodeClick = (row: ProductDetailData) => {
|
const handleProductCodeClick = (row: ProductDetailData) => {
|
||||||
const spdm = row.spdm || row.code || ''
|
const spdm = row.spdm || row.code || ''
|
||||||
router.push({
|
router.push({
|
||||||
path: '/reports/lijun/reportpage6/detail',
|
path: '/reports/lijun/reportpage6/detail',
|
||||||
query: {
|
query: {
|
||||||
spdm: spdm
|
// 关键商品编码
|
||||||
|
spdm,
|
||||||
|
// 透传当前大盘查询条件,明细页可直接还原
|
||||||
|
rq: queryParams.rq,
|
||||||
|
rq2: queryParams.rq2,
|
||||||
|
ckdm: queryParams.ckdm.join(','),
|
||||||
|
pp: queryParams.pp.join(','),
|
||||||
|
season: queryParams.season.join(','),
|
||||||
|
zgj: queryParams.zgj.join(','),
|
||||||
|
category: queryParams.category.join(','),
|
||||||
|
line: queryParams.line.join(',')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user