完善 新版UI

This commit is contained in:
小肥羊 2025-11-04 16:08:36 +08:00
parent 16d058d2b9
commit 963448382d
241 changed files with 25322 additions and 2374 deletions

1
.gitignore vendored
View File

@ -367,3 +367,4 @@ VideoAnalysis/AICore/_Static/
VideoAnalysisCore/AICore/_Static/
VideoAnalysis/WebUI/node_modules/
VideoAnalysis/WebUI/dist/
VideoAnalysis/WebUI/.vscode/

View File

@ -1,25 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<base href="/" />
<link href="_content/AntDesign/css/ant-design-blazor.css" rel="stylesheet" />
<link href="_content/AntDesign.ProLayout/css/ant-design-pro-layout-blazor.css" rel="stylesheet" />
<link href="Learn.VideoAnalysis.styles.css" rel="stylesheet" />
<link rel="icon" type="image/png" href="favicon.png" />
<HeadOutlet @rendermode="InteractiveServer" />
</head>
<body>
<Routes @rendermode="InteractiveServer" />
<script type="text/javascript" src="@("https://unpkg.com/@antv/g2plot@2.4.17/dist/g2plot.min.js")"></script>
<script src="_content/AntDesign/js/ant-design-blazor.js"></script>
@* <script src="_content/AntDesign.Charts/ant-design-charts-blazor.js"></script> *@
<script src="_framework/blazor.web.js"></script>
</body>
</html>

View File

@ -1,36 +0,0 @@
@page "/Error"
@using System.Diagnostics
<PageTitle>Error</PageTitle>
<h1 class="text-danger">错误页面.</h1>
<h2 class="text-danger">处理您的请求时出错。</h2>
@if (ShowRequestId)
{
<p>
<strong>Request ID:</strong> <code>@RequestId</code>
</p>
}
<h3>开发模式</h3>
<p>
切换到<strong>Development</strong>环境将显示有关发生的错误的更多详细信息。
</p>
<p>
<strong> 不应为已部署的应用程序启用开发环境。</strong>
它可能导致向最终用户显示来自异常的敏感信息。
对于本地调试,通过将 <strong>ASPNETCORE_ENVIRONMENT</strong> 环境变量设置为 <strong>Development</strong> 来启用 <strong> 开发 </strong> 环境
并重新启动应用程序。
</p>
@code{
[CascadingParameter]
private HttpContext? HttpContext { get; set; }
private string? RequestId { get; set; }
private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
protected override void OnInitialized() =>
RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
}

View File

@ -1,36 +0,0 @@
@namespace VideoAnalysisRazor.Layouts
@using static AntDesign.IconType
@inherits LayoutComponentBase
<AntDesign.ProLayout.BasicLayout
Logo="@("https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg")"
MenuData="_menuData"
Context="学习视频分析"
MenuAccordion
Title="学习视频分析"
@bind-Collapsed="collapsed">
<HeaderContentRender>
<Space Size="@("24")">
<SpaceItem>
<Icon Class="action" Type="@(collapsed?"menu-unfold":"menu-fold")" OnClick="Toggle" />
</SpaceItem>
<SpaceItem>
<Icon Class="action" Type="reload" Theme="outline" OnClick="Reload" />
</SpaceItem>
<SpaceItem>
<Icon Type="api" Theme="outline" OnClick="ToSwagger" />
</SpaceItem>
</Space>
</HeaderContentRender>
<RightContentRender>
</RightContentRender>
<ChildContent>
<ReuseTabs ></ReuseTabs>
</ChildContent>
<FooterRender>
<FooterView Copyright="2024 重庆远轩教育科技有限公司" Links="new LinkItem[0]"></FooterView>
</FooterRender>
</AntDesign.ProLayout.BasicLayout>
<SettingDrawer />

View File

@ -1,106 +0,0 @@
using AntDesign.Extensions.Localization;
using AntDesign.ProLayout;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Identity.Client.Extensions.Msal;
using Microsoft.JSInterop;
using System.Globalization;
using System.Net.Http.Json;
namespace VideoAnalysisRazor.Layouts
{
public partial class BasicLayout : LayoutComponentBase, IDisposable
{
private MenuDataItem[] _menuData;
[Inject] private NavigationManager NavigationManager { get; set; } = default!;
[Inject] IHttpContextAccessor HttpContextAccessor { get; set; } = default!;
[Inject] private ReuseTabsService TabService { get; set; }
[Inject] private IJSRuntime JSRuntime { get; set; }
[Inject] private ProtectedSessionStorage session { get; set; } = default!;
bool collapsed;
void Toggle()
{
collapsed = !collapsed;
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
}
protected override async Task OnInitializedAsync()
{
if (!await CheckLogin())
{
NavigationManager.NavigateTo("/Login");
}
_menuData = [
new MenuDataItem
{
Path = "/",
Name = "任务队列",
Key = "VideoTaskPage",
Icon = "unordered-list",
},
new MenuDataItem
{
Path = "/Project",
Name = "课堂指标",
Key = "EvaluationProject",
Icon = "question-circle",
HideInMenu = true,
},
new MenuDataItem
{
Path = "/Login",
Name = "登录页",
Key = "Login",
HideInMenu = true,
},
new MenuDataItem
{
Path = "/VideoTaskShow",
Name = "视频任务预览",
Key = "VideoTaskShow",
HideInMenu = true,
},
new MenuDataItem
{
Path = "/NodeSubscriptionPage",
Name = "文件订阅",
Key = "NodeSubscriptionPage",
Icon="clock-circle",
HideInMenu = true,
}
];
}
public async Task<bool> CheckLogin()
{
try
{
return (await session.GetAsync<bool>("Login")).Value;
}
catch
{
return false;
}
}
void Reload()
{
TabService.ReloadPage();
}
async Task ToSwagger()
{
await JSRuntime.InvokeVoidAsync("open", "/swagger/index.html", "_blank");
}
public void Dispose()
{
}
}
}

View File

@ -1,15 +0,0 @@
using Microsoft.AspNetCore.Mvc.RazorPages;
using System.Security.Policy;
namespace Learn.VideoAnalysis.Components.Pages.Dto
{
public class TaskShowRoute : PageModel
{
public int ID;
public void OnGet(int id)
{
ID = id;
}
}
}

View File

@ -1,88 +0,0 @@
@page "/Project"
@using AntDesign
@using AntDesign.TableModels
@using System.ComponentModel.DataAnnotations
@using SqlSugar
@using VideoAnalysisCore.Model
@using UserCenter.Model.Enum
<Table @ref="_table" Loading="tableLoading" TItem="CourseGradingCriteria" ScrollY="600px"
PageSize="20" Total="_total" DataSource="_dataSource" @bind-SelectedRows="_selectedRows" OnChange="OnChange">
<TitleTemplate>
<Flex Justify="end" Gap="10">
<Button Type="primary" @onclick="()=> StartEdit()">编辑</Button>
</Flex>
</TitleTemplate>
<ColumnDefinitions Context="row">
<PropertyColumn Property="c=>c.Id" Width="130px" Filterable="true" Sortable="true" />
<PropertyColumn Property="c=>c.Subject" Filterable="true" Width="130px" />
<PropertyColumn Property="c=>c.TotalScore" Width="130px" />
<PropertyColumn Property="c=>c.PassScore" Width="130px" />
<PropertyColumn Property="c=>c.NamePrompt" />
</ColumnDefinitions>
</Table>
@{
RenderFragment modelfooter = @<Template>
<Button OnClick="@EditOnOkAsync" @key="@( "submit" )"
Type="primary"
Loading="@modalBtnLoading">
提交
</Button>
<Button OnClick="()=>modalShow = false" @key="@( "back" )">取消</Button>
</Template>;
}
<Modal Title="@("编辑学科课堂指标")" Visible="modalShow" Width="650"
Footer="@modelfooter">
<Form @ref="form" Model="rowData" LabelAlign="AntLabelAlignType.Left">
<GridRow>
<GridCol Span="24">
<FormItem Label="学科指标">
<div style="display:flex;">
<EnumSelect TEnum="SubjectEnum" @bind-Value="editSubject" AllowClear />
<Button OnClick="SubjectEnumSelect" Type="primary" Icon="@IconType.Outline.Search">
查询
</Button>
</div>
</FormItem>
</GridCol>
<GridCol Span="12">
<FormItem Label="满分分值">
<AntDesign.InputNumber Precision="1" @bind-Value="context.TotalScore" Min="1" Max="100" DefaultValue="10" PlaceHolder="满分分值" />
</FormItem>
</GridCol>
<GridCol Span="12">
<FormItem Label="及格分值">
<AntDesign.InputNumber Precision="1" @bind-Value="context.PassScore" Min="1" Max="@context.TotalScore"
DefaultValue="6" PlaceHolder="及格分值" />
</FormItem>
</GridCol>
<GridCol Span="24">
<FormItem Label="标准提问词">
<TextArea Rows="2" @bind-Value="@context.NamePrompt" />
</FormItem>
</GridCol>
<GridCol Span="24">
<FormItem Label="学科提示词" Rules="[new(){ Min=1 }]">
<Button OnClick="EditAddRow" Type="primary" Style="margin-bottom:16px" Size="small">
添加
</Button>
<Table PaginationPosition="none" @ref="tableRef" ScrollY="350px" PageSize="20"DataSource="_editSource" TItem="CourseGradingCriteria" Context="row" Size="TableSize.Small">
<ActionColumn Width="80px">
<Button Type="@ButtonType.Text" Danger @onclick="() => _editSource.Remove(row)">删除</Button>
</ActionColumn>
<PropertyColumn Width="50px" Property="c=>c.TotalScore" />
<PropertyColumn Width="50px" Property="c=>c.PassScore" />
<PropertyColumn Property="c=>c.NamePrompt" />
</Table>
</FormItem>
</GridCol>
</GridRow>
</Form>
</Modal>
@code
{
}

View File

@ -1,154 +0,0 @@
using AntDesign;
using AntDesign.TableModels;
using FreeRedis;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using SqlSugar;
using System.Drawing;
using System.Linq.Expressions;
using System.Reflection;
using UserCenter.Model;
using UserCenter.Model.Enum;
using VideoAnalysisCore.Common;
using VideoAnalysisCore.Model.Enum;
using VideoAnalysisCore.Model;
using Learn.VideoAnalysis.API.Expand;
namespace Learn.VideoAnalysis.Components.Pages
{
public partial class EvaluationProject : ComponentBase
{
[Inject] private ConfirmService ComfirmService { get; set; } = default!;
[Inject] private ModalService ModalService { get; set; } = default!;
[Inject] private Repository<CourseGradingCriteria> criteria { get; set; } = default!;
[Inject] private INotificationService _notice { get; set; } = default!;
IEnumerable<CourseGradingCriteria> _selectedRows = [];
ITable _table;
IForm? form;
List<CourseGradingCriteria> _dataSource = null;
RefAsync<int> _total = 0;
bool tableLoading = false;
Table<CourseGradingCriteria> tableRef;
List<CourseGradingCriteria> _editSource = null;
bool modalShow =false;
bool modalBtnLoading = false;
CourseGradingCriteria rowData;
SubjectEnum editSubject;
async Task SubjectEnumSelect()
{
_editSource = await criteria.GetListAsync(x => x.Subject.Value == editSubject);
await this.InvokeAsync(StateHasChanged);
}
void EditAddRow()
{
if (form is not null && form.Validate())
{
var data = rowData;
data.Subject = editSubject;
if (_editSource is null)
_editSource = new() { data };
else
_editSource.Add(data);
rowData = new();
StateHasChanged();
}
}
/// <summary>
/// 新增或者修改
/// </summary>
void StartEdit()
{
rowData = new();
modalShow = true;
}
async Task EditOnOkAsync()
{
var data = rowData;
modalBtnLoading = true;
if (_editSource is null || _editSource.Count == 0)
{
await _notice.Open(new NotificationConfig()
{
Message = "提示",
Description = "无效的学科课堂指标数据",
NotificationType = NotificationType.Warning
});
modalBtnLoading = false;
return;
}
if (_editSource.Sum(x => x.TotalScore) != 100)
{
await _notice.Open(new NotificationConfig()
{
Message = "提示",
Description = "课堂指标 总分不满100!请完善",
NotificationType = NotificationType.Warning
});
modalBtnLoading = false;
return;
}
await criteria.DeleteAsync(s => s.Subject == editSubject);
await criteria.InsertRangeAsync(_editSource);
_table.ReloadData();
modalShow = false;
modalBtnLoading = false;
StateHasChanged();
}
/// <summary>
/// 分页 查询 筛选 时
/// </summary>
/// <param name="query"></param>
async Task OnChange(QueryModel<CourseGradingCriteria> query)
{
tableLoading = true;
List<IConditionalModel> where = default!;
if (query.FilterModel != null && ((query.FilterModel?.Count() ?? 0) > 0))
{
where = query.ToSqlSugerWhere();
}
_dataSource = await criteria.AsQueryable()
.Where(where)
.ToPageListAsync(query.PageIndex, query.PageSize, _total);
tableLoading = false;
StateHasChanged();
}
/// <summary>
/// 删除行
/// </summary>
/// <param name="row"></param>
/// <returns></returns>
async Task Delete(CourseGradingCriteria row)
{
if (!await Comfirm($"确定要删除这条数据吗? [{row.NamePrompt}]?"))
return;
await criteria.DeleteByIdAsync(row.Id);
_table.ReloadData();
}
/// <summary>
/// 初始化
/// </summary>
protected override void OnInitialized()
{
}
private async Task<bool> Comfirm(string message)
{
return await ComfirmService.Show(message, "提示", ConfirmButtons.YesNo, ConfirmIcon.Warning) == ConfirmResult.Yes;
}
}
}

View File

@ -1,9 +0,0 @@
input[aria-hidden="true"] {
display: none !important;
}
.displayNone {
display:none !important;
}
.ant-table-pagination {
display:none !important;
}

View File

