2024年11月

【引言】

本案例将实现一个随机密码生成器。用户可以自定义密码的长度以及包含的字符类型(大写字母、小写字母、数字、特殊字符),最后通过点击按钮生成密码,并提供一键复制功能。

【环境准备】

•操作系统:Windows 10
•开发工具:DevEco Studio NEXT Beta1 Build Version: 5.0.3.806
•目标设备:华为Mate60 Pro
•开发语言:ArkTS
•框架:ArkUI
•API版本:API 12

【项目结构】

本项目主要由一个入口组件
PasswordGeneratorPage
和一个可观察的类
PasswordOption
组成。
PasswordOption
类用于定义密码选项,包括选项名称、字符集、是否选中和是否启用的状态。

1. PasswordOption类

@ObservedV2
class PasswordOption {
  name: string; // 选项名称
  characters: string; // 该选项对应的字符集
  @Trace selected: boolean = true; // 是否选中,默认为true
  @Trace enabled: boolean = true; // 是否启用,默认为true

  constructor(name: string, characters: string) {
    this.name = name;
    this.characters = characters;
  }
}

2. PasswordGeneratorPage组件

该组件包含密码选项、密码长度设置、生成密码和复制密码的功能。

@Entry
@Component
struct PasswordGeneratorPage {
  @State options: PasswordOption[] = [
    new PasswordOption("大写字母", "ABCDEFGHIJKLMNOPQRSTUVWXYZ"),
    new PasswordOption("小写字母", "abcdefghijklmnopqrstuvwxyz"),
    new PasswordOption("数字", "0123456789"),
    new PasswordOption("特殊字符", "!@#$%^&*()_+-=[]{}|;:,.<>?"),
  ];
  @State passwordLength: number = 10; // 默认密码长度
  @State generatedPassword: string = ''; // 生成的密码

  // 生成密码的方法
  generatePassword() {
    let characterSet = '';
    for (let option of this.options) {
      if (option.selected) {
        characterSet += option.characters;
      }
    }
    let password = '';
    for (let i = 0; i < this.passwordLength; i++) {
      const randomIndex = Math.floor(Math.random() * characterSet.length);
      password += characterSet[randomIndex];
    }
    this.generatedPassword = password;
  }

  // 复制到剪贴板的方法
  copyToClipboard(text: string) {
    const pasteboardData = pasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN, text);
    const systemPasteboard = pasteboard.getSystemPasteboard();
    systemPasteboard.setData(pasteboardData);
    promptAction.showToast({ message: '已复制' });
  }

  // 构建页面布局的方法
  build() {
    // 页面布局代码...
  }
}

功能实现

1. 生成密码

用户可以选择不同的字符集(大写字母、小写字母、数字、特殊字符),并设置密码长度。点击“生成密码”按钮后,系统将根据选中的选项生成随机密码。

2. 复制密码

生成的密码可以通过点击“复制”按钮复制到剪贴板,用户将收到“已复制”的提示。

用户界面

用户界面采用了简洁的设计,包含标题、密码长度设置、选项选择、生成密码按钮和复制按钮。通过动态生成选项的UI元素,用户可以方便地选择所需的字符集。

总结

本文介绍了如何使用鸿蒙NEXT框架开发一个随机密码生成器。通过简单的代码实现,我们可以快速构建出实用的功能。希望这个案例能为你的开发提供灵感和帮助。

【完整代码】

// 导入剪贴板服务
import { pasteboard } from '@kit.BasicServicesKit';
// 导入弹窗提示服务
import { promptAction } from '@kit.ArkUI';

// 使用装饰器定义一个可观察的类,用于密码选项
@ObservedV2
class PasswordOption {
  name: string; // 选项名称
  characters: string; // 该选项对应的字符集
  // 定义是否选中,默认为true
  @Trace selected: boolean = true;
  // 定义是否启用,默认为true
  @Trace enabled: boolean = true;

  // 构造函数,初始化name和characters
  constructor(name: string, characters: string) {
    this.name = name;
    this.characters = characters;
  }
}

// 使用装饰器定义一个入口组件
@Entry
@Component
struct PasswordGeneratorPage {
  // 定义密码选项数组
  @State options: PasswordOption[] = [
    new PasswordOption("大写字母", "ABCDEFGHIJKLMNOPQRSTUVWXYZ"),
    new PasswordOption("小写字母", "abcdefghijklmnopqrstuvwxyz"),
    new PasswordOption("数字", "0123456789"),
    new PasswordOption("特殊字符", "!@#$%^&*()_+-=[]{}|;:,.<>?"),
  ];
  // 定义密码长度状态,默认值为10
  @State passwordLength: number = 10;
  // 基础间距
  @State baseSpacing: number = 30;
  // 生成的密码
  @State generatedPassword: string = '';
  // 是否启用复制按钮
  @State isCopyButtonEnabled: boolean = false;
  // 主题色
  @State primaryColor: string = '#71dec7';
  // 字体颜色
  @State fontColor: string = "#2e2e2e";

  // 生成密码的方法
  generatePassword() {
    let characterSet = ''; // 初始化字符集合
    // 遍历所有选项,如果选项被选中则加入字符集合
    for (let option of this.options) {
      if (option.selected) {
        characterSet += option.characters
      }
    }
    let password = ''; // 初始化密码字符串
    // 根据密码长度生成随机密码
    for (let i = 0; i < this.passwordLength; i++) {
      const randomIndex = Math.floor(Math.random() * characterSet.length);
      password += characterSet[randomIndex];
    }
    this.generatedPassword = password; // 更新生成的密码
  }

  // 复制到剪贴板的方法
  copyToClipboard(text: string) {
    const pasteboardData = pasteboard.createData(pasteboard.MIMETYPE_TEXT_PLAIN, text); // 创建剪贴板数据
    const systemPasteboard = pasteboard.getSystemPasteboard(); // 获取系统剪贴板
    systemPasteboard.setData(pasteboardData); // 将数据放入剪切板
    promptAction.showToast({ message: '已复制' }); // 显示复制成功的提示
  }

  // 检查选项选择状态的方法
  checkOptionsSelection() {
    let selectedCount = 0; // 记录已选中的选项数量
    let lastSelectedIndex = 0; // 记录最后一个选中的选项索引
    // 遍历所有选项
    for (let i = 0; i < this.options.length; i++) {
      this.options[i].enabled = true; // 默认启用所有选项
      if (this.options[i].selected) {
        lastSelectedIndex = i; // 更新最后一个选中的选项索引
        selectedCount++; // 增加选中计数
      }
    }
    // 如果只有一个选项被选中,则禁用该选项防止其被取消选中
    if (selectedCount === 1) {
      this.options[lastSelectedIndex].enabled = false;
    }
  }

