2024年12月

大家好,我是汤师爷~

新零售业务涉及多个销售渠道,每个渠道都有其独特的业务特点,需要相应的营销方式、运营策略和供应链管理。

主要销售渠道包括:实体门店(包括直营连锁店、加盟门店)、电商平台销售(如淘宝、天猫、京东、拼多多等)、新兴流量平台(如抖音、小红书、快手等短视频平台)、本地生活平台(如美团、饿了么)。

尽管新零售的销售渠道多样化,但其核心理念始终是以消费者为中心,致力于提供便利、优质且无缝的购物体验。

全渠道交易模式

线上与线下的无缝连接是全渠道交易的核心。消费者既可以在线上下单后,到门店取货或选择配送到家,也可以直接到店体验和消费,享受灵活的购物体验。

这种模式充分发挥了各渠道的独特优势。线上渠道提供便捷的购物体验,线下渠道则增强消费者的信任感,两者相辅相成,满足了消费者的多样化需求。

当消费者在销售渠道完成交易后,商品可能分布在不同的仓储点或门店中。订单履约系统会根据预约时间和收货地址等信息,进行智能路由,优化配送路径,以最快速度、最低成本完成配送,从而提升消费体验。

全渠道交易模式通过将多渠道与不同服务能力进行深度融合,既提升了企业的运营效率,又为消费者带来了更加丰富的购物体验。

线上线下交易流程

线上交易流程涵盖了两个核心业务场景:一是电商购物流程,让消费者能够随时随地在线选购商品,并通过快递完成物流配送;二是O2O购物流程,将线上订购与线下门店服务结合。

线下交易流程则主要以门店收银流程为核心,这是实体门店运营中最基础且最关键的交易环节,涉及商品扫码、价格计算、支付处理等一系列操作。

电商购物流程

电商购物流程是一个完整的线上交易过程,包括消费者进店浏览、将商品加入购物车、确认订单并结算、提交订单、等待商家发货、物流配送,最后确认收货完成交易。整个过程全部在线上完成,为消费者提供便捷的购物体验。电商购物流程主要流程环节包括:

  • 消费者进店:用户通过各种入口(如小程序、分享链接、广告等)访问电商店铺,浏览商品列表和详情页面,了解商品信息和价格。
  • 加入购物车:用户在浏览过程中,可以将心仪的商品添加到购物车中暂存,方便后续统一结算,也可以随时调整商品数量或删除不需要的商品。
  • 确认订单并结算:用户在购物车中选择要购买的商品,确认收货地址、选择配送方式、使用优惠券或积分等,系统会自动计算订单总金额。
  • 提交订单:确认订单信息无误后,用户选择支付方式(如支付宝、微信支付、银行卡等)完成支付,系统生成正式订单。
  • 等待发货:仓库收到订单后进行订单处理,包括拣货、商品打包、出库等准备工作。
  • 物流配送:仓库将商品交付给物流公司,物流公司按照收货地址进行配送,用户可以实时查看物流信息和包裹位置。
  • 确认收货:用户收到商品后,检查商品完好无损,确认商品符合预期后,在系统中确认收货,完成交易。

O2O购物流程

O2O(Online to Offline)购物流程是一种将线上订购与线下服务相结合的购物模式。消费者通过手机应用或小程序在线下单,然后可以选择到实体店自提或由商家提供即时配送服务。这种模式特别适用于餐饮、生鲜等即时性需求强、配送半径有限的本地化商品和服务。

为什么门店需要拓展线上渠道?

传统的实体门店服务范围有限,只能吸引周边500米以内的消费者。因此,如何拓展服务范围,吸引更多的消费者到店,成为了店家迫切需要解决的问题。

缺乏忠实顾客,客户基础不稳,往往是一次性购物,门店无法形成有效的顾客回流。在当前的市场环境下,构建并维护粉丝群体,成为了商家的核心竞争力。

运营成本不断增长,包括租金和人工成本的上涨,但是广告投放、宣传又成本高昂,且难以追踪效果,达不到预期目标。如何有效吸引新客和提升销售业绩,变得至关重要。

电商不断挤压生存空间,随着网购成为人们的一种生活习惯,由于其方便和价格优势,再加上退换货几乎不产生成本,电商对于实体门店构成了巨大的竞争压力。

O2O购物流程主要流程环节包括:

  • 消费者基于LBS进店:用户通过手机应用或小程序,根据当前位置搜索附近的门店,查看门店商品、营业时间和评价等信息,选择合适的门店进行购物。
  • 加入购物车:浏览门店商品列表,将需要的商品添加到购物车中,可以随时调整商品数量或删除不需要的商品。
  • 选择履约方式并结算:确认购物车商品后,用户可以选择到店自提或即时配送服务,系统会根据所选方式计算相应的配送费用,同时可以使用优惠券或会员折扣。
  • 提交订单:确认订单信息和配送方式后,用户选择支付方式完成支付,系统生成正式订单。
  • 派单到门店:系统将订单信息推送给相应的门店,门店收到订单通知并确认接单。
  • 门店备货:门店员工根据订单信息准备商品,确保商品质量和数量符合要求。
  • 自提或骑手配送:如果是自提订单,用户到店出示取货码领取商品;如果是配送订单,门店将商品交给配送骑手进行配送。
  • 确认收货:用户收到商品后,检查商品状态,确认无误后在系统中完成收货,交易完成。

电商与O2O流程差异

电商购物流程主要服务于远程配送的商品交易,而O2O购物流程则针对需要到店自提或即时配送的场景。

我们以瑞幸咖啡为例,下图为瑞幸小程序首页,有到店取、幸运送、电商购的购物入口,其中到店取、幸运送为O2O购物模式,电商购为电商购物模式。

两种业务模式的主要差异如下:

对比维度 电商购物模式 O2O购物模式
消费场所 完全在线上进行,从进店、选择商品、下单、支付到收货,消费者在线上即可完成购物全过程。 结合了线上和线下的消费场景。消费者可能在线上选购商品或服务,但可能在实体店进行自提或体验服务。
服务范围 通常覆盖全国地区,不太受地理位置的限制。 服务范围受限于实体店的位置,更侧重于本地化服务。
物流配送 赖于第三方物流或自建物流进行商品配送,消费者通常在家中等待收取快递。 消费者可以到店自提商品,或者通过骑手配送商品。
售后服务 售后服务主要通过线上进行沟通和处理,包括退货、换货、维修等。 后服务可以在线上进行,也可以提供线下服务点,让消费者有更多选择。

