创建示例项目
This commit is contained in:
commit
bd0c9bbf4a
|
@ -0,0 +1,35 @@
|
|||
# 编译目录
|
||||
bin/
|
||||
obj/
|
||||
.idea/
|
||||
.vscode/
|
||||
.vs/
|
||||
|
||||
# Nuget包目录
|
||||
packages/
|
||||
|
||||
|
||||
# 编译结果
|
||||
[Dd]ebug/
|
||||
[Rr]elease/
|
||||
x64/
|
||||
*_i.c
|
||||
*_p.c
|
||||
*.ilk
|
||||
*.meta
|
||||
*.obj
|
||||
*.pch
|
||||
*.pdb
|
||||
*.pgc
|
||||
*.pgd
|
||||
*.rsp
|
||||
*.sbr
|
||||
*.tlb
|
||||
*.tli
|
||||
*.tlh
|
||||
*.tmp
|
||||
*.log
|
||||
*.vspscc
|
||||
*.vssscc
|
||||
.builds
|
||||
.fake
|
|
@ -0,0 +1,22 @@
|
|||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core", "Core\Core.csproj", "{E6270F58-996E-4DC2-8E18-34ECF4E222AB}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Example", "Example\Example.csproj", "{09918FD7-8BF2-4DAF-BAB9-1C97E04E458B}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{E6270F58-996E-4DC2-8E18-34ECF4E222AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E6270F58-996E-4DC2-8E18-34ECF4E222AB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E6270F58-996E-4DC2-8E18-34ECF4E222AB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E6270F58-996E-4DC2-8E18-34ECF4E222AB}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{09918FD7-8BF2-4DAF-BAB9-1C97E04E458B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{09918FD7-8BF2-4DAF-BAB9-1C97E04E458B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{09918FD7-8BF2-4DAF-BAB9-1C97E04E458B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{09918FD7-8BF2-4DAF-BAB9-1C97E04E458B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
EndGlobal
|
|
@ -0,0 +1,14 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>Demo</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Extensions" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
|
@ -0,0 +1,38 @@
|
|||
using Demo.Models;
|
||||
|
||||
namespace Demo.Features;
|
||||
|
||||
/// <summary>
|
||||
/// 默认的功能类,做委托容器用
|
||||
/// </summary>
|
||||
/// <typeparam name="TMetadata"></typeparam>
|
||||
public sealed class DefaultFeature<TMetadata> : Feature<TMetadata>
|
||||
where TMetadata : IMetadata
|
||||
{
|
||||
public DefaultFeature(string name, FeatureFunction<TMetadata>? executeFunction = null,
|
||||
FeatureFunctionAsync<TMetadata>? executeFunctionAsync = null,
|
||||
int order = 1)
|
||||
{
|
||||
_executeFunction = executeFunction;
|
||||
_executeFunctionAsync = executeFunctionAsync;
|
||||
Name = name;
|
||||
Order = order;
|
||||
}
|
||||
|
||||
private readonly FeatureFunction<TMetadata>? _executeFunction;
|
||||
|
||||
private readonly FeatureFunctionAsync<TMetadata>? _executeFunctionAsync;
|
||||
|
||||
public override void Execute(TMetadata metadata)
|
||||
{
|
||||
_executeFunction?.Invoke(this, metadata);
|
||||
}
|
||||
|
||||
public override async ValueTask ExecuteAsync(TMetadata metadata)
|
||||
{
|
||||
if (_executeFunctionAsync is not null)
|
||||
{
|
||||
await _executeFunctionAsync.Invoke(this, metadata);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
using Demo.Models;
|
||||
|
||||
namespace Demo.Features;
|
||||
|
||||
/// <summary>
|
||||
/// 抽象功能类
|
||||
/// </summary>
|
||||
public abstract class Feature<TMetadata>
|
||||
where TMetadata : IMetadata
|
||||
{
|
||||
/// <summary>
|
||||
/// 执行顺序
|
||||
/// </summary>
|
||||
public int Order { get; init; }
|
||||
|
||||
public bool Finished { get; protected set; }
|
||||
|
||||
public string Name { get; init; } = String.Empty;
|
||||
|
||||
public abstract void Execute(TMetadata metadata);
|
||||
|
||||
public virtual ValueTask ExecuteAsync(TMetadata metadata)
|
||||
{
|
||||
Execute(metadata);
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
|
||||
public virtual void OnException(FeatureExceptionContext<TMetadata> context)
|
||||
{
|
||||
throw context.Exception;
|
||||
}
|
||||
|
||||
public virtual ValueTask OnExceptionAsync(FeatureExceptionContext<TMetadata> context)
|
||||
{
|
||||
throw context.Exception;
|
||||
}
|
||||
}
|
||||
|
||||
public delegate void FeatureFunction<TMetadata>(Feature<TMetadata> feature, TMetadata metadata)
|
||||
where TMetadata : IMetadata;
|
||||
|
||||
public delegate ValueTask FeatureFunctionAsync<TMetadata>(Feature<TMetadata> feature, TMetadata metadata)
|
||||
where TMetadata : IMetadata;
|
|
@ -0,0 +1,19 @@
|
|||
using Demo.Models;
|
||||
|
||||
namespace Demo.Features;
|
||||
|
||||
public class FeatureException<TMetadata> : Exception
|
||||
where TMetadata : IMetadata
|
||||
{
|
||||
public TMetadata Metadata { get; }
|
||||
public Feature<TMetadata> Feature { get; }
|
||||
|
||||
private Exception _innerException;
|
||||
|
||||
public FeatureException(TMetadata metadata, Feature<TMetadata> feature, Exception ex) : base(ex.Message)
|
||||
{
|
||||
Metadata = metadata;
|
||||
Feature = feature;
|
||||
_innerException = ex;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
using Demo.Models;
|
||||
|
||||
namespace Demo.Features;
|
||||
|
||||
/// <summary>
|
||||
/// 功能运行时异常上下文
|
||||
/// </summary>
|
||||
public class FeatureExceptionContext<TMetadata>
|
||||
where TMetadata : IMetadata
|
||||
{
|
||||
public Feature<TMetadata> Feature { get; }
|
||||
public FeatureException<TMetadata> Exception { get; }
|
||||
public FeatureExceptionContext(Feature<TMetadata> feature, FeatureException<TMetadata> exception)
|
||||
{
|
||||
Feature = feature;
|
||||
Exception = exception;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 表示异常是否已处理
|
||||
/// </summary>
|
||||
public bool Handled { get; set; } = false;
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
using System.Collections.ObjectModel;
|
||||
using Demo.Models;
|
||||
|
||||
namespace Demo.Features;
|
||||
|
||||
/// <summary>
|
||||
/// 功能列表容器
|
||||
/// </summary>
|
||||
/// <typeparam name="TMetadata"></typeparam>
|
||||
public class FeatureList<TMetadata> : Dictionary<string, Feature<TMetadata>>
|
||||
where TMetadata : IMetadata
|
||||
{
|
||||
public FeatureList(IDictionary<string, Feature<TMetadata>>? list = null)
|
||||
: base(list ?? new Dictionary<string, Feature<TMetadata>>())
|
||||
{
|
||||
}
|
||||
}
|
|
@ -0,0 +1,144 @@
|
|||
using System.ComponentModel.Design;
|
||||
using Demo.Models;
|
||||
|
||||
namespace Demo.Features;
|
||||
|
||||
public class FeatureWrapper<TMetadata>
|
||||
where TMetadata : IMetadata
|
||||
{
|
||||
private readonly TMetadata _metadata;
|
||||
|
||||
protected FeatureList<TMetadata> Features { get; }
|
||||
|
||||
public FeatureWrapper(TMetadata metadata, FeatureList<TMetadata>? features = null)
|
||||
{
|
||||
_metadata = metadata;
|
||||
Features = features ?? new FeatureList<TMetadata>();
|
||||
}
|
||||
|
||||
public FeatureWrapper<TMetadata> DropFeature(string name)
|
||||
{
|
||||
if (String.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
throw new CheckoutException("功能名称不能为空或空白字符");
|
||||
}
|
||||
Features.Remove(name);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加功能
|
||||
/// </summary>
|
||||
/// <param name="feature"></param>
|
||||
/// <typeparam name="TFeature"></typeparam>
|
||||
/// <returns></returns>
|
||||
public FeatureWrapper<TMetadata> WithFeature<TFeature>(TFeature feature)
|
||||
where TFeature : Feature<TMetadata>
|
||||
{
|
||||
if (feature is null)
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
if (String.IsNullOrWhiteSpace(feature.Name))
|
||||
{
|
||||
throw new CheckoutException("功能名称不能为空或空白字符");
|
||||
}
|
||||
Features.TryAdd(feature.Name, feature);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加功能
|
||||
/// </summary>
|
||||
/// <param name="name"></param>
|
||||
/// <param name="feature"></param>
|
||||
/// <param name="order"></param>
|
||||
/// <returns></returns>
|
||||
public FeatureWrapper<TMetadata> WithFeature(string name, FeatureFunction<TMetadata> feature, int order = 1)
|
||||
{
|
||||
if (String.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
throw new CheckoutException("功能名称不能为空或空白字符");
|
||||
}
|
||||
if (feature is not null)
|
||||
{
|
||||
Features.TryAdd(name, new DefaultFeature<TMetadata>(name, feature, order: order));
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加功能
|
||||
/// </summary>
|
||||
/// <param name="name"></param>
|
||||
/// <param name="feature"></param>
|
||||
/// <param name="order"></param>
|
||||
/// <returns></returns>
|
||||
public FeatureWrapper<TMetadata> WithFeature(string name, FeatureFunctionAsync<TMetadata> feature, int order = 1)
|
||||
{
|
||||
if (String.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
throw new CheckoutException("功能名称不能为空或空白字符");
|
||||
}
|
||||
if (feature is not null)
|
||||
{
|
||||
Features.TryAdd(name, new DefaultFeature<TMetadata>(name, executeFunctionAsync: feature, order: order));
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
public void Execute()
|
||||
{
|
||||
foreach (var feature in Features.Values.OrderBy(f => f.Order))
|
||||
{
|
||||
try
|
||||
{
|
||||
feature.Execute(_metadata);
|
||||
if (feature.Finished)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
feature.OnException(new FeatureExceptionContext<TMetadata>(feature, new FeatureException<TMetadata>(_metadata, feature, ex)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Execute(string name)
|
||||
{
|
||||
if (Features.TryGetValue(name, out var feature))
|
||||
{
|
||||
feature.Execute(_metadata);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new KeyNotFoundException(name);
|
||||
}
|
||||
}
|
||||
|
||||
public async ValueTask ExecuteAsync()
|
||||
{
|
||||
foreach (var feature in Features.Values.OrderBy(f => f.Order))
|
||||
{
|
||||
try
|
||||
{
|
||||
await feature.ExecuteAsync(_metadata);
|
||||
if (feature.Finished)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await feature.OnExceptionAsync(new FeatureExceptionContext<TMetadata>(feature, new FeatureException<TMetadata>(_metadata, feature, ex)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
using Demo.Features;
|
||||
using Demo.Primitives;
|
||||
|
||||
namespace Demo.Models;
|
||||
|
||||
public abstract class Chip : IMetadata
|
||||
{
|
||||
/// <summary>
|
||||
/// 属性
|
||||
/// </summary>
|
||||
public MetadataPropertySet Properties { get; }
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
public Version Version { get; set; } = new Version(0, 0, 0);
|
||||
|
||||
/// <summary>
|
||||
/// 功能
|
||||
/// </summary>
|
||||
public virtual FeatureWrapper<Chip> Features { get; }
|
||||
|
||||
/// <summary>
|
||||
/// 公式
|
||||
/// </summary>
|
||||
public FormulaExecutor Formula { get; protected set; }
|
||||
|
||||
protected Chip(Chip? @base = null, bool needInherit = true)
|
||||
{
|
||||
if (needInherit)
|
||||
{
|
||||
Properties = @base?.Properties ?? new MetadataPropertySet();
|
||||
}
|
||||
else
|
||||
{
|
||||
Properties = new MetadataPropertySet();
|
||||
}
|
||||
Features = new FeatureWrapper<Chip>(this);
|
||||
Formula = new FormulaExecutor(null, null);
|
||||
Initialize();
|
||||
}
|
||||
|
||||
public MetadataPropertySet.MetadataProperty? this[string name]
|
||||
{
|
||||
get => Properties.GetValue(name);
|
||||
set => Properties.SetValue(name, value);
|
||||
}
|
||||
|
||||
|
||||
public Chip SetProperty(string name, object? value)
|
||||
{
|
||||
Properties.SetValue(name, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public TValue? GetProperty<TValue>(string name)
|
||||
{
|
||||
return Properties.GetValue(name).Cast<TValue>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 初始化
|
||||
/// </summary>
|
||||
protected virtual void Initialize()
|
||||
{
|
||||
// var properties = this.GetType().GetProperties();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
namespace Demo.Models;
|
||||
|
||||
/// <summary>
|
||||
/// 公式模型类
|
||||
/// </summary>
|
||||
public class Formula
|
||||
{
|
||||
/// <summary>
|
||||
/// 是否启用
|
||||
/// </summary>
|
||||
public bool Enabled { get; set; } = true;
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
namespace Demo.Models;
|
||||
|
||||
public class FormulaExecutionContext
|
||||
{
|
||||
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
using Demo.Features;
|
||||
|
||||
namespace Demo.Models;
|
||||
|
||||
/// <summary>
|
||||
/// 元数据
|
||||
/// </summary>
|
||||
public interface IMetadata
|
||||
{
|
||||
/// <summary>
|
||||
/// 属性
|
||||
/// </summary>
|
||||
MetadataPropertySet Properties { get; }
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
using System.Collections;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace Demo.Models;
|
||||
|
||||
public class MetadataPropertySet : IReadOnlyDictionary<string, MetadataPropertySet.MetadataProperty>
|
||||
{
|
||||
private readonly IDictionary<string, MetadataProperty> _properties;
|
||||
|
||||
public MetadataPropertySet()
|
||||
{
|
||||
_properties = new Dictionary<string, MetadataProperty>();
|
||||
}
|
||||
|
||||
public bool ContainsKey(string key)
|
||||
{
|
||||
return _properties.ContainsKey(key);
|
||||
}
|
||||
|
||||
public bool TryGetValue(string key, out MetadataProperty value)
|
||||
{
|
||||
return _properties.TryGetValue(key, out value);
|
||||
}
|
||||
|
||||
public IEnumerable<string> Keys => _properties.Keys;
|
||||
public IEnumerable<MetadataProperty> Values => _properties.Values;
|
||||
|
||||
public MetadataProperty? this[string name]
|
||||
{
|
||||
get => GetValue(name);
|
||||
}
|
||||
|
||||
internal void SetValue(string name, object value)
|
||||
{
|
||||
if (_properties.TryGetValue(name, out var property))
|
||||
{
|
||||
property.Value = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
property = new MetadataProperty(name, value);
|
||||
_properties.TryAdd(name, property);
|
||||
}
|
||||
}
|
||||
|
||||
internal MetadataProperty? GetValue(string name)
|
||||
{
|
||||
if (_properties.TryGetValue(name, out var property))
|
||||
{
|
||||
return property;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public class MetadataProperty
|
||||
{
|
||||
public bool IsNull { get; private set; }
|
||||
public Type? Type { get; private set; }
|
||||
|
||||
private object? _value;
|
||||
/// <summary>
|
||||
/// 属性值
|
||||
/// </summary>
|
||||
public object? Value
|
||||
{
|
||||
get => _value;
|
||||
set
|
||||
{
|
||||
IsNull = value is null;
|
||||
Type = value?.GetType();
|
||||
_value = value;
|
||||
}
|
||||
}
|
||||
|
||||
public string Name { get; init; }
|
||||
|
||||
public TValue? Cast<TValue>()
|
||||
{
|
||||
if (typeof(TValue) == Type)
|
||||
{
|
||||
return (TValue)Convert.ChangeType(Value, Type);
|
||||
}
|
||||
|
||||
return (TValue)Value;
|
||||
}
|
||||
|
||||
public MetadataProperty(string name, object? value)
|
||||
{
|
||||
(Name, Value) = (name, value);
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Value?.ToString()!;
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerator<KeyValuePair<string, MetadataProperty>> GetEnumerator()
|
||||
{
|
||||
return _properties.GetEnumerator();
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return GetEnumerator();
|
||||
}
|
||||
|
||||
public int Count => _properties.Count;
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
using Demo.Models;
|
||||
|
||||
namespace Demo.Primitives;
|
||||
|
||||
public class FormulaExecutor : IComputable<FormulaExecutionContext, decimal>
|
||||
{
|
||||
private IFormulaEngine _engine;
|
||||
private FormulaExpressionParser _parser;
|
||||
|
||||
public FormulaExecutor(IFormulaEngine engine, FormulaExpressionParser parser)
|
||||
{
|
||||
_engine = engine;
|
||||
_parser = parser;
|
||||
}
|
||||
|
||||
public decimal Compute(FormulaExecutionContext context)
|
||||
{
|
||||
Console.WriteLine("计算过程");
|
||||
return default;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
namespace Demo.Primitives;
|
||||
|
||||
public class FormulaExpressionParser
|
||||
{
|
||||
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
namespace Demo.Primitives;
|
||||
|
||||
/// <summary>
|
||||
/// 可计算单元
|
||||
/// </summary>
|
||||
public interface IComputable<TInput, TResult>
|
||||
{
|
||||
/// <summary>
|
||||
/// 计算
|
||||
/// </summary>
|
||||
/// <param name="input"></param>
|
||||
/// <returns></returns>
|
||||
TResult Compute(TInput input);
|
||||
|
||||
// TResult Compute(Func<TInput, TResult> computor);
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
namespace Demo.Primitives;
|
||||
|
||||
/// <summary>
|
||||
/// 公式引擎
|
||||
/// </summary>
|
||||
public interface IFormulaEngine
|
||||
{
|
||||
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Core\Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
|
@ -0,0 +1,41 @@
|
|||
using Demo.Models;
|
||||
|
||||
try
|
||||
{
|
||||
IcndChip icndChip = new IcndChip();
|
||||
icndChip.SetProperty("Code", "1234")
|
||||
.SetProperty("Name", "芯片");
|
||||
icndChip.Features.Execute(); // 执行所有功能
|
||||
|
||||
icndChip.Features
|
||||
.DropFeature("场频自适应功能")
|
||||
.Execute();
|
||||
|
||||
icndChip.Features.Execute("场频自适应功能");
|
||||
|
||||
icndChip.Formula.Compute(new FormulaExecutionContext() // 计算公式(需要调整)
|
||||
{
|
||||
|
||||
});
|
||||
|
||||
Console.WriteLine(icndChip.GetProperty<string>("Code"));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine(ex);
|
||||
}
|
||||
public class IcndChip : Chip
|
||||
{
|
||||
protected override void Initialize()
|
||||
{
|
||||
Name = "ICND芯片";
|
||||
Features.WithFeature("场频自适应功能", (f, m) =>
|
||||
{
|
||||
Console.WriteLine($"{m.Name}调用{f.Name}");
|
||||
}).WithFeature("其他功能", (f, m) =>
|
||||
{
|
||||
Console.WriteLine($"{m.Name}调用{f.Name},得到属性[\"Code\"]值{m.GetProperty<string>("Code")}");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue