Cocos Creator3.8 项目实战(七)Listview 控件的实现和使用

news2024/11/19 3:31:32

滚动列表在游戏中也很常见,比如排行榜 、充值记录等,在这些场景中,都有共同的特点, 那就是:数据量大 , 结构相同


在cocoscreator 中,没有现成的 Listview 控件, 无奈之下, 只能自己动手 用ScrollView 来实现一个。这样,有类似需求的朋友,能专注业务功能的开发,就不用重复造轮了。


⚠️ 文末附 ListView.ts 完整源码, 可直接拿去使用。

下面以排行榜Listview 实现为例,进行详细说明。


ListView 实现效果:

在这里插入图片描述


ListView 实现原理:

ListView 实现方式,类似 Android的 ListView 。

采用了AbsAdapter 适配器,用于设置数据,更新视图页面,获取数据数量,计算 item 显示位置等。

采用了 ScrollView 配合 item 预制体Prefab 来实现,动态生成列表项, 支持调整 item 项的间距,支持横向和竖向滚动 。

ListView 还设计了简单的上/下拉通知, 只需要初始化时设置相应回调方法即可。


使用步骤:

step 1 ,在creator层级管理器中,新建 ScrollView 节点,并做如下配置:

这里命名为 sore_rank_listview


在这里插入图片描述


step 2 ,独立新建一个item 预制体文件

这里命名为:score_rank_item ,添加了以下属性和布局

请添加图片描述


在这里插入图片描述


step 3 ,在层级管理器中,选择score_rank_item 节点,然后在creator属性检查器中,挂载ScoreRankItem.ts 脚本,并做如下属性配置:

请添加图片描述


step 4 ,在层级管理器中,选择Listview 节点,然后在creator属性检查器中,挂载Listview.ts 脚本,并做如下配置:

在这里插入图片描述

参数解释:

  • Spacing :用来约定item 之间的间距
  • SpawnCount: 用来约定超过可见区域的额外显示项数,可以调整滚动时的平滑性。
  • Item Template :独立的item 预制体
  • scroollview : 滚动条控件,在这里和 listview 控件是同一个节点

step 5 ,根据排行榜显示内容,我们准备了一个数据结构

export class RankItemData {
   
    /** 用户ID */
    userid:number;
    
    /** 用户昵称 */
    nickName:string;

    /** 排行名次 */
    topLevel:number;
    
    /** 自定义头像id */
    faceid:number;

    /** VIP */
    vipLevel:number;

    /** 金币 */
    score:number;

    reset(){
       this.userid = 0;
       this.nickName = '';
       this.topLevel = 0;
       this.faceid = 0;
       this.vipLevel = 0;
       this.score = 0;
    }
}

step 6 ,我们需要准备数据列表或者是数组

 // 离线测试代码
 let datas:Array<RankItemData>= new Array<RankItemData>;
 for(let i=0;i<100;i++)
 {
   let itemData:RankItemData = new RankItemData();
   itemData.userid = 1000+i;
   itemData.faceid= 1;
   itemData.nickName="userName"+i;
   itemData.topLevel = i+1;
   itemData.vipLevel = i % 7 + 1;
   itemData.score = (101 - i)*10000;
   datas[i] = itemData;  
 }

step 7 ,我们需要一个数据到Item的适配层, ListView 组件类中提供了一个基类AbsAdapter ,我们实现它。

只需要继承此类,重写updateView()函数,对相应索引的itemComponent进行数据设置即可:

class ScoreRankListAdapter extends AbsAdapter {

​    updateView(item:Node, posIndex: number) {
​        let comp = item.getComponent(ScoreRankItemComp);
​        if (comp) {
​            let data = this.getItem(posIndex);
​            comp.setData(this.getItem(posIndex));
​        }
​    }
}

step 8,数据显示和更新

@property(ListView)
private scoreRankListView:ListView;
    
private _scoreRankListAdapter: ScoreRankListAdapter | null = null;
get scoreRankListAdapter(): ScoreRankListAdapter {
 if (!this._scoreRankListAdapter) {
     this._scoreRankListAdapter = new ScoreRankListAdapter();
  }
  return this._scoreRankListAdapter;
}    
    

this.scoreRankListAdapter.setDataSet(args);
this.scoreRankListView.setAdapter(this.scoreRankListAdapter);
  
    

step 9、ScoreRankItem.ts 源码

import { _decorator,Component,Label, Sprite} from "cc";
const { ccclass, property } = _decorator;


@ccclass
export  class ScoreRankItem extends Component {
   
   
    @property(Label)
    private labelLevel!:Label;

   
    @property(Sprite)
    private spriteAvatr!:Sprite;

       
    @property(Label)
    private lableNickName!:Label;
       
    @property(Label)
    private labelVip!:Label;
       
    @property(Label)
    private labelScore!:Label;


    @property(Sprite)
    private spriteLevel1!:Sprite;
    
    @property(Sprite)
    private spriteLevel2!:Sprite;

    @property(Sprite)
    private spriteLevel3!:Sprite;


    public setData(data: any) {
        const itemData = data as RankItemData;
        this.lableNickName.string = itemData.nickName;
        this.labelVip.string = "VIP " + String(itemData.vipLevel);
        this.labelScore.string =  String(itemData.score);
        ...
    }
}


step 10、ListView.ts 源码

import { _decorator,Component,Prefab,NodePool,ScrollView,Node,instantiate,UITransform, Vec3,sys} from "cc";
const { ccclass, property } = _decorator;


@ccclass
export class ListView extends Component {


    @property(Prefab)
    protected itemTemplate: Prefab = null;

    /**
     * 滚动视图
     */
    @property(ScrollView)
    protected scrollView:ScrollView = null;

    /**
     * 用来约定item 之间的间距
     */
    
    @property
    protected spacing: number = 1;

    /**
     * 用来约定超过可见区域的额外显示项数,可以调整滚动时的平滑性.
     * 比可见元素多缓存3个, 缓存越多,快速滑动越流畅,但同时初始化越慢.
     */
    @property
    protected spawnCount: number = 2;


    /**
     * 设置ScrollView组件的滚动方向,即可自动适配 竖向/横向滚动.
     */
    protected horizontal: boolean = false;

    protected content: Node = null;

    protected adapter: AbsAdapter = null;

    protected readonly _items: NodePool = new NodePool();
    // 记录当前填充在树上的索引. 用来快速查找哪些位置缺少item了.
    protected readonly _filledIds: { [key: number]: number } = {};


    // 初始时即计算item的高度.因为布局时要用到.
    protected _itemHeight: number = 1;

    protected _itemWidth: number = 1;

    protected _itemsVisible: number = 1;

    protected lastStartIndex: number = -1;

    protected scrollTopNotifyed: boolean = false;
    protected scrollBottomNotifyed: boolean = false;

    protected pullDownCallback: () => void = null;
    protected pullUpCallback: () => void = null;

    private initialize:boolean = false;

  
 
    public onLoad() {
        this.init()
    }

    public start(): void {  

    }

    public init() {

        if(!this.initialize) {
            this.initView();
            this.addEvent();
            this.initialize = true;
        }
    }

    private initView(){

        if (this.scrollView) {
            this.content = this.scrollView.content;
            this.horizontal = this.scrollView.horizontal;
            const parentTransform = this.content.getParent().getComponent(UITransform);

            if (this.horizontal) {
                this.scrollView.vertical = false

                this.content.getComponent(UITransform).anchorX = 0;
                this.content.getComponent(UITransform).anchorY = parentTransform.anchorY;
                this.content.position = new Vec3(0-parentTransform.width *parentTransform.anchorX,0,0); 

            } else {

                this.scrollView.vertical = true;
                this.content.getComponent(UITransform).anchorX = parentTransform.anchorX;
                this.content.getComponent(UITransform).anchorY = 1;
                this.content.position = new Vec3(0, parentTransform.height * parentTransform.anchorY,0); 
            }
        } 
        
        let itemOne = this._items.get() || instantiate(this.itemTemplate);
        this._items.put(itemOne);
        this._itemHeight = itemOne.getComponent(UITransform).height || 10;
        this._itemWidth = itemOne.getComponent(UITransform).width || 10;
        if (this.horizontal) {
            this._itemsVisible = Math.ceil(this.content.getParent().getComponent(UITransform).width / this._itemWidth);
        } else {
            this._itemsVisible = Math.ceil(this.content.getParent().getComponent(UITransform).height / this._itemHeight);
        }
    }