线下购物流程

线下收银流程是实体门店中完成商品交易的标准操作流程。它包括顾客选购商品、收银员扫码或手动录入商品信息、计算金额、选择支付方式(如现金、银行卡或移动支付)、完成支付交易、打印小票等环节。线下收银流程主要流程环节包括:

  • 消费者选品:客户在店内挑选商品并带到收银台。
  • 录入商品:当客户选好商品后,店员会在POS系统里输入商品信息,通常是扫描条码或手动输入商品代码。
  • 选择支付方式:店员询问客户希望使用的支付方式。POS系统支持多种支付方式,包括现金、信用卡、借记卡、移动支付(如微信支付、支付宝)等。
  • 消费者支付:根据客户所选的支付方式进行支付。银行卡使用POS机刷卡支付;移动支付时,读取用户手机付款码,进行支付;对于现金交易,店员收钱并找零。
  • 打印小票:交易完成后,POS系统会自动打印购物小票。小票详细列出了购买的商品、数量、价格、支付方式以及交易时间等信息,可能包含保修信息或退换货政策。
  • 售后:包括退换货处理、顾客咨询和解决可能出现的任何问题。顾客如果对购买的商品不满意,可以凭借小票到店内进行退换。

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

python拉取grafana监控图形

python通过grafana提供的api接口拉取grafana监控图形并保存至word文档生成日报发送邮件

前置条件:

1.grafana平台需要安装grafana-image-renderer 插件,用于生成静态图形

页面可以检查是否已安装

未安装会进入如下页面:

从API接口拉取图片会提示:

安装方式参考:

https://grafana.com/grafana/plugins/grafana-image-renderer/

该插件对内存大小有一定要求:

如果已安装该插件,点击Direct link rendered image后会显示一个静态监控图形页面

参考:
https://cloud.tencent.com/document/product/1437/65674

2.在grafana页面生成api_keys,用于接口请求认证

不同版本获取方式不同 此示例为V9.2.6版,入口如下:

点击Add API key添加,可以指定有效期,弹出密钥的界面记得把密钥复制下来,否则关闭后就看不到了。

代码示例:

  • 该示例是将Grafana获取的多张图形保存到本地的panels目录下(多线程执行),
  • 通过python的Image模块进行图片拼接(拼接方式可自定义),
  • 读取日志模版文档(模版中预留了
    等字符用于定位),
  • 将拼好的图片插入到日志模版文档并替换文档中的日期信息,
  • 保存生成新的文档,删除本地panels目录下的图片缓存,并发送邮件。
# -*- coding: UTF-8 -*-
import smtplib
import requests
import os
import shutil
import time
from PIL import Image
from docx import Document
from docx.shared import Inches
from datetime import datetime, timedelta
from concurrent.futures import ThreadPoolExecutor, as_completed
from email.header import Header
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.header import make_header


# 配置 Grafana API 参数
GRAFANA_HOST = "http://xxxxxxxxxxx"
# Grafana API 密钥具有时效性,如过期,请联系运维人员生成新的api_keys
API_TOKEN = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
# 仪表板ID
DASHBOARD_UID = "xxxxxxxxxx"
# 图形ID
PANEL_ID_DATA = {"PANEL_1":[30,31,2,10,12,14,16,18,20,22,24,26,28],
                 "PANEL_2":[37, 39, 41, 43, 45, 47, 49, 51, 53],}
ORG_ID = "1"
#日志模版文件
document_path = "xxxxxxxx日志 - 模板.docx"
# 邮箱接收人
RECEIVERS = ["xxxxxx.com", "xxx.com"]
# 保存图片的文件名
folder = "panels"

#邮件发送
def send_mail(filename):
    sender = 'xxxxxxx.com'
    receiver = ','.join(RECEIVERS)
    smtpserver = 'xxxxxx.com'
    user = 'xxxxxx.com'
    password = 'xxxxxxxx'
    mail_title = filename.split('/')[1]
    mail_title = mail_title.split('.')[0]
    # 创建一个带附件的实例
    message = MIMEMultipart()
    message['From'] = sender
    message['To'] = receiver
    message['Subject'] = Header(mail_title, 'utf-8')

    # 邮件正文内容
    message.attach(MIMEText(f'hello,附件为 {mail_title},请查收,如有问题请与我联系。', 'plain', 'utf-8'))
    # 构造附件
    file_msg = MIMEText(open(filename, 'rb').read(), 'base64', 'UTF-8')
    file_msg["Content-Type"] = 'application/octet-stream;name="%s"' % make_header([(filename, 'UTF-8')]).encode('UTF-8')
    file_msg["Content-Disposition"] = 'attachment;filename= "%s"' % make_header([(filename, 'UTF-8')]).encode('UTF-8')
    message.attach(file_msg)
    try:
       
        smtpObj = smtplib.SMTP(smtpserver)
        smtpObj.starttls()
        smtpObj.login(user, password)
        smtpObj.sendmail(sender, receiver, message.as_string())
        print("邮件发送成功!!!")
    except Exception as e:
        print(e)
        print("邮件发送失败!!!")
    finally:
        smtpObj.quit()




def center_insert_img(doc, img,date_str):
    """插入图片"""
    date_obj = datetime.strptime(date_str, '%Y-%m-%d')
    # 将datetime对象格式化为指定的中文格式字符串
    formatted_date = date_obj.strftime('%Y年%m月%d日')
    for paragraph in doc.paragraphs:
        # 根据文档中的占位符定位图片插入的位置
        if '<datetime>' in paragraph.text:
            paragraph.text = paragraph.text.replace('<datetime>', formatted_date)
        elif '<img1>' in paragraph.text:
            # 把占位符去掉
            paragraph.text = paragraph.text.replace('<img1>', '')
            # 添加一个文字块
            run = paragraph.add_run('')
            # 添加一个’回车换行效果‘
            run.add_break()
            # 添加图片并指定大小
            run.add_picture(img[0], width=Inches(6.2))
        elif '<img2>' in paragraph.text:
            # 把占位符去掉
            paragraph.text = paragraph.text.replace('<img2>', '')
            # 添加一个文字块
            run = paragraph.add_run('')
            # 添加一个’回车换行效果‘
            run.add_break()
            # 添加图片并指定大小
            run.add_picture(img[1], width=Inches(6.2))

def save_img_to_doc(img,day_time):
    """把图片保存到doc文件中的指定位置"""
    tpl_doc = document_path
    current_year = datetime.now().year
    if not os.path.exists(f"{current_year}_doc"):
        os.makedirs(f"{current_year}_doc")
    res_doc = f'{current_year}_doc/日志报告_{day_time}.docx'
    # 打开模板文件
    document = Document(tpl_doc)
    # 插入图片居中
    center_insert_img(document, img,day_time)
    # 保存结果文件
    document.save(res_doc)
    return res_doc

def download_image(render_url, output_path, headers):
    """下载单张图片的函数"""
    try:
        response = requests.get(render_url, headers=headers, stream=True)
        if response.status_code == 200 and "image/png" in response.headers.get("Content-Type", ""):
            with open(output_path, "wb") as f:
                for chunk in response.iter_content(chunk_size=8192):
                    f.write(chunk)
            return output_path
        else:
            print(f"获取图形失败: {render_url}")
            print(f"状态码: {response.status_code}, 返回内容: {response.text}")
            return None
    except Exception as e:
        print(f"下载图片出错: {e}")
        return None


def merge_images(image_paths, output_path, layout):
    """合并图片"""

    images = [Image.open(path) for path in image_paths]
    width, height = images[0].size
    new_image = object
    if layout == "grid_3xN":
        num_rows = (len(images) + 2) // 3
        new_image = Image.new('RGB', (3 * width, num_rows * height))
        for idx, img in enumerate(images):
            x, y = (idx % 3) * width, (idx // 3) * height
            new_image.paste(img, (x, y))
    elif layout == "grid_3x3":
        new_image = Image.new('RGB', (3 * width, 3 * height))
        for idx, img in enumerate(images[:9]):
            x, y = (idx % 3) * width, (idx // 3) * height
            new_image.paste(img, (x, y))

    new_image.save(output_path)
    new_image.close()


def download_grafana_panel():
    # start = time.time()
    time_tuple = generate_daily_timestamps_and_dates()
    image_paths = []
    save_img_to_doc_paths = []
    headers = {"Authorization": f"Bearer {API_TOKEN}"}
    tasks = []

    # 创建下载目录
    daily_folder = f"{folder}/{time_tuple[0]}"
    os.makedirs(daily_folder, exist_ok=True)

    # 使用线程池并行下载图片 
    # 此处的的URL仅供参考,I6xasdas要替换为你页面的实际值
    # 也就是你点击Direct link rendered image后显示的静态监控图形页面的URL
    with ThreadPoolExecutor(max_workers=5) as executor:
        for PANEL_NAME, PANEL_ID_list in PANEL_ID_DATA.items():
            for PANEL_ID in PANEL_ID_list:
                render_url = f'{GRAFANA_HOST}/render/d-solo/I6xasdas/{DASHBOARD_UID}?orgId=1&from={time_tuple[1]}&to={time_tuple[2]}&panelId={PANEL_ID}&width=1000&height=500&tz=Asia%2FShanghai'
                output_path = f"{daily_folder}/{DASHBOARD_UID}_panel_{PANEL_ID}.png"
                tasks.append(executor.submit(download_image, render_url, output_path, headers))

        # 收集任务结果
        for future in as_completed(tasks):
            result = future.result()
            if result:
                image_paths.append(result)

    # 合并图片 (第一组 3xN)
    if len(image_paths) > 0:
        output_path1 = f"{daily_folder}/panel_01.jpg"
        merge_images(image_paths[:13], output_path1, layout="grid_3xN")
        save_img_to_doc_paths.append(output_path1)

    # 合并图片 (第二组 3x3)
    if len(image_paths) > 13:
        output_path2 = f"{daily_folder}/panel_02.jpg"
        merge_images(image_paths[12:21], output_path2, layout="grid_3x3")
        save_img_to_doc_paths.append(output_path2)

    # 插入文档保存
    doc_file_path = save_img_to_doc(save_img_to_doc_paths, time_tuple[0])
    # 删除缓存图片
    remove_dir(f"{folder}/{time_tuple[0]}")
    #发送邮件
    send_mail(doc_file_path)
    # end_time = time.time() - start
    # print(f"巡检报告成功保存并删除图片缓存!耗时{round(end_time,2)}秒")



def generate_daily_timestamps_and_dates():
    current_date_time = datetime.now()
    # 提取日期部分并格式化为年-月-日的字符串形式
    formatted_date = current_date_time.strftime('%Y-%m-%d')
    # 将输入的日期字符串转换为datetime对象
    start_date = datetime.strptime(formatted_date, '%Y-%m-%d')
    last_day_start_date = start_date - timedelta(days=1)

    # 获取当天的起始时间(将时间设置为00:00:00)
    start_of_day = last_day_start_date.replace(hour=0, minute=0, second=0, microsecond=0)
    # 将datetime对象转换为时间戳(单位:秒)
    timestamp = int(start_of_day.timestamp()*1000)
    date_str = start_of_day.strftime('%Y-%m-%d')

    return (date_str, timestamp, timestamp + 86399*1000)


def remove_dir(directory):
    try:
        shutil.rmtree(directory)
    except OSError as e:
        print(f"删除目录时出错: {e}")
        if e.errno == 32:  # 文件正在被占用
            print("文件被占用,等待释放后重试...")
            time.sleep(0.1)  # 等待 0.1 秒后再次尝试
            try:
                shutil.rmtree(directory)
                print(f"第二次尝试删除成功: {directory}")
            except Exception as e2:
                print(f"仍然无法删除目录: {e2}")


if __name__ == "__main__":
    download_grafana_panel()

说明

该文章是属于OverallAuth2.0系列文章,每周更新一篇该系列文章(从0到1完成系统开发)。

该系统文章,我会尽量说的非常详细,做到不管新手、老手都能看懂。

说明:OverallAuth2.0 是一个简单、易懂、功能强大的权限+可视化流程管理系统。

友情提醒:本篇文章是属于系列文章,看该文章前,建议先看之前文章,可以更好理解项目结构。

qq群:801913255,进群有什么不懂的尽管问,群主都会耐心解答。

有兴趣的朋友,请关注我吧(*^▽^*)。

关注我,学不会你来打我

实现功能


1、系统登录后的可视化面板页面


原本不想写这篇文章,因为它和我们的系统权限、框架没有实质性的关系,但耐不住群友的软磨硬泡,便答应了下来。

我起初的设想,这块功能直接上传到码云上,群友可以根据自己搭建的系统,酌情修改。

安装echarts

npm install echarts --save

搭建面板页面

在panel文件夹,打开index页面,编写布局代码

<template>
  <div style="display: flex; height: 1080px">
    <div style="width: 80%">
      <div
style
="height: 25%; display: flex; margin-bottom: 5px" class="boxStyle" > <div style="width: 50%" class="boxStyle"> </div> <div
style
="width: 50%; margin-left: 5px;" class="boxStyle" > 2 </div> </div> <div style="height: 45% ;margin-bottom: 5px" class="boxStyle"> 3 </div> <div style="height: 28%" class="boxStyle"> 4 </div> </div> <div
style
="width: 20%; margin-left: 5px; display: flow-root" class="panelContent boxStyle" > <div
style
="width: 100%; height: 50%; margin-bottom: 5px" class="boxStyle" >特色功能</div> <div style="width: 100%; height: 50%" class="boxStyle">关于作者</div> </div> </div> </template> <script lang="ts" >import* as echarts from "echarts";
import { defineComponent }
from "vue";
export
defaultdefineComponent({
props: {
//openPageData: {//type: Object as PropType<buttonModel>,//required: true,//}, },

setup(props, context) {
return{};
},
components: {},
});
</script> <style scoped>.panelContent {
font
-size: 12px;
justify
-content: right;
align
-items: center;
}
.boxStyle {
border: 1px solid #
00152914;
box
-shadow: 0 1px 4px #00152914;
}
</style>

布局样式如下

填充盒子1的内容

在api文件夹下,创建一个文件夹panel,然后再该文件夹下创建一个echarts.ts的文件,编写如下代码

export const echartsOne ={
title: {
text:
"系统六芒星图",
},
color: [
'#67F9D8'],//backgroundColor: "#013954",//背景样式 radar: {//雷达图的指示器,表示多个变量的标签 indicator: [
{ name:
"好用", max: 5},
{ name:
"易懂", max: 5},
{ name:
"简单", max: 5},
{ name:
"通用", max: 5},
{ name:
"灵活", max: 5},
{ name:
"学习", max: 5},
],
splitArea: {
areaStyle: {
color: [
'#adbecf','#77EADF', '#26C3BE', '#64AFE9', '#428BD4','#2177cd'],
shadowColor:
'rgba(0, 0, 0, 0.2)',
shadowBlur:
10}
},
axisName: {
formatter:
'【{value}】',
color:
'#428BD4'},
},
series: [
{
type:
"radar",//雷达图的数据 data: [
{
value: [
5, 5, 5, 5, 5, 5],
},
],
},
],
};

该代码是echarts的雷达图代码。

回到页面,我们把盒子1里的内容替换成

<div id="echarts-one" style="width: 100%; height: 100%"></div>
然后再setup下添加如下代码
  onMounted(() =>{
GetEchartsOneData();
});
function GetEchartsOneData() {
var myChart = echarts.init(document.getElementById("echarts-one"));
myChart.setOption(echartsOne);
}

注意这里的onMounted钩子函数需要添加引用,echartsOne也需要导入。点击vue的提示即可。

完成以上代码,我们预览看下效果

照葫芦画瓢,我们开始编写盒子2、3、4的内容

在echarts.ts中添加

//南丁格尔玫瑰图
export const echartsTWO ={
title: {
text:
"模块访问占比",
},
toolbox: {
show:
true,
},
legend: {
bottom:
"10",
},
//backgroundColor: "#013954",//背景样式 series: [
{
name:
"Nightingale Chart",
type:
"pie",
radius: [
25, 80],
center: [
"50%", "50%"],
roseType:
"area",//itemStyle: {//borderRadius: 8,//}, data: [
{ value:
40, name: "菜单权限"},
{ value:
38, name: "角色权限"},
{ value:
32, name: "列权限"},
{ value:
30, name: "行权限"},
{ value:
28, name: "按钮权限"},
{ value:
18, name: "接口权限"},
{ value:
26, name: "流程"},
{ value:
22, name: "表单"},
],
},
],
};
//中国地图 export const chinaGeoCoordMap = ref<any>({
黑龙江: [
127.9688, 45.368],
内蒙古: [
110.3467, 41.4899],
吉林: [
125.8154, 44.2584],
北京市: [
116.4551, 40.2539],
辽宁: [
123.1238, 42.1216],
河北: [
114.4995, 38.1006],
天津: [
117.4219, 39.4189],
山西: [
112.3352, 37.9413],
陕西: [
109.1162, 34.2004],
甘肃: [
103.5901, 36.3043],
宁夏: [
106.3586, 38.1775],
青海: [
101.4038, 36.8207],
新疆: [
87.9236, 43.5883],
西藏: [
91.11, 29.97],
四川: [
103.9526, 30.7617],
重庆: [
108.384366, 30.439702],
山东: [
117.1582, 36.8701],
河南: [
113.4668, 34.6234],
江苏: [
118.8062, 31.9208],
安徽: [
117.29, 32.0581],
湖北: [
114.3896, 30.6628],
浙江: [
119.5313, 29.8773],
福建: [
119.4543, 25.9222],
江西: [
116.0046, 28.6633],
湖南: [
113.0823, 28.2568],
贵州: [
106.6992, 26.7682],
云南: [
102.9199, 25.4663],
广东: [
113.12244, 23.009505],
广西: [
108.479, 23.1152],
海南: [
110.3893, 19.8516],
上海: [
121.4648, 31.2891],
});
export
const chinaDatas =[
[
{
name:
"黑龙江",
value:
0,
},
],
[
{
name:
"内蒙古",
value:
0,
},
],
[
{
name:
"吉林",
value:
0,
},
],
[
{
name:
"辽宁",
value:
0,
},
],
[
{
name:
"河北",
value:
0,
},
],
[
{
name:
"天津",
value:
0,
},
],
[
{
name:
"山西",
value:
0,
},
],
[
{
name:
"陕西",
value:
0,
},
],
[
{
name:
"甘肃",
value:
0,
},
],
[
{
name:
"宁夏",
value:
0,
},
],
[
{
name:
"青海",
value:
0,
},
],
[
{
name:
"新疆",
value:
0,
},
],
[
{
name:
"西藏",
value:
0,
},
],
[
{
name:
"四川",
value:
0,
},
],
[
{
name:
"重庆",
value:
0,
},
],
[
{
name:
"山东",
value:
0,
},
],
[
{
name:
"河南",
value:
0,
},
],
[
{
name:
"江苏",
value:
0,
},
],
[
{
name:
"安徽",
value:
0,
},
],
[
{
name:
"湖北",
value:
0,
},
],
[
{
name:
"浙江",
value:
0,
},
],
[
{
name:
"福建",
value:
0,
},
],
[
{
name:
"江西",
value:
0,
},
],
[
{
name:
"湖南",
value:
0,
},
],
[
{
name:
"贵州",
value:
0,
},
],
[
{
name:
"广西",
value:
0,
},
],
[
{
name:
"海南",
value:
0,
},
],
[
{
name:
"上海",
value:
1,
},
],
];
var convertData = function (data: string |any[]) {var res =[];for (var i = 0; i < data.length; i++) {var dataItem =data[i];var fromCoord = chinaGeoCoordMap.value[dataItem[0].name];var toCoord = [103.9526, 30.7617];if (fromCoord &&toCoord) {
res.push([
{
coord: fromCoord,
value: dataItem[
0].value,
},
{
coord: toCoord,
},
]);
}
}
returnres;
};
export
constseries: {
type:
string;
zlevel: number;
coordinateSystem:
string;
effect: {
show: boolean;
period: number;
//箭头指向速度,值越小速度越快 trailLength: number; //特效尾迹长度[0,1]值越大,尾迹越长重 symbol: string; //箭头图标 symbolSize: number;
brushType:
string;
scale: number
};
rippleEffect:any;
label: {},
symbol:
string;
symbolSize: {},
itemStyle: {},
lineStyle: {
normal: {
width: number;
//尾迹线条宽度 opacity: number; //尾迹线条透明度 curveness: number; //尾迹线条曲直度 };
};
data: any
}[]
=[];
[[
"四川", chinaDatas asany]].forEach(function (item, i) {
series.push(
{
type:
"lines",
coordinateSystem:
"geo",
zlevel:
2,
rippleEffect:[],
effect: {
show:
true,
period:
4, //箭头指向速度,值越小速度越快 trailLength: 0.02, //特效尾迹长度[0,1]值越大,尾迹越长重 symbol: "arrow", //箭头图标 symbolSize: 5, //图标大小 brushType: "",
scale:
0},
label: [],
symbol:
"",
symbolSize: [],
itemStyle: [],
lineStyle: {
normal: {
width:
1, //尾迹线条宽度 opacity: 1, //尾迹线条透明度 curveness: 0.3, //尾迹线条曲直度 },
},
data: convertData(item[
1]),
},
{
type:
"effectScatter",
coordinateSystem:
"geo".toString(),
zlevel:
2,
effect:{}
asany,
rippleEffect: {
//涟漪特效 period: 4, //动画时间,值越小速度越快 brushType: "stroke", //波纹绘制方式 stroke, fill scale: 4,
show:
false,
trailLength:
0,
symbol:
"",
symbolSize:
0},
label: {
normal: {
show:
true,
position:
"right", //显示位置 offset: [5, 0], //偏移设置 formatter: function (params: { data: { name: any } }) {//圆环显示文字 return params.data.name;
},
fontSize:
13,
},
emphasis: {
show:
true,
},
},
symbol:
"circle",
symbolSize: function (val: number[]) {
return 5 + val[2] * 5; //圆环大小 },
itemStyle: {
normal: {
show:
false,
color:
"#f00",
},
},
lineStyle: {
normal: {
width:
1, //尾迹线条宽度 opacity: 1, //尾迹线条透明度 curveness: 0.3, //尾迹线条曲直度 },
},
data: item[
1].map(function (
dataItem: {
name: any;
value: any;
}[]
) {
return{
name: dataItem[
0].name,
value: chinaGeoCoordMap.value[dataItem[
0].name].concat([dataItem[0].value]),
};
}),
},
//被攻击点 {
type:
"scatter",
coordinateSystem:
"geo",
zlevel:
2,
rippleEffect:{}
asany,
effect: {
period:
4,
brushType:
"stroke",
scale:
4,
show:
false,
trailLength:
0,
symbol:
"",
symbolSize:
0},
label: {
normal: {
show:
true,
position:
"right",//offset:[5, 0], color: "#0f0",
formatter:
"{b}",
textStyle: {
color:
"#0f0",
},
},
emphasis: {
show:
true,
color:
"#f60",
},
},
symbol:
"pin",
symbolSize:
50,
itemStyle: [],
lineStyle:
'' asany,
data: [
{
name: item[
0],
value: chinaGeoCoordMap.value[item[
0].toString()].concat([10]),
},
],
}
);
});
export
const echartsThree ={
title: {
text:
"各省访问数量",
},
tooltip: {
trigger:
"item",
backgroundColor:
"rgba(166, 200, 76, 0.82)",
borderColor:
"#FFFFCC",
showDelay:
0,
hideDelay:
0,
enterable:
true,
transitionDuration:
0,
extraCssText:
"z-index:100",
formatter: function (
params: { name: any; value: { [x: string]: any }; seriesIndex: number },
ticket: any,
callback: any
) {
//根据业务自己拓展要显示的内容 var res = "";var name = params.name;var value = params.value[params.seriesIndex + 1];
res
= "<span style='color:#fff;'>" + name + "</span><br/>数据:" +value;returnres;
},
},
//backgroundColor: "#013954", visualMap: {//图例值控制 min: 0,
max:
1,
calculable:
true,
show:
true,
color: [
"#f44336", "#fc9700", "#ffde00", "#ffde00", "#00eaff"],
textStyle: {
color:
"#fff",
},
},
geo: {
map:
"china",
zoom:
1.2,
label: {
emphasis: {
show:
false,
},
},
roam:
false, //是否允许缩放 itemStyle: {
normal: {
color:
"rgba(51, 69, 89, .5)", //地图背景色 borderColor: "#516a89", //省市边界线00fcff 516a89 borderWidth: 1,
},
emphasis: {
color:
"rgba(37, 43, 61, .5)", //悬浮背景 },
},
},
series: series,
};
//堆叠图 export const echartsFour ={
title: {
text:
"系统访问量走势图",
},
//backgroundColor: "#6a7985",//背景样式 tooltip: {
trigger:
"axis",
axisPointer: {
type:
"cross",
label: {
backgroundColor:
"#6a7985",
},
},
},
legend: {
data: [
"菜单权限", "角色权限", "按钮权限", "行权限", "列权限"],
},
toolbox: {
//feature: {//saveAsImage: {},//}, },
grid: {
left:
"3%",
right:
"4%",
bottom:
"3%",
containLabel:
true,
},
xAxis: [
{
type:
"category",
boundaryGap:
false,
data: [
"星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期天"],
},
],
yAxis: [
{
type:
"value",
},
],
series: [
{
name:
"菜单权限",
type:
"line",
stack:
"Total",
areaStyle: {},
emphasis: {
focus:
"series",
},
data: [
120, 132, 101, 134, 90, 230, 210],
},
{
name:
"角色权限",
type:
"line",
stack:
"Total",
areaStyle: {},
emphasis: {
focus:
"series",
},
data: [
220, 182, 191, 234, 290, 330, 310],
},
{
name:
"按钮权限",
type:
"line",
stack:
"Total",
areaStyle: {},
emphasis: {
focus:
"series",
},
data: [
150, 232, 201, 154, 190, 330, 410],
},
{
name:
"行权限",
type:
"line",
stack:
"Total",
areaStyle: {},
emphasis: {
focus:
"series",
},
data: [
320, 332, 301, 334, 390, 330, 320],
},
{
name:
"列权限",
type:
"line",
stack:
"Total",
label: {
show:
true,
position:
"top",
},
areaStyle: {},
emphasis: {
focus:
"series",
},
data: [
820, 932, 901, 934, 1290, 1330, 1320],
},
],
};