  // 构建页面布局的方法
  build() {
    Column() {
      // 标题栏
      Text("随机密码生成")
        .width('100%')// 设置宽度为100%
        .height(54)// 设置高度为54
        .fontSize(18)// 设置字体大小
        .fontWeight(600)// 设置字体粗细
        .backgroundColor(Color.White)// 设置背景颜色
        .textAlign(TextAlign.Center)// 设置文本居中对齐
        .fontColor(this.fontColor); // 设置字体颜色

      // 密码长度设置部分
      Column() {
        Row() {
          Text(`密码长度:`)// 密码长度标签
            .fontWeight(600)
            .fontSize(18)
            .fontColor(this.fontColor);
          Text(`${this.passwordLength}`)// 显示当前密码长度
            .fontWeight(600)
            .fontSize(18)
            .fontColor(this.primaryColor);
        }
        .margin({ top: `${this.baseSpacing}lpx`, left: `${this.baseSpacing}lpx` });

        // 滑动条设置密码长度
        Row() {
          Text('4').fontColor(this.fontColor).width(20);
          Slider({
            value: this.passwordLength, // 当前值
            min: 4, // 最小值
            max: 32, // 最大值
            style: SliderStyle.InSet // 滑动条样式
          })
            .layoutWeight(1)// 布局权重
            .blockColor(Color.White)// 滑块颜色
            .trackColor('#EBEBEB')// 轨道颜色
            .trackThickness(30)// 轨道厚度
            .blockSize({ width: 55, height: 55 })// 滑块大小
            .selectedColor(this.primaryColor)// 选中颜色
            .onChange((value: number, mode: SliderChangeMode) => {
              this.passwordLength = value; // 更新密码长度
              console.info('value:' + value + 'mode' + mode.toString); // 打印日志
            });
          Text('32').fontColor(this.fontColor).width(20);
        }.margin({
          left: `${this.baseSpacing}lpx`,
          right: `${this.baseSpacing}lpx`,
          top: `${this.baseSpacing}lpx`,
        });

        // 选项设置部分
        Text('选项')
          .fontWeight(600)
          .fontSize(18)
          .fontColor(this.fontColor)
          .margin({ left: `${this.baseSpacing}lpx`, top: `${this.baseSpacing}lpx`, bottom: `${this.baseSpacing}lpx` });

        // 动态生成每个选项的UI元素
        ForEach(this.options, (option: PasswordOption, index: number) => {
          Row() {
            Text(option.name)// 选项名称
              .fontWeight(400)
              .fontSize(16)
              .fontColor(this.fontColor)
              .layoutWeight(1);
            Toggle({ type: ToggleType.Switch, isOn: option.selected })// 切换按钮
              .width('100lpx')
              .height('50lpx')
              .enabled(option.enabled)// 是否启用切换
              .selectedColor(this.primaryColor)
              .onChange((isOn: boolean) => {
                option.selected = isOn; // 更新选项状态
                this.checkOptionsSelection(); // 检查选项选择状态
              });
          }
          .width('100%')
          .padding({
            left: `${this.baseSpacing}lpx`,
            right: `${this.baseSpacing}lpx`,
            top: `${this.baseSpacing / 3}lpx`,
            bottom: `${this.baseSpacing / 3}lpx`
          })
          .hitTestBehavior(HitTestMode.Block)
          .onClick(() => {
            if (option.enabled) {
              option.selected = !option.selected; // 切换选项状态
            }
          });
        });

        // 生成密码按钮
        Text('生成密码')
          .fontColor(Color.White)
          .backgroundColor(this.primaryColor)
          .height(54)
          .textAlign(TextAlign.Center)
          .borderRadius(10)
          .fontSize(18)
          .width(`${650 - this.baseSpacing * 2}lpx`)
          .margin({
            top: `${this.baseSpacing}lpx`,
            left: `${this.baseSpacing}lpx`,
            right: `${this.baseSpacing}lpx`,
            bottom: `${this.baseSpacing}lpx`
          })
          .clickEffect({ level: ClickEffectLevel.HEAVY, scale: 0.8 })// 点击效果
          .onClick(() => {
            this.generatePassword(); // 生成密码
          });
      }
      .width('650lpx')
      .margin({ top: 20 })
      .backgroundColor(Color.White)
      .borderRadius(10)
      .alignItems(HorizontalAlign.Start);

      // 显示生成的密码
      Column() {
        Text(`密码结果:`)
          .fontWeight(600)
          .fontSize(18)
          .fontColor(this.fontColor)
          .margin({
            top: `${this.baseSpacing}lpx`,
            left: `${this.baseSpacing}lpx`,
          });
        Text(`${this.generatedPassword}`)// 显示生成的密码
          .width('650lpx')
          .fontColor(this.primaryColor)
          .fontSize(18)
          .textAlign(TextAlign.Center)
          .padding({ left: 5, right: 5 })
          .margin({
            top: `${this.baseSpacing / 3}lpx`
          });

        // 复制按钮
        Text('复制')
          .enabled(this.generatedPassword ? true : false)// 只有生成了密码才启用复制按钮
          .fontColor(Color.White)
          .backgroundColor(this.primaryColor)
          .height(54)
          .textAlign(TextAlign.Center)
          .borderRadius(10)
          .fontSize(18)
          .width(`${650 - this.baseSpacing * 2}lpx`)
          .margin({
            top: `${this.baseSpacing}lpx`,
            left: `${this.baseSpacing}lpx`,
            right: `${this.baseSpacing}lpx`,
            bottom: `${this.baseSpacing}lpx`
          })
          .clickEffect({ level: ClickEffectLevel.HEAVY, scale: 0.8 })
          .onClick(() => {
            this.copyToClipboard(this.generatedPassword); // 复制密码
          });
      }
      .width('650lpx')
      .backgroundColor(Color.White)
      .borderRadius(10)
      .margin({ top: `${this.baseSpacing}lpx` })
      .alignItems(HorizontalAlign.Start);
    }
    .height('100%')
    .width('100%')
    .backgroundColor("#f2f3f5"); // 页面背景颜色
  }
}