@ -1,54 +0,0 @@
@page "/Login"
@using AntDesign
@using AntDesign.TableModels
@using System.ComponentModel.DataAnnotations
@using SqlSugar
@using VideoAnalysisCore.Model
@using VideoAnalysisCore.Model.Dto
@attribute [ReuseTabsPage(Ignore = true)]
<section style="width:100%;height:100%">
<!-- 背景颜色 -->
<div class="color"></div>
<div class="color"></div>
<div class="color"></div>
<div class="box">
<!-- 背景圆 -->
<div class="circle" style="--x:0"></div>
<div class="circle" style="--x:1"></div>
<div class="circle" style="--x:2"></div>
<div class="circle" style="--x:3"></div>
<div class="circle" style="--x:4"></div>
<!-- 登录框 -->
<div class="container">
<div class="form">
<h2>登录 视频分析平台</h2>
<div class="cform">
<div class="inputBox">
<input type="text" placeholder="账号" @bind="InputAccount">
</div>
<div class="inputBox">
<input type="password" placeholder="密码" @bind="InputPassword">
</div>
<div class="inputBox">
<input type="button" class="submit" value="登录" @onclick="() => LoginFunAsync()">
</div>
@* <p class="forget">
忘记密码?
<a href="#">
点击这里
</a>
</p> *@
@* <p class="forget">
没有账户?
<a href="#">
注册
</a>
</p> *@
</div>
</div>
</div>
</div>
</section>

View File

@ -1,72 +0,0 @@
using AntDesign;
using AntDesign.TableModels;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Server.ProtectedBrowserStorage;
using Microsoft.AspNetCore.DataProtection.KeyManagement;
using Microsoft.AspNetCore.Mvc;
using SqlSugar;
using System.Linq.Expressions;
using System.Threading.Tasks;
using VideoAnalysisCore.Common;
using VideoAnalysisCore.Model.Enum;
using VideoAnalysisCore.Model;
using VideoAnalysisCore.Model.Dto;
namespace Learn.VideoAnalysis.Components.Pages
{
public partial class Login : ComponentBase
{
[Inject] IHttpContextAccessor HttpContextAccessor { get; set; } = default!;
[Inject] private Repository<VideoTask> taskDB { get; set; } = default!;
[Inject] private NavigationManager NavigationManager { get; set; } = default!;
[Inject] private INotificationService _notice { get; set; } = default!;
[Inject] private ProtectedSessionStorage session { get; set; } = default!;
/// <summary>
/// 输入的账号
/// </summary>
public string InputAccount = string.Empty;
/// <summary>
/// 输入的密码
/// </summary>
public string InputPassword= string.Empty;
/// <summary>
/// 初始化
/// </summary>
protected override void OnInitialized()
{
}
/// <summary>
/// 登录函数
/// </summary>
public async Task LoginFunAsync()
{
if (string.IsNullOrEmpty(InputAccount) || string.IsNullOrEmpty(InputPassword))
{
await _notice.Open(new NotificationConfig()
{
Message = "提示",
Description = "账号/密码必填",
NotificationType = NotificationType.Warning
});
}
if (InputAccount ==AppCommon.Config.Admin.Account && InputPassword == AppCommon.Config.Admin.Password)
{
await session.SetAsync("Login", true);
NavigationManager.NavigateTo("/");
}
else
{
await _notice.Open(new NotificationConfig()
{
Message = "提示",
Description = "账号/密码输入错误",
NotificationType = NotificationType.Warning
});
}
}
}
}

View File

@ -1,242 +0,0 @@
input[aria-hidden="true"] {
display: none !important;
}
/* 清除浏览器默认边距
使边框和内边距的值包含在元素的width和height内 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
/* 使用flex布局让内容垂直和水平居中 */
section {
/* 相对定位 */
position: relative;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
/* linear-gradient() 函数用于创建一个表示两种或多种颜色线性渐变的图片 */
background: linear-gradient(to bottom, #f1f4f9, #dff1ff);
}
/* 背景颜色 */
section .color {
/* 绝对定位 */
position: absolute;
/* 使用filter(滤镜) 属性,给图像设置高斯模糊*/
filter: blur(200px);
}
/* :nth-child(n) 选择器匹配父元素中的第 n 个子元素 */
section .color:nth-child(1) {
top: -350px;
width: 600px;
height: 600px;
background: #ff359b;
}
section .color:nth-child(2) {
bottom: -150px;
left: 100px;
width: 500px;
height: 500px;
background: #fffd87;
}
section .color:nth-child(3) {
bottom: 50px;
right: 100px;
width: 500px;
height: 500px;
background: #00d2ff;
}
.box {
position: relative;
}
/* 背景圆样式 */
.box .circle {
position: absolute;
background: rgba(255, 255, 255, 0.1);
/* backdrop-filter属性为一个元素后面区域添加模糊效果 */
backdrop-filter: blur(5px);
box-shadow: 0 25px 45px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(255, 255, 255, 0.5);
border-right: 1px solid rgba(255, 255, 255, 0.2);
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 50%;
/* 使用filter(滤镜) 属性改变颜色
hue-rotate(deg) 给图像应用色相旋转
calc() 函数用于动态计算长度值
var() 函数调用自定义的CSS属性值x*/
filter: hue-rotate(calc(var(--x) * 70deg));
/* 调用动画animate需要10s完成动画
linear表示动画从头到尾的速度是相同的
infinite指定动画应该循环播放无限次*/
animation: animate 10s linear infinite;
/* 动态计算动画延迟几秒播放 */
animation-delay: calc(var(--x) * -1s);
}
/* 背景圆动画 */
@keyframes animate {
0%, 100%, {
transform: translateY(-50px);
}
50% {
transform: translateY(50px);
}
}
.box .circle:nth-child(1) {
top: -50px;
right: -60px;
width: 100px;
height: 100px;
}
.box .circle:nth-child(2) {
top: 150px;
left: -100px;
width: 120px;
height: 120px;
z-index: 2;
}
.box .circle:nth-child(3) {
bottom: 50px;
right: -60px;
width: 80px;
height: 80px;
z-index: 2;
}
.box .circle:nth-child(4) {
bottom: -80px;
left: 100px;
width: 60px;
height: 60px;
}
.box .circle:nth-child(5) {
top: -80px;
left: 140px;
width: 60px;
height: 60px;
}
/* 登录框样式 */
.container {
position: relative;
width: 400px;
min-height: 400px;
height: 400px;
background: rgba(255, 255, 255, 0.1);
display: flex;
justify-content: center;
align-items: center;
backdrop-filter: blur(5px);
box-shadow: 0 25px 45px rgba(0, 0, 0, 0.1);
border: 1px solid rgba(255, 255, 255, 0.5);
border-right: 1px solid rgba(255, 255, 255, 0.2);
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
}
.form {
position: relative;
width: 100%;
height: 100%;
padding: 50px;
background: rgba(255, 255, 255, 0.1);
}
/* 登录标题样式 */
.form h2 {
position: relative;
color: #fff;
font-size: 24px;
font-weight: 600;
letter-spacing: 5px;
margin-bottom: 30px;
cursor: pointer;
}
/* 登录标题的下划线样式 */
.form h2::before {
content: "";
position: absolute;
left: 0;
bottom: -10px;
width: 0px;
height: 3px;
background: #fff;
transition: 0.5s;
}
.form h2:hover:before {
width: 53px;
}
.form .inputBox {
width: 100%;
margin-top: 20px;
}
/* 输入框样式 */
.form .inputBox input {
width: 100%;
padding: 10px 20px;
background: rgba(255, 255, 255, 0.2);
outline: none;
border: none;
border-radius: 30px;
border: 1px solid rgba(255, 255, 255, 0.5);
border-right: 1px solid rgba(255, 255, 255, 0.2);
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
font-size: 16px;
letter-spacing: 1px;
color: #fff;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.05);
}
.form .inputBox input::placeholder {
color: #fff;
}
/* 登录按钮样式 */
.submit {
background: #fff !important;
color: #666 !important;
max-width: 100px;
margin-bottom: 20px;
font-weight: 600;
cursor: pointer !important;
}
.forget {
margin-top: 6px;
color: #fff;
letter-spacing: 1px;
}
.forget a {
color: #fff;
font-weight: 600;
text-decoration: none;
}

View File

@ -1,56 +0,0 @@
@page "/NodeSubscriptionPage"
@using AntDesign
@using AntDesign.TableModels
@using System.ComponentModel.DataAnnotations
@using SqlSugar
@using VideoAnalysisCore.Model
@using UserCenter.Model.Enum
<Table @ref="_table" Loading="tableLoading" TItem="NodeSubscription" ScrollY="600px"
PageSize="20" Total="_total" DataSource="_dataSource" @bind-SelectedRows="_selectedRows" OnChange="OnChange">
<TitleTemplate>
<Flex Justify="end" Gap="10">
</Flex>
</TitleTemplate>
<ColumnDefinitions Context="row">
<ActionColumn Title="操作" Width="130px">
<Button Type="primary" @onclick="()=> StartEdit(row)">编辑</Button>
</ActionColumn>
<PropertyColumn Property="c=>c.Id" Width="130px" Filterable="true" Sortable="true" />
<PropertyColumn Property="c=>c.Subject" Filterable="true" Width="130px" />
<PropertyColumn Property="c=>c.TaskType" Width="230px" Filterable="true" />
<PropertyColumn Property="c=>c.NodeId" Width="130px" Filterable="true" />
<PropertyColumn Property="c=>c.Enable" Width="100px" Filterable="true" />
<PropertyColumn Property="c=>c.LastId" Width="200px" />
<PropertyColumn Property="c=>c.CreateTime" />
</ColumnDefinitions>
</Table>
@{
RenderFragment modelfooter = @<Template>
<Button OnClick="@EditOnOkAsync" @key="@( "submit" )"
Type="primary"
Loading="@modalBtnLoading">
提交
</Button>
<Button OnClick="()=>modalShow = false" @key="@( "back" )">取消</Button>
</Template>;
}
<Modal Title="@("编辑订阅节点")" Visible="modalShow" Width="650"
Footer="@modelfooter">
<Form @ref="form" Model="rowData" LabelAlign="AntLabelAlignType.Left">
<GridRow>
<GridCol Span="24">
<FormItem Label="是否启用">
<Switch Rows="1" @bind-Value="@context.Enable" />
</FormItem>
</GridCol>
</GridRow>
</Form>
</Modal>
@code
{
}

View File

@ -1,131 +0,0 @@
using AntDesign;
using AntDesign.TableModels;
using FreeRedis;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using SqlSugar;
using System.Drawing;
using System.Linq.Expressions;
using System.Reflection;
using UserCenter.Model;
using UserCenter.Model.Enum;
using VideoAnalysisCore.Common;
using VideoAnalysisCore.Model.Enum;
using VideoAnalysisCore.Model;
using Learn.VideoAnalysis.API.Expand;
namespace Learn.VideoAnalysis.Components.Pages
{
public partial class NodeSubscriptionPage : ComponentBase
{
[Inject] private ConfirmService ComfirmService { get; set; } = default!;
[Inject] private ModalService ModalService { get; set; } = default!;
[Inject] private Repository<NodeSubscription> criteria { get; set; } = default!;
[Inject] private INotificationService _notice { get; set; } = default!;
IEnumerable<NodeSubscription> _selectedRows = [];
ITable _table;
IForm? form;
List<NodeSubscription> _dataSource = null;
RefAsync<int> _total = 0;
bool tableLoading = false;
Table<NodeSubscription> tableRef;
List<NodeSubscription> _editSource = null;
bool modalShow =false;
bool modalBtnLoading = false;
NodeSubscription rowData;
SubjectEnum editSubject;
async Task SubjectEnumSelect()
{
_editSource = await criteria.GetListAsync(x => x.Subject == editSubject);
await this.InvokeAsync(StateHasChanged);
}
void EditAddRow()
{
if (form is not null && form.Validate())
{
var data = rowData;
data.Subject = editSubject;
if (_editSource is null)
_editSource = new() { data };
else
_editSource.Add(data);
rowData = new();
StateHasChanged();
}
}
/// <summary>
/// 新增或者修改
/// </summary>
void StartEdit(NodeSubscription data)
{
rowData = data?? new();
modalShow = true;
}
async Task EditOnOkAsync()
{
var data = rowData;
modalBtnLoading = true;
await criteria.UpdateAsync(data);
_table.ReloadData();
modalShow = false;
modalBtnLoading = false;
StateHasChanged();
}
/// <summary>
/// 分页 查询 筛选 时
/// </summary>
/// <param name="query"></param>
async Task OnChange(QueryModel<NodeSubscription> query)
{
tableLoading = true;
List<IConditionalModel> where = default!;
if (query.FilterModel != null && ((query.FilterModel?.Count() ?? 0) > 0))
{
where = query.ToSqlSugerWhere();
}
_dataSource = await criteria.AsQueryable()
.Where(where)
.ToPageListAsync(query.PageIndex, query.PageSize, _total);
tableLoading = false;
StateHasChanged();
}
/// <summary>
/// 删除行
/// </summary>
/// <param name="row"></param>
/// <returns></returns>
async Task Delete(NodeSubscription row)
{
if (!await Comfirm($"确定要删除这条数据吗? [{row.NodeId}]?"))
return;
await criteria.DeleteByIdAsync(row.Id);
_table.ReloadData();
}
/// <summary>
/// 初始化
/// </summary>
protected override void OnInitialized()
{
}
private async Task<bool> Comfirm(string message)
{
return await ComfirmService.Show(message, "提示", ConfirmButtons.YesNo, ConfirmIcon.Warning) == ConfirmResult.Yes;
}
}
}

View File

@ -1,9 +0,0 @@
input[aria-hidden="true"] {
display: none !important;
}
.displayNone {
display:none !important;
}
.ant-table-pagination {
display:none !important;
}

View File

@ -1,103 +0,0 @@
@page "/"
@using AntDesign
@using AntDesign.TableModels
@using System.ComponentModel.DataAnnotations
@using VideoAnalysisCore.Controllers.Dto
@using SqlSugar
@using VideoAnalysisCore.Model
@using VideoAnalysisCore.Model.Dto
@using VideoAnalysisCore.Model.Enum;
<Table @ref="_table" Loading="tableLoading" TItem="VideoTaskDto" ScrollY="600px" PageSize="10" Total="_total" DataSource="_dataSource"
OnRowClick="(r)=>r.Expanded = !r.Expanded"
@bind-SelectedRows="_selectedRows" OnChange="OnChange"
OnExpand="OnExpand">
<TitleTemplate>
<Flex Justify="end" Gap="10">
<Button Type="primary" @onclick="ShowErrorTask">错误任务</Button>
</Flex>
</TitleTemplate>
<ColumnDefinitions Context="row">
<Selection />
<PropertyColumn Property="c=>c.Id" Width="110px" Filterable="true" Sortable="true" />
<PropertyColumn Property="c=>c.TagId" Width="160px" Filterable="true" />
<PropertyColumn Property="c=>c.VideoType" Width="100px" />
<PropertyColumn Property="c=>c.LastEnum" Width="150px" />
<PropertyColumn Property="c=>c.Subject" Width="100px" />
<PropertyColumn Property="c=>c.ComeFrom" Width="100px" />
<PropertyColumn Property="c=>c.MediaUrl" Width="320px" />
<PropertyColumn Property="c=>c.CreateTime" />
</ColumnDefinitions>
<ExpandTemplate Context="rowData">
<Descriptions Title="任务详情" Bordered>
<DescriptionsItem Title="@rowData.Data.LastEnum.ToString()">
@rowData.Data.Progress
</DescriptionsItem>
<DescriptionsItem Title="操作" Span="2">
<Button Type="@ButtonType.Primary"
Loading="rowRestartLoading"
OnClick="()=>RowRload(rowData)">
刷新数据
</Button>
<Button Type="@ButtonType.Primary" Danger @onclick="() => ReStartClick(rowData.Data)">
重试
</Button>
<Button Type="@ButtonType.Primary" Icon="@IconType.Outline.Search" @onclick="() => PreviewTask(rowData.Data)">
预览任务
</Button>
</DescriptionsItem>
<DescriptionsItem Title="任务时间轴" Span="6">
<Steps Current="@((int)rowData.Data.LastEnum)" Status="@rowData.Data.TaskStatus">
<Step Title="下载文件"
Description="@RowST(rowData,RedisChannelEnum.下载文件)" />
<Step Title="分离音频"
Description="@RowST(rowData,RedisChannelEnum.分离音频)" />
<Step Title="解析字幕"
Description="@RowST(rowData,RedisChannelEnum.解析字幕)" />
<Step Title="AI模型分析"
Description="@RowST(rowData,RedisChannelEnum.AI模型分析)" />
<Step Title="AI分析试题"
Description="@RowST(rowData,RedisChannelEnum.AI分析试题)" />
<Step Title="结束任务"
Description="@RowST(rowData,RedisChannelEnum.结束任务)" />
</Steps>
</DescriptionsItem>
@if (!string.IsNullOrEmpty(@rowData.Data.ErrorMessage))
{
<DescriptionsItem Title="任务异常" Span="3">
@rowData.Data.ErrorMessage
</DescriptionsItem>
}
</Descriptions>
</ExpandTemplate>
</Table>
<Modal Title="重试任务"
Width="400"
OnOk="ReStart"
@bind-Visible="@modalShow">
<Title Level="3">ID : @reStartTask.Id</Title>
<p></p>
<p>将从哪个步骤重试?</p>
<Select Style="width:220px;"
DataSource="SelectDataSource"
LabelName="@nameof(TextValue.Text)"
ValueName="@nameof(TextValue.Value)"
@bind-Value="@selectEnum">
</Select>
<br />
<br />
</Modal>

View File

@ -1,187 +0,0 @@
using AntDesign;
using AntDesign.TableModels;
using FFmpeg.NET.Services;
using Learn.VideoAnalysis.API.Expand;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.DataProtection.KeyManagement;
using SqlSugar;
using System.Data.Common;
using System.Linq.Expressions;
using System.Threading.Tasks;
using VideoAnalysisCore.Common;
using VideoAnalysisCore.Controllers.Dto;
using VideoAnalysisCore.Model;
using VideoAnalysisCore.Model.Dto;
using VideoAnalysisCore.Model.Enum;
namespace Learn.VideoAnalysis.Components.Pages
{
public partial class VideoTaskPage : ComponentBase
{
[Inject] private ConfirmService ComfirmService { get; set; } = default!;
[Inject] private Repository<VideoTask> taskDB { get; set; } = default!;
[Inject] private NavigationManager NavigationManager { get; set; } = default!;
[Inject] private RedisManager redisManager { get; set; } = default!;
[Inject] private INotificationService _notice { get; set; } = default!;
IEnumerable<VideoTaskDto> _selectedRows = [];
ITable _table;
List<VideoTaskDto> _dataSource = null;
RefAsync<int> _total = 0;
bool modalShow = false;
bool tableLoading = false;
private VideoTaskDto selectData;
private bool rowRestartLoading = false;
private VideoTaskDto reStartTask;
static TextValue[] SelectDataSource =
Enum.GetValues(typeof(RedisChannelEnum))
.Cast<RedisChannelEnum>()
.Select(s => new TextValue(s.ToString(), (int)s))
.ToArray();
int selectEnum = 1;
int selectDefaultValue = 1;
/// <summary>
/// 点击重试
/// </summary>
/// <param name="query"></param>
async Task ReStartClick(VideoTaskDto query)
{
selectDefaultValue =
(await redisManager.Redis.HMGetAsync<int>(RedisExpandKey.Task(query.Id), "LastEnum")).FirstOrDefault();
selectEnum = selectDefaultValue;
reStartTask = query;
modalShow = true;
}
void PreviewTask(VideoTaskDto task)
{
NavigationManager.NavigateTo("/VideoTaskShow/" + task.Id);
}
/// <summary>
/// 重试
/// </summary>
/// <param name="query"></param>
async Task ReStart()
{
await redisManager.ClearTaskError(reStartTask.Id);
await Task.Run(async () =>
await redisManager.InsertChannel((RedisChannelEnum)selectEnum, reStartTask.Id)
);
modalShow = false;
}
private QueryModel<VideoTaskDto> lastQuery = null;
/// <summary>
/// 分页 查询 筛选 时
/// </summary>
/// <param name="query"></param>
/// <param name="changed"></param>
async Task ShowErrorTask(MouseEventArgs e)
{
_dataSource = await taskDB.AsQueryable()
.Where(s => s.ErrorMessage != null && s.ErrorMessage != string.Empty)
.Select<VideoTaskDto>()
.ToListAsync();
_total = _dataSource.Count();
tableLoading = false;
StateHasChanged();
}
/// <summary>
/// 分页 查询 筛选 时
/// </summary>
/// <param name="query"></param>
async Task OnChange(QueryModel<VideoTaskDto> query)
{
lastQuery = query;
tableLoading = true;
List<IConditionalModel> where = default!;
if (query.FilterModel != null && ((query.FilterModel?.Count() ?? 0) > 0))
{
where = query.ToSqlSugerWhere();
}
_dataSource = await taskDB.AsQueryable()
.Where(where)
.Select<VideoTaskDto>()
.OrderByDescending(s => s.Id)
.ToPageListAsync(query.PageIndex, query.PageSize, _total);
tableLoading = false;
StateHasChanged();
}
/// <summary>
/// 刷新数据
/// </summary>
/// <param name="rowData"></param>
public void RowRload(RowData<VideoTaskDto> rowData)
{
rowRestartLoading = true;
var item = rowData.Data;
if (item is null)
return;
var data = redisManager.Redis.HMGet<string>(RedisExpandKey.Task(item.Id),
"Progress", "LastEnum", "StartTime", "ErrorMessage");
item.Progress = data[0];
item.LastEnum = data[1] == null ? default : data[1].ToEnum<RedisChannelEnum>() ?? default;
item.StartTimeDic = data[2] == null ? null : System.Text.Json.JsonSerializer.Deserialize<Dictionary<RedisChannelEnum, DateTime>>(data[2]) ?? null;
item.ErrorMessage = data[3];
rowRestartLoading = false;
var statusStr = "wait";
var dic = rowData.Data.StartTimeDic;
if (dic is null)
statusStr = "wait";
else if (!string.IsNullOrEmpty(rowData.Data.ErrorMessage))
statusStr = "error";
else if (dic.ContainsKey(RedisChannelEnum.))
statusStr = "finish";
item.TaskStatus = statusStr;
StateHasChanged();
}
/// <summary>
///
/// </summary>
/// <returns></returns>
private string RowST(RowData<VideoTaskDto> rowData, RedisChannelEnum e)
{
var dic = rowData.Data.StartTimeDic;
if (dic is null || !dic.ContainsKey(e))
return "--";
return dic[e].ToString();
}
private void OnExpand(RowData<VideoTaskDto> rowData)
{
if (rowData.Expanded)
RowRload(rowData);
}
/// <summary>
/// 在渲染页面之后
/// </summary>
/// <param name="firstRender"></param>
/// <returns></returns>
protected override async Task OnAfterRenderAsync(bool firstRender)
{
}
/// <summary>
/// 初始化
/// </summary>
protected override void OnInitialized()
{
}
private async Task<bool> Comfirm(string message)
{
return await ComfirmService.Show(message, "提示", ConfirmButtons.YesNo, ConfirmIcon.Warning) == ConfirmResult.Yes;
}
}
}

View File

@ -1,8 +0,0 @@
input[aria-hidden="true"] {
display: none !important;
}
.task_status_tag {
display:flex;
}

View File

@ -1,135 +0,0 @@
@page "/VideoTaskShow/{taskId:long}"
<div id="video-container">
@if (videoKnows != null)
{
<div id="segmentsContainer" class="sc">
<h2>
<button class="gudingBtn" onclick="gd(this)">🔒</button></h2>
@for (int i = 0; i < videoKnows.Length; i++)
{
var item = videoKnows[i];
<div class="knowDiv">
<div class="knowTtile">
<div style="cursor: pointer" onclick="spClick(@i,this)">
<div class="knowTtileTheme">@getF(item) @item.Theme</div>
<span class="kSpan">#@item.KnowPointId @item.KnowPoint</span>
</div>
<div>概览: @item.Content</div>
<br />
@if (item.QuestionArr != null)
{
@foreach (var q in item.QuestionArr)
{
<div class="knowQuestion" onclick="spClickTime(@q.StartTime)">
<h3>问题: <span class="kSpan">@q.StartTime 秒</span></h3>
<div class="kSpan">@q.TopicStem</div>
<div>@q.Question</div>
<img style="text-align:center" src="@q.PPTImageUrl" width="320" height="180" />
</div>
<br />
}
}
<br />
<br />
</div>
<button class="kBtn" >
<span>@getF(item) @item.Theme</span>
<br /><span class="kSpan textEllipsis">#@item.KnowPointId @item.KnowPoint</span>
</button>
</div>
}
</div>
}
<video id="videoPlayer" controls autoplay>
<source type="video/mp4" />
</video>
<div id="subtitleArea" class="subtitles"></div>
<div id="subtitleArea1" class="subtitles" style="
bottom: 101px;
background-color: rgb(99 129 103 / 50%);
"></div>
</div>
<script defer src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js" crossorigin="anonymous"></script>
<script>
MathJax = {
tex: {
inlineMath: [['$', '$'], ['\(', '\)']],
displayMath: [['$$', '$$'], ['\[', '\]']]
}
};
window.b = []
window.subtitles = []
window.subtitles1 = []
var displayButton = []
var lastSegments = null;
function init() {
const videoPlayer = document.getElementById('videoPlayer');
const subtitleArea = document.getElementById('subtitleArea');
const subtitleArea1 = document.getElementById('subtitleArea1');
//视频时间变化时
videoPlayer.addEventListener('timeupdate', function () {
if (displayButton.length == 0) initKD()
const currentTime = videoPlayer.currentTime;
subtitleArea.textContent = '';
subtitleArea1.textContent = '';
//字幕
window.subtitles.forEach(subtitle => {
let textContent = subtitle.text
if (currentTime >= subtitle.start
&& currentTime <= subtitle.end
&& subtitleArea.textContent != textContent) {
subtitleArea.textContent = textContent;
subtitleArea1.textContent = window.subtitles1[window.subtitles.indexOf(subtitle)].text;
}
});
//时间片
let segment = displayButton.findLast(s => currentTime >= s.startTime)
if (segment) {
segment.button.style.backgroundColor = "rgb(238, 200, 118)";
if (lastSegments && lastSegments != segment) lastSegments.button.style.backgroundColor = "rgb(240, 249, 235)";
lastSegments = segment
}
});
}
function initKD() {
let btns = document.getElementsByClassName("kBtn")
if(btns.length == 0) return
displayButton = window.b.map((s, i) => { return { ...s, button: btns[i] } })
}
//后端传递初始化数据
function setDB(a,a1, b,c) {
console.log("setDB", a1,a, b, c)
window.subtitles = a
window.subtitles1 = a1
window.b = b
const videoPlayer = document.getElementById('videoPlayer');
videoPlayer.src = c
init()
}
//点击时间片时
function spClick(i, button) {
videoPlayer.currentTime = displayButton[i].startTime;
}
function spClickTime(startTime) {
videoPlayer.currentTime = startTime;
}
function gd(btn) {
let e = btn.parentElement.parentElement
if (e.style.right == "0px") {
btn.innerHTML = "🔒"
e.style.right = "-300px"
} else {
e.style.right = "0px"
btn.innerHTML = "🔓"
}
}
</script>

View File

@ -1,129 +0,0 @@
using AntDesign;
using AntDesign.TableModels;
using FFmpeg.NET.Services;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.DataProtection.KeyManagement;
using Microsoft.JSInterop;
using SqlSugar;
using System.Linq.Expressions;
using System.Threading.Tasks;
using VideoAnalysisCore.AICore.GPT.Dto;
using VideoAnalysisCore.AICore.SherpaOnnx;
using VideoAnalysisCore.Common;
using VideoAnalysisCore.Model.Enum;
using VideoAnalysisCore.Model;
using VideoAnalysisCore.Model.Dto;
using static System.Runtime.InteropServices.JavaScript.JSType;
using System.Text.Json;
namespace Learn.VideoAnalysis.Components.Pages
{
public partial class VideoTaskShow : ComponentBase
{
/// <summary>
/// 任务id
/// </summary>
[Parameter]
public long? taskId { get; set; }
[Inject] private ConfirmService ComfirmService { get; set; } = default!;
[Inject] private IHttpContextAccessor HttpContext { get; set; } = default!;
[Inject] private Repository<VideoTask> taskDB { get; set; } = default!;
[Inject] private Repository<VideoQuestion> videoQuestionDB { get; set; } = default!;
[Inject] private Repository<VideoQuestionKonw> videoQuestionKonwDB { get; set; } = default!;
[Inject] private Repository<VideoKonwPoint> videoKonwPointDB { get; set; } = default!;
[Inject] private RedisManager redisManager { get; set; } = default!;
[Inject] private IJSRuntime JSRuntime { get; set; } = default!;
private VideoTask nowTask { get; set; } = default!;
private string videoPath { get; set; } = default!;
/// <summary>
/// 分段
/// </summary>
private VideoKnowRes[] videoKnows { get; set; } = default!;
/// <summary>
/// 在渲染页面之后
/// </summary>
/// <param name="firstRender"></param>
/// <returns></returns>
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
}
}
public string getF(VideoKnowRes segment)
{
var sf = ((int)((segment.StartTime ?? 0) / 60)).ToString().PadLeft(2, '0');
var sm = ((int)((segment.StartTime ?? 0) % 60)).ToString().PadLeft(2, '0');
return $"{sf}:{sm}";
//var ef = ((int)((segment.EndTime ?? 0) / 60)).ToString().PadLeft(2, '0');
//var em = ((int)((segment.EndTime ?? 0) % 60)).ToString().PadLeft(2, '0');
//return $"{sf}:{sm} - {ef}: {em}";
}
/// <summary>
/// 初始化
/// </summary>
protected override async void OnInitialized()
{
if (this.taskId is null)
return;
long taskId = this.taskId.Value;
nowTask = await taskDB.GetFirstAsync(s => s.Id == taskId);
if (nowTask is null)
return;
var captionsArr = JsonSerializer.Deserialize<SenseVoiceRes[]>(nowTask.Captions);
var captionsArr1 = JsonSerializer.Deserialize<SenseVoiceRes[]>(nowTask.CaptionsAI??"[]") ;
redisManager.Redis.HMGet<SenseVoiceRes[]>(RedisExpandKey.Task(taskId), "Captions").FirstOrDefault();
var konwArr = await videoKonwPointDB.AsQueryable()
.Where(s => s.VideoTaskId == nowTask.Id)
.ToArrayAsync();
videoKnows = konwArr
.GroupBy(s => s.StartTime)
.Select(s => new VideoKnowRes()
{
Content = s.First().Content,
StartTime = s.First().StartTime,
EndTime = s.First().EndTime,
Theme = s.First().Theme,
StageId = s.First().StageId,
KnowPoint = string.Join(',', s.Select(x => x.KnowPoint))
}).ToArray();
videoPath = nowTask.MediaUrl; //AppCommon.GetVideoPath(nowTask.Id.ToString());
if (nowTask.VideoType == AttachmentsInfoType.)
{
var questionArr = await videoQuestionDB
.AsQueryable().Where(s => s.VideoTaskId == nowTask.Id)
.Select<VideoQuestionShowDto>()
.ToArrayAsync();
var konwDic = (await videoQuestionKonwDB
.AsQueryable().Where(s => s.VideoTaskId == nowTask.Id)
.ToArrayAsync()).GroupBy(s=>s.VideoQuestionId)
.ToDictionary(s=>s.Key);
foreach (var item in questionArr.Where(s=> konwDic.ContainsKey(s.Id)))
item.KonwArr = konwDic[item.Id].ToArray();
foreach (var item in videoKnows)
item.QuestionArr = questionArr
.Where(s => s.StageId == item.StageId).ToArray();
}
await JSRuntime
.InvokeVoidAsync("setDB", captionsArr, captionsArr1, videoKnows, videoPath);
StateHasChanged();
}
private async Task<bool> Comfirm(string message)
{
return await ComfirmService.Show(message, "提示", ConfirmButtons.YesNo, ConfirmIcon.Warning) == ConfirmResult.Yes;
}
}
}

View File

@ -1,118 +0,0 @@

* {
padding: 0;
margin: 0;
}
#video-container {
position: relative;
width: 1660px;
height: 850px;
float: left;
overflow-x: hidden;
}
.kSpan {
color: rgba(120, 120, 120,0.66);
font-size: 0.8rem;
width: 330px;
}
.textEllipsis {
white-space: nowrap; /* 禁止换行 */
overflow: hidden; /* 隐藏溢出内容 */
text-overflow: ellipsis; /* 显示省略号 */
}
video {
width: 94%;
height: 85%;
}
.gudingBtn {
width: 32px;
height: 32px;
/* border-radius: 16px; */
line-height: 27px;
text-align: center;
}
.subtitles {
position: absolute;
bottom: 200px;
width: 100%;
text-align: center;
color: white;
background-color: rgba(0, 0, 0, 0.7);
font-size: 18px;
}
#segmentsContainer:is(:hover) {
right: 0px !important;
}
#segmentsContainer {
transition: right 0.7s;
z-index: 999;
overflow-x: hidden;
background-color: #e3e3e3c2;
position: absolute;
right: -300px;
display: flex;
flex-direction: column;
width: 400px;
height: 750px;
gap: 10px;
overflow-y: scroll;
float: left;
flex-wrap: nowrap;
padding: 10px;
align-content: flex-start;
justify-content: flex-start;
align-items: center;
}
.kBtn {
width: 340px;
height: 60px;
font-size: 1.3rem;
text-align: left;
cursor: pointer;
color: rgb(103, 194, 58);
background-color: rgb(240, 249, 235);
border: 1px solid rgb(179, 225, 157);
}
.knowTtileTheme {
font-size: 1.3rem;
cursor: pointer;
color: rgb(103, 194, 58);
}
.kBtn:hover {
background-color: rgb(248, 230, 191) !important;
border: 1px solid rgb(206, 187, 81);
}
.knowDiv {
}
.knowQuestion {
cursor: pointer;
border: 2px solid #ff000059;
border-radius: 10px;
background-color: #cddc393d;
}
.knowDiv:hover .knowTtile {
width: 340px;
display: block;
background-color: rgb(240, 249, 235);
border: 1px solid rgb(179, 225, 157);
}
.knowTtile {
position: absolute;
text-align: left;
display: none;
}

View File

@ -1,6 +0,0 @@
namespace Learn.VideoAnalysis.Components.Resources;
internal class I18n
{
}

View File

@ -1,129 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="menu.account.center" xml:space="preserve">
<value>Account Center</value>
</data>
<data name="menu.account.logout" xml:space="preserve">
<value>Logout</value>
</data>
<data name="menu.account.settings" xml:space="preserve">
<value>Settings</value>
</data>
</root>

View File

@ -1,129 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="menu.account.center" xml:space="preserve">
<value>个人中心</value>
</data>
<data name="menu.account.logout" xml:space="preserve">
<value>退出登录</value>
</data>
<data name="menu.account.settings" xml:space="preserve">
<value>个人设置</value>
</data>
</root>

View File

@ -1,18 +0,0 @@
@using Learn.VideoAnalysis.Components.Pages
<Router AppAssembly="typeof(Program).Assembly">
<Found Context="routeData">
<CascadingValue Value="routeData">
@if (routeData.PageType == typeof(Login))
{
<RouteView RouteData="@routeData" />
}
else
{
<RouteView RouteData="routeData" DefaultLayout="typeof(VideoAnalysisRazor.Layouts.BasicLayout)" />
}
</CascadingValue>
<FocusOnNavigate RouteData="routeData" Selector="h1" />
</Found>
</Router>
<AntContainer />

View File

@ -1,15 +0,0 @@
@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using static Microsoft.AspNetCore.Components.Web.RenderMode
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using AntDesign
@using AntDesign.ProLayout
@using AntDesign.Extensions.Localization
@using Learn.VideoAnalysis
@using VideoAnalysisRazor
@using Learn.VideoAnalysis.Components

View File

@ -0,0 +1,70 @@
using System.IdentityModel.Tokens.Jwt;
using Microsoft.Extensions.DependencyInjection;
using System.Net;
using Microsoft.IdentityModel.Tokens;
using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using VideoAnalysisCore.Common;
namespace Learn.Archives.API.Expand
{
public static class AuthorizeExpand
{
public static IServiceCollection AddPermissionAuthentication(this IServiceCollection services)
{
services.AddAuthentication()
.AddJwtBearer(Authentication.vdAdmin, options =>
{
options.RequireHttpsMetadata = false;
options.UseSecurityTokenValidators = true;
options.MapInboundClaims = false; // .NET 5+
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
options.TokenValidationParameters = new TokenValidationParameters
{
SaveSigninToken = false,//保存token,后台验证token是否生效(重要)
RequireExpirationTime = true, // 设置请求需要携带accesstoken的过期时间
ValidateIssuer = false,//必须验证签发人
ValidateAudience = false,//验证受众
ValidateLifetime = true,//是否验证Token有效期
ValidateIssuerSigningKey = true,//是否验证签名,不验证 会被篡改数据,不安全
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(AppCommon.Config.AuthKey.Secret)),//解密的密钥
};
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var token = context.Request.Headers["Authorization"].FirstOrDefault();
// 3. 安全提取令牌
if (!string.IsNullOrEmpty(token) && token.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
{
// 移除"Bearer "前缀并清除两端空格
token = token.Substring("Bearer ".Length).Trim();
context.Token = token;
}
return Task.CompletedTask;
},
OnAuthenticationFailed = context =>
{
context.Response.StatusCode = 403;
return Task.CompletedTask;
},
OnChallenge = context =>
{
context.HandleResponse();
if (context.Response.StatusCode == 403)
return Task.CompletedTask;
context.Response.Clear();
context.Response.ContentType = "application/json";
context.Response.StatusCode = 401;
var data = new { Code = 401, Message = context.Error + context.AuthenticateFailure?.Message };
context.Response.WriteAsync(data.ToJson());
return Task.CompletedTask;
}
};
});
return services;
}
}
}

View File

@ -1,48 +0,0 @@
using SqlSugar;
using AntDesign;
using AntDesign.TableModels;
namespace Learn.VideoAnalysis.API.Expand
{
public static class SearchExpand
{
/// <summary>
/// 转换 ant 查询枚举 到 sqlsuger枚举
/// </summary>
/// <param name="filterOperator">ant 查询枚举</param>
/// <returns></returns>
/// <exception cref="ArgumentOutOfRangeException"></exception>
public static ConditionalType ConvertToConditionalType(TableFilterCompareOperator filterOperator)
{
return filterOperator switch
{
TableFilterCompareOperator.Equals => ConditionalType.Equal,
TableFilterCompareOperator.Contains => ConditionalType.Like,
TableFilterCompareOperator.StartsWith => ConditionalType.LikeLeft,
TableFilterCompareOperator.EndsWith => ConditionalType.LikeRight,
TableFilterCompareOperator.GreaterThan => ConditionalType.GreaterThan,
TableFilterCompareOperator.LessThan => ConditionalType.LessThan,
TableFilterCompareOperator.GreaterThanOrEquals => ConditionalType.GreaterThanOrEqual,
TableFilterCompareOperator.LessThanOrEquals => ConditionalType.LessThanOrEqual,
TableFilterCompareOperator.Condition => ConditionalType.In,
TableFilterCompareOperator.NotEquals => ConditionalType.NoEqual,
TableFilterCompareOperator.IsNull => ConditionalType.IsNullOrEmpty,
TableFilterCompareOperator.IsNotNull => ConditionalType.IsNot,
TableFilterCompareOperator.NotContains => ConditionalType.NoLike,
TableFilterCompareOperator.TheSameDateWith => ConditionalType.EqualNull,
TableFilterCompareOperator.Between => ConditionalType.Range,
_ => throw new ArgumentOutOfRangeException(nameof(filterOperator), filterOperator, "未知的枚举类型!")
};
}
public static List<IConditionalModel> ToSqlSugerWhere(this QueryModel qm)
{
return qm.FilterModel.SelectMany(s => s.Filters.Select(x => new ConditionalModel()
{
FieldName = s.FieldName,
ConditionalType = ConvertToConditionalType(x.FilterCompareOperator),
FieldValue = x.Value.GetType().IsEnum ? ((int)x.Value).ToString() : x.Value.ToString(),
} as IConditionalModel)).ToList();
}
}
}

View File

@ -1,5 +1,4 @@
global using AntDesign;

global using VideoAnalysisCore.Model;
global using VideoAnalysisCore.Model.Dto;
global using VideoAnalysisCore.Model.Enum;

View File

@ -18,6 +18,7 @@
<ItemGroup>
<None Remove="Dockerfile" />
<None Remove="sources.list" />
<None Remove="WebUI\dist\index.html" />
</ItemGroup>
<ItemGroup>
@ -27,16 +28,20 @@
<Content Include="sources.list">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="WebUI\dist\index.html">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\VideoAnalysisCore\VideoAnalysisCore.csproj" />
<PackageReference Include="AlibabaCloud.SDK.Vod20170321" Version="3.6.1" />
<PackageReference Include="AntDesign.ProLayout" Version="0.20.3" />
<PackageReference Include="Mapster.DependencyInjection" Version="1.0.2-pre01" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="8.13.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.18" />
<PackageReference Include="Mapster.DependencyInjection" Version="1.0.2-pre01" />
<PackageReference Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.13.0" />
<PackageReference Include="AlibabaCloud.SDK.Vod20170321" Version="3.6.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="8.0.5" />
<PackageReference Include="AntDesign.Extensions.Localization" Version="0.20.3" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="8.0.0" />
<PackageReference Include="System.Net.Http.Json" Version="8.0.0" />
@ -47,16 +52,6 @@
<Content Update="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Update="Components\Pages\NodeSubscriptionPage.razor">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</Content>
<Content Update="Components\Pages\Login.razor">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</Content>
</ItemGroup>
<ItemGroup>
<Folder Include="WebUI\" />
</ItemGroup>
</Project>

View File

@ -1,5 +1,4 @@
using VideoAnalysisCore.Common;
using Learn.VideoAnalysis.Components;
using Microsoft.OpenApi.Models;
using VideoAnalysisCore.AICore.SherpaOnnx;
using Mapster;
@ -100,9 +99,6 @@ namespace Learn.VideoAnalysis
return new HttpClient();
});
//VideoAnalysisRazor.Program.AddClientServices(builder.Services);
builder.Services.AddAntDesign();
builder.Services.AddMapster();
builder.Services.AddCorsExpand();
@ -140,7 +136,7 @@ namespace Learn.VideoAnalysis
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(AppCommon.WebUIFile),
RequestPath = "/web",
RequestPath = "/ui",
});
app.UseAntiforgery();

