2024年11月

前言

推荐一款集成了超过100款控件的流行 XAML 控件库,同时提供了一系列常用的 .NET 帮助类-CookPopularUI。它可以简化开发流程,让我们能够更加专注于核心业务逻辑的实现。

让我们一起学习如何使用 CookPopularUI,并详细了解其提供的丰富控件内容。

项目介绍

CookPopularUI
不仅提供了丰富的控件选择,包括但不限于数据网格、图表、导航菜单、对话框等,还特别注重于提升开发效率。通过内置的帮助类,可以轻松处理诸如数据绑定、异步操作、文件操作等常见任务,而无需从头开始编写大量代码。

另外,该控件库对多个版本的 .NET 提供了良好的支持,无论是 .NET Framework 还是 .NET Core/.NET 5+,都能确保应用的兼容性和稳定性。

项目特点

  • 丰富的控件库:CookPopularUI 包含了多种类型的控件,如数据网格、图表、导航菜单、对话框等,满足不同应用场景的需求。
  • 常用 .NET 帮助类:内置了大量 .NET 帮助类,简化了数据绑定、异步操作、文件处理等常见开发任务。
  • 高度可定制化:所有控件都支持高度自定义,您可以根据自己的需求调整样式和行为。
  • 良好的文档和示例:提供了详细的文档和丰富的示例代码,帮助您快速上手并高效开发。
  • 多版本支持:支持多个版本的 .NET,无论是 .NET Framework 还是 .NET Core/.NET 5+,都能确保兼容性和稳定性。

项目使用

1、添加Nuget包引用

<PackageReferenceInclude="CookPopularUI.WPF"Version="1.0.1-preview2" />

2、添加如下代码即可全部引用(两种方式皆可)

<Application.Resources>
    <ResourceDictionary>
          <ResourceDictionary.MergedDictionaries>
              <!--<ResourceDictionary Source="pack://application:,,,/CookPopularUI.WPF;component/Themes/DefaultPopularColor.xaml" />-->
              <!--<ResourceDictionary Source="pack://application:,,,/CookPopularUI.WPF;component/Themes/DefaultPopularControl.xaml" />-->
              <ui:PopularThemeLanguage="English"Theme="Light" />
          </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
</Application.Resources>

项目控件

1、Border

2、DataGrid

3、Message

项目地址

Gitee:
https://gitee.com/CookCSharp/CookPopularUI

总结

本文展示了部分功能和内容,如有需求访问案例地址获取详细信息。希望本文能在WPF控件开发方面为各位提供有益的帮助。期待大家在评论区留言交流,分享您的宝贵经验和建议。

最后

如果你觉得这篇文章对你有帮助,不妨点个赞支持一下!你的支持是我继续分享知识的动力。如果有任何疑问或需要进一步的帮助,欢迎随时留言。也可以加入微信公众号
[DotNet技术匠]
社区,与其他热爱技术的同行一起交流心得,共同成长!

前言

深入理解相机视口,摸索相机视口旋转功能,背景透明或者不透明。
本篇,实现了一个左下角旋转HUD且背景透明的相机视口。


Demo

请添加图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述


HUD相机的坐标

抬头HUD就是通过投影矩阵来实现,具体可参看《OSG开发笔记(二十):OSG使用HUD显示文字》

  • Hud要单独创建一个新相机
  • 注意关闭光照,不受光照影响,所以内容以同一亮度显示
  • 关闭深度测试
  • 渲染顺序设置为POST,否则可能会被场景中的其他图形所覆盖。
  • 设置参考贴为绝对型:setReferenceFrame(osg::Transform:ABSOLUTE_RF)
  • 使其不受父节点变换的影响:setMatrix(osg::Matrix::identity())
  • 投影矩阵通常会设置为屏幕尺寸大小


相机(Camera)

相机(osg::Camera)和视口(Viewport)是两个核心概念,对于理解OSG中的三维场景渲染至关重要。
相机在OSG中用于模拟真实世界中的摄影机,它负责捕捉和渲染三维场景。相机类(osg::Camera)继承自osg::Transform和osg::CullSetting类,用来管理OSG中的模型——视图矩阵。相机的管理主要是通过各种变换实现的,这些变换包括:

  • 视点变换:设置视点的方向和位置。默认情况下,视点定位为坐标原点,指向Y正方向。可以通过调整视点的位置和参考点的位置来改变相机的观察方向和角度。
  • 投影变换:由于显示器只能用二维图像显示三维物体,因此要靠投影来降低维数。投影变换的目的是定义一个视景体,使视景体外多余的部分被裁减掉,最终进入图像的只是视景体内的有关部分。OSG支持两种投影方式:透视投影(Perspective Projection)和正视投影(Orthographic Projection)。透视投影能够模拟人眼的视觉效果,使远处的物体看起来更小,而正视投影则保持物体的大小不变,不受距离影响。
  • 视口变换:将视景体内投影的物体显示在二维的视口平面上。即将经过几何变换、投影变换和裁剪变换后的物体显示于屏幕窗口内指定的区域内,这个区域通常为矩形,称为视口。


视口(ViewPort)

具体来说,视口变换涉及以下几个参数:

  • 屏幕左下角的坐标:定义了视口在屏幕上的左下角位置。
  • 屏幕宽度和高度:定义了视口的宽度和高度,即相机捕捉的场景在屏幕上显示的区域大小。
    在OSG中,可以通过调用相机的setViewport方法来设置视口。例如:
pCamera->setViewport(new osg::Viewport(0, 0, width, height));

这行代码创建了一个新的视口,并将其设置为相机的当前视口。其中,0和0是屏幕左下角的坐标,width和height是视口的宽度和高度。


相机与视口的关系

相机和视口在OSG中紧密相连,共同决定了三维场景的渲染效果。相机负责捕捉和渲染场景,而视口则定义了相机捕捉的场景在屏幕上的显示位置和大小。通过调整相机的各种变换和设置视口的大小和位置,可以实现丰富的三维视觉效果和交互体验。


设置相机观察函数

void setViewMatrixAsLookAt(const osg::Vec3d& eye, const osg::Vec3d& center, const osg::Vec3d& up);
  • eye:表示相机的位置。这是一个三维向量,指定了相机在世界坐标系中的位置。
  • center:表示相机观察的中心点。这也是一个三维向量,指定了相机应该对准的物体或场景的中心位置。
  • up:表示哪个方向是正方向。这同样是一个三维向量,通常用于指定相机的上方方向(例如,通常设置为 (0,0,1) 表示Y轴正方向为上方)。
    设置相机位置和方向:通过指定 eye、center 和 up 三个参数,你可以精确地控制相机的位置和姿态。eye 和 center 之间的向量表示相机的观察方向,而 up 向量则用于确定相机的上方方向。
    关闭漫游器:在使用 setViewMatrixAsLookAt 函数之前,通常需要关闭相机的漫游器(Camera Manipulator)。这是因为漫游器会自动更新相机的观察矩阵,从而覆盖你通过 setViewMatrixAsLookAt 设置的参数。可以通过调用 viewer->setCameraManipulator(NULL) 来关闭漫游器。
    坐标系:OSG 使用右手坐标系,其中 X 轴向右,Y 轴向上,Z 轴向前。因此,在设置 eye、center 和 up 参数时,需要确保它们符合右手坐标系的规则。
    视图矩阵:setViewMatrixAsLookAt 函数实际上是通过设置相机的视图矩阵来实现相机位置和姿态的调整。视图矩阵是一个 4x4 的矩阵,用于将相机坐标系中的点转换到世界坐标系中。
    setViewMatrixAsLookAt 是一个强大的函数,它允许你以直观的方式设置相机的位置和姿态。通过合理地使用这个函数,你可以创建出各种复杂而逼真的三维场景和视觉效果。


Demo关键源码


创建Hud相机

    // 步骤一:创建HUD摄像机
// osg::ref_ptr<osg::Camera> pCamera = new osg::Camera;
osg::ref_ptr<HudRotateCamera> pCamera = new HudRotateCamera;
pCamera->setMasterCamera(_pViewer->getCamera());
// 步骤二:设置投影矩阵
// pCamera->setProjectionMatrix(osg::Matrix::ortho2D(0, 1280, 0, 800));
// 步骤三:设置视图矩阵,同时确保不被场景中其他图形位置变换影响, 使用绝对帧引用
pCamera->setReferenceFrame(osg::Transform::ABSOLUTE_RF);
pCamera->setViewMatrix(osg::Matrix::identity());
// 步骤四:清除深度缓存
pCamera->setClearMask(GL_DEPTH_BUFFER_BIT);
// 步骤五:设置POST渲染顺序(最后渲染)
// pCamera->setRenderOrder(osg::Camera::PRE_RENDER); // 渲染不显示
// pCamera->setRenderOrder(osg::Camera::NESTED_RENDER);
pCamera->setRenderOrder(osg::Camera::POST_RENDER);
// 步骤六:设置为不接收事件,始终得不到焦点
pCamera->setAllowEventFocus(false);

// osg::ref_ptr<osg::Geode> pGeode = new osg::Geode();
// pGeode = new osg::Geode();
osg::ref_ptr<osg::StateSet> pStateSet = pGeode->getOrCreateStateSet();
// 步骤七:关闭光照
pStateSet->setMode(GL_LIGHTING, osg::StateAttribute::OFF);
// 步骤九:关闭深度测试
pStateSet->setMode(GL_DEPTH_TEST, osg::StateAttribute::OFF);

// pGeode->addDrawable(pGeometry.get());

pCamera->addChild(pGeode.get());

pGroup->addChild(pCamera.get());


HudRotateCamera.h

#ifndef HUDROTATECAMERA_H
#define HUDROTATECAMERA_H

#include "osg/Camera"
#include "osg/CopyOp"

class HudRotateCamera : public osg::Camera
{
public:
HudRotateCamera();

HudRotateCamera(const HudRotateCamera& copy, const osg::CopyOp &copyOp = osg::CopyOp::SHALLOW_COPY);

META_Node(osg, HudRotateCamera);

public:
void setMasterCamera(Camera* camera);

public:
virtual void traverse(osg::NodeVisitor& nodeVisitor);

protected:
virtual ~HudRotateCamera();

protected:
osg::observer_ptr<Camera> _pMasterCamera; // 新增了相机,主要是用来获取举证的
};

#endif // HUDROTATECAMERA_H


HudRotateCamera.cpp

#include "HudRotateCamera.h"

HudRotateCamera::HudRotateCamera(): Camera()
{

}

HudRotateCamera::HudRotateCamera(const HudRotateCamera & copy, const osg::CopyOp & copyOp)
: Camera(copy, copyOp),
_pMasterCamera(copy._pMasterCamera)
{

}

HudRotateCamera::~HudRotateCamera()
{

}

void HudRotateCamera::setMasterCamera(osg::Camera *camera)
{
_pMasterCamera = camera;
}

void HudRotateCamera::traverse(osg::NodeVisitor &nodeVisitor)
{
double fovy, aspectRatio, vNear, vFar;
_pMasterCamera->getProjectionMatrixAsPerspective(fovy, aspectRatio, vNear, vFar);

// 设置投影矩阵,使缩放不起效果, 改为正投影,正投影不会随相机的拉近拉远而放大、缩小,这样就没有缩放效果,
// 放大缩小是根据左右,上下距离,越大就物体越小,越小就物体越大
this->setProjectionMatrixAsOrtho(-50 * aspectRatio,
50 * aspectRatio,
-50,
50,
vNear,
vFar);
// 让坐标轴模型位于窗体左下角
osg::Vec3 vec3(-40, -40, 0);
if (_pMasterCamera.valid())
{
// 改变视图矩阵, 让移动位置固定
osg::Matrix matrix = _pMasterCamera->getViewMatrix();

// 让移动固定, 即始终位于窗体右下角,否则鼠标左键按住模型可以拖动或按空格键时模型会动
matrix.setTrans(vec3);
this->setViewMatrix(matrix);
}
osg::Camera::traverse(nodeVisitor);
}


工程模板v1.35.0

在这里插入图片描述


入坑


入坑一:没有按照预期的方式全屏显示在正中间


问题

想一直显示在中间,且能旋转,移动中心,但是实际效果如下,方格100x100,间距1.0,测试:
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述


尝试

将面方格缩小为10x10,线放小,测试:
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

缩小正交投影:
在这里插入图片描述
在这里插入图片描述

可能跟相机查看位置有关,新增相机位置和方向等信息:
在这里插入图片描述

没什么影响:
在这里插入图片描述

这里可能理解有问题,我们需要区域投影到视口,那么一个是投影的区域三维区域的大小,一个是投影到桌面2D他的大小,这里其实类似于HUD,通过HUD的方式,添加了几行代码设置投影矩阵:
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

经过测试,可以跳过相机调整视口、中心改变后也会移动,所以他一种在其位置区域。
然后回到前面,发现也可以,再次摸索,发现如下特点:
在这里插入图片描述

所以此时,纵横都是10,所以窗口大小要是符合1:1(10:10=1:1)的比例,改为400,400测试,还是一样:
在这里插入图片描述

但是调整为800x800就好了:
在这里插入图片描述

放最大也不会截取少了:
在这里插入图片描述

所以这个有点搞不明白了,总之是解决了,且投影矩阵和正交矩阵都可以解决,测试投影矩阵和正交举证都收视口大小影响,但是影响具体不知,就好像800x800是最小一样(其他的没测了,只测了400x400、600x600不行,看比例800x800是最小正好满窗口了)。
在这里插入图片描述

又怀疑投过去的区域小了,将区域放大,其位置反倒缩小,所以跟理解不一样:

  • 一种是理解直接投射过去,投射过去区域变大所以变大(不是的);
  • 一种是投射过去区域不变,那么区域变大视图区域可见空间范围变大(实际是这样,但是视口对屏幕的大小未变);
    综合以上,又测试了加大视口,也正常,所以怀疑有可能是qt和osg结合的时候这个地方设置了一个最小值,而可能吧,欢迎探讨,这里深究暂时也没结果,且解决了,所以不继续深究了。


解决

修改相机视口大小为最小800x800。
在这里插入图片描述


后续补充

后续查看做的这个qtosg兼容类,做的时候,自己设置的800x800,就是这个原因了:
在这里插入图片描述