一般的系统登统界面,设计好看一些,系统会增色不少,而常规的桌面程序,包括Web上的很多界面,都借助于背景图片的效果来增色添彩,本篇随笔介绍基于WxPython来做一个登录界面效果,并对系统登录界面在不同系统上(WIndows和MacOS) 进行测试对比,调整最佳的处理方案。

1、登录界面的设计

如前面所讲,我们在登录界面上放置一个图片占位全部,并在合适的位置上添加用户的登录账号和密码输入即可,剩下的就是对登录的请求和响应处理了。

我们基于WxPython来处理,放置图片一般就是用 wx.StaticBitmap,图片我们可以预先把他转换为嵌入的图片对象即可,如我们可以通过 img2py 命令来进行添加,把图片文件生成嵌入图片的对象。

如命令:

img2py -a -F -n quit public/images/quit.ico testimage.py

会在对应的地方生成或者最佳对应的图片内容,如下所示:

我们把它整合在对应的文件中使用即可。

创建一个继承 wx.Frame 但是没有常规对话框的标题框,默认的样式是
style=wx.DEFAULT_FRAME_STYLE

我们不需要标题框,让对话框界面自定义关闭按钮,让它更加好看,因此设置样式
style=wx.FRAME_NO_TASKBAR
下面是对话框的界面效果,我们先看一下。

其中关闭按钮,背景按钮、登录按钮,都是我们使用图片来处理的,按钮采用 wx.BitmapButton 来处理即可。

登录界面的窗体如下所示。

importtestimage as testimageimportwxclassLoginFrame(wx.Frame):def __init__(self, parent, title):
super(LoginFrame, self).
__init__(parent, title=title, size=(600, 375),
style
=wx.FRAME_NO_TASKBAR)
self.InitUI()
defInitUI(self):
panel
=wx.Panel(self)
wx.StaticBitmap(panel,
-1, testimage.login_backimg.Bitmap,
pos
= (0, 0), size=(-1, -1), style=
wx.BORDER_NONE)
font
= wx.Font(16, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD)

self.userInput
= wx.TextCtrl(panel, -1, value="admin", style=wx.TE_PROCESS_ENTER|wx.TE_CENTER|wx.CENTER, pos=(80, 330), size=(120, 28))
self.userInput.SetHint(
"输入账号")
self.userInput.SetFont(font)

self.passwordInput
= wx.TextCtrl(panel, -1, pos =(280, 330), size=(120, 28), style=wx.TE_PASSWORD| wx.TE_PROCESS_ENTER|wx.TE_CENTER)
self.passwordInput.SetHint(
"输入密码")
self.passwordInput.SetFont(font)

loginButton
=wx.BitmapButton(panel, -1, testimage.login_btn.Bitmap, pos= (410, 328),size= (92, 30))
self.Bind(wx.EVT_BUTTON, self.OnLogin, loginButton)

quitButton
= wx.BitmapButton(panel, -1, testimage.quit.Bitmap, pos= (560, 10),size= (32, 30))
self.Bind(wx.EVT_BUTTON, self.OnQuit, quitButton)

self.Centre()