View File

@ -0,0 +1,4 @@
> 1%
last 2 versions
not dead
not ie 11

View File

@ -0,0 +1,21 @@
node_modules
.DS_Store
dist
dist-ssr
*.local
.eslintcache
report.html
yarn.lock
npm-debug.log*
.pnpm-error.log*
.pnpm-debug.log
tests/**/coverage/
# Editor directories and files
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
tsconfig.tsbuildinfo

View File

@ -0,0 +1,14 @@
# http://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
insert_final_newline = false
trim_trailing_whitespace = false

5
VideoAnalysis/WebUI/.env Normal file
View File

@ -0,0 +1,5 @@
# 平台本地运行端口号
VITE_PORT = 8848
# 是否隐藏首页 隐藏 true 不隐藏 false 勿删除VITE_HIDE_HOME只需在.env文件配置
VITE_HIDE_HOME = false

View File

@ -0,0 +1,17 @@
# 平台本地运行端口号
VITE_PORT = 8848
# 开发环境读取配置文件路径
VITE_PUBLIC_PATH = /
# 开发环境路由历史模式Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数"
VITE_ROUTER_HISTORY = "hash"
# 接口地址
VITE_API_BASEURL = "http://192.168.2.33:5238/api"
# # 接口地址
# VITE_API_BASEURL = "https://learn-archives-admin-dev.23544.com/api"
# #数据中心后台地址
# VITE_API_USERCENTER_URL = "https://dca.w.23544.com:8843/api"

View File

@ -0,0 +1,19 @@
# 线上环境平台打包路径
VITE_PUBLIC_PATH = /
# 线上环境路由历史模式Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数"
VITE_ROUTER_HISTORY = "hash"
# 是否在打包时使用cdn替换本地库 替换 true 不替换 false
VITE_CDN = false
# 是否启用gzip压缩或brotli压缩分两种情况删除原始文件和不删除原始文件
# 压缩时不删除原始文件的配置gzip、brotli、both同时开启 gzip 与 brotli 压缩、none不开启压缩默认
# 压缩时删除原始文件的配置gzip-clear、brotli-clear、both-clear同时开启 gzip 与 brotli 压缩、none不开启压缩默认
VITE_COMPRESSION = "none"
# 接口地址
VITE_API_BASEURL = "https://learn-archives-admin.23544.com/api"
#数据中心后台地址
VITE_API_USERCENTER_URL = "https://dcb.23544.com/api"

View File

@ -0,0 +1,22 @@
# 预发布也需要生产环境的行为
# https://cn.vitejs.dev/guide/env-and-mode.html#modes
# NODE_ENV = development
VITE_PUBLIC_PATH = /
# 预发布环境路由历史模式Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数"
VITE_ROUTER_HISTORY = "hash"
# 是否在打包时使用cdn替换本地库 替换 true 不替换 false
VITE_CDN = false
# 是否启用gzip压缩或brotli压缩分两种情况删除原始文件和不删除原始文件
# 压缩时不删除原始文件的配置gzip、brotli、both同时开启 gzip 与 brotli 压缩、none不开启压缩默认
# 压缩时删除原始文件的配置gzip-clear、brotli-clear、both-clear同时开启 gzip 与 brotli 压缩、none不开启压缩默认
VITE_COMPRESSION = "none"
# 接口地址
VITE_API_BASEURL = "https://learn-archives-admin-dev.23544.com/api"
#数据中心后台地址
VITE_API_USERCENTER_URL = "https://dca.w.23544.com:8843/api"

View File

@ -0,0 +1,20 @@
{
"*.{js,jsx,ts,tsx}": [
"prettier --cache --ignore-unknown --write",
"eslint --cache --fix"
],
"{!(package)*.json,*.code-snippets,.!({browserslist,npm,nvm})*rc}": [
"prettier --cache --write--parser json"
],
"package.json": ["prettier --cache --write"],
"*.vue": [
"prettier --write",
"eslint --cache --fix",
"stylelint --fix --allow-empty-input"
],
"*.{css,scss,html}": [
"prettier --cache --ignore-unknown --write",
"stylelint --fix --allow-empty-input"
],
"*.md": ["prettier --cache --ignore-unknown --write"]
}

View File

@ -0,0 +1,11 @@
{
"default": true,
"MD003": false,
"MD033": false,
"MD013": false,
"MD001": false,
"MD025": false,
"MD024": false,
"MD007": { "indent": 4 },
"no-hard-tabs": false
}

View File

@ -0,0 +1,4 @@
shell-emulator=true
shamefully-hoist=true
enable-pre-post-scripts=false
strict-peer-dependencies=false

View File

@ -0,0 +1 @@
v22.14.0

View File

@ -0,0 +1,9 @@
// @ts-check
/** @type {import("prettier").Config} */
export default {
bracketSpacing: true,
singleQuote: false,
arrowParens: "avoid",
trailingComma: "none"
};

View File

@ -0,0 +1,4 @@
/dist/*
/public/*
public/*
src/style/reset.scss

View File

@ -0,0 +1,20 @@
FROM node:20-alpine as build-stage
WORKDIR /app
RUN corepack enable
RUN corepack prepare pnpm@latest --activate
RUN npm config set registry https://registry.npmmirror.com
COPY .npmrc package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY . .
RUN pnpm build
FROM nginx:stable-alpine as production-stage
COPY --from=build-stage /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020-present, pure-admin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,47 @@
<h1>vue-pure-admin Lite Editionno i18n version</h1>
[![license](https://img.shields.io/github/license/pure-admin/vue-pure-admin.svg)](LICENSE)
**English** | [中文](./README.md)
## Introduce
The simplified version is based on the shelf extracted from [vue-pure-admin](https://github.com/pure-admin/vue-pure-admin), which contains main functions and is more suitable for actual project development. The packaged size is introduced globally [element-plus](https://element-plus.org) is still below `2.3MB`, and the full version of the code will be permanently synchronized. After enabling `brotli` compression and `cdn` to replace the local library mode, the package size is less than `350kb`
## `js` version
[Click me to view js version](https://pure-admin.cn/pages/js/)
## `max` version
[Click me to view the max version](https://pure-admin.cn/pages/max/)
## Supporting video
[Click me to view UI design](https://www.bilibili.com/video/BV17g411T7rq)
[Click me to view the rapid development tutorial](https://www.bilibili.com/video/BV1kg411v7QT)
## Nanny-level documents
[Click me to view vue-pure-admin documentation](https://pure-admin.cn/)
[Click me to view @pureadmin/utils documentation](https://pure-admin-utils.netlify.app)
## Quality service, software outsourcing, sponsorship support
[Click me to view details](https://pure-admin.cn/pages/service/)
## Preview
[Click me to view the preview station](https://pure-admin-thin.netlify.app/#/login)
## Maintainer
[xiaoxian521](https://github.com/xiaoxian521)
## ⚠️ Attention
The Lite version does not accept any issues and prs. If you have any questions, please go to the full version [issues](https://github.com/pure-admin/vue-pure-admin/issues/new/choose) to mention, thank you!
## License
[MIT © 2020-present, pure-admin](./LICENSE)

View File

@ -0,0 +1,51 @@
<h1>vue-pure-admin精简版非国际化版本</h1>
[![license](https://img.shields.io/github/license/pure-admin/vue-pure-admin.svg)](LICENSE)
**中文** | [English](./README.en-US.md)
## 介绍
精简版是基于 [vue-pure-admin](https://github.com/pure-admin/vue-pure-admin) 提炼出的架子,包含主体功能,更适合实际项目开发,打包后的大小在全局引入 [element-plus](https://element-plus.org) 的情况下仍然低于 `2.3MB`,并且会永久同步完整版的代码。开启 `brotli` 压缩和 `cdn` 替换本地库模式后,打包大小低于 `350kb`
## 版本选择
当前是非国际化版本,如果您需要国际化版本 [请点击](https://github.com/pure-admin/pure-admin-thin/tree/i18n)
## `js` 版本
[点我查看 js 版本](https://pure-admin.cn/pages/js/)
## `max` 版本
[点我查看 max 版本](https://pure-admin.cn/pages/max/)
## 配套视频
[点我查看 UI 设计](https://www.bilibili.com/video/BV17g411T7rq)
[点我查看快速开发教程](https://www.bilibili.com/video/BV1kg411v7QT)
## 配套保姆级文档
[点我查看 vue-pure-admin 文档](https://pure-admin.cn/)
[点我查看 @pureadmin/utils 文档](https://pure-admin-utils.netlify.app)
## 优质服务、软件外包、赞助支持
[点我查看详情](https://pure-admin.cn/pages/service/)
## 预览
[查看预览](https://pure-admin-thin.netlify.app/#/login)
## 维护者
[xiaoxian521](https://github.com/xiaoxian521)
## ⚠️ 注意
精简版不接受任何 `issues``pr`,如果有问题请到完整版 [issues](https://github.com/pure-admin/vue-pure-admin/issues/new/choose) 去提,谢谢!
## 许可证
[MIT © 2020-present, pure-admin](./LICENSE)

View File

@ -0,0 +1,55 @@
import { Plugin as importToCDN } from "vite-plugin-cdn-import";
/**
* @description `cdn`使cdn模式 .env.production VITE_CDN true
* cdnhttps://www.bootcdn.cn当然你也可以选择 https://unpkg.com 或者 https://www.jsdelivr.com
* 使jscss文件cdn
*/
export const cdn = importToCDN({
//prodUrl解释 name: 对应下面modules的nameversion: 自动读取本地package.json中dependencies依赖中对应包的版本号path: 对应下面modules的path当然也可写完整路径会替换prodUrl
prodUrl: "https://cdn.bootcdn.net/ajax/libs/{name}/{version}/{path}",
modules: [
{
name: "vue",
var: "Vue",
path: "vue.global.prod.min.js"
},
{
name: "vue-router",
var: "VueRouter",
path: "vue-router.global.min.js"
},
// 项目中没有直接安装vue-demi但是pinia用到了所以需要在引入pinia前引入vue-demihttps://github.com/vuejs/pinia/blob/v2/packages/pinia/package.json#L77
{
name: "vue-demi",
var: "VueDemi",
path: "index.iife.min.js"
},
{
name: "pinia",
var: "Pinia",
path: "pinia.iife.min.js"
},
{
name: "element-plus",
var: "ElementPlus",
path: "index.full.min.js",
css: "index.min.css"
},
{
name: "axios",
var: "axios",
path: "axios.min.js"
},
{
name: "dayjs",
var: "dayjs",
path: "dayjs.min.js"
},
{
name: "echarts",
var: "echarts",
path: "echarts.min.js"
}
]
});

View File

@ -0,0 +1,63 @@
import type { Plugin } from "vite";
import { isArray } from "@pureadmin/utils";
import compressPlugin from "vite-plugin-compression";
export const configCompressPlugin = (
compress: ViteCompression
): Plugin | Plugin[] => {
if (compress === "none") return null;
const gz = {
// 生成的压缩包后缀
ext: ".gz",
// 体积大于threshold才会被压缩
threshold: 0,
// 默认压缩.js|mjs|json|css|html后缀文件设置成true压缩全部文件
filter: () => true,
// 压缩后是否删除原始文件
deleteOriginFile: false
};
const br = {
ext: ".br",
algorithm: "brotliCompress",
threshold: 0,
filter: () => true,
deleteOriginFile: false
};
const codeList = [
{ k: "gzip", v: gz },
{ k: "brotli", v: br },
{ k: "both", v: [gz, br] }
];
const plugins: Plugin[] = [];
codeList.forEach(item => {
if (compress.includes(item.k)) {
if (compress.includes("clear")) {
if (isArray(item.v)) {
item.v.forEach(vItem => {
plugins.push(
compressPlugin(Object.assign(vItem, { deleteOriginFile: true }))
);
});
} else {
plugins.push(
compressPlugin(Object.assign(item.v, { deleteOriginFile: true }))
);
}
} else {
if (isArray(item.v)) {
item.v.forEach(vItem => {
plugins.push(compressPlugin(vItem));
});
} else {
plugins.push(compressPlugin(item.v));
}
}
}
});
return plugins;
};

View File

@ -0,0 +1,57 @@
import type { Plugin } from "vite";
import gradient from "gradient-string";
import { getPackageSize } from "./utils";
import dayjs, { type Dayjs } from "dayjs";
import duration from "dayjs/plugin/duration";
import boxen, { type Options as BoxenOptions } from "boxen";
dayjs.extend(duration);
const welcomeMessage = gradient(["cyan", "magenta"]).multiline(
`您好! 欢迎使用 pure-admin 开源项目\n我们为您精心准备了下面两个贴心的保姆级文档\nhttps://pure-admin.cn\nhttps://pure-admin-utils.netlify.app`
);
const boxenOptions: BoxenOptions = {
padding: 0.5,
borderColor: "cyan",
borderStyle: "round"
};
export function viteBuildInfo(): Plugin {
let config: { command: string };
let startTime: Dayjs;
let endTime: Dayjs;
let outDir: string;
return {
name: "vite:buildInfo",
configResolved(resolvedConfig) {
config = resolvedConfig;
outDir = resolvedConfig.build?.outDir ?? "dist";
},
buildStart() {
console.log(boxen(welcomeMessage, boxenOptions));
if (config.command === "build") {
startTime = dayjs(new Date());
}
},
closeBundle() {
if (config.command === "build") {
endTime = dayjs(new Date());
getPackageSize({
folder: outDir,
callback: (size: string) => {
console.log(
boxen(
gradient(["cyan", "magenta"]).multiline(
`🎉 恭喜打包完成(总用时${dayjs
.duration(endTime.diff(startTime))
.format("mm分ss秒")}${size}`
),
boxenOptions
)
);
}
});
}
}
};
}

View File