在页面添加

把盒子2所在的地方替换成<div id="echarts-tow" style="width: 100%; height: 100%"></div>
把盒子3所在的地方替换成<div id="echarts-three" style="width: 100%; height: 100%"></div>
把盒子4所在的地方替换成<div id="echarts-four" style="width: 100%; height: 100%"></div>
 onMounted(() =>{
GetEchartsOneData();
GetEchartsTwoData();
GetEchartsThreeData();
GetEchartsFourData();
});
//六芒星图 function GetEchartsOneData() {var myChart = echarts.init(document.getElementById("echarts-one"));
myChart.setOption(echartsOne);
}
//南丁格尔玫瑰图 function GetEchartsTwoData() {var myChart = echarts.init(document.getElementById("echarts-tow"));
myChart.setOption(echartsTWO);
}
//中国地图 function GetEchartsThreeData() {var myChart = echarts.init(document.getElementById("echarts-three"));
echarts.registerMap(
"china", chinaJson as any); //注册可用的地图 myChart.setOption(echartsThree);
}
//堆叠图 function GetEchartsFourData() {var myChart = echarts.init(document.getElementById("echarts-four"));
myChart.setOption(echartsFour);
}

特别注意

1、在添加中国地图时,需要下载一个中国地图的json包,放在项目中(我是放在和echarts.ts同级目录下)。下载地址:https://datav.aliyun.com/portal/school/atlas/area_selector