入坑二:相机视口区域不透明


问题

当作最前面的文本hud,是可以透明,但是这里进行调整之后,无法透明。
在这里插入图片描述


尝试

修改了语句,可以透明了部分,但是没了。
在这里插入图片描述

在这里插入图片描述


解决

相机是一个投影矩阵,没有透明,但是文字hud为什么透明呢?。


入坑三:内置几何体关闭光照后纯白色


问题

关闭光照后,几何体白色
在这里插入图片描述


原理

光照关闭要设置颜色,不想设置颜色,就单独给体开放关照。


解决

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述


入坑四:文本看不到


问题

文本看不到,旋转后发现是太大了。
在这里插入图片描述

位置较大。


尝试

测试下是把整个坐标区域的展示范围扩大,那么实际看起来就是缩小。


解决

在这里插入图片描述

在这里插入图片描述


入坑五:hud旋转中心不对


问题

Hud旋转中心不对
在这里插入图片描述

这时旋转中心还不对,可能需要调整旋转中心
在这里插入图片描述


原理

开始去修改矩阵,发现都不对,其实中间点一直是0,0,0,其就是中心,那么我们设置文本的显示点不是从0,0开始即可。
下面将四边形的角点改为0,0,0来标识,然后修改文本的中心点:
在这里插入图片描述


解决

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述


入坑六:hud旋转反向了


问题

旋转是反的,然后光照也反了,变换矩阵有问题
在这里插入图片描述


原理

数据几何变换算来算去很费劲,直接测试的结论:
在这里插入图片描述

代码写反了,让上下反向了,应该是-50~50


解决

在这里插入图片描述

在这里插入图片描述

还剩下光照问题,这个不好咋弄了,反正是关闭光照,或者是自己手动添加光源,用系统的可能有点问题。
在这里插入图片描述

在这里插入图片描述

这个暂时没解决,实际使用就是用一个,可以从长度单独给这个相机设置一个光源,这里因为需求本身不需要投影,不做测试了。

JavaScript 中栈的运用

在 JavaScript 中,栈(Stack)是一种非常有用的数据结构,它遵循后进先出(Last In First Out,LIFO)的原则。在本文中,我们将深入探讨栈的概念以及在 JavaScript 中的实际运用。

一、栈的概念

栈是一种线性数据结构,它只能在一端进行插入(称为入栈或压栈,push)和删除(称为出栈或弹栈,pop)操作。想象一下一摞盘子,你只能从最上面拿盘子(出栈)或者把盘子放在最上面(入栈)。

栈通常具有以下几个基本操作:

  1. push(element)
    :将一个元素压入栈顶。
  2. pop()
    :弹出栈顶元素并返回它。
  3. peek()
    :查看栈顶元素,但不弹出它。
  4. isEmpty()
    :判断栈是否为空。

二、在 JavaScript 中实现栈

以下是用 JavaScript 实现一个简单栈的代码:

class Stack {
    constructor() {
        this.items = [];
    }

    push(element) {
        this.items.push(element);
    }

    pop() {
        if (this.isEmpty()) {
            return "Underflow";
        }
        return this.items.pop();
    }

    peek() {
        if (this.isEmpty()) {
            return null;
        }
        return this.items[this.items.length - 1];
    }

    isEmpty() {
        return this.items.length === 0;
    }

    size() {
        return this.items.length;
    }
}

三、栈的实际运用

(一)表达式求值

  1. 中缀表达式转后缀表达式
    在计算机科学中,将中缀表达式转换为后缀表达式是栈的一个重要应用。中缀表达式是我们通常使用的算术表达式形式,如
    (2 + 3) * 4
    。后缀表达式则是将运算符放在操作数之后,例如
    2 3 + 4 *

算法步骤如下:

  • 初始化一个空栈用于存储运算符。
  • 从左到右遍历中缀表达式。
  • 如果遇到操作数,直接输出。
  • 如果遇到左括号,将其压入栈。
  • 如果遇到右括号,弹出栈中的运算符并输出,直到遇到左括号,然后丢弃左括号。
  • 如果遇到运算符,根据其优先级进行处理。如果栈顶运算符的优先级高于或等于当前运算符,则弹出栈顶运算符并输出;否则,将当前运算符压入栈。
  • 遍历结束后,将栈中的剩余运算符依次弹出并输出。

以下是用 JavaScript 实现中缀表达式转后缀表达式的代码:

function infixToPostfix(expression) {
    const stack = new Stack();
    let postfix = "";
    const precedence = {
        '+': 1,
        '-': 1,
        '*': 2,
        '/': 2
    };

    for (let char of expression) {
        if (/[0-9]/.test(char)) {
            postfix += char;
        } else if (char === '(') {
            stack.push(char);
        } else if (char === ')') {
            while (!stack.isEmpty() && stack.peek()!== '(') {
                postfix += stack.pop();
            }
            stack.pop(); // 弹出左括号
        } else {
            while (!stack.isEmpty() && precedence[stack.peek()] >= precedence[char]) {
                postfix += stack.pop();
            }
            stack.push(char);
        }
    }

    while (!stack.isEmpty()) {
        postfix += stack.pop();
    }

    return postfix;
}
  1. 后缀表达式求值
    一旦将中缀表达式转换为后缀表达式,就可以很容易地对后缀表达式进行求值。

算法步骤如下:

  • 从左到右遍历后缀表达式。
  • 如果遇到操作数,将其压入栈。
  • 如果遇到运算符,弹出栈中的两个操作数,进行相应的运算,然后将结果压回栈。
  • 遍历结束后,栈中唯一的元素就是表达式的结果。

以下是用 JavaScript 实现后缀表达式求值的代码:

function evaluatePostfix(postfix) {
    const stack = new Stack();
    for (let char of postfix) {
        if (/[0-9]/.test(char)) {
            stack.push(parseInt(char));
        } else {
            const operand2 = stack.pop();
            const operand1 = stack.pop();
            switch (char) {
                case '+':
                    stack.push(operand1 + operand2);
                    break;
                case '-':
                    stack.push(operand1 - operand2);
                    break;
                case '*':
                    stack.push(operand1 * operand2);
                    break;
                case '/':
                    stack.push(operand1 / operand2);
                    break;
            }
        }
    }
    return stack.pop();
}

(二)函数调用栈

在 JavaScript 中,当一个函数调用另一个函数时,会在内存中创建一个称为调用栈(Call Stack)的结构。调用栈是一种栈数据结构,它用于跟踪函数的调用顺序。

例如:

function functionA() {
    console.log("Inside functionA");
    functionB();
}

function functionB() {
    console.log("Inside functionB");
}

functionA();


functionA
被调用时,它的执行上下文被压入调用栈。当
functionA
调用
functionB
时,
functionB
的执行上下文也被压入调用栈。当
functionB
执行完毕后,它的执行上下文从调用栈中弹出。然后,
functionA
继续执行,直到它也执行完毕,其执行上下文也从调用栈中弹出。

这种机制确保了函数的正确执行顺序和变量的作用域管理。

(三)深度优先搜索(DFS)

深度优先搜索是一种图遍历算法,它可以使用栈来实现。

以下是用 JavaScript 实现深度优先搜索的代码:

class Graph {
    constructor() {
        this.adjacencyList = {};
    }

    addVertex(vertex) {
        if (!this.adjacencyList[vertex]) {
            this.adjacencyList[vertex] = [];
        }
    }

    addEdge(vertex1, vertex2) {
        this.adjacencyList[vertex1].push(vertex2);
        this.adjacencyList[vertex2].push(vertex1);
    }

    dfs(startVertex) {
        const stack = new Stack();
        const visited = {};
        stack.push(startVertex);
        visited[startVertex] = true;

        while (!stack.isEmpty()) {
            const currentVertex = stack.pop();
            console.log(currentVertex);

            for (let neighbor of this.adjacencyList[currentVertex]) {
                if (!visited[neighbor]) {
                    stack.push(neighbor);
                    visited[neighbor] = true;
                }
            }
        }
    }
}

可以使用以下方式调用:

const graph = new Graph();
graph.addVertex('A');
graph.addVertex('B');
graph.addVertex('C');
graph.addVertex('D');
graph.addVertex('E');

graph.addEdge('A', 'B');
graph.addEdge('A', 'C');
graph.addEdge('B', 'D');
graph.addEdge('C', 'E');

graph.dfs('A');

在这个例子中,深度优先搜索从给定的起始顶点开始,使用栈来存储待访问的顶点。每次从栈中弹出一个顶点,访问它,并将其未访问过的邻居顶点压入栈。

(四)括号匹配

检查一个字符串中的括号是否匹配是栈的另一个常见应用。

算法步骤如下:

  • 初始化一个空栈。
  • 遍历字符串中的每个字符。
  • 如果遇到左括号,将其压入栈。
  • 如果遇到右括号,检查栈是否为空。如果为空,说明右括号没有匹配的左括号,返回 false。如果栈不为空,弹出栈顶元素,检查弹出的左括号是否与当前右括号匹配。如果不匹配,返回 false。
  • 遍历结束后,如果栈为空,说明所有括号都匹配,返回 true;否则,返回 false。

以下是用 JavaScript 实现括号匹配的代码:

function isBalanced(str) {
    const stack = new Stack();
    for (let char of str) {
        if (char === '(' || char === '[' || char === '{') {
            stack.push(char);
        } else if (char === ')' || char === ']' || char === '}') {
            if (stack.isEmpty()) {
                return false;
            }
            const top = stack.pop();
            if ((char === ')' && top!== '(') || (char === ']' && top!== '[') || (char === '}' && top!== '{')) {
                return false;
            }
        }
    }
    return stack.isEmpty();
}