    public async setAdapter(adapter: AbsAdapter) {

        if (this.adapter === adapter) {
            this.notifyUpdate();
            return;
        }

        this.adapter = adapter;
        if (this.adapter == null) {
            console.error("adapter 为空.")
            return
        }
        if (this.itemTemplate == null) {
            console.error("Listview 未设置待显示的Item模板.");
            return;
        }



        this.notifyUpdate();
    }

 

    public getItemIndex(height: number): number {
        return Math.floor(Math.abs(height / ((this._itemHeight + this.spacing))));
    }

    public getPositionInView(item:Node) {
        let worldPos = item.getParent().getComponent(UITransform).convertToWorldSpaceAR(item.position);
        let viewPos = this.scrollView.node.getComponent(UITransform).convertToNodeSpaceAR(worldPos);
        return viewPos;
    }

    // 数据变更了需要进行更新UI显示, 可只更新某一条.
    public notifyUpdate(updateIndex?: number[]) {

        if (this.adapter == null) {
            console.log("notifyUpdate","this.adapter is null");
            return;
        }

        if(this.content ==null){  
            console.log("notifyUpdate","this.content is null");
            return;
        }
        
        if (updateIndex && updateIndex.length > 0) {
            updateIndex.forEach(i => {
                if (this._filledIds.hasOwnProperty(i)) {
                    delete this._filledIds[i];
                }
            })
        } else {
            Object.keys(this._filledIds).forEach(key => {
                delete this._filledIds[key];
            })
        }
        this.recycleAll();
        this.lastStartIndex = -1;
        if (this.horizontal) {
            this.content.getComponent(UITransform).width = this.adapter.getCount() * (this._itemWidth + this.spacing) + this.spacing;
        } else {
            this.content.getComponent(UITransform).height = this.adapter.getCount() * (this._itemHeight + this.spacing) + this.spacing; // get total content height
        }
        this.scrollView.scrollToTop()
    }

    public scrollToTop(anim: boolean = false) {
        this.scrollView.scrollToTop(anim ? 1 : 0);
    }

    public scrollToBottom(anim: boolean = false) {
        this.scrollView.scrollToBottom(anim ? 1 : 0);
    }

    public scrollToLeft(anim: boolean = false) {
        this.scrollView.scrollToLeft(anim ? 1 : 0);
    }

    public scrollToRight(anim: boolean = false) {
        this.scrollView.scrollToRight(anim ? 1 : 0);
    }

    // 下拉事件.
    public pullDown(callback: () => void, this$: any) {
        this.pullDownCallback = callback.bind(this$);
    }

    // 上拉事件.
    public pullUp(callback: () => void, this$: any) {
        this.pullUpCallback = callback.bind(this$);
    }

    protected update(dt) {
        const startIndex = this.checkNeedUpdate();
        if (startIndex >= 0) {
            this.updateView(startIndex);
        }
    }

    // 向某位置添加一个item.
    protected _layoutVertical(child: Node, posIndex: number) {
        this.content.addChild(child);
        // 增加一个tag 属性用来存储child的位置索引.
        child["_tag"] = posIndex;
        this._filledIds[posIndex] = posIndex;
        child.setPosition(0, -child.getComponent(UITransform).height * (0.5 + posIndex) - this.spacing * (posIndex + 1));
    }

    // 向某位置添加一个item.
    protected _layoutHorizontal(child: Node, posIndex: number) {
        this.content.addChild(child);
        // 增加一个tag 属性用来存储child的位置索引.
        child["_tag"] = posIndex;
        this._filledIds[posIndex] = posIndex;
        child.setPosition(child.getComponent(UITransform).width * (child.getComponent(UITransform).anchorX + posIndex) + this.spacing * posIndex, 0);
    }

    // 获取可回收item
    protected getRecycleItems(beginIndex: number, endIndex: number): Node[] {
        const children = this.content.children;
        const recycles = []
        children.forEach(item => {
            if (item["_tag"] < beginIndex || item["_tag"] > endIndex) {
                recycles.push(item);
                delete this._filledIds[item["_tag"]];
            }
        })
        return recycles;
    }

    protected recycleAll() {

        const children = this.content.children;
        if(children==undefined || children==null) {
            return;
        }
        
        this.content.removeAllChildren();
        children.forEach(item => {
            this._items.put(item);
        })
    }