2、在tsconfig.json文件中需要添加"resolveJsonModule": true,的配置,该配置可以让系统允许导入json。

预览

兼容性调整

对应echarts来说,每个图表,它是固定的,就算设置的是百分比,也不随着窗体的大小而自适应屏幕,如下图

要解决以上问题,我们只需要添加一个方法即可

 //图标兼容性调整
function resizeEchart(myChart:any)
{
//监听窗口大小变化(适用于一个页面多个图形) window.addEventListener('resize',()=>{myChart.resize();})
}

然后再
myChart.setOption()方法后面添加
resizeEchart(myChart);即可解决兼容性问题,如图

  //堆叠图
function GetEchartsFourData() {var myChart = echarts.init(document.getElementById("echarts-four"));
myChart.setOption(echartsFour);
resizeEchart(myChart);
}

结语

我们的OverallAuth2.0项目也正式迈入功能开发阶段,可能文章内容逐渐开始复杂化,如果你感兴趣的话,也有跟着博主从0到1搭建权限管理系统的兴趣。

那么请加qq群:801913255,进群有什么不懂的尽管问,群主都会耐心解答。

后端WebApi
预览地址:http://139.155.137.144:8880/swagger/index.html

前端vue 预览地址:http://139.155.137.144:8881

关注公众号:发送【权限】,获取前后端代码

有兴趣的朋友,请关注我微信公众号吧(*^▽^*)。

关注我:一个全栈多端的宝藏博主,定时分享技术文章,不定时分享开源项目。关注我,带你认识不一样的程序世界

简介

在前两篇文章中,我们详细探讨了如何利用采样数据来估计回归曲线。接下来,在本节中,我们将深入讨论如何处理分类问题。

章节安排

  1. 背景介绍
  2. 数学方法
  3. 程序实现

背景介绍

线性可分


线性可分
是指在多维空间
\(\mathbb{R}^D\)
中,对于任意两个类别的数据,总是存在一个超平面,可以将这两个类别的数据点完全分开。

在二分类问题中,如果数据集是线性可分的,那么可以找到一个超平面,使得屏幕的一侧的所有点属于一个类别,而另一侧的所有点都属于另一个类别。

设数据集
\(D=\{\text{X},\text{y}\}\)
,其中
\(\text{X}\)
为输入特征向量,
\(\text{y}\)
为类别标签。

如果存在一个超平面
\(L:z=Xw+b\)
,使得:

\[\begin{align*}
\forall y_i=1,\text{x}_iw+b>0\\
\forall y_i=0,\text{x}_iw+b<0
\end{align*}
\]

则称该数据集
\(\text{X}\)
是线性可分的;其中
\(w\)
为权重向量,
\(b\)
为偏置项,
\(\text{x}_i\)
为第
\(i\)
组数据,是矩阵
\(X\)
的第
\(i\)
行.

在一些情形下,并不是严格线性可分的,也就是说不存在一个超平面能够将所有不同类别的点完全分隔开来。这种情况下,我们可能会考虑使用“
宽松的线性可分
”(
Soft Margin
)的概念。
在宽松线性可分中,定义松弛变量
\(\xi\)
,原条件改为

\[\begin{align*}
\forall y_i&=1,\text{x}_iw+b>-\xi\\
\forall y_i&=0,\text{x}_iw+b<\xi
\end{align*}
\]

注意到,对于任何一个超平面
\(L\)
,总是存在一个足够大的松弛变量
\(\xi\)
使得该超平面满足宽松线性可分条件。
因此,一般认为,对于某一个超平面
\(L_i\)
,使得其满足宽松线性条件的最小常数
\(\xi_i\)
越小,则说明该直线划分效果越好。

初识激活函数


超平面
\(L\)
是一个从
\(\mathbb{R}^D\)