上面代码,在MacOS上运行界面,还算不错,不过在WIndows上,输入框和标签都无法正常显示,需要主动鼠标单击的时候,才出现,输入焦点也不太正常,后来搜索解决方案才发现是系统界面刷新的问题,不能使用
wx.StaticBitmap
,而需要使用刷新绘制背景图片的方式才可以正常,因此改动一下,使用 EVT_ERASE_BACKGROUND 事件处理的方式绘制背景方式。在初始化界面的时候,绑定该 EVT_ERASE_BACKGROUND 事件,如下代码所示。

self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBack)
    defOnEraseBack(self, event: wx.EraseEvent):
dc
=event.GetDC()if notdc:
dc
=wx.ClientDC(self)
rect
=self.GetUpdateRegion().GetBox()
dc.SetClippingRect(rect)
dc.Clear()
dc.DrawBitmap(testimage.login_backimg.Bitmap, 0, 0)

这样测试MacOS和WIndows均正常显示。

2、登录界面的优化及对接登录处理

稍作调整,在启动背景上添加一些文字显示,最终界面效果如下所示。

具体就是在绘制背景获得ClientDC 对象后绘制文本即可,如下代码所示。

    defOnEraseBack(self, event: wx.EraseEvent):
dc
=event.GetDC()if notdc:
dc
=wx.ClientDC(self)
rect
=self.GetUpdateRegion().GetBox()
dc.SetClippingRect(rect)
dc.Clear()
dc.DrawBitmap(images.login_backimg.Bitmap, 0, 0)

colour
= wx.Colour(52, 94, 150)
dc.SetTextForeground(colour)
dc.SetFont(
wx.Font(
16, wx.FONTFAMILY_SWISS, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD)
)
rect
= wx.Rect(10, 210, 580, 30)
dc.DrawLabel(
"基于 wxPython 开发的 GUI 应用", rect, wx.ALIGN_CENTER | wx.ALIGN_TOP
)
rect = wx.Rect(10, 240, 580, 30)
dc.DrawLabel(
"COPYRIGHT © 2024 广州爱奇迪软件科技有限公司",
rect,
wx.ALIGN_CENTER | wx.ALIGN_TOP,
)

为了对接实际的Python开发的FastApi后端,我们对增加一个login.py的API对接类,如下代码所示。

from .base_api importBaseApifrom entity.common importAjaxResponse, AuthenticateResultimportrequestsclassLogin(BaseApi):"""登录处理--API接口类"""api_name= "login"

    def __init__(self):
super().
__init__(self.api_name)def authenticate2(self, json) ->AuthenticateResult:"""同步登录接口

:param json: 登录参数
{
"loginname": "string",
"password": "string",
"systemtype": "string",
}
:return: 登录结果
"""url= f"{self.base_url}/authenticate"data= requests.post(url, json=json).json()

result
=AjaxResponse[AuthenticateResult].model_validate(data)return result.result

增加登录过程的提示处理,代码如下所示。

    defon_login(self, event):self.LoginSync()defLoginSync(self):#print("开始同步登录")
        login_name =self.txtLoginName.GetValue()
login_pwd
=self.txtLoginPwd.GetValue()if login_name == "":
MessageUtil.show_tips(self,
"请输入用户名!")returnjson_data={"loginname": f"{login_name}","password": f"{login_pwd}","systemtype": "WareMis",
}
message= "正在尝试登陆获取令牌,请等待..."icon= get_bitmap("appIcon", 16)
busy
= PBI.PyBusyInfo(message, parent=self, title="登陆处理中...", icon=icon)try:
wx.Yield()
#刷新界面 result=api_login.authenticate2(json_data)ifresult.success:#print("登录成功", result) self._accesstoken =result.accesstoken
ApiClient.set_access_token(result.accesstoken)
self.is_login
=True
self.Close()
else:
MessageUtil.show_tips(self,
"用户名或密码错误!")exceptException as e:#print(e) MessageUtil.show_tips(
self,
"登录失败!错误信息如下:", extended_message=str(e)
)
finally:#关闭提示框 del busy

运行登录后,界面提示获取令牌的信息

成功后跳转到主界面窗体上。