四、总结

栈是一种强大的数据结构,在 JavaScript 中有许多实际应用。从表达式求值到函数调用栈,从图的遍历到括号匹配,栈都发挥了重要作用。理解栈的概念和操作,以及如何在 JavaScript 中实现和应用栈,对于编写高效的代码和解决各种编程问题非常有帮助。

大家好,我是汤师爷~

今天聊聊
商品概念模型设计。

优秀的商品概念模型应具备充分的灵活性和抽象性,以适应不同行业的需求变化,并在系统升级或业务调整时,能最小化重构的工作量。

商品模型是商品管理系统的核心,整体来看,可以划分为三个关键部分:

  • 基础资料:用于定义和管理商品的基本要素,是商品的“元数据”。包括但不限于品牌、类目、属性库、单位等关键信息。通过标准化这些基础资料,可以确保整个系统中商品信息的一致性和标准化。
  • 商品主档信息:这部分是商品的核心描述,包含关联的基础资料信息,以及商品的一些描述信息,例如名称、副标题、图片、编码、条码、商品描述、销售配置、供应链配置、财务配置等。
  • 渠道差异化信息:在全渠道零售环境中,这部分非常重要,它可以针对不同渠道提供个性化配置,例如渠道特定的价格、销售配置等。

基础资料

接下来,我们将详细介绍商品系统的基础资料。它包括商品类目、属性库和多单位等重要组成部分,这些元素共同构建了商品的基本框架。

1、商品类目

商品类目是一种系统化的分类方法,用于组织和管理各种商品。它为商品提供了一个层次化的结构,使商品能够被有效地归类、检索和管理。

商品类目通常采用树状结构,从顶层的大类逐步细分到具体的小类。每个层级都代表了商品的不同特征或属性。合理的类目管理能显著提升商品管理效率:

  • 对于消费者
    :类目导航优化了商品发现的路径,让用户能快速找到目标商品。
  • 对于商家
    :类目为商品提供了标准化的组织方式,帮助运营人员快速筛选、定位商品,支持库存管理和销售分析等操作。

商品类目通常分为前台类目和后台类目,以满足消费者和商家的不同需求。

前台类目面向消费者,优化了商品浏览和搜索体验。其核心特征是灵活性,根据用户购物习惯、促销活动和市场趋势动态调整。如图所示。

通过这样的调整,前台类目能够快速响应市场需求,提升用户购物体验和转化率。

后台类目主要服务于商家,为商品管理和数据分析提供稳定的框架。与前台类目的灵活性不同,后台类目结构相对固定,变更频率低。例如,按类目统计某一季度的商品销售额;通过后台类目筛选滞销商品,制定清库存策略。

2、属性库

属性库通过集中化管理,为商品搜索、分类和分析提供了坚实的基础,同时简化了复杂商品属性的维护流程。

商品属性,又称为产品属性、商品参数,是产品本身固有的特征。

不同行业的商品,差异非常大,有很多行业差异化属性。根据使用目的、用途不同,演化出各式各样的属性,有的用于展示,有的用于分析,有的用于经营管控。下面根据商品属性不同的分类法,逐一展开描述:

  • 描述属性:包括商品名称、商品描述、规格、型号、产地、等级、生产厂商、商品图片等。这些属性主要用于向消费者展示商品的基本信息。
  • 统计属性:品牌、类目、系列、款式、适用人群、适用年龄等。这类属性为商品数据统计和分析提供依据,例如统计某品牌的月销售额。
  • 考核属性:一般用于组织业绩考核。例如,基于品牌、分类或系列统计的销售额,用于评估部门或员工的业绩。
  • 物流属性:长、宽、高、净重、毛重、重量单位等。这些属性影响配送成本和仓储规划。例如,大件商品需要特殊的仓储和配送方案。
  • 管控属性:是否季节商品、是否有保险、是否支持配送、是否支持打折、是否保质期管控、是否串码管理等。这类属性为运营管控提供支持,例如控制保质期商品的销售策略。
  • 销售渠道属性:针对不同销售渠道的特殊属性。例如,美团、饿了么平台上商品的最小购买数量或平台分类。
  • 规格属性:该属性是组成SKU的特殊属性,直接影响 SKU 的生成,例如衣服的颜色、尺寸等。这类属性不仅影响消费者购买决策,也直接关系到商家的库存管理。

为了避免属性重复创建,同时提高管理效率和数据一致性,通常会建立一个统一的属性库。

属性库的结构由三部分组成:属性组、属性项、属性值。

  • 属性组:
    顶层分类,用于按属性的共性特征管理属性。例如,手机的属性组可包括“外观属性”(颜色、材质)和“性能属性”(处理器、运行内存)。
  • 属性项:
    具体的属性名,用于定义商品的某个特征,例如颜色、尺码、口味等。
  • 属性值:
    属性的具体内容,例如“颜色”的值为红色、绿色、蓝色等。