@ -0,0 +1,29 @@
/**
* `vite.config.ts` `optimizeDeps.include`
* `vite` include esm node_modules/.vite
* include里vite 使 node_modules/.vite
* 使 src/main.ts include vite node_modules/.vite
*/
const include = [
"qs",
"mitt",
"dayjs",
"axios",
"pinia",
"vue-types",
"js-cookie",
"vue-tippy",
"pinyin-pro",
"sortablejs",
"@vueuse/core",
"@pureadmin/utils",
"responsive-storage"
];
/**
*
* 使
*/
const exclude = ["@iconify/json"];
export { include, exclude };

View File

@ -0,0 +1,66 @@
import { cdn } from "./cdn";
import vue from "@vitejs/plugin-vue";
import { viteBuildInfo } from "./info";
import svgLoader from "vite-svg-loader";
import Icons from "unplugin-icons/vite";
import type { PluginOption } from "vite";
import vueJsx from "@vitejs/plugin-vue-jsx";
import tailwindcss from "@tailwindcss/vite";
import { configCompressPlugin } from "./compress";
import removeNoMatch from "vite-plugin-router-warn";
import { visualizer } from "rollup-plugin-visualizer";
import removeConsole from "vite-plugin-remove-console";
import { codeInspectorPlugin } from "code-inspector-plugin";
// import { vitePluginFakeServer } from "vite-plugin-fake-server";
export function getPluginsList(
VITE_CDN: boolean,
VITE_COMPRESSION: ViteCompression
): PluginOption[] {
const lifecycle = process.env.npm_lifecycle_event;
return [
tailwindcss(),
vue(),
// jsx、tsx语法支持
vueJsx(),
/**
* DOM IDE
* Mac Option + Shift
* Windows Alt + Shift
* https://inspector.fe-dev.cn/guide/start.html
*/
codeInspectorPlugin({
bundler: "vite",
hideConsole: true
}),
viteBuildInfo(),
/**
* vue-router动态路由警告No match found for location with path
* https://github.com/vuejs/router/issues/521 和 https://github.com/vuejs/router/issues/359
* vite-plugin-router-warn只在开发环境下启用vue-router文件并且只在服务启动或重启时运行一次
*/
removeNoMatch(),
// mock支持
// vitePluginFakeServer({
// logger: false,
// include: "mock",
// infixName: false,
// enableProd: true
// }),
// svg组件化支持
svgLoader(),
// 自动按需加载图标
Icons({
compiler: "vue3",
scale: 1
}),
VITE_CDN ? cdn : null,
configCompressPlugin(VITE_COMPRESSION),
// 线上环境删除console
removeConsole({ external: ["src/assets/iconfont/iconfont.js"] }),
// 打包分析
lifecycle === "report"
? visualizer({ open: true, brotliSize: true, filename: "report.html" })
: (null as any)
];
}

View File

@ -0,0 +1,110 @@
import dayjs from "dayjs";
import { readdir, stat } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, resolve } from "node:path";
import { sum, formatBytes } from "@pureadmin/utils";
import {
name,
version,
engines,
dependencies,
devDependencies
} from "../package.json";
/** 启动`node`进程时所在工作目录的绝对路径 */
const root: string = process.cwd();
/**
* @description
* @param dir `build`
* @param metaUrl `url``build``import.meta.url`
*/
const pathResolve = (dir = ".", metaUrl = import.meta.url) => {
// 当前文件目录的绝对路径
const currentFileDir = dirname(fileURLToPath(metaUrl));
// build 目录的绝对路径
const buildDir = resolve(currentFileDir, "build");
// 解析的绝对路径
const resolvedPath = resolve(currentFileDir, dir);
// 检查解析的绝对路径是否在 build 目录内
if (resolvedPath.startsWith(buildDir)) {
// 在 build 目录内,返回当前文件路径
return fileURLToPath(metaUrl);
}
// 不在 build 目录内,返回解析后的绝对路径
return resolvedPath;
};
/** 设置别名 */
const alias: Record<string, string> = {
"@": pathResolve("../src"),
"@build": pathResolve()
};
/** 平台的名称、版本、运行所需的`node`和`pnpm`版本、依赖、最后构建时间的类型提示 */
const __APP_INFO__ = {
pkg: { name, version, engines, dependencies, devDependencies },
lastBuildTime: dayjs(new Date()).format("YYYY-MM-DD HH:mm:ss")
};
/** 处理环境变量 */
const wrapperEnv = (envConf: Recordable): ViteEnv => {
// 默认值
const ret: ViteEnv = {
VITE_PORT: 8848,
VITE_PUBLIC_PATH: "",
VITE_ROUTER_HISTORY: "",
VITE_CDN: false,
VITE_HIDE_HOME: "false",
VITE_COMPRESSION: "none"
};
for (const envName of Object.keys(envConf)) {
let realName = envConf[envName].replace(/\\n/g, "\n");
realName =
realName === "true" ? true : realName === "false" ? false : realName;
if (envName === "VITE_PORT") {
realName = Number(realName);
}
ret[envName] = realName;
if (typeof realName === "string") {
process.env[envName] = realName;
} else if (typeof realName === "object") {
process.env[envName] = JSON.stringify(realName);
}
}
return ret;
};
const fileListTotal: number[] = [];
/** 获取指定文件夹中所有文件的总大小 */
const getPackageSize = options => {
const { folder = "dist", callback, format = true } = options;
readdir(folder, (err, files: string[]) => {
if (err) throw err;
let count = 0;
const checkEnd = () => {
++count == files.length &&
callback(format ? formatBytes(sum(fileListTotal)) : sum(fileListTotal));
};
files.forEach((item: string) => {
stat(`${folder}/${item}`, async (err, stats) => {
if (err) throw err;
if (stats.isFile()) {
fileListTotal.push(stats.size);
checkEnd();
} else if (stats.isDirectory()) {
getPackageSize({
folder: `${folder}/${item}/`,
callback: checkEnd
});
}
});
});
files.length === 0 && callback(0);
});
};
export { root, pathResolve, alias, __APP_INFO__, wrapperEnv, getPackageSize };

View File

@ -0,0 +1,35 @@
// @ts-check
/** @type {import("@commitlint/types").UserConfig} */
export default {
ignores: [commit => commit.includes("init")],
extends: ["@commitlint/config-conventional"],
rules: {
"body-leading-blank": [2, "always"],
"footer-leading-blank": [1, "always"],
"header-max-length": [2, "always", 108],
"subject-empty": [2, "never"],
"type-empty": [2, "never"],
"type-enum": [
2,
"always",
[
"feat",
"fix",
"perf",
"style",
"docs",
"test",
"refactor",
"build",
"ci",
"chore",
"revert",
"wip",
"workflow",
"types",
"release"
]
]
}
};

View File

@ -0,0 +1,173 @@
import js from "@eslint/js";
import tseslint from "typescript-eslint";
import pluginVue from "eslint-plugin-vue";
import * as parserVue from "vue-eslint-parser";
import configPrettier from "eslint-config-prettier";
import pluginPrettier from "eslint-plugin-prettier";
import { defineConfig, globalIgnores } from "eslint/config";
export default defineConfig([
globalIgnores([
"**/.*",
"dist/*",
"*.d.ts",
"public/*",
"src/assets/**",
"src/**/iconfont/**"
]),
{
...js.configs.recommended,
languageOptions: {
globals: {
// types/index.d.ts
RefType: "readonly",
EmitType: "readonly",
TargetContext: "readonly",
ComponentRef: "readonly",
ElRef: "readonly",
ForDataType: "readonly",
AnyFunction: "readonly",
PropType: "readonly",
Writable: "readonly",
Nullable: "readonly",
NonNullable: "readonly",
Recordable: "readonly",
ReadonlyRecordable: "readonly",
Indexable: "readonly",
DeepPartial: "readonly",
Without: "readonly",
Exclusive: "readonly",
TimeoutHandle: "readonly",
IntervalHandle: "readonly",
Effect: "readonly",
ChangeEvent: "readonly",
WheelEvent: "readonly",
ImportMetaEnv: "readonly",
Fn: "readonly",
PromiseFn: "readonly",
ComponentElRef: "readonly",
parseInt: "readonly",
parseFloat: "readonly"
}
},
plugins: {
prettier: pluginPrettier
},
rules: {
...configPrettier.rules,
...pluginPrettier.configs.recommended.rules,
"no-debugger": "off",
"no-unused-vars": [
"error",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_"
}
],
"prettier/prettier": [
"error",
{
endOfLine: "auto"
}
]
}
},
...tseslint.config({
extends: [...tseslint.configs.recommended, "plugin:prettier/recommended"],
files: ["**/*.?([cm])ts", "**/*.?([cm])tsx"],
rules: {
"@typescript-eslint/no-redeclare": "error",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/prefer-as-const": "warn",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-unused-expressions": "off",
"@typescript-eslint/no-unsafe-function-type": "off",
"@typescript-eslint/no-import-type-side-effects": "error",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/consistent-type-imports": [
"error",
{ disallowTypeAnnotations: false, fixStyle: "inline-type-imports" }
],
"@typescript-eslint/prefer-literal-enum-member": [
"error",
{ allowBitwiseExpressions: true }
],
"@typescript-eslint/no-unused-vars": [
"error",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_"
}
]
}
}),
{
files: ["**/*.d.ts"],
rules: {
"eslint-comments/no-unlimited-disable": "off",
"import/no-duplicates": "off",
"no-restricted-syntax": "off",
"unused-imports/no-unused-vars": "off"
}
},
{
files: ["**/*.?([cm])js"],
rules: {
"@typescript-eslint/no-require-imports": "off"
}
},
{
files: ["**/*.vue"],
languageOptions: {
globals: {
$: "readonly",
$$: "readonly",
$computed: "readonly",
$customRef: "readonly",
$ref: "readonly",
$shallowRef: "readonly",
$toRef: "readonly"
},
parser: parserVue,
parserOptions: {
ecmaFeatures: {
jsx: true
},
extraFileExtensions: [".vue"],
parser: tseslint.parser,
sourceType: "module"
}
},
plugins: {
"@typescript-eslint": tseslint.plugin,
vue: pluginVue
},
processor: pluginVue.processors[".vue"],
rules: {
...pluginVue.configs.base.rules,
...pluginVue.configs.essential.rules,
...pluginVue.configs.recommended.rules,
"no-undef": "off",
"no-unused-vars": "off",
"vue/no-v-html": "off",
"vue/require-default-prop": "off",
"vue/require-explicit-emits": "off",
"vue/multi-word-component-names": "off",
"vue/no-setup-props-reactivity-loss": "off",
"vue/html-self-closing": [
"error",
{
html: {
void: "always",
normal: "always",
component: "always"
},
svg: "always",
math: "always"
}
]
}
}
]);

View File