\(\mathbb{R}\)
的映射(函数)。其值域为
\((-\infty,\infty)\)
。然而,在实际应用中,通常希望输出的范围现在
\([0,1]\)
之间,以便于解释和处理。为了实现这一目标,通常会引入
激活函数

Sigmoid
函数是一个经典的激活函数,因其连续性和较低的计算复杂度而在机器学习中得到了广泛的应用。Sigmoid 函数的定义如下:

\[\sigma(z)=\frac{1}{1+e^{-z}}
\]

主要特点

  1. 连续性和可导性:
    Sigmoid 函数及其导数都是连续的,这使得它非常适合用于基于梯度下降的优化算法。

  2. 输出范围:
    Sigmoid 函数的输出范围是 ((0, 1)),这使其在二分类问题中特别有用。它可以将线性组合的输出转换为一个概率值,从而更容易解释模型的预测结果。

  3. 计算复杂度:
    Sigmoid 函数的计算相对简单,不涉及复杂的数学运算,这有助于提高模型的训练速度。同时,其导函数可以方便的从原函数计算,即:
    \(\sigma'(z) = \sigma(z) \cdot (1 - \sigma(z))\)

逻辑回归


逻辑回归(Logistic Regression)是一种广泛应用于分类问题的统计模型和机器学习算法。尽管名称中包含“回归”,但它实际上主要用于解决分类问题,特别是二分类问题。

工作原理


  1. 线性组合

    首先,逻辑回归模型对输入特征进行线性组合,也称对输入进行评估:


    \[z = \mathbf{w}^T \mathbf{x} + b
    \]

  2. Sigmoid变换

    然后,将评估的结果
    \(z\)
    通过Sigmoid函数进行变换:


    \[\hat{y} = \sigma(z) = \frac{1}{1 + e^{-(\mathbf{w}^T \mathbf{x} + b)}}
    \]


    Sigmoid函数的输出
    \(\hat{y}\)
    可以解释为样本属于正类的概率。

  3. 决策边界

    通常,选择一个阈值(例如
    \(0.5\)
    )来决定分类结果:


    \[y = \begin{cases}
    1 & \text{如果 } \hat{y} \geq 0.5 \\
    0 & \text{如果 } \hat{y} < 0.5
    \end{cases}
    \]

损失函数


逻辑回归的损失函数通常采用
对数损失(Log Loss)
或称交叉熵损失(Cross-Entropy Loss):

\[L(\mathbf{w}, b) = -\frac{1}{N} \sum_{i=1}^{N} \left[ y_i \log(\hat{y}_i) + (1 - y_i) \log(1 - \hat{y}_i) \right]
\]

其中,
\(N\)
是样本数量,
\(y_i\)
是真实标签,
\(\hat{y}_i\)
是预测的概率值。

优化


本文将介绍如何采用
梯度下降法
优化逻辑回归模型。
在梯度下降法中,核心的部分是计算损失
\(\text{LOSS}\)
关于参数
\(\text{w}\)

\(b\)
的梯度,其反应了参数更新的方向和步长。

通常采用链式法则计算梯度,以参数
\(\text{w}\)
为例,有:

\[\nabla_{\text{w}}\text{LOSS}=\frac{\partial \text{LOSS}}{\partial \text{w}}=
\frac{\partial \text{LOSS}}{\partial \hat{\text{y}}}
\frac{\partial \hat{\text{y}}}{\partial \text{z}}
\frac{\partial \text{z}}{\partial \text{w}}
\]

梯度下降法中,采用梯度的反方向作为更新方向,其公式为:

\[w:=w-\lambda\cdot\nabla_{\text{w}}\text{LOSS}
\]

其中,
\(\lambda\)
为学习率。

程序实现


在上一篇文章《
机器学习:线性回归(下)
》中已经讲述了超平面
\(L\)
的实现方法;因此,本文中将讨论诸如
激活函数

对数损失
等上一章为设计的部分的程序实现。

激活函数


下述函数用于计算输入矩阵或向量的每个元素的Sigmoid函数值。

MatrixXd Sigmoid::cal(const MatrixXd& input) {
    return input.unaryExpr([](double x) { return 1.0 / (1.0 + exp(-x)); });
}

这段代码是一个简短的函数实现,代码解释如下:

  1. input.unaryExpr

    unaryExpr
    是Eigen库中的一个函数,用于对矩阵或向量的每个元素应用一个给定的单变量函数。在这里,
    input
    是一个Eigen矩阵或向量。

  2. Lambda函数

    [](double x) { return 1.0 / (1.0 + exp(-x)); }
    是一个Lambda函数,它定义了一个匿名函数,接受一个
    double
    类型的参数
    x
    ,并返回
    1.0 / (1.0 + exp(-x))
    。这个函数实现了Sigmoid函数的计算。

MatrixXd Sigmoid::grad(const MatrixXd& input) {
    Matrix temp = input.unaryExpr([](double x) { return 1.0 / (1.0 + exp(-x)); });
    return temp.cwiseProduct((1 - temp.array()).matrix());
}

这段代码实现了激活函数的梯度的计算,类似与
Sigmoid::cal()
,先计算激活函数
\(\sigma(z)\)
的值,再采用逐个元素相乘
cwiseProduct
计算(即Hadamard乘积)

\[A \circ B = \begin{bmatrix}
a_{11} & a_{12} \\
a_{21} & a_{22}
\end{bmatrix}
\circ
\begin{bmatrix}
b_{11} & b_{12} \\
b_{21} & b_{22}
\end{bmatrix}
=
\begin{bmatrix}
a_{11} \cdot b_{11} & a_{12} \cdot b_{12} \\
a_{21} \cdot b_{21} & a_{22} \cdot b_{22}
\end{bmatrix}
\]

对数损失

下述函数分别采用
Eigen
的矩阵计算方法,实现了对数损失及对数损失的梯度的计算

double LogisticLoss::computeLoss(const MatrixXd& predicted, const MatrixXd& actual) {
    MatrixXd log_predicted = predicted.unaryExpr([](double p) { return log(p); });
    MatrixXd log_1_minus_predicted = predicted.unaryExpr([](double p) { return log(1 - p); });

    MatrixXd term1 = actual.cwiseProduct(log_predicted);
    // MatrixXd term2 = (1 - actual).cwiseProduct(log_1_minus_predicted);
    MatrixXd term2 = (1 - actual.array()).matrix().cwiseProduct(log_1_minus_predicted);

    double loss = -(term1 + term2).mean();

    return loss;
}

MatrixXd LogisticLoss::computeGradient(const MatrixXd& predicted, const MatrixXd& actual) {
    MatrixXd temp1 = predicted - actual;
    MatrixXd temp2 = predicted.cwiseProduct((1 - predicted.array()).matrix());

    return (temp1).cwiseQuotient(temp2);
}

为了便于读者理解、学习,下面给出了
LogisticLoss::computeLoss()
函数的标量方法实现(采用矩阵索引):

double computeLoss(const MatrixXd& predicted, const MatrixXd& actual) {
    int n = predicted.rows();
    double loss = 0.0;
    for (int i = 0; i < n; ++i) {
        double p = predicted(i, 0);
        double y = actual(i, 0);
        loss += -(y * log(p) + (1 - y) * log(1 - p));
    }
    return loss / n;
}

11月26日在华为Mate品牌盛典上,全新Mate70系列及多款全场景新品正式亮相。在游戏领域,HarmonyOS NEXT加持下游戏的性能得到充分释放。HarmonyOS SDK为开发者提供了软硬协同的系统级图形加速解决方案------Graphics Accelerate Kit(图形加速服务),帮助游戏应用快速集成超帧、GTX自适应稳态渲染和自适应刷新率等渲染优化能力,并解决游戏运行不流畅、卡顿掉帧等痛点体验问题,让接入HarmonyOS SDK的手游,如《决胜巅峰》等,在Mate新机上的体验实现大幅提升。

image

独特大视野,推塔体验细腻又流畅

《决胜巅峰》在HarmonyOS NEXT上首发,并且在Mate X6折叠屏上首次以大视野和高帧率的形式呈现给MOBA类游戏玩家。在Mate X6折叠屏手机的超大视野下,《决胜巅峰》的可视画面提升25%,能够更全面地观察战场。通过超帧技术,将帧率突破到120帧,使得每一次技能释放、每一次推塔,都显得更加细腻、顺畅,极大地提升了游戏的沉浸感,为玩家带来前所未有的对战快感。

在对快速反应要求极高的MOBA类游戏中,超帧技术带来的120帧效果让画面更为流畅,帧率也更稳定,帮助玩家更好地掌控局势,提升整体的游戏体验。在团战中,华丽的技能和流畅的动画结合,也使得战斗画面更加震撼。

优化性能开销,游戏效果不 "打折"

通过结合《决胜巅峰》游戏开发者主动提供的游戏过程关键信息,可以实现GTX自适应稳态渲染和自适应刷新率等游戏加速方案,帮助开发者打造高画质、高流畅、低功耗的非凡游戏体验。

自适应稳态渲染
可根据游戏场景、GPU负载等信息,动态调整渲染负载,减少游戏掉帧,让玩家获得一个更加稳定的游戏体验。

image

自适应稳态渲染技术接入架构示意图

自适应稳态渲染接入代码:

// 初始化ABR实例,配置Buffer分辨率因子范围属性,结合具体游戏分辨率、画质设置合适的范围
// 例如设置ABR对Buffer分辨率进行0.8~1.0倍的自适应调整
errorCode = HMS_ABR_SetScaleRange(context_, 0.8f, 1.0f);
if (errorCode != ABR_SUCCESS) {
    GOLOGE("HMS_ABR_SetScaleRange execution failed, error code: %d.", errorCode);
    return false;
}
// 激活ABR上下文实例
errorCode = HMS_ABR_Activate(context_);
if (errorCode != ABR_SUCCESS) {
    GOLOGE("HMS_ABR_Activate execution failed, error code: %d.", errorCode);
    return false;
}
// 相机运动数据结构体,设置每帧实时相机运动数据
ABR_CameraData cameraData;
cameraData.position = static_cast<ABR_Vector3>(camera_.GetPosition());
cameraData.rotation = static_cast<ABR_Vector3>(camera_.GetRotation());
// 每帧相机运动数据更新
errorCode = HMS_ABR_UpdateCameraData(context_, &cameraData);
if (errorCode != ABR_SUCCESS) {
    GOLOGE("HMS_ABR_UpdateCameraData execution failed, error code: %d.", errorCode);
    return false;
}
//渲染前的准备,绑定目标帧缓冲索引,清空颜色缓冲 renderer_->BeginRenderTarget(fbo,BACKGROUND.x_, BACKGROUND.y_, BACKGROUND.z_, 1.0F); 
//在Buffer渲染前调用,执行失败不影响Buffer正常渲染 
errorCode = HMS_ABR_MarkFrameBuffer_GLES(context_);
if (errorCode != ABR_SUCCESS) {
    GOLOGE("HMS_ABR_MarkFrameBuffer_GLES execution failed, error code: %d.", errorCode);
}
//调用绘制方法进行渲染 
opaqueLayer_Render(sceneDelta, camera_.GetviewMatrix() .lastViewProj_); 
//获取每帧的缩放信息 
float scale; 
erorCode =HMS_ABR_GetScale(context_. &scale); 
GOLOGD("Scale is %f, ". scale); 
if (errorCode != ABR_SUCCESS) {
GOLOGE("HMS_ABR_GetScale execution failed, error code: %d.", errorCode);
}
//将帧缓冲索引绑定为默认值0 
renderer_->EndRenderTarget();

image

点击查看自适应稳态渲染接入教程

据统计,多数游戏120FPS档位下,其实约70%渲染场景是没有变化的,因此主场景里将近7成的渲染都并非是必要的。

自适应刷新率
可以根据游戏场景、玩家操作、CPU负载、GPU负载、手机温度等信息,智能地调整游戏帧率与屏幕刷新率,保证体验基础上实现降低功耗。在玩家休闲状态、游戏画面变化幅度小的情况下,帧率可适当调低,节省电量、控制温度;而在玩家操作剧烈、游戏画面急速变化时,帧率则提升到极致水平,保证玩家的游戏体验。

image

自适应刷新率接入架构示意图

image

自适应刷新率接入流程示意图

image

自适应刷新率接入代码示意图

image

点击查看自适应刷新率接入教程

除了图形加速服务(Graphics Accelerate Kit),HarmonyOS SDK还提供更多图形开放能力,为游戏等重载应用的图形渲染提供帮助,帮助开发者打造高帧率、高画质、低功耗的用户体验。

探索更多

访问
图形加速服务
(Graphics Accelerate Kit),了解更多详情开始使用。

*
本文所提及数据均为内部实验室测试结果

*
本文引用素材来自三方授权,因版本/机型不同可能存在变化,以实际为准。