3、多单位

在零售场景中,不同消费者对商品的计量需求千差万别。例如,消费者希望按瓶购买饮料,而企业客户则希望按整箱下单。多单位功能为这一需求提供了灵活解决方案。

这种灵活性体现在多个方面:

  • 销售多样性:例如,一种饮料可以按瓶购买,也可以按整箱(含多瓶)购买。
  • 库存精确管理:商家可以同时跟踪单品和批量单位的库存,提高库存管理的精确度。
  • 定价策略优化:可以为不同单位设置不同价格,如单瓶价格和整箱优惠价,刺激消费。
  • 物流效率提升:支持按不同单位发货,优化仓储和配送流程。

在实现层面,多单位功能需要以下概念模型支持:

  • 单位:是商品的计量标准,如件、盒、瓶、公斤等。多单位功能则允许一个SKU支持多种计量方式。
  • 单位转换关系:为每种商品定义单位间的转换关系(如1箱=12瓶),并在库存管理、物流发货中支持动态换算。

多单位功能不仅满足了不同消费者的购买需求,还为商家提供了更灵活的经营策略,同时简化了库存管理流程,提高了整体运营效率。

商品主档信息

在介绍完商品系统的基础资料后,我们将深入探讨商品主档信息。

1、商品

商品指商家在零售环境中提供的具体产品或服务,旨在满足消费者的多样化需求。例如,在服装行业,一件商品可能有多种颜色和尺码的规格供消费者选择;而在生鲜行业,商品可能按照重量或数量进行销售。

商品的多样性不仅体现在种类上,还体现在规格和属性特征上,这些共同构成了商品在零售系统中的完整定义。

2、SKU

SKU(Stock Keeping Unit)是库存量单位,也称为最小库存单元,是库存管理的基本单位。

SKU 明确定义了具体商品的规格属性值。例如,"iPhone 16"这款商品的关键规格包括颜色(黑色、红色、银色、金色)和容量(128G、256G、512G),可以组合出 4×3=12 个 SKU。

之所以称为"最小库存单元",是因为 SKU 是库存管理中的实际管理对象。每个 SKU 都有明确的规格、价格、库存和条形码,是不可再细分的管理单位。无论是商品的采购、入库、销售、出库还是库存盘点,系统跟踪的对象都是 SKU。

3、商品类型

在新零售业务中,商品种类繁多,为了更高效地管理商品数据,需要将商品进行类型划分。商品类型不仅影响库存管理和交易方式,也直接决定消费者的购买体验。

  • 实物商品:以有形实体存在,不能通过网络来传递,必须依赖传统的物流运输系统来传递。例如,鸡蛋、大米、手机等。
  • 服务商品:能够实现交易的无形商品,无需物流参与,就能完成交易,例如,话费充值、游戏点券、线上课程等。
  • 组合商品:组合商品是由多个单独售卖的商品组成的捆绑销售商品,例如:下午茶套餐(包含咖啡、蛋糕、小食)、七夕美妆组合(包含口红、香水、护肤品)等。
  • 多规格商品:多规格商品是由多个 SKU 组成的商品集合,消费者只能选择其中一个 SKU。例如,以iphone16为例,关键规格有颜色(黑色、红色、银色、金色)、容量(128G、256G、512G),消费者选中了”黑色128G的iphone16“进行下单。

4、商品状态

商品状态是商品生命周期管理的核心,贯穿商品从创建到退市的全过程。在新零售系统中,商品状态用于标识商品在业务流程中的具体阶段,不同的状态对应不同的管理和运营策略。

商品的生命周期状态包括建档、新品、正常、预淘汰、淘汰、清理、待归档等。

  • 建档:
    商品信息初次录入系统,完成基础数据的创建,包括名称、类目、品牌、规格和价格等。这一阶段的商品尚未对外展示,仅供内部审核和信息完善,确保商品上线前具备完整的基础信息。
  • 新品:
    商品审核通过并正式上线,进入市场的初期阶段。此时通常会通过新品标签、首页推荐等方式进行重点推广,并配合首发折扣或赠品活动,吸引消费者关注。
  • 正常:
    商品进入稳定销售阶段,成为商家常规运营的一部分。此状态下的商品以正常定价销售,同时可能参与常规促销活动(如满减、限时折扣),以维持销量。
  • 预淘汰:
    商品销量下降或市场需求减弱,逐步退出核心销售渠道。在此阶段,商家减少库存补货,并通过特定促销活动(如清仓折扣)加速库存清理。
  • 淘汰:
    商品已停止销售,但库存尚未完全清理完毕。此状态的商品通常从前台下架,仅通过特定渠道(如线下门店或促销专区)进行有限销售,直至库存耗尽。
  • 清理:
    商品进入最终清仓阶段,彻底准备退市。商家集中处理剩余库存,通过降价促销、库存转移或销毁计划等手段清空库存,确保资源优化配置。
  • 待归档:
    商品生命周期结束,进入历史归档阶段。在此状态下,商品不可编辑或销售,但其数据被保留,用于查询和分析历史销售记录,为业务决策提供数据支持。