@ -0,0 +1,88 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="Expires" content="0">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Cache-control" content="no-cache">
<meta http-equiv="Cache" content="no-cache">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<meta name="renderer" content="webkit" />
<meta
name="viewport"
content="width=device-width,initial-scale=1.0,minimum-scale=1.0,maximum-scale=1.0,user-scalable=0"
/>
<title>pure-admin-thin</title>
<link rel="icon" href="/favicon.ico" />
</head>
<body>
<div id="app">
<style>
html,
body,
#app {
position: relative;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
overflow: hidden;
}
.loader,
.loader::before,
.loader::after {
width: 2.5em;
height: 2.5em;
border-radius: 50%;
animation: load-animation 1.8s infinite ease-in-out;
animation-fill-mode: both;
}
.loader {
position: relative;
top: 0;
margin: 80px auto;
font-size: 10px;
color: #406eeb;
text-indent: -9999em;
transform: translateZ(0);
transform: translate(-50%, 0);
animation-delay: -0.16s;
}
.loader::before,
.loader::after {
position: absolute;
top: 0;
content: "";
}
.loader::before {
left: -3.5em;
animation-delay: -0.32s;
}
.loader::after {
left: 3.5em;
}
@keyframes load-animation {
0%,
80%,
100% {
box-shadow: 0 2.5em 0 -1.3em;
}
40% {
box-shadow: 0 2.5em 0 0;
}
}
</style>
<div class="loader"></div>
</div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@ -0,0 +1,159 @@
{
"name": "pure-admin-thin",
"version": "6.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "NODE_OPTIONS=--max-old-space-size=4096 vite",
"serve": "pnpm dev",
"build": "rimraf dist && NODE_OPTIONS=--max-old-space-size=8192 vite build",
"build:staging": "rimraf dist && vite build --mode staging",
"report": "rimraf dist && vite build",
"preview": "vite preview",
"preview:build": "pnpm build && vite preview",
"typecheck": "tsc --noEmit && vue-tsc --noEmit --skipLibCheck",
"svgo": "svgo -f . -r",
"clean:cache": "rimraf .eslintcache && rimraf pnpm-lock.yaml && rimraf node_modules && pnpm store prune && pnpm install",
"lint:eslint": "eslint --cache --max-warnings 0 \"{src,mock,build}/**/*.{vue,js,ts,tsx}\" --fix",
"lint:prettier": "prettier --write \"src/**/*.{js,ts,json,tsx,css,scss,vue,html,md}\"",
"lint:stylelint": "stylelint --cache --fix \"**/*.{html,vue,css,scss}\" --cache-location node_modules/.cache/stylelint/",
"lint": "pnpm lint:eslint && pnpm lint:prettier && pnpm lint:stylelint",
"prepare": "husky",
"preinstall": "npx only-allow pnpm"
},
"keywords": [
"pure-admin-thin",
"vue-pure-admin",
"element-plus",
"tailwindcss",
"pure-admin",
"typescript",
"pinia",
"vue3",
"vite",
"esm"
],
"homepage": "https://github.com/pure-admin/pure-admin-thin",
"repository": {
"type": "git",
"url": "git+https://github.com/pure-admin/pure-admin-thin.git"
},
"bugs": {
"url": "https://github.com/pure-admin/vue-pure-admin/issues"
},
"license": "MIT",
"author": {
"name": "xiaoxian521",
"email": "pureadmin@163.com",
"url": "https://github.com/xiaoxian521"
},
"dependencies": {
"@pureadmin/descriptions": "^1.2.1",
"@pureadmin/table": "^3.2.1",
"@pureadmin/utils": "^2.6.0",
"@vueuse/core": "^13.1.0",
"@vueuse/motion": "^3.0.3",
"animate.css": "^4.1.1",
"axios": "^1.9.0",
"dayjs": "^1.11.13",
"echarts": "^5.6.0",
"element-plus": "^2.9.8",
"js-cookie": "^3.0.5",
"localforage": "^1.10.0",
"mitt": "^3.0.1",
"nprogress": "^0.2.0",
"path-browserify": "^1.0.1",
"pinia": "^3.0.2",
"pinyin-pro": "^3.26.0",
"qs": "^6.14.0",
"responsive-storage": "^2.2.0",
"sortablejs": "^1.15.6",
"vue": "^3.5.13",
"vue-router": "^4.5.0",
"vue-tippy": "^6.7.0",
"vue-types": "^6.0.0"
},
"devDependencies": {
"@commitlint/cli": "^19.8.0",
"@commitlint/config-conventional": "^19.8.0",
"@commitlint/types": "^19.8.0",
"@eslint/js": "^9.25.1",
"@faker-js/faker": "^9.7.0",
"@iconify/json": "^2.2.331",
"@iconify/vue": "4.2.0",
"@tailwindcss/vite": "^4.1.4",
"@types/js-cookie": "^3.0.6",
"@types/node": "^20.17.30",
"@types/nprogress": "^0.2.3",
"@types/path-browserify": "^1.0.3",
"@types/qs": "^6.9.18",
"@types/sortablejs": "^1.15.8",
"@vitejs/plugin-vue": "^5.2.3",
"@vitejs/plugin-vue-jsx": "^4.1.2",
"boxen": "^8.0.1",
"code-inspector-plugin": "^0.20.10",
"cssnano": "^7.0.6",
"eslint": "^9.25.1",
"eslint-config-prettier": "^10.1.2",
"eslint-plugin-prettier": "^5.2.6",
"eslint-plugin-vue": "^10.0.0",
"gradient-string": "^3.0.0",
"husky": "^9.1.7",
"lint-staged": "^15.5.1",
"postcss": "^8.5.3",
"postcss-html": "^1.8.0",
"postcss-load-config": "^6.0.1",
"postcss-scss": "^4.0.9",
"prettier": "^3.5.3",
"rimraf": "^6.0.1",
"rollup-plugin-visualizer": "^5.14.0",
"sass": "^1.87.0",
"stylelint": "^16.19.0",
"stylelint-config-recess-order": "^6.0.0",
"stylelint-config-recommended-vue": "^1.6.0",
"stylelint-config-standard-scss": "^14.0.0",
"stylelint-prettier": "^5.0.3",
"svgo": "^3.3.2",
"tailwindcss": "^4.1.4",
"typescript": "^5.8.3",
"typescript-eslint": "^8.31.0",
"unplugin-icons": "^22.1.0",
"vite": "^6.3.3",
"vite-plugin-cdn-import": "^1.0.1",
"vite-plugin-compression": "^0.5.1",
"vite-plugin-fake-server": "^2.2.0",
"vite-plugin-remove-console": "^2.2.0",
"vite-plugin-router-warn": "^1.0.0",
"vite-svg-loader": "^5.1.0",
"vue-eslint-parser": "^10.1.3",
"vue-tsc": "^2.2.10"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=22.0.0",
"pnpm": ">=9"
},
"pnpm": {
"allowedDeprecatedVersions": {
"are-we-there-yet": "*",
"sourcemap-codec": "*",
"lodash.isequal": "*",
"domexception": "*",
"w3c-hr-time": "*",
"inflight": "*",
"npmlog": "*",
"rimraf": "*",
"stable": "*",
"gauge": "*",
"abab": "*",
"glob": "*"
},
"onlyBuiltDependencies": [
"@parcel/watcher",
"core-js",
"es5-ext",
"esbuild",
"typeit",
"vue-demi"
]
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,8 @@
// @ts-check
/** @type {import('postcss-load-config').Config} */
export default {
plugins: {
...(process.env.NODE_ENV === "production" ? { cssnano: {} } : {})
}
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" class="icon" viewBox="0 0 1024 1024"><path fill="#386BF3" d="M410.558.109c0 210.974-300.876 361.752-300.876 633.548 0 174.943 134.704 316.787 300.876 316.787s300.877-141.817 300.877-316.787C711.408 361.752 410.558 210.974 410.558.109"/><path fill="#C3D2FB" d="M613.469 73.665c0 211.055-300.877 361.914-300.877 633.547C312.592 882.156 447.296 1024 613.47 1024s300.876-141.817 300.876-316.788C914.29 435.58 613.469 284.72 613.469 73.665"/><path fill="#303F5B" d="M312.592 707.212c0-183.713 137.636-312.171 226.723-441.39 81.702 106.112 172.12 218.74 172.12 367.726A309.755 309.755 0 0 1 420.36 950.064a323.1 323.1 0 0 1-107.769-242.852z"/></svg>

After

Width:  |  Height:  |  Size: 706 B

View File

@ -0,0 +1,26 @@
{
"Version": "6.0.0",
"Title": "AI视频分析",
"FixedHeader": true,
"HiddenSideBar": false,
"MultiTagsCache": false,
"KeepAlive": true,
"Layout": "vertical",
"Theme": "light",
"DarkMode": false,
"OverallStyle": "light",
"Grey": false,
"Weak": false,
"HideTabs": false,
"HideFooter": false,
"Stretch": false,
"SidebarStatus": true,
"EpThemeColor": "#409EFF",
"ShowLogo": true,
"ShowModel": "chrome",
"MenuArrowIconNoTransition": false,
"CachingAsyncRoutes": false,
"TooltipEffect": "light",
"ResponsiveStorageNameSpace": "responsive-",
"MenuSearchHistory": 6
}

View File

@ -0,0 +1,26 @@
<template>
<el-config-provider :locale="currentLocale">
<router-view />
<ReDialog />
</el-config-provider>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import { ElConfigProvider } from "element-plus";
import { ReDialog } from "@/components/ReDialog";
import zhCn from "element-plus/es/locale/lang/zh-cn";
export default defineComponent({
name: "app",
components: {
[ElConfigProvider.name]: ElConfigProvider,
ReDialog,
},
computed: {
currentLocale() {
return zhCn;
},
},
});
</script>

View File

@ -0,0 +1,20 @@
import { ComboModel } from "@/components/hTable/hTable";
import { http } from "@/utils/http";
import type { Res } from "@/utils/http/types";
/**
* @description
* @param {string} type type=StatusEnum
* @return {object}
*/
export function getenum(type) {
return http.request<ComboModel[]>("get", `Public/enum/${type}`);
}
/**
* @description
* @param {string} type type=StatusEnum
* @return {object}
*/
export function getenumDic(type) {
return http.request<any>("get", `Public/enum/${type}/Dic`);
}

View File

@ -0,0 +1,31 @@
import { http } from "@/utils/http";
import type { Res } from "@/utils/http/types";
import type { ComboModel } from "@/components/hTable/hTable";
export class hTableAPI {
url = "";
/** 构造函数 */
constructor(url) {
this.url = url;
}
PageList(data = {}) {
return http.request<Res<any>>("post", `${this.url}/PageList`, { data });
}
Info(tag) {
const pUrl = `${this.url}/${tag}`;
let getUrl = pUrl;
return http.request<Res<any>>("get", getUrl);
}
edit(data) {
return http.request<Res<any>>("post", `${this.url}/Edit`, { data });
}
delete(data) {
return http.request<Res<any>>("post", `${this.url}/Del`, { data });
}
querycombo(data = {}) {
return http.request<Res<ComboModel[]>>("post", `${this.url}/QueryCombo`, {
data
});
}
}

View File

@ -0,0 +1,15 @@
import { http } from "@/utils/http";
type Result = {
success: boolean;
data: Array<any>;
};
export const getAsyncRoutes = () => {
return new Promise<Result>((resolve, reject) => {
resolve({
success: true,
data: []
});
});
};

View File

@ -0,0 +1,43 @@
import { http } from "@/utils/http";
import type { Res } from "@/utils/http/types";
export type UserResult = {
/** 头像 */
avatar: string;
/** 用户名 */
userName: string;
/** 昵称 */
nickName: string;
/** 当前登录用户的角色 */
roles: Array<string>;
/** 按钮级别权限 */
permissions: Array<string>;
/** `token` */
accessToken: string;
/** 用于调用刷新`accessToken`的接口时所需的`token` */
refreshToken: string;
/** `accessToken`的过期时间(格式'xxxx/xx/xx xx:xx:xx' */
expires: Date;
};
export type RefreshTokenResult = {
success: boolean;
data: {
/** `token` */
accessToken: string;
/** 用于调用刷新`accessToken`的接口时所需的`token` */
refreshToken: string;
/** `accessToken`的过期时间(格式'xxxx/xx/xx xx:xx:xx' */
expires: Date;
};
};
/** 登录 */
export const getLogin = (data?: object) => {
return http.request<UserResult>("post", "/api/Public/Login", { data });
};
/** 刷新`token` */
export const refreshTokenApi = (data?: object) => {
return http.request<RefreshTokenResult>("post", "/refresh-token", { data });
};

View File

@ -0,0 +1,58 @@
import { http } from "@/utils/http";
import type { Res } from "@/utils/http/types";
// 定义类型
export interface Question {
startTime: number;
topicStem: string;
question: string;
pPTImageUrl: string;
}
export interface VideoKnowRes {
Theme: string;
Content: string;
KnowPoint: string;
KnowPointId: number;
QuestionArr: Question[];
startTime: number;
}
export interface SenseVoiceRes {
text: string;
start: number;
end: number;
}
export interface ShowTaskInfoRes {
captions: SenseVoiceRes[];
captions1: SenseVoiceRes[];
VideoKnows: VideoKnowRes[];
MediaUrl: string;
}
export interface RowRloadResult {
progress: string;
lastEnum: string;
startTime: string;
errorMessage: string;
}
/** 刷新任务实时数据 */
export const RowRload = (id: any) => {
return http.request<RowRloadResult>("post", "/api/VideoTask/RowRload", {
params: id
});
};
/** 重试任务 */
export const ReStart = (id: any, selectEnum: number) => {
return http.request<any>("post", "/api/VideoTask/ReStart", {
params: { id, selectEnum }
});
};
/** 重试任务 */
export const ShowTaskInfo = (id: any) => {
return http.request<ShowTaskInfoRes>("post", "/api/VideoTask/ShowTaskInfo", {
params: { id }
});
};

View File

@ -0,0 +1,27 @@
@font-face {
font-family: "iconfont"; /* Project id 2208059 */
src:
url("iconfont.woff2?t=1671895108120") format("woff2"),
url("iconfont.woff?t=1671895108120") format("woff"),
url("iconfont.ttf?t=1671895108120") format("truetype");
}
.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.pure-iconfont-tabs:before {
content: "\e63e";
}
.pure-iconfont-logo:before {
content: "\e620";
}
.pure-iconfont-new:before {
content: "\e615";
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,30 @@
{
"id": "2208059",
"name": "pure-admin",
"font_family": "iconfont",
"css_prefix_text": "pure-iconfont-",
"description": "pure-admin-iconfont",
"glyphs": [
{
"icon_id": "20594647",
"name": "Tabs",
"font_class": "tabs",
"unicode": "e63e",
"unicode_decimal": 58942
},
{
"icon_id": "22129506",
"name": "PureLogo",
"font_class": "logo",
"unicode": "e620",
"unicode_decimal": 58912
},
{
"icon_id": "7795615",
"name": "New",
"font_class": "new",
"unicode": "e615",
"unicode_decimal": 58901
}
]
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" class="icon" viewBox="0 0 1024 1024"><path fill="#386BF3" d="M410.558.109c0 210.974-300.876 361.752-300.876 633.548 0 174.943 134.704 316.787 300.876 316.787s300.877-141.817 300.877-316.787C711.408 361.752 410.558 210.974 410.558.109"/><path fill="#C3D2FB" d="M613.469 73.665c0 211.055-300.877 361.914-300.877 633.547C312.592 882.156 447.296 1024 613.47 1024s300.876-141.817 300.876-316.788C914.29 435.58 613.469 284.72 613.469 73.665"/><path fill="#303F5B" d="M312.592 707.212c0-183.713 137.636-312.171 226.723-441.39 81.702 106.112 172.12 218.74 172.12 367.726A309.755 309.755 0 0 1 420.36 950.064a323.1 323.1 0 0 1-107.769-242.852z"/></svg>

After

Width:  |  Height:  |  Size: 706 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.7 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M2.88 18.054a35.9 35.9 0 0 1 8.531-16.32.8.8 0 0 1 1.178 0q.25.27.413.455a35.9 35.9 0 0 1 8.118 15.865c-2.141.451-4.34.747-6.584.874l-2.089 4.178a.5.5 0 0 1-.894 0l-2.089-4.178a44 44 0 0 1-6.584-.874m6.698-1.123 1.157.066L12 19.527l1.265-2.53 1.157-.066a42 42 0 0 0 4.227-.454A33.9 33.9 0 0 0 12 4.09a33.9 33.9 0 0 0-6.649 12.387q2.093.334 4.227.454M12 15a3 3 0 1 1 0-6 3 3 0 0 1 0 6m0-2a1 1 0 1 0 0-2 1 1 0 0 0 0 2"/></svg>

After

Width:  |  Height:  |  Size: 533 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M11.38 2.019a7.5 7.5 0 1 0 10.6 10.6C21.662 17.854 17.316 22 12.001 22 6.477 22 2 17.523 2 12c0-5.315 4.146-9.661 9.38-9.981"/></svg>

After

Width:  |  Height:  |  Size: 262 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12 18a6 6 0 1 1 0-12 6 6 0 0 1 0 12M11 1h2v3h-2zm0 19h2v3h-2zM3.515 4.929l1.414-1.414L7.05 5.636 5.636 7.05zM16.95 18.364l1.414-1.414 2.121 2.121-1.414 1.414zm2.121-14.85 1.414 1.415-2.121 2.121-1.414-1.414 2.121-2.121zM5.636 16.95l1.414 1.414-2.121 2.121-1.414-1.414zM23 11v2h-3v-2zM4 11v2H1v-2z"/></svg>

After

Width:  |  Height:  |  Size: 435 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" class="iconify iconify--ant-design" viewBox="0 0 1024 1024"><path fill="currentColor" d="M864 170h-60c-4.4 0-8 3.6-8 8v518H310v-73c0-6.7-7.8-10.5-13-6.3l-141.9 112a8 8 0 0 0 0 12.6l141.9 112c5.3 4.2 13 .4 13-6.3v-75h498c35.3 0 64-28.7 64-64V178c0-4.4-3.6-8-8-8"/></svg>

After

Width:  |  Height:  |  Size: 332 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" class="re-screen" color="#00000073" viewBox="0 0 16 16"><path fill="currentColor" d="M3.5 4H1V3h2V1h1v2.5zM13 3V1h-1v2.5l.5.5H15V3zm-1 9.5V15h1v-2h2v-1h-2.5zM1 12v1h2v2h1v-2.5l-.5-.5zm11-1.5-.5.5h-7l-.5-.5v-5l.5-.5h7l.5.5zM10 7H6v2h4z"/></svg>

After

Width:  |  Height:  |  Size: 308 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" class="re-screen" color="#00000073" viewBox="0 0 16 16"><path fill="currentColor" d="M3 12h10V4H3zm2-6h6v4H5zM2 6H1V2.5l.5-.5H5v1H2zm13-3.5V6h-1V3h-3V2h3.5zM14 10h1v3.5l-.5.5H11v-1h3zM2 13h3v1H1.5l-.5-.5V10h1z"/></svg>

After

Width:  |  Height:  |  Size: 283 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" class="iconify iconify--mdi" viewBox="0 0 24 24"><path fill="currentColor" d="M1 7h6v2H3v2h4v2H3v2h4v2H1zm10 0h4v2h-4v2h2a2 2 0 0 1 2 2v2c0 1.11-.89 2-2 2H9v-2h4v-2h-2a2 2 0 0 1-2-2V9c0-1.1.9-2 2-2m8 0h2a2 2 0 0 1 2 2v1h-2V9h-2v6h2v-1h2v1c0 1.11-.89 2-2 2h-2a2 2 0 0 1-2-2V9c0-1.1.9-2 2-2"/></svg>

After

Width:  |  Height:  |  Size: 360 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" class="icon" viewBox="0 0 1024 1024"><path d="M554 849.574c0 23.365-18.635 42.307-42 42.307s-42-18.941-42-42.307V662.719c0-23.365 18.635-42.307 42-42.307v-7.051c23.365 0 42 25.993 42 49.358z"/><path d="M893 888.5c0 17.397-14.103 31.5-31.5 31.5h-700c-17.397 0-31.5-14.103-31.5-31.5s14.103-31.5 31.5-31.5h700c17.397 0 31.5 14.103 31.5 31.5m33-714.074C926 135.484 894.686 105 855.744 105H168.256C129.314 105 98 135.484 98 174.426V533h828zM98 630.988C98 669.931 129.314 702 168.256 702h687.488C894.686 702 926 669.931 926 630.988V596H98z"/></svg>

After

Width:  |  Height:  |  Size: 605 B

View File

@ -0,0 +1 @@
<svg width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M13.79 10.21a1 1 0 0 0 1.42 0 1 1 0 0 0 0-1.42l-2.5-2.5a1 1 0 0 0-.33-.21 1 1 0 0 0-.76 0 1 1 0 0 0-.33.21l-2.5 2.5a1 1 0 0 0 1.42 1.42l.79-.8v5.18l-.79-.8a1 1 0 0 0-1.42 1.42l2.5 2.5a1 1 0 0 0 .33.21.94.94 0 0 0 .76 0 1 1 0 0 0 .33-.21l2.5-2.5a1 1 0 0 0-1.42-1.42l-.79.8V9.41ZM7 4h10a1 1 0 0 0 0-2H7a1 1 0 0 0 0 2m10 16H7a1 1 0 0 0 0 2h10a1 1 0 0 0 0-2"/></svg>

After

Width:  |  Height:  |  Size: 439 B

View File

@ -0,0 +1 @@
<svg width="32" height="32" fill="currentColor" data-icon="holder" viewBox="64 64 896 896"><path d="M300 276.5a56 56 0 1 0 56-97 56 56 0 0 0-56 97m0 284a56 56 0 1 0 56-97 56 56 0 0 0-56 97M640 228a56 56 0 1 0 112 0 56 56 0 0 0-112 0m0 284a56 56 0 1 0 112 0 56 56 0 0 0-112 0M300 844.5a56 56 0 1 0 56-97 56 56 0 0 0-56 97M640 796a56 56 0 1 0 112 0 56 56 0 0 0-112 0"/></svg>

After

Width:  |  Height:  |  Size: 373 B

View File

@ -0,0 +1 @@
<svg width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M22 4V2H2v2h9v14.17l-5.5-5.5-1.42 1.41L12 22l7.92-7.92-1.42-1.41-5.5 5.5V4z"/></svg>

After

Width:  |  Height:  |  Size: 161 B

View File

@ -0,0 +1 @@
<svg width="32" height="32" viewBox="0 0 24 24"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 11A8.1 8.1 0 0 0 4.5 9M4 5v4h4m-4 4a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"/></svg>

After

Width:  |  Height:  |  Size: 235 B

View File

@ -0,0 +1 @@
<svg width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M3.34 17a10 10 0 0 1-.978-2.326 3 3 0 0 0 .002-5.347A10 10 0 0 1 4.865 4.99a3 3 0 0 0 4.631-2.674 10 10 0 0 1 5.007.002 3 3 0 0 0 4.632 2.672A10 10 0 0 1 20.66 7c.433.749.757 1.53.978 2.326a3 3 0 0 0-.002 5.347 10 10 0 0 1-2.501 4.337 3 3 0 0 0-4.631 2.674 10 10 0 0 1-5.007-.002 3 3 0 0 0-4.632-2.672A10 10 0 0 1 3.34 17m5.66.196a5 5 0 0 1 2.25 2.77q.75.071 1.499.001A5 5 0 0 1 15 17.197a5 5 0 0 1 3.525-.565q.435-.614.748-1.298A5 5 0 0 1 18 12c0-1.26.47-2.437 1.273-3.334a8 8 0 0 0-.75-1.298A5 5 0 0 1 15 6.804a5 5 0 0 1-2.25-2.77q-.75-.071-1.499-.001A5 5 0 0 1 9 6.803a5 5 0 0 1-3.525.565 8 8 0 0 0-.748 1.298A5 5 0 0 1 6 12a5 5 0 0 1-1.273 3.334 8 8 0 0 0 .75 1.298A5 5 0 0 1 9 17.196M12 15a3 3 0 1 1 0-6 3 3 0 0 1 0 6m0-2a1 1 0 1 0 0-2 1 1 0 0 0 0 2"/></svg>

After

Width:  |  Height:  |  Size: 840 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@ -0,0 +1,5 @@
import auth from "./src/auth";
const Auth = auth;
export { Auth };

View File

@ -0,0 +1,20 @@
import { defineComponent, Fragment } from "vue";
import { hasAuth } from "@/router/utils";
export default defineComponent({
name: "Auth",
props: {
value: {
type: undefined,
default: []
}
},
setup(props, { slots }) {
return () => {
if (!slots) return null;
return hasAuth(props.value) ? (
<Fragment>{slots.default?.()}</Fragment>
) : null;
};
}
});

View File

@ -0,0 +1,29 @@
import { ElCol } from "element-plus";
import { h, defineComponent } from "vue";
// 封装element-plus的el-col组件
export default defineComponent({
name: "ReCol",
props: {
value: {
type: Number,
default: 24
}
},
render() {
const attrs = this.$attrs;
const val = this.value;
return h(
ElCol,
{
xs: val,
sm: val,
md: val,
lg: val,
xl: val,
...attrs
},
{ default: () => this.$slots.default() }
);
}
});

View File

@ -0,0 +1,69 @@
import { ref } from "vue";
import reDialog from "./index.vue";
import { useTimeoutFn } from "@vueuse/core";
import { withInstall } from "@pureadmin/utils";
import type {
EventType,
ArgsType,
DialogProps,
ButtonProps,
DialogOptions
} from "./type";
const dialogStore = ref<Array<DialogOptions>>([]);
/** 打开弹框 */
const addDialog = (options: DialogOptions) => {
const open = () =>
dialogStore.value.push(Object.assign(options, { visible: true }));
if (options?.openDelay) {
useTimeoutFn(() => {
open();
}, options.openDelay);
} else {
open();
}
};
/** 关闭弹框 */
const closeDialog = (options: DialogOptions, index: number, args?: any) => {
dialogStore.value[index].visible = false;
options.closeCallBack && options.closeCallBack({ options, index, args });
const closeDelay = options?.closeDelay ?? 200;
useTimeoutFn(() => {
dialogStore.value.splice(index, 1);
}, closeDelay);
};
/**
* @description
* @param value
* @param key `title`
* @param index `0``index`
*/
const updateDialog = (value: any, key = "title", index = 0) => {
dialogStore.value[index][key] = value;
};
/** 关闭所有弹框 */
const closeAllDialog = () => {
dialogStore.value = [];
};
/** 使`addDialog`
* https://github.com/pure-admin/vue-pure-admin/blob/main/src/App.vue#L4
* https://github.com/pure-admin/vue-pure-admin/blob/main/src/App.vue#L12
* https://github.com/pure-admin/vue-pure-admin/blob/main/src/App.vue#L22
*/
const ReDialog = withInstall(reDialog);
export type { EventType, ArgsType, DialogProps, ButtonProps, DialogOptions };
export {
ReDialog,
dialogStore,
addDialog,
closeDialog,
updateDialog,
closeAllDialog
};

View File

@ -0,0 +1,206 @@
<script setup lang="ts">
import {
type EventType,
type ButtonProps,
type DialogOptions,
closeDialog,
dialogStore
} from "./index";
import { ref, computed } from "vue";
import { isFunction } from "@pureadmin/utils";
import Fullscreen from "~icons/ri/fullscreen-fill";
import ExitFullscreen from "~icons/ri/fullscreen-exit-fill";
defineOptions({
name: "ReDialog"
});
const sureBtnMap = ref({});
const fullscreen = ref(false);
const footerButtons = computed(() => {
return (options: DialogOptions) => {
return options?.footerButtons?.length > 0
? options.footerButtons
: ([
{
label: "取消",
text: true,
bg: true,
btnClick: ({ dialog: { options, index } }) => {
const done = () =>
closeDialog(options, index, { command: "cancel" });
if (options?.beforeCancel && isFunction(options?.beforeCancel)) {
options.beforeCancel(done, { options, index });
} else {
done();
}
}
},
{
label: "确定",
type: "primary",
text: true,
bg: true,
popconfirm: options?.popconfirm,
btnClick: ({ dialog: { options, index } }) => {
if (options?.sureBtnLoading) {
sureBtnMap.value[index] = Object.assign(
{},
sureBtnMap.value[index],
{
loading: true
}
);
}
const closeLoading = () => {
if (options?.sureBtnLoading) {
sureBtnMap.value[index].loading = false;
}
};
const done = () => {
closeLoading();
closeDialog(options, index, { command: "sure" });
};
if (options?.beforeSure && isFunction(options?.beforeSure)) {
options.beforeSure(done, { options, index, closeLoading });
} else {
done();
}
}
}
] as Array<ButtonProps>);
};
});
const fullscreenClass = computed(() => {
return [
"el-icon",
"el-dialog__close",
"-translate-x-2",
"cursor-pointer",
"hover:text-[red]!"
];
});
function eventsCallBack(
event: EventType,
options: DialogOptions,
index: number,
isClickFullScreen = false
) {
if (!isClickFullScreen) fullscreen.value = options?.fullscreen ?? false;
if (options?.[event] && isFunction(options?.[event])) {
return options?.[event]({ options, index });
}
}
function handleClose(
options: DialogOptions,
index: number,
args = { command: "close" }
) {
closeDialog(options, index, args);
eventsCallBack("close", options, index);
}
</script>
<template>
<el-dialog
v-for="(options, index) in dialogStore"
:key="index"
v-bind="options"
v-model="options.visible"
class="pure-dialog"
:fullscreen="fullscreen ? true : options?.fullscreen ? true : false"
@closed="handleClose(options, index)"
@opened="eventsCallBack('open', options, index)"
@openAutoFocus="eventsCallBack('openAutoFocus', options, index)"
@closeAutoFocus="eventsCallBack('closeAutoFocus', options, index)"
>
<!-- header -->
<template
v-if="options?.fullscreenIcon || options?.headerRenderer"
#header="{ close, titleId, titleClass }"
>
<div
v-if="options?.fullscreenIcon"
class="flex items-center justify-between"
>
<span :id="titleId" :class="titleClass">{{ options?.title }}</span>
<i
v-if="!options?.fullscreen"
:class="fullscreenClass"
@click="
() => {
fullscreen = !fullscreen;
eventsCallBack(
'fullscreenCallBack',
{ ...options, fullscreen },
index,
true
);
}
"
>
<IconifyIconOffline
class="pure-dialog-svg"
:icon="
options?.fullscreen
? ExitFullscreen
: fullscreen
? ExitFullscreen
: Fullscreen
"
/>
</i>
</div>
<component
:is="options?.headerRenderer({ close, titleId, titleClass })"
v-else
/>
</template>
<component
v-bind="options?.props"
:is="options.contentRenderer({ options, index })"
@close="args => handleClose(options, index, args)"
/>
<!-- footer -->
<template v-if="!options?.hideFooter" #footer>
<template v-if="options?.footerRenderer">
<component :is="options?.footerRenderer({ options, index })" />
</template>
<span v-else>
<template v-for="(btn, key) in footerButtons(options)" :key="key">
<el-popconfirm
v-if="btn.popconfirm"
v-bind="btn.popconfirm"
@confirm="
btn.btnClick({
dialog: { options, index },
button: { btn, index: key }
})
"
>
<template #reference>
<el-button v-bind="btn">{{ btn?.label }}</el-button>
</template>
</el-popconfirm>
<el-button
v-else
v-bind="btn"
:loading="key === 1 && sureBtnMap[index]?.loading"
@click="
btn.btnClick({
dialog: { options, index },
button: { btn, index: key }
})
"
>
{{ btn?.label }}
</el-button>
</template>
</span>
</template>
</el-dialog>
</template>

Some files were not shown because too many files have changed in this diff Show More