以上就是实际使用wxpython开发跨平台桌面应用的登录界面,对接了后端FastAPI的WebAPI项目,该项目使用 FastAPI, SQLAlchemy, Pydantic, Pydantic-settings, Redis, JWT 构建的项目,数据库访问采用异步方式。 数据库操作和控制器操作,采用基类继承的方式减少重复代码,提高代码复用性。 支持Mysql、Mssql、Postgresql、Sqlite等多种数据库接入,通过配置可以指定数据库连接方式。。

前言

本文介绍一款基于 .NET 6 开发的高级报表工具。该工具支持多种数据源(如数据库、Excel 文件、API 服务等),并具备强大的数据处理和可视化功能。通过内置的集合函数和类 Excel 界面,用户可以轻松设计和生成复杂的报表,满足各类业务需求。

项目介绍

CellReport 是一款专为复杂统计报表设计的工具,支持从数据库、Excel 文件、API 服务等多数据源获取数据,并通过内置的集合函数进行数据组织。其报表设计界面类似 Excel,确保数据展示直观易用。

开发 CellReport 的目的是为了快速制作日常统计报表。通过融合集合运算理念,该工具帮助用户摆脱传统存储过程的束缚,专注于特定指标的加工,并在设计阶段灵活组合数据,大大简化了报表的创建和维护。

项目功能

  • 强大的数据处理能力

CellReport 支持多种数据源接入(如数据库、Excel、CSV 等),并提供内置的数据清洗和转换功能,帮助用户快速准备报表所需数据。

  • 丰富的报表模板

提供多种报表模板(包括柱状图、折线图、饼图、散点图等),通过简单的拖拽和编辑,即可生成专业且美观的统计报表。

  • 智能的数据分析

内置智能数据分析功能,可根据数据特征自动推荐合适的报表类型和可视化方案,同时支持自定义分析公式和算法,增加报表的深度和洞察力。

  • 高效的报表运行

采用先进的计算引擎和缓存技术,即使处理大量数据也能保持高效运行。支持定时任务和数据更新提醒,确保报表数据的实时性和准确性。

项目特点

  • 全面的报表类型:支持常规的清单、分组、交叉报表。 多源与分片:处理多个数据源和分片数据。
  • 数据集运算:支持多数据集的集合运算。
  • 单元格操作:提供单元格扩展和引用功能。
  • 内置脚本引擎:集成类JavaScript语言引擎,支持自定义脚本。
  • 丰富的函数库:提供多种内置函数,支持自定义扩展。
  • 丰富的页面元素:预定义报表组件、ECharts图表组件、数据展示组件等。
  • 灵活的数据引用:报表元素间的数据引用灵活,局部刷新设置便捷。

项目技术

  • 前端设计器
  • 采用 Vue.js、LuckySheet 和 ECharts 等前沿的前端技术,构建了一个基于网页的报表设计环境,提供了直观且功能丰富的用户界面。
  • 后端报表引擎
  • 核心部分基于 .NET 6 开发,实现了报表的高效处理和渲染,支持复杂的报表逻辑和高性能的数据处理能力。

项目效果

1、预览地址

http://20.169.235.199/index.html

测试用户/密码: test/123456

2、部分效果

项目地址

GitHub:
https://github.com/NoneDay/CellReport

Gitee:
https://gitee.com/NoneDay/CellReport

总结

本文示例仅展示了项目工具的部分功能。感兴趣的朋友可以通过项目地址获取更多详细信息。希望本文能在报表开发方面为大家提供有益的帮助。欢迎在评论区留言交流,分享您的宝贵经验和建议。

最后

如果你觉得这篇文章对你有帮助,不妨点个赞支持一下!你的支持是我继续分享知识的动力。如果有任何疑问或需要进一步的帮助,欢迎随时留言。

也可以加入微信公众号
[DotNet技术匠]
社区,与其他热爱技术的同行一起交流心得,共同成长!
优秀是一种习惯,欢迎大家留言学习!

Open WebUI和Ollama介绍