渠道差异化信息

在全渠道零售环境中,不同销售渠道的用户特征和需求各异。商家需要通过渠道差异化策略,灵活调整商品展示、价格体系及运营方式,才能更高效地满足消费者需求。

1、渠道级商品与 SKU 管理

为适应多渠道需求,新零售体系采用多层次的商品管理结构:

  • 商品库
    :作为企业商品信息的主数据,集中存储和管理所有商品数据,确保信息的一致性。
  • 渠道级商品
    :针对不同渠道(如微信商城、美团外卖、饿了么外卖、抖音、小红书等渠道)设置商品差异化信息。

2、多维度商品价格策略

渠道差异化管理的核心在于灵活的定价策略,通过多维度的价格体系满足不同场景需求:

  • 指导价
    :厂商建议的零售价格,为商家提供定价参考。
  • 渠道价格
    :根据渠道特性制定差异化定价,如外卖平台因配送成本较高,售价通常高于线下门店。
  • 日历价格
    :针对不同时间段制定动态定价策略,例如早餐时段的特价优惠。
  • 成本价
    :精确到 SKU 的单品成本,作为统计利润的基础依据。

3、渠道差异化的其他关键信息

其他的一些渠道相关的差异化信息:

  • 渠道销售状态
    :控制商品的上下架状态。例如,某商品可能仅在特定平台渠道展示,而其他平台隐藏。
  • 配送方式
    :支持快递、同城配送、自提等多种配送方式。例如,社区团购用户更倾向于自提,而电商用户更依赖快递配送。
  • 其他销售设置
    :设置购买数量上限、销售时段等规则。例如,通过限定销售时段实现高峰期促销,或设定单次购买数量上限避免库存被抢购。

本文已收录于,我的技术网站:
tangshiye.cn
里面有,算法Leetcode详解,面试八股文、BAT面试真题、简历模版、架构设计,等经验分享。

工作电脑为一体机。所有的USB接口都在屏幕的后面。插拔U盘极不方便。于是搜索USB小物件,看能不能通过小物件将USB的接口延长到屏幕前

寻觅一番,找到一个中意的小物件——
ELEKS MAKER 极客桌面控制器

如上图所示,小物件有着朋克风,充满现代感。带有3个USB2.0接口,日常工作使用足够。另外还有三个小按键,以及一个大旋钮。大旋钮可以旋转,也可以当按钮用。

按照使用说明,大旋钮的旋转是控制系统的音量,大旋钮的按键是切换系统是否静音。

也可以通过控制程序,来自定义大旋钮按键的功能。

于是,有了个想法,
将大旋钮的的按键,自定义为锁屏
。当我离开工位,按一下大旋钮,就将工作电脑锁屏了。

于是,打开控制程序,如下图

可以看出,控制程序分别对
三个按钮

一个大旋钮
的功能进行了设定的功能

但是
!对“按下按钮”的这个动作,只能指定多媒体键,也就是在若干个指定的多媒体键中间选择一个。并不能选择其他的快捷键。比如,锁屏的快捷键是Win+L,却是没法设定。

于是,在网上搜索一番。找到了两篇有用的文章

【转载】修改Windows下键盘按键对应功能的一些方案

Win10 64位电脑如何以桌面快捷方式创建一个一键锁屏程序?

第一篇文章,讲解了,如何修改
多媒体键
对应的功能

如下面所示,通过注册表,将
计算器多媒体键
的功能改为指向记事本(notepad.exe)

Windows Registry Editor Version 5.00
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\AppKey\18]
"ShellExecute"="notepad.exe"

其中,ShellExecute字段指向其他程序。

也可以用Association字段指向扩展名,调用该扩展名对应的程序。

例如:

"Association"="mailto",表示调用默认的邮件程序

"Association"=".doc",表示调用默认的doc的程序,一般是Word

第二篇文章,讲解了,如何通过快捷方式,设定锁屏

在快捷方式下,通过

rundll32.exe user32.dll,LockWorkStation

调用系统的锁频程序

于是,灵光一闪,将上面两篇文章的内容合二为一

1、将按下按钮的多媒体快捷键改为“计算器”

2、编写注册表,将计算器多媒体键的对应的内容改为指向锁屏。

Windows Registry Editor Version 5.00
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\AppKey\18]
"ShellExecute"="rundll32.exe user32.dll,LockWorkStation"

将注册表导入到系统内之后

此时,按小物件的大按钮,电脑立刻锁屏!