    // 填充View.
    protected updateView(startIndex) {
        let itemStartIndex = startIndex;
        // 比实际元素多3个.
        let itemEndIndex = itemStartIndex + this._itemsVisible + (this.spawnCount || 2);
        const totalCount = this.adapter.getCount();
        if (itemStartIndex >= totalCount) {
            return;
        }

        if (itemEndIndex > totalCount) {
            itemEndIndex = totalCount;
            if (itemStartIndex > 0 && (!this.scrollBottomNotifyed)) {
                this.notifyScrollToBottom()
                this.scrollBottomNotifyed = true;
            }
        } else {
            this.scrollBottomNotifyed = false;
        }

        // 回收需要回收的元素位置.向上少收一个.向下少收2个.
        const recyles = this.getRecycleItems(itemStartIndex - (this.spawnCount || 2), itemEndIndex);
        recyles.forEach(item => {
            this._items.put(item);
        })

        // 查找需要更新的元素位置.
        const updates = this.findUpdateIndex(itemStartIndex, itemEndIndex)

        // 更新位置.
        for (let index of updates) {
            let child = this.adapter._getView(this._items.get() || instantiate(this.itemTemplate), index);
            this.horizontal ?
                this._layoutHorizontal(child, index) :
                this._layoutVertical(child, index);
        }
    }

    // 检测是否需要更新UI.
    protected checkNeedUpdate(): number {
        if (this.adapter == null) {
            return -1;
        }

        let scroll = this.horizontal ?
         (-this.content.position.x - this.content.getParent().getComponent(UITransform).width * this.content.getParent().getComponent(UITransform).anchorX)
            : (this.content.position.y - this.content.getParent().getComponent(UITransform).height * this.content.getParent().getComponent(UITransform).anchorY);
        

        let itemStartIndex = Math.floor(scroll / ((this.horizontal ? this._itemWidth : this._itemHeight) + this.spacing));
        if (itemStartIndex < 0 && !this.scrollTopNotifyed) {
            this.notifyScrollToTop();
            this.scrollTopNotifyed = true;
            return itemStartIndex;
        }
        // 防止重复触发topNotify.仅当首item不可见后才能再次触发
        if (itemStartIndex > 0) {
            this.scrollTopNotifyed = false;
        }

        if (this.lastStartIndex != itemStartIndex) {
            this.lastStartIndex = itemStartIndex;
            return itemStartIndex;
        }

        return -1;
    }

    // 查找需要补充的元素索引.
    protected findUpdateIndex(itemStartIndex: number, itemEndIndex: number): number[] {
        const d = [];
        for (let i = itemStartIndex; i < itemEndIndex; i++) {
            if (this._filledIds.hasOwnProperty(i)) {
                continue;
            }
            d.push(i);
        }
        return d;
    }

    protected notifyScrollToTop() {
        if (!this.adapter || this.adapter.getCount() <= 0) {
            return;
        }
        if (this.pullDownCallback) {
            this.pullDownCallback();
        }
    }

    protected notifyScrollToBottom() {
        if (!this.adapter || this.adapter.getCount() <= 0) {
            return;
        }
        if (this.pullUpCallback) {
            this.pullUpCallback();
        }
    }

    protected addEvent() {
        this.content.on(this.isMobile() ? Node.EventType.TOUCH_END : Node.EventType.MOUSE_UP, () => {
            this.scrollTopNotifyed = false;
            this.scrollBottomNotifyed = false;
        }, this)
        this.content.on(this.isMobile() ? Node.EventType.TOUCH_CANCEL : Node.EventType.MOUSE_LEAVE, () => {
            this.scrollTopNotifyed = false;
            this.scrollBottomNotifyed = false;
        }, this);
    }

    protected isMobile(): boolean {
        return (sys.isMobile)
    }
}

// 数据绑定的辅助适配器
export abstract class AbsAdapter {

    private dataSet: any[] = [];

    public setDataSet(data: any[]) {
        this.dataSet = data;
    }

    public getCount(): number {
        return this.dataSet.length;
    }

    public getItem(posIndex: number): any {
        return this.dataSet[posIndex];
    }

    public _getView(item: Node, posIndex: number): Node {
        this.updateView(item, posIndex);
        return item;
    }

    public abstract updateView(item: Node, posIndex: number);
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1064915.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

[python 刷题] 4 Median of Two Sorted Arrays

[python 刷题] 4 Median of Two Sorted Arrays 题目&#xff1a; Given two sorted arrays nums1 and nums2 of size m and n respectively, return the median of the two sorted arrays. The overall run time complexity should be O ( l o g ( m n ) ) O(log (mn)) O(lo…

lv8 嵌入式开发-网络编程开发 14

目录 1 I/O基本概念 1.1 IO概念 1.2 同步和异步 1.3 阻塞和非阻塞 2 五种I/O模型 2.1 阻塞IO 2.2 非阻塞I/O 2.3 多路复用I/O ​编辑 2.4 信号驱动式I/O ​编辑 2.5 异步I/O模型​编辑 3 五种I/O模型比较 4 练习 1 I/O基本概念 1.1 IO概念 I/O即数据的读取&#x…

功能定义-前方交通穿行提示制动

功能概述 前方交通穿行提示(Front Cross Traffic Alert)&#xff0c;简称FCTA&#xff0c;其功能表现为在车辆低速前进时&#xff0c;实时监测车辆前部横向接近的其他道路使用者&#xff0c;并在可能发生碰撞风险时发出警告信息 前方交通穿行制动(Front Cross Traffic Braking…

算法题:分发饼干

这个题目是贪心算法的基础练习题&#xff0c;解决思路是排序双指针谈心法&#xff0c;先将两个数组分别排序&#xff0c;优先满足最小胃口的孩子。&#xff08;本题完整题目附在了最后面&#xff09; 代码如下&#xff1a; class Solution(object):def findContentChildren(se…

【Unity ShaderGraph】| 快速制作一个实用的 模型溶解效果

前言 【Unity ShaderGraph】| 快速制作一个实用的 模型溶解效果一、效果展示二、简易溶解效果三、进阶溶解效果四、应用实例 前言 本文将使用ShaderGraph制作一个模型溶解的效果&#xff0c;可以直接拿到项目中使用。对ShaderGraph还不了解的小伙伴可以参考这篇文章&#xff1…

【Docker内容大集合】Docker从认识到实践再到底层原理大汇总

前言 那么这里博主先安利一些干货满满的专栏了&#xff01; 首先是博主的高质量博客的汇总&#xff0c;这个专栏里面的博客&#xff0c;都是博主最最用心写的一部分&#xff0c;干货满满&#xff0c;希望对大家有帮助。 高质量博客汇总https://blog.csdn.net/yu_cblog/categ…

Android SurfaceFlinger导读(04)理解BufferQueue

该系列文章总纲链接&#xff1a;Android GUI系统之SurfaceFlinger 系列文章目录 说明&#xff1a; 关于导读&#xff1a;导读部分主要是方便初学者理解SurfaceFlinger代码中的机制&#xff0c;为后面分析代码打下一个更好的基础&#xff0c;这样就可以把更多的精力放在surfac…

使用opencv及FFmpeg编辑视频

使用opencv及FFmpeg编辑视频 1.融合两个视频2.为视频添加声音2.1 安装ffmpy Python包2.2 下载ffmpeg2.3 代码实现 3.效果参考文献 帮朋友做了一个小作业&#xff0c;具体实现分为几个过程&#xff1a; 将两个mp4格式视频融合到一起为新视频添加声音 1.融合两个视频 其中一个…

AI智能创作系统ChatGPT商业运营源码+AI绘画系统/支持GPT联网提问/支持Midjourney绘画+支持国内AI提问模型+Prompt应用

一、AI创作系统 SparkAi创作系统是基于OpenAI很火的ChatGPT进行开发的Ai智能问答系统&#xff0c;支持国内AI提问模型。本期针对源码系统整体测试下来非常完美&#xff0c;可以说SparkAi是目前国内一款的ChatGPT对接OpenAI软件系统。那么如何搭建部署AI创作ChatGPT&#xff1f…

VSCode Intellij IDEA CE 数据库连接

VSCode & Intellij IDEA CE 数据库连接 大概记一下现在正在用的几个工具/插件 VSCode VSCode 里面的工具我下载了很多&#xff0c;如果只是链接 MySQL 的话&#xff0c;可能用 Jun Han 这位大佬的 MySQL 就好了&#xff1a; 使用这个插件直接打开 .sql 文件单击运行就能…

VMware16.1.2安装及密钥

文章目录 一、VMware 16 虚拟机下载二、安装步骤三、VMware 各版本注册密钥1 、VMware 16密钥2 、VMware 14密钥3 、VMware 15密钥4 、VMware 17密钥 一、VMware 16 虚拟机下载 VMware 16 下载地址&#xff1a; https://www.vmware.com/cn/products/workstation-pro/workstati…

数据结构与算法设计分析——贪心算法的应用

目录 一、贪心算法的定义二、贪心算法的基本步骤三、贪心算法的性质&#xff08;一&#xff09;最优子结构性质&#xff08;二&#xff09;贪心选择性质 四、贪心算法的应用&#xff08;一&#xff09;哈夫曼树——哈夫曼编码&#xff08;二&#xff09;图的应用——求最小生成…

《Linux 内核设计与实现》13. 虚拟文件系统

通用文件接口 VFS 使得可以直接使用 open()、read()、write() 这样的系统调用而无需考虑具体文件系统和实际物理介质。 好处&#xff1a;新的文件系统和新类型的存储介质需要挂载时&#xff0c;程序无需重写&#xff0c;甚至无需重新编译。 VFS 将各种不同的文件系统抽象后采…

[强网杯 2022]factor有感

可直接私信&#xff0b;Q 3431550587 此题记录主要是他运用了几个新看见的攻击思路和拜读了一篇论文&#xff0c;所以写写。题目源码&#xff1a; #encoding:utf-8 from Crypto.Util.number import * from gmpy2 import * from random import randint from flag import flagd…

云服务仿真:完全模拟 AWS 服务的本地体验 | 开源日报 No.45

localstack/localstack Stars: 48.7k License: NOASSERTION LocalStack 是一个云服务仿真器&#xff0c;可以在您的笔记本电脑或 CI 环境中以单个容器运行。它提供了一个易于使用的测试/模拟框架&#xff0c;用于开发云应用程序。主要功能包括&#xff1a; 在本地机器上完全…

CTF之CTF(夺旗赛)介绍

什么是CTF&#xff1f; CTF&#xff08;Capture The Flag&#xff0c;中文一般译作“夺旗赛”&#xff09;在网络安全领域中指的是网络安全技术人员之间进行技术竞技的一种比赛形式。CTF起源于1996年DEFCON全球黑客大会&#xff0c;用以代替之前黑客们通过互相发起真实攻击进行…

最新AI智能创作系统源码SparkAi系统V2.6.3/AI绘画系统/支持GPT联网提问/支持Prompt应用/支持国内AI模型

一、智能AI创作系统 SparkAi创作系统是基于OpenAI很火的ChatGPT进行开发的Ai智能问答系统&#xff0c;已支持OpenAIGPT全模型国内AI全模型&#xff0c;已支持国内AI模型 百度文心一言、微软Azure、阿里云通义千问模型、清华智谱AIChatGLM、科大讯飞星火大模型等。本期针对源码…

Javascript - 轮播图

轮播图也称banner图、广告图、焦点图、滑片。是指在一个模块或者窗口,通过鼠标点击或手指滑动后,可以看到多张图片。这些图片统称为轮播图,这个模块叫做轮播模块。可以通过运用 javascript去实现定时自动转换图片。以下通过一个小Demo演示如何运用Javascript实现。 <!DOCTYP…

d3dcompiler_47.dll是什么文件?游戏确实d3dcompiler_47.dll的常用解决方法

d3dcompiler_47.dll 是一个动态链接库&#xff08;DLL&#xff09;文件&#xff0c;属于 Microsoft DirectX 软件组件的一部分。它主要负责处理 DirectX 中的图形和多媒体内容&#xff0c;以确保游戏和应用程序能够正常运行。d3dcompiler_47.dll 的主要功能是将 DirectX API 转…

基于SpringBoot的在线宠物用品交易网站

目录 前言 一、技术栈 二、系统功能介绍 用户信息管理 商品分类管理 品牌信息管理 商品信息管理 商品信息 我的收藏 我的订单 三、核心代码 1、登录模块 2、文件上传模块 3、代码封装 前言 随着信息技术在管理上越来越深入而广泛的应用&#xff0c;管理信息系统的实…