Open WebUI 是一个功能丰富且用户友好的自托管 Web 用户界面(WebUI),它被设计用于与大型语言模型(LLMs)进行交互,特别是那些由 Ollama 或与 OpenAI API 兼容的服务所支持的模型。Open WebUI 提供了完全离线运行的能力,这意味着用户可以在没有互联网连接的情况下与模型进行对话,这对于数据隐私和安全敏感的应用场景尤为重要。
以下是 Open WebUI 的一些主要特点:

  1. 直观的界面:Open WebUI 的界面受到 ChatGPT 的启发,提供了一个清晰且用户友好的聊天界面,使得与大型语言模型的交互变得直观。
  2. 扩展性:这个平台是可扩展的,意味着可以通过添加新的插件或功能来定制和增强其能力,适应不同的使用场景和需求。
  3. 离线操作:Open WebUI 支持完全离线运行,不依赖于网络连接,适合在任何设备上使用,无论是在飞机上还是在偏远地区。
  4. 兼容性:它兼容多种 LLM 运行器,包括 Ollama 和 OpenAI 的 API,这使得用户可以从多个来源选择和运行不同的语言模型。
  5. 自托管:用户可以在自己的服务器或设备上部署 Open WebUI,这为数据隐私和控制提供了更高的保障。
  6. Markdown 和 LaTeX 支持:Open WebUI 提供了全面的 Markdown 和 LaTeX 功能,让用户可以生成富文本输出,这在科学和学术交流中非常有用。
  7. 本地 RAG 集成:检索增强生成(RAG)功能允许模型利用本地存储的数据进行更深入和具体的回答,增强了聊天交互的功能。

Ollama 是一个开源项目,其主要目标是简化大型语言模型(LLMs)的部署和运行流程,使得用户能够在本地机器或私有服务器上轻松运行这些模型,而无需依赖云服务。以下是 Ollama 的一些主要特点和功能:

  1. 简化部署: Ollama 设计了简化的过程来在 Docker 容器中部署 LLMs,这大大降低了管理和运行这些模型的复杂性,使得非专业人员也能部署和使用。
  2. 捆绑模型组件: 它将模型的权重、配置和相关数据打包成一个被称为 Modelfile 的单元,这有助于优化模型的设置和配置细节,包括 GPU 的使用情况。
  3. 支持多种模型: Ollama 支持一系列大型语言模型,包括但不限于 Llama 2、Code Llama、Mistral 和 Gemma 等。用户可以根据自己的具体需求选择和定制模型。
  4. 跨平台支持: Ollama 支持 macOS 和 Linux 操作系统,Windows 平台的预览版也已经发布,这使得它在不同操作系统上的兼容性更好。
  5. 命令行操作: 用户可以通过简单的命令行指令启动和运行大型语言模型。例如,运行 Gemma 2B 模型只需要执行 ollama run gemma:2b 这样的命令。
  6. 自定义和扩展性: Ollama 的设计允许用户根据特定需求定制和创建自己的模型,这为模型的个性化使用提供了可能。

通过 Ollama,用户可以获得以下好处:

  • 隐私保护:由于模型在本地运行,因此数据不需要上传到云端,从而保护了用户的隐私。
  • 成本节约:避免了云服务的费用,尤其是对于大量请求的情况。
  • 响应速度:本地部署可以减少延迟,提供更快的响应时间。
  • 灵活性:用户可以自由选择和配置模型,以满足特定的应用需求。
    image

我们可以轻松的使用tong2.5和llama3大模型
image

快速使用

阿里云对Open WebUI做了预集成,可以通过链接,完成一键部署
image

部署后可以通过返回的登录地址直接使用.
image

image

简介

image
烂大街的资料不再赘述,简单来说就是
给代码看的注释

Attribute的使用场景

Attribute不仅仅局限于C#中,在整个.NET框架中都提供了非常大的拓展点,任何地方都有Attribute的影子

  1. 编译器层
    比如 Obsolete,Conditional
  2. C#层
    GET,POST,Max,Range,Require
  3. CLR VM层
    StructLayout,DllImport
  4. JIT 层
    MethodImpl

Attribute在C#中的调用

举个常用的例子,读取枚举上的自定义特性。

    public enum Test
    {
        [EnumDescription("hhhhhh")]
        None = 0,
        [EnumDescription("xxxxxx")]
        Done =1
    }
	private static IEnumerable<string> GetEnumDescriptions(this Enum e)
	{
		IEnumerable<string> result = null;
        var type = e.GetType();
        var fieldInfo = type.GetField(e.ToString());
        var attr = fieldInfo?.GetCustomAttributes(typeof(EnumDescriptionAttribute), false);
        if (attr?.Length > 0)
        {
			result = attr.Cast<EnumDescriptionAttribute>().Select(x => x.Description);
        }
		return result ?? Enumerable.Empty<string>();
	}

可以看到,Attribute底层在C#中实现依旧是依赖反射,所以为什么说Attribute是
写给代码看的注释
,因此对反射的优化思路也可以用在Attribute中。
比如在代码中,使用Dictionary缓存结果集。避免过多调用反射造成的性能问题。

        private static IEnumerable<string> GetEnumDescriptionsCache(this Enum e)
        {
            var key = $"{e.GetType().Name}_{e.ToString()}";
            if (_enumMap.ContainsKey(key))
            {
                return _enumMap[key];
            }
            else
            {
                var result = GetEnumDescriptions(e);
                _enumMap.TryAdd(key, result);
                return result;
            }
        }

循环100000次造成的性能差距还是很明显的
image

Newtonsoft.Json对Attrubute的使用

以JsonConverter为蓝本举例说明。

    public class Person
    {
        [JsonConverter(typeof(DateTimeConverter))]
        public DateTime CreateTime { get; set; }
    }
	public class DateTimeConverter : JsonConverter<DateTime>
    {
        public override DateTime ReadJson(JsonReader reader, Type objectType, DateTime existingValue, bool hasExistingValue, JsonSerializer serializer)
        {
            if (reader.Value == null)
                return DateTime.MinValue;

            if (DateTime.TryParse(reader.Value.ToString(), out DateTime result))
                return result;

            return DateTime.MinValue;
        }

        public override void WriteJson(JsonWriter writer, DateTime value, JsonSerializer serializer)
        {
            writer.WriteValue(value.ToString("yyyy-MM-dd HH:mm:ss"));
        }
    }

定义了一个Attribute:JsonConverter.其底层调用如下:

        [RequiresUnreferencedCode(MiscellaneousUtils.TrimWarning)]
        [RequiresDynamicCode(MiscellaneousUtils.AotWarning)]
        public static JsonConverter? GetJsonConverter(object attributeProvider)
        {
			// 底层还是调用Reflection,为了性能,也缓存了对象元数据。
            JsonConverterAttribute? converterAttribute = GetCachedAttribute<JsonConverterAttribute>(attributeProvider);

            if (converterAttribute != null)
            {
                Func<object[]?, object> creator = CreatorCache.Instance.Get(converterAttribute.ConverterType);
                if (creator != null)
                {
                    return (JsonConverter)creator(converterAttribute.ConverterParameters);
                }
            }

            return null;
        }

https://github.com/JamesNK/Newtonsoft.Json/blob/master/Src/Newtonsoft.Json/Serialization/JsonTypeReflector.cs

Attribute在CLR上的调用

    public class NativeMethods
    {
        [DllImport("xxxxx", EntryPoint = "add", CallingConvention = CallingConvention.Cdecl)]
        public extern static int ManagedAdd(int a, int b);
    }

在CLR中,同样用来调用 C/C++ 的导出函数。有兴趣的朋友可以使用windbg查看线程调用栈。以及在MetaData中有一张ImplMap表,存储着C#方法与C++函数的mapping关系

Attribute在JIT上的调用

    public class Person
    {
        public int id { get; set; } = 0;

        [MethodImpl(MethodImplOptions.Synchronized)]
        public void SyncMethod()
        {
            id++;
        }
    }

JIT会自动为该Attribute注入同步代码
image
image

其本质就是注入lock同步块代码,只是颗粒度在整个方法上。相对比较大

结论

Attrubute在C#层面,底层使用反射。因此使用自定义Attribute时,酌情使用缓存来提高性能