feat: 添加管理员实验板管理界面
This commit is contained in:
		
							
								
								
									
										33
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										33
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							@@ -9,6 +9,7 @@
 | 
			
		||||
      "version": "0.1.0",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@svgdotjs/svg.js": "^3.2.4",
 | 
			
		||||
        "@tanstack/vue-table": "^8.21.3",
 | 
			
		||||
        "@types/lodash": "^4.17.16",
 | 
			
		||||
        "@vueuse/core": "^13.5.0",
 | 
			
		||||
        "async-mutex": "^0.5.0",
 | 
			
		||||
@@ -1743,6 +1744,19 @@
 | 
			
		||||
        "tailwindcss": "4.1.4"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@tanstack/table-core": {
 | 
			
		||||
      "version": "8.21.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz",
 | 
			
		||||
      "integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=12"
 | 
			
		||||
      },
 | 
			
		||||
      "funding": {
 | 
			
		||||
        "type": "github",
 | 
			
		||||
        "url": "https://github.com/sponsors/tannerlinsley"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@tanstack/virtual-core": {
 | 
			
		||||
      "version": "3.13.12",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz",
 | 
			
		||||
@@ -1753,6 +1767,25 @@
 | 
			
		||||
        "url": "https://github.com/sponsors/tannerlinsley"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@tanstack/vue-table": {
 | 
			
		||||
      "version": "8.21.3",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@tanstack/vue-table/-/vue-table-8.21.3.tgz",
 | 
			
		||||
      "integrity": "sha512-rusRyd77c5tDPloPskctMyPLFEQUeBzxdQ+2Eow4F7gDPlPOB1UnnhzfpdvqZ8ZyX2rRNGmqNnQWm87OI2OQPw==",
 | 
			
		||||
      "license": "MIT",
 | 
			
		||||
      "dependencies": {
 | 
			
		||||
        "@tanstack/table-core": "8.21.3"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=12"
 | 
			
		||||
      },
 | 
			
		||||
      "funding": {
 | 
			
		||||
        "type": "github",
 | 
			
		||||
        "url": "https://github.com/sponsors/tannerlinsley"
 | 
			
		||||
      },
 | 
			
		||||
      "peerDependencies": {
 | 
			
		||||
        "vue": ">=3.2"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/@tanstack/vue-virtual": {
 | 
			
		||||
      "version": "3.13.12",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@tanstack/vue-virtual/-/vue-virtual-3.13.12.tgz",
 | 
			
		||||
 
 | 
			
		||||
@@ -13,6 +13,7 @@
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@svgdotjs/svg.js": "^3.2.4",
 | 
			
		||||
    "@tanstack/vue-table": "^8.21.3",
 | 
			
		||||
    "@types/lodash": "^4.17.16",
 | 
			
		||||
    "@vueuse/core": "^13.5.0",
 | 
			
		||||
    "async-mutex": "^0.5.0",
 | 
			
		||||
 
 | 
			
		||||
@@ -62,6 +62,7 @@ try
 | 
			
		||||
            options.Authority = "http://localhost:5000";
 | 
			
		||||
            options.RequireHttpsMetadata = false;
 | 
			
		||||
        });
 | 
			
		||||
    // Add JWT Token Authorization Policy
 | 
			
		||||
    builder.Services.AddAuthorization(options =>
 | 
			
		||||
    {
 | 
			
		||||
        options.AddPolicy("Admin", policy =>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
using System.IdentityModel.Tokens.Jwt;
 | 
			
		||||
using System.Net;
 | 
			
		||||
using System.Security.Claims;
 | 
			
		||||
using System.Text;
 | 
			
		||||
using Microsoft.AspNetCore.Authorization;
 | 
			
		||||
@@ -17,7 +18,10 @@ public class DataController : ControllerBase
 | 
			
		||||
{
 | 
			
		||||
    private static NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger();
 | 
			
		||||
 | 
			
		||||
    public class GetUserInfoResponse
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// [TODO:description]
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    public class UserInfo
 | 
			
		||||
    {
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// 用户的唯一标识符
 | 
			
		||||
@@ -118,7 +122,7 @@ public class DataController : ControllerBase
 | 
			
		||||
    [Authorize]
 | 
			
		||||
    [HttpGet("GetUserInfo")]
 | 
			
		||||
    [EnableCors("Users")]
 | 
			
		||||
    [ProducesResponseType(typeof(GetUserInfoResponse), StatusCodes.Status200OK)]
 | 
			
		||||
    [ProducesResponseType(typeof(UserInfo), StatusCodes.Status200OK)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status500InternalServerError)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
 | 
			
		||||
@@ -139,7 +143,7 @@ public class DataController : ControllerBase
 | 
			
		||||
            return BadRequest("用户不存在");
 | 
			
		||||
 | 
			
		||||
        var user = ret.Value.Value;
 | 
			
		||||
        return Ok(new GetUserInfoResponse
 | 
			
		||||
        return Ok(new UserInfo
 | 
			
		||||
        {
 | 
			
		||||
            ID = user.ID,
 | 
			
		||||
            Name = user.Name,
 | 
			
		||||
@@ -187,5 +191,85 @@ public class DataController : ControllerBase
 | 
			
		||||
            return StatusCode(StatusCodes.Status500InternalServerError, "注册失败,请稍后重试");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 新增板子(管理员权限)
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [Authorize("Admin")]
 | 
			
		||||
    [HttpPost("AddBoard")]
 | 
			
		||||
    [EnableCors("Users")]
 | 
			
		||||
    [ProducesResponseType(typeof(int), StatusCodes.Status200OK)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status500InternalServerError)]
 | 
			
		||||
    public IActionResult AddBoard(string name, string ipAddr, int port)
 | 
			
		||||
    {
 | 
			
		||||
        if (string.IsNullOrWhiteSpace(name))
 | 
			
		||||
            return BadRequest("板子名称不能为空");
 | 
			
		||||
        if (string.IsNullOrWhiteSpace(ipAddr))
 | 
			
		||||
            return BadRequest("IP地址不能为空");
 | 
			
		||||
        if (port <= 0 || port > 65535)
 | 
			
		||||
            return BadRequest("端口号不合法");
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            using var db = new Database.AppDataConnection();
 | 
			
		||||
            var ret = db.AddBoard(name, ipAddr, port);
 | 
			
		||||
            return Ok(ret);
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error(ex, "新增板子时发生异常");
 | 
			
		||||
            return StatusCode(StatusCodes.Status500InternalServerError, "新增失败,请稍后重试");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 删除板子(管理员权限)
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [Authorize("Admin")]
 | 
			
		||||
    [HttpDelete("DeleteBoard")]
 | 
			
		||||
    [EnableCors("Users")]
 | 
			
		||||
    [ProducesResponseType(typeof(int), StatusCodes.Status200OK)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status500InternalServerError)]
 | 
			
		||||
    public IActionResult DeleteBoard(Guid id)
 | 
			
		||||
    {
 | 
			
		||||
        if (id == Guid.Empty)
 | 
			
		||||
            return BadRequest("板子Guid不能为空");
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            using var db = new Database.AppDataConnection();
 | 
			
		||||
            var ret = db.DeleteBoardByID(id);
 | 
			
		||||
            return Ok(ret);
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error(ex, "删除板子时发生异常");
 | 
			
		||||
            return StatusCode(StatusCodes.Status500InternalServerError, "删除失败,请稍后重试");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// 获取全部板子(管理员权限)
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [Authorize("Admin")]
 | 
			
		||||
    [HttpGet("GetAllBoards")]
 | 
			
		||||
    [EnableCors("Users")]
 | 
			
		||||
    [ProducesResponseType(typeof(Database.Board[]), StatusCodes.Status200OK)]
 | 
			
		||||
    [ProducesResponseType(StatusCodes.Status500InternalServerError)]
 | 
			
		||||
    public IActionResult GetAllBoards()
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            using var db = new Database.AppDataConnection();
 | 
			
		||||
            var boards = db.GetAllBoard();
 | 
			
		||||
            return Ok(boards);
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            logger.Error(ex, "获取全部板子时发生异常");
 | 
			
		||||
            return StatusCode(StatusCodes.Status500InternalServerError, "获取失败,请稍后重试");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
using DotNext;
 | 
			
		||||
using Microsoft.AspNetCore.Authorization;
 | 
			
		||||
using Microsoft.AspNetCore.Cors;
 | 
			
		||||
using Microsoft.AspNetCore.Mvc;
 | 
			
		||||
 | 
			
		||||
@@ -24,6 +25,7 @@ public class RemoteUpdateController : ControllerBase
 | 
			
		||||
    /// <param name="bitstream2">比特流文件2</param>
 | 
			
		||||
    /// <param name="bitstream3">比特流文件3</param>
 | 
			
		||||
    /// <returns>上传结果</returns>
 | 
			
		||||
    [Authorize("Admin")]
 | 
			
		||||
    [HttpPost("UploadBitstream")]
 | 
			
		||||
    [EnableCors("Users")]
 | 
			
		||||
    [ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
 | 
			
		||||
@@ -129,6 +131,7 @@ public class RemoteUpdateController : ControllerBase
 | 
			
		||||
    /// <param name="address"> 设备地址 </param>
 | 
			
		||||
    /// <param name="port"> 设备端口 </param>
 | 
			
		||||
    /// <param name="bitstreamNum"> 比特流位号 </param>
 | 
			
		||||
    [Authorize("Admin")]
 | 
			
		||||
    [HttpPost("DownloadBitstream")]
 | 
			
		||||
    [EnableCors("Users")]
 | 
			
		||||
    [ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
 | 
			
		||||
@@ -179,6 +182,7 @@ public class RemoteUpdateController : ControllerBase
 | 
			
		||||
    /// <param name="port">设备端口</param>
 | 
			
		||||
    /// <param name="bitstreamNum">比特流编号</param>
 | 
			
		||||
    /// <returns>总共上传比特流的数量</returns>
 | 
			
		||||
    [Authorize("Admin")]
 | 
			
		||||
    [HttpPost("DownloadMultiBitstreams")]
 | 
			
		||||
    [EnableCors("Users")]
 | 
			
		||||
    [ProducesResponseType(typeof(int), StatusCodes.Status200OK)]
 | 
			
		||||
@@ -239,6 +243,7 @@ public class RemoteUpdateController : ControllerBase
 | 
			
		||||
    /// <param name="port">设备端口</param>
 | 
			
		||||
    /// <param name="bitstreamNum">比特流编号</param>
 | 
			
		||||
    /// <returns>操作结果</returns>
 | 
			
		||||
    [Authorize("Admin")]
 | 
			
		||||
    [HttpPost("HotResetBitstream")]
 | 
			
		||||
    [EnableCors("Users")]
 | 
			
		||||
    [ProducesResponseType(typeof(bool), StatusCodes.Status200OK)]
 | 
			
		||||
@@ -267,6 +272,7 @@ public class RemoteUpdateController : ControllerBase
 | 
			
		||||
    /// <param name="address">[TODO:parameter]</param>
 | 
			
		||||
    /// <param name="port">[TODO:parameter]</param>
 | 
			
		||||
    /// <returns>[TODO:return]</returns>
 | 
			
		||||
    [Authorize("Admin")]
 | 
			
		||||
    [HttpPost("GetFirmwareVersion")]
 | 
			
		||||
    [EnableCors("Users")]
 | 
			
		||||
    [ProducesResponseType(typeof(UInt32), StatusCodes.Status200OK)]
 | 
			
		||||
 
 | 
			
		||||
@@ -98,6 +98,12 @@ public class Board
 | 
			
		||||
    [NotNull]
 | 
			
		||||
    public required BoardStatus Status { get; set; }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// [TODO:description]
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    [NotNull]
 | 
			
		||||
    public string FirmVersion { get; set; } = "1.0.0";
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// [TODO:description]
 | 
			
		||||
    /// </summary>
 | 
			
		||||
@@ -273,6 +279,35 @@ public class AppDataConnection : DataConnection
 | 
			
		||||
        return this.Insert(board);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// [TODO:description]
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="name">[TODO:parameter]</param>
 | 
			
		||||
    /// <returns>[TODO:return]</returns>
 | 
			
		||||
    public int DeleteBoardByName(string name)
 | 
			
		||||
    {
 | 
			
		||||
        return this.Board.Where(board => board.BoardName == name).Delete();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// [TODO:description]
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="id">[TODO:parameter]</param>
 | 
			
		||||
    /// <returns>[TODO:return]</returns>
 | 
			
		||||
    public int DeleteBoardByID(Guid id)
 | 
			
		||||
    {
 | 
			
		||||
        return this.Board.Where(board => board.ID == id).Delete();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// [TODO:description]
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <returns>[TODO:return]</returns>
 | 
			
		||||
    public Board[] GetAllBoard()
 | 
			
		||||
    {
 | 
			
		||||
        return this.Board.ToArray();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// [TODO:description]
 | 
			
		||||
    /// </summary>
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										308
									
								
								src/APIClient.ts
									
									
									
									
									
								
							
							
						
						
									
										308
									
								
								src/APIClient.ts
									
									
									
									
									
								
							@@ -482,11 +482,57 @@ export class DataClient {
 | 
			
		||||
        return Promise.resolve<string>(null as any);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 测试管理员用户认证,需携带有效 JWT
 | 
			
		||||
     * @return 认证成功信息
 | 
			
		||||
     */
 | 
			
		||||
    testAdminAuth(): Promise<string> {
 | 
			
		||||
        let url_ = this.baseUrl + "/api/Data/TestAdminAuth";
 | 
			
		||||
        url_ = url_.replace(/[?&]$/, "");
 | 
			
		||||
 | 
			
		||||
        let options_: RequestInit = {
 | 
			
		||||
            method: "GET",
 | 
			
		||||
            headers: {
 | 
			
		||||
                "Accept": "application/json"
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        return this.http.fetch(url_, options_).then((_response: Response) => {
 | 
			
		||||
            return this.processTestAdminAuth(_response);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected processTestAdminAuth(response: Response): Promise<string> {
 | 
			
		||||
        const status = response.status;
 | 
			
		||||
        let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
 | 
			
		||||
        if (status === 200) {
 | 
			
		||||
            return response.text().then((_responseText) => {
 | 
			
		||||
            let result200: any = null;
 | 
			
		||||
            let resultData200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver);
 | 
			
		||||
                result200 = resultData200 !== undefined ? resultData200 : <any>null;
 | 
			
		||||
    
 | 
			
		||||
            return result200;
 | 
			
		||||
            });
 | 
			
		||||
        } else if (status === 401) {
 | 
			
		||||
            return response.text().then((_responseText) => {
 | 
			
		||||
            let result401: any = null;
 | 
			
		||||
            let resultData401 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver);
 | 
			
		||||
            result401 = ProblemDetails.fromJS(resultData401);
 | 
			
		||||
            return throwException("A server side error occurred.", status, _responseText, _headers, result401);
 | 
			
		||||
            });
 | 
			
		||||
        } else if (status !== 200 && status !== 204) {
 | 
			
		||||
            return response.text().then((_responseText) => {
 | 
			
		||||
            return throwException("An unexpected server error occurred.", status, _responseText, _headers);
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
        return Promise.resolve<string>(null as any);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获取当前用户信息
 | 
			
		||||
     * @return 用户信息,包括ID、用户名、邮箱和板卡ID
 | 
			
		||||
     */
 | 
			
		||||
    getUserInfo(): Promise<GetUserInfoResponse> {
 | 
			
		||||
    getUserInfo(): Promise<UserInfo> {
 | 
			
		||||
        let url_ = this.baseUrl + "/api/Data/GetUserInfo";
 | 
			
		||||
        url_ = url_.replace(/[?&]$/, "");
 | 
			
		||||
 | 
			
		||||
@@ -502,14 +548,14 @@ export class DataClient {
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected processGetUserInfo(response: Response): Promise<GetUserInfoResponse> {
 | 
			
		||||
    protected processGetUserInfo(response: Response): Promise<UserInfo> {
 | 
			
		||||
        const status = response.status;
 | 
			
		||||
        let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
 | 
			
		||||
        if (status === 200) {
 | 
			
		||||
            return response.text().then((_responseText) => {
 | 
			
		||||
            let result200: any = null;
 | 
			
		||||
            let resultData200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver);
 | 
			
		||||
            result200 = GetUserInfoResponse.fromJS(resultData200);
 | 
			
		||||
            result200 = UserInfo.fromJS(resultData200);
 | 
			
		||||
            return result200;
 | 
			
		||||
            });
 | 
			
		||||
        } else if (status === 400) {
 | 
			
		||||
@@ -535,7 +581,7 @@ export class DataClient {
 | 
			
		||||
            return throwException("An unexpected server error occurred.", status, _responseText, _headers);
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
        return Promise.resolve<GetUserInfoResponse>(null as any);
 | 
			
		||||
        return Promise.resolve<UserInfo>(null as any);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -602,6 +648,172 @@ export class DataClient {
 | 
			
		||||
        }
 | 
			
		||||
        return Promise.resolve<boolean>(null as any);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 新增板子(管理员权限)
 | 
			
		||||
     * @param name (optional) 
 | 
			
		||||
     * @param ipAddr (optional) 
 | 
			
		||||
     * @param port (optional) 
 | 
			
		||||
     */
 | 
			
		||||
    addBoard(name: string | undefined, ipAddr: string | undefined, port: number | undefined): Promise<number> {
 | 
			
		||||
        let url_ = this.baseUrl + "/api/Data/AddBoard?";
 | 
			
		||||
        if (name === null)
 | 
			
		||||
            throw new Error("The parameter 'name' cannot be null.");
 | 
			
		||||
        else if (name !== undefined)
 | 
			
		||||
            url_ += "name=" + encodeURIComponent("" + name) + "&";
 | 
			
		||||
        if (ipAddr === null)
 | 
			
		||||
            throw new Error("The parameter 'ipAddr' cannot be null.");
 | 
			
		||||
        else if (ipAddr !== undefined)
 | 
			
		||||
            url_ += "ipAddr=" + encodeURIComponent("" + ipAddr) + "&";
 | 
			
		||||
        if (port === null)
 | 
			
		||||
            throw new Error("The parameter 'port' cannot be null.");
 | 
			
		||||
        else if (port !== undefined)
 | 
			
		||||
            url_ += "port=" + encodeURIComponent("" + port) + "&";
 | 
			
		||||
        url_ = url_.replace(/[?&]$/, "");
 | 
			
		||||
 | 
			
		||||
        let options_: RequestInit = {
 | 
			
		||||
            method: "POST",
 | 
			
		||||
            headers: {
 | 
			
		||||
                "Accept": "application/json"
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        return this.http.fetch(url_, options_).then((_response: Response) => {
 | 
			
		||||
            return this.processAddBoard(_response);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected processAddBoard(response: Response): Promise<number> {
 | 
			
		||||
        const status = response.status;
 | 
			
		||||
        let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
 | 
			
		||||
        if (status === 200) {
 | 
			
		||||
            return response.text().then((_responseText) => {
 | 
			
		||||
            let result200: any = null;
 | 
			
		||||
            let resultData200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver);
 | 
			
		||||
                result200 = resultData200 !== undefined ? resultData200 : <any>null;
 | 
			
		||||
    
 | 
			
		||||
            return result200;
 | 
			
		||||
            });
 | 
			
		||||
        } else if (status === 400) {
 | 
			
		||||
            return response.text().then((_responseText) => {
 | 
			
		||||
            let result400: any = null;
 | 
			
		||||
            let resultData400 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver);
 | 
			
		||||
            result400 = ProblemDetails.fromJS(resultData400);
 | 
			
		||||
            return throwException("A server side error occurred.", status, _responseText, _headers, result400);
 | 
			
		||||
            });
 | 
			
		||||
        } else if (status === 500) {
 | 
			
		||||
            return response.text().then((_responseText) => {
 | 
			
		||||
            return throwException("A server side error occurred.", status, _responseText, _headers);
 | 
			
		||||
            });
 | 
			
		||||
        } else if (status !== 200 && status !== 204) {
 | 
			
		||||
            return response.text().then((_responseText) => {
 | 
			
		||||
            return throwException("An unexpected server error occurred.", status, _responseText, _headers);
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
        return Promise.resolve<number>(null as any);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 删除板子(管理员权限)
 | 
			
		||||
     * @param id (optional) 
 | 
			
		||||
     */
 | 
			
		||||
    deleteBoard(id: string | undefined): Promise<number> {
 | 
			
		||||
        let url_ = this.baseUrl + "/api/Data/DeleteBoard?";
 | 
			
		||||
        if (id === null)
 | 
			
		||||
            throw new Error("The parameter 'id' cannot be null.");
 | 
			
		||||
        else if (id !== undefined)
 | 
			
		||||
            url_ += "id=" + encodeURIComponent("" + id) + "&";
 | 
			
		||||
        url_ = url_.replace(/[?&]$/, "");
 | 
			
		||||
 | 
			
		||||
        let options_: RequestInit = {
 | 
			
		||||
            method: "DELETE",
 | 
			
		||||
            headers: {
 | 
			
		||||
                "Accept": "application/json"
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        return this.http.fetch(url_, options_).then((_response: Response) => {
 | 
			
		||||
            return this.processDeleteBoard(_response);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected processDeleteBoard(response: Response): Promise<number> {
 | 
			
		||||
        const status = response.status;
 | 
			
		||||
        let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
 | 
			
		||||
        if (status === 200) {
 | 
			
		||||
            return response.text().then((_responseText) => {
 | 
			
		||||
            let result200: any = null;
 | 
			
		||||
            let resultData200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver);
 | 
			
		||||
                result200 = resultData200 !== undefined ? resultData200 : <any>null;
 | 
			
		||||
    
 | 
			
		||||
            return result200;
 | 
			
		||||
            });
 | 
			
		||||
        } else if (status === 400) {
 | 
			
		||||
            return response.text().then((_responseText) => {
 | 
			
		||||
            let result400: any = null;
 | 
			
		||||
            let resultData400 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver);
 | 
			
		||||
            result400 = ProblemDetails.fromJS(resultData400);
 | 
			
		||||
            return throwException("A server side error occurred.", status, _responseText, _headers, result400);
 | 
			
		||||
            });
 | 
			
		||||
        } else if (status === 500) {
 | 
			
		||||
            return response.text().then((_responseText) => {
 | 
			
		||||
            return throwException("A server side error occurred.", status, _responseText, _headers);
 | 
			
		||||
            });
 | 
			
		||||
        } else if (status !== 200 && status !== 204) {
 | 
			
		||||
            return response.text().then((_responseText) => {
 | 
			
		||||
            return throwException("An unexpected server error occurred.", status, _responseText, _headers);
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
        return Promise.resolve<number>(null as any);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 获取全部板子(管理员权限)
 | 
			
		||||
     */
 | 
			
		||||
    getAllBoards(): Promise<Board[]> {
 | 
			
		||||
        let url_ = this.baseUrl + "/api/Data/GetAllBoards";
 | 
			
		||||
        url_ = url_.replace(/[?&]$/, "");
 | 
			
		||||
 | 
			
		||||
        let options_: RequestInit = {
 | 
			
		||||
            method: "GET",
 | 
			
		||||
            headers: {
 | 
			
		||||
                "Accept": "application/json"
 | 
			
		||||
            }
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        return this.http.fetch(url_, options_).then((_response: Response) => {
 | 
			
		||||
            return this.processGetAllBoards(_response);
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected processGetAllBoards(response: Response): Promise<Board[]> {
 | 
			
		||||
        const status = response.status;
 | 
			
		||||
        let _headers: any = {}; if (response.headers && response.headers.forEach) { response.headers.forEach((v: any, k: any) => _headers[k] = v); };
 | 
			
		||||
        if (status === 200) {
 | 
			
		||||
            return response.text().then((_responseText) => {
 | 
			
		||||
            let result200: any = null;
 | 
			
		||||
            let resultData200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver);
 | 
			
		||||
            if (Array.isArray(resultData200)) {
 | 
			
		||||
                result200 = [] as any;
 | 
			
		||||
                for (let item of resultData200)
 | 
			
		||||
                    result200!.push(Board.fromJS(item));
 | 
			
		||||
            }
 | 
			
		||||
            else {
 | 
			
		||||
                result200 = <any>null;
 | 
			
		||||
            }
 | 
			
		||||
            return result200;
 | 
			
		||||
            });
 | 
			
		||||
        } else if (status === 500) {
 | 
			
		||||
            return response.text().then((_responseText) => {
 | 
			
		||||
            return throwException("A server side error occurred.", status, _responseText, _headers);
 | 
			
		||||
            });
 | 
			
		||||
        } else if (status !== 200 && status !== 204) {
 | 
			
		||||
            return response.text().then((_responseText) => {
 | 
			
		||||
            return throwException("An unexpected server error occurred.", status, _responseText, _headers);
 | 
			
		||||
            });
 | 
			
		||||
        }
 | 
			
		||||
        return Promise.resolve<Board[]>(null as any);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class DDSClient {
 | 
			
		||||
@@ -2445,7 +2657,8 @@ export interface IProblemDetails {
 | 
			
		||||
    [key: string]: any;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class GetUserInfoResponse implements IGetUserInfoResponse {
 | 
			
		||||
/** [TODO:description] */
 | 
			
		||||
export class UserInfo implements IUserInfo {
 | 
			
		||||
    /** 用户的唯一标识符 */
 | 
			
		||||
    id!: string;
 | 
			
		||||
    /** 用户的名称 */
 | 
			
		||||
@@ -2455,7 +2668,7 @@ export class GetUserInfoResponse implements IGetUserInfoResponse {
 | 
			
		||||
    /** 用户关联的板卡ID */
 | 
			
		||||
    boardID!: string;
 | 
			
		||||
 | 
			
		||||
    constructor(data?: IGetUserInfoResponse) {
 | 
			
		||||
    constructor(data?: IUserInfo) {
 | 
			
		||||
        if (data) {
 | 
			
		||||
            for (var property in data) {
 | 
			
		||||
                if (data.hasOwnProperty(property))
 | 
			
		||||
@@ -2473,9 +2686,9 @@ export class GetUserInfoResponse implements IGetUserInfoResponse {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static fromJS(data: any): GetUserInfoResponse {
 | 
			
		||||
    static fromJS(data: any): UserInfo {
 | 
			
		||||
        data = typeof data === 'object' ? data : {};
 | 
			
		||||
        let result = new GetUserInfoResponse();
 | 
			
		||||
        let result = new UserInfo();
 | 
			
		||||
        result.init(data);
 | 
			
		||||
        return result;
 | 
			
		||||
    }
 | 
			
		||||
@@ -2490,7 +2703,8 @@ export class GetUserInfoResponse implements IGetUserInfoResponse {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface IGetUserInfoResponse {
 | 
			
		||||
/** [TODO:description] */
 | 
			
		||||
export interface IUserInfo {
 | 
			
		||||
    /** 用户的唯一标识符 */
 | 
			
		||||
    id: string;
 | 
			
		||||
    /** 用户的名称 */
 | 
			
		||||
@@ -2501,6 +2715,82 @@ export interface IGetUserInfoResponse {
 | 
			
		||||
    boardID: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** FPGA 板子类,表示板子信息 */
 | 
			
		||||
export class Board implements IBoard {
 | 
			
		||||
    /** FPGA 板子的唯一标识符 */
 | 
			
		||||
    id!: string;
 | 
			
		||||
    /** FPGA 板子的名称 */
 | 
			
		||||
    boardName!: string;
 | 
			
		||||
    /** [TODO:description] */
 | 
			
		||||
    ipAddr!: string;
 | 
			
		||||
    /** [TODO:description] */
 | 
			
		||||
    port!: number;
 | 
			
		||||
    /** [TODO:description] */
 | 
			
		||||
    status!: BoardStatus;
 | 
			
		||||
    /** [TODO:description] */
 | 
			
		||||
    firmVersion!: string;
 | 
			
		||||
 | 
			
		||||
    constructor(data?: IBoard) {
 | 
			
		||||
        if (data) {
 | 
			
		||||
            for (var property in data) {
 | 
			
		||||
                if (data.hasOwnProperty(property))
 | 
			
		||||
                    (<any>this)[property] = (<any>data)[property];
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    init(_data?: any) {
 | 
			
		||||
        if (_data) {
 | 
			
		||||
            this.id = _data["id"];
 | 
			
		||||
            this.boardName = _data["boardName"];
 | 
			
		||||
            this.ipAddr = _data["ipAddr"];
 | 
			
		||||
            this.port = _data["port"];
 | 
			
		||||
            this.status = _data["status"];
 | 
			
		||||
            this.firmVersion = _data["firmVersion"];
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    static fromJS(data: any): Board {
 | 
			
		||||
        data = typeof data === 'object' ? data : {};
 | 
			
		||||
        let result = new Board();
 | 
			
		||||
        result.init(data);
 | 
			
		||||
        return result;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    toJSON(data?: any) {
 | 
			
		||||
        data = typeof data === 'object' ? data : {};
 | 
			
		||||
        data["id"] = this.id;
 | 
			
		||||
        data["boardName"] = this.boardName;
 | 
			
		||||
        data["ipAddr"] = this.ipAddr;
 | 
			
		||||
        data["port"] = this.port;
 | 
			
		||||
        data["status"] = this.status;
 | 
			
		||||
        data["firmVersion"] = this.firmVersion;
 | 
			
		||||
        return data;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** FPGA 板子类,表示板子信息 */
 | 
			
		||||
export interface IBoard {
 | 
			
		||||
    /** FPGA 板子的唯一标识符 */
 | 
			
		||||
    id: string;
 | 
			
		||||
    /** FPGA 板子的名称 */
 | 
			
		||||
    boardName: string;
 | 
			
		||||
    /** [TODO:description] */
 | 
			
		||||
    ipAddr: string;
 | 
			
		||||
    /** [TODO:description] */
 | 
			
		||||
    port: number;
 | 
			
		||||
    /** [TODO:description] */
 | 
			
		||||
    status: BoardStatus;
 | 
			
		||||
    /** [TODO:description] */
 | 
			
		||||
    firmVersion: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** [TODO:description] */
 | 
			
		||||
export enum BoardStatus {
 | 
			
		||||
    Busy = 0,
 | 
			
		||||
    Available = 1,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class SystemException extends Exception implements ISystemException {
 | 
			
		||||
 | 
			
		||||
    constructor(data?: ISystemException) {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,93 +1,122 @@
 | 
			
		||||
import { DataClient } from '@/APIClient'
 | 
			
		||||
import { DataClient } from "@/APIClient";
 | 
			
		||||
 | 
			
		||||
export class AuthManager {
 | 
			
		||||
  // 存储token到localStorage
 | 
			
		||||
  public static setToken(token: string): void {
 | 
			
		||||
    localStorage.setItem('authToken', token)
 | 
			
		||||
    localStorage.setItem("authToken", token);
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  // 从localStorage获取token
 | 
			
		||||
  public static getToken(): string | null {
 | 
			
		||||
    return localStorage.getItem('authToken')
 | 
			
		||||
    return localStorage.getItem("authToken");
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  // 清除token
 | 
			
		||||
  public static clearToken(): void {
 | 
			
		||||
    localStorage.removeItem('authToken')
 | 
			
		||||
    localStorage.removeItem("authToken");
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  // 检查是否已认证
 | 
			
		||||
  public static async isAuthenticated(): Promise<boolean> {
 | 
			
		||||
    return await AuthManager.verifyToken()
 | 
			
		||||
    return await AuthManager.verifyToken();
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  // 为HTTP请求添加Authorization header
 | 
			
		||||
  public static addAuthHeader(client: any): void {
 | 
			
		||||
    const token = AuthManager.getToken()
 | 
			
		||||
    const token = AuthManager.getToken();
 | 
			
		||||
    if (token && client.http) {
 | 
			
		||||
      const originalFetch = client.http.fetch
 | 
			
		||||
      const originalFetch = client.http.fetch;
 | 
			
		||||
      client.http.fetch = (url: RequestInfo, init?: RequestInit) => {
 | 
			
		||||
        if (!init) init = {}
 | 
			
		||||
        if (!init.headers) init.headers = {}
 | 
			
		||||
        
 | 
			
		||||
        if (!init) init = {};
 | 
			
		||||
        if (!init.headers) init.headers = {};
 | 
			
		||||
 | 
			
		||||
        // 添加Authorization header
 | 
			
		||||
        if (typeof init.headers === 'object' && init.headers !== null) {
 | 
			
		||||
          (init.headers as any)['Authorization'] = `Bearer ${token}`
 | 
			
		||||
        if (typeof init.headers === "object" && init.headers !== null) {
 | 
			
		||||
          (init.headers as any)["Authorization"] = `Bearer ${token}`;
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        return originalFetch(url, init)
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
        return originalFetch(url, init);
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  // 创建已配置认证的API客户端
 | 
			
		||||
  public static createAuthenticatedClient(): DataClient {
 | 
			
		||||
    const client = new DataClient()
 | 
			
		||||
    AuthManager.addAuthHeader(client)
 | 
			
		||||
    return client
 | 
			
		||||
    const client = new DataClient();
 | 
			
		||||
    AuthManager.addAuthHeader(client);
 | 
			
		||||
    return client;
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  // 登录函数
 | 
			
		||||
  public static async login(username: string, password: string): Promise<boolean> {
 | 
			
		||||
  public static async login(
 | 
			
		||||
    username: string,
 | 
			
		||||
    password: string,
 | 
			
		||||
  ): Promise<boolean> {
 | 
			
		||||
    try {
 | 
			
		||||
      const client = new DataClient()
 | 
			
		||||
      const token = await client.login(username, password)
 | 
			
		||||
      
 | 
			
		||||
      const client = new DataClient();
 | 
			
		||||
      const token = await client.login(username, password);
 | 
			
		||||
 | 
			
		||||
      if (token) {
 | 
			
		||||
        AuthManager.setToken(token)
 | 
			
		||||
        
 | 
			
		||||
        AuthManager.setToken(token);
 | 
			
		||||
 | 
			
		||||
        // 验证token
 | 
			
		||||
        const authClient = AuthManager.createAuthenticatedClient()
 | 
			
		||||
        await authClient.testAuth()
 | 
			
		||||
        
 | 
			
		||||
        return true
 | 
			
		||||
        const authClient = AuthManager.createAuthenticatedClient();
 | 
			
		||||
        await authClient.testAuth();
 | 
			
		||||
 | 
			
		||||
        return true;
 | 
			
		||||
      }
 | 
			
		||||
      return false
 | 
			
		||||
      return false;
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      AuthManager.clearToken()
 | 
			
		||||
      throw error
 | 
			
		||||
      AuthManager.clearToken();
 | 
			
		||||
      throw error;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  // 登出函数
 | 
			
		||||
  public static logout(): void {
 | 
			
		||||
    AuthManager.clearToken()
 | 
			
		||||
    AuthManager.clearToken();
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
 | 
			
		||||
  // 验证当前token是否有效
 | 
			
		||||
  public static async verifyToken(): Promise<boolean> {
 | 
			
		||||
    try {
 | 
			
		||||
      const token = AuthManager.getToken()
 | 
			
		||||
      const token = AuthManager.getToken();
 | 
			
		||||
      if (!token) {
 | 
			
		||||
        return false
 | 
			
		||||
        return false;
 | 
			
		||||
      }
 | 
			
		||||
      
 | 
			
		||||
      const client = AuthManager.createAuthenticatedClient()
 | 
			
		||||
      await client.testAuth()
 | 
			
		||||
      return true
 | 
			
		||||
 | 
			
		||||
      const client = AuthManager.createAuthenticatedClient();
 | 
			
		||||
      await client.testAuth();
 | 
			
		||||
      return true;
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      AuthManager.clearToken()
 | 
			
		||||
      return false
 | 
			
		||||
      AuthManager.clearToken();
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
  // 验证管理员权限
 | 
			
		||||
  public static async verifyAdminAuth(): Promise<boolean> {
 | 
			
		||||
    try {
 | 
			
		||||
      const token = AuthManager.getToken();
 | 
			
		||||
      if (!token) {
 | 
			
		||||
        return false;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const client = AuthManager.createAuthenticatedClient();
 | 
			
		||||
      await client.testAdminAuth();
 | 
			
		||||
      return true;
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      // 如果是401错误,说明token有效但不是管理员,不需要清除token
 | 
			
		||||
      // 如果是其他错误,可能token无效,清除token
 | 
			
		||||
      if (
 | 
			
		||||
        error &&
 | 
			
		||||
        typeof error === "object" &&
 | 
			
		||||
        "status" in error &&
 | 
			
		||||
        error.status !== 401
 | 
			
		||||
      ) {
 | 
			
		||||
        AuthManager.clearToken();
 | 
			
		||||
      }
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,19 +1,27 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="bg-base-200 min-h-screen">
 | 
			
		||||
    <main class="hero min-h-screen bg-base-200">
 | 
			
		||||
      <div class="hero-content flex-col xl:flex-row-reverse gap-8 xl:gap-12 py-10 px-4">        <!-- 例程轮播容器 -->
 | 
			
		||||
        <div class="w-full flex justify-center" style="min-width: 650px;">
 | 
			
		||||
      <div
 | 
			
		||||
        class="hero-content flex-col xl:flex-row-reverse gap-8 xl:gap-12 py-10 px-4"
 | 
			
		||||
      >
 | 
			
		||||
        <!-- 例程轮播容器 -->
 | 
			
		||||
        <div class="w-full flex justify-center" style="min-width: 650px">
 | 
			
		||||
          <TutorialCarousel :autoRotationInterval="3000" />
 | 
			
		||||
        </div>
 | 
			
		||||
        <!-- 内容容器 -->
 | 
			
		||||
        <div class="content-container max-w-md lg:max-w-2xl transform transition-all duration-500 ease-in-out">
 | 
			
		||||
        <div
 | 
			
		||||
          class="content-container max-w-md lg:max-w-2xl transform transition-all duration-500 ease-in-out"
 | 
			
		||||
        >
 | 
			
		||||
          <h1 class="text-4xl md:text-5xl font-bold mb-3 relative group">
 | 
			
		||||
            <span class="relative z-10 bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">
 | 
			
		||||
            <span
 | 
			
		||||
              class="relative z-10 bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent"
 | 
			
		||||
            >
 | 
			
		||||
              Welcome to
 | 
			
		||||
            </span>
 | 
			
		||||
            <span class="text-base-content">FPGA Web Lab!</span>
 | 
			
		||||
            <span
 | 
			
		||||
              class="absolute bottom-0 left-0 w-0 h-1 bg-primary transition-all duration-500 ease-in-out group-hover:w-3/4"></span>
 | 
			
		||||
              class="absolute bottom-0 left-0 w-0 h-1 bg-primary transition-all duration-500 ease-in-out group-hover:w-3/4"
 | 
			
		||||
            ></span>
 | 
			
		||||
          </h1>
 | 
			
		||||
 | 
			
		||||
          <p class="py-6 text-lg opacity-80 leading-relaxed">
 | 
			
		||||
@@ -22,59 +30,63 @@
 | 
			
		||||
            designs seamlessly.
 | 
			
		||||
          </p>
 | 
			
		||||
          <div class="flex flex-wrap gap-4 actions-container">
 | 
			
		||||
            <router-link to="/project"
 | 
			
		||||
              class="btn btn-primary text-base-100 shadow-lg transform transition-all duration-300 hover:scale-105 hover:shadow-xl hover:-translate-y-1">
 | 
			
		||||
              <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 24 24" fill="none"
 | 
			
		||||
                stroke="currentColor" stroke-width="2">
 | 
			
		||||
            <router-link
 | 
			
		||||
              to="/project"
 | 
			
		||||
              class="btn btn-primary text-base-100 shadow-lg transform transition-all duration-300 hover:scale-105 hover:shadow-xl hover:-translate-y-1"
 | 
			
		||||
            >
 | 
			
		||||
              <svg
 | 
			
		||||
                xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
                class="h-5 w-5 mr-2"
 | 
			
		||||
                viewBox="0 0 24 24"
 | 
			
		||||
                fill="none"
 | 
			
		||||
                stroke="currentColor"
 | 
			
		||||
                stroke-width="2"
 | 
			
		||||
              >
 | 
			
		||||
                <path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"></path>
 | 
			
		||||
                <path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"></path>
 | 
			
		||||
              </svg>
 | 
			
		||||
              进入工程界面
 | 
			
		||||
            </router-link>
 | 
			
		||||
 | 
			
		||||
            <router-link to="/login"
 | 
			
		||||
              class="btn btn-secondary text-base-100 shadow-lg transform transition-all duration-300 hover:scale-105 hover:shadow-xl hover:-translate-y-1">
 | 
			
		||||
              <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 24 24" fill="none"
 | 
			
		||||
                stroke="currentColor" stroke-width="2">
 | 
			
		||||
            <router-link
 | 
			
		||||
              to="/login"
 | 
			
		||||
              class="btn btn-secondary text-base-100 shadow-lg transform transition-all duration-300 hover:scale-105 hover:shadow-xl hover:-translate-y-1"
 | 
			
		||||
            >
 | 
			
		||||
              <svg
 | 
			
		||||
                xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
                class="h-5 w-5 mr-2"
 | 
			
		||||
                viewBox="0 0 24 24"
 | 
			
		||||
                fill="none"
 | 
			
		||||
                stroke="currentColor"
 | 
			
		||||
                stroke-width="2"
 | 
			
		||||
              >
 | 
			
		||||
                <rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
 | 
			
		||||
                <path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
 | 
			
		||||
              </svg>
 | 
			
		||||
              登录
 | 
			
		||||
            </router-link>
 | 
			
		||||
 | 
			
		||||
            <router-link to="/user"
 | 
			
		||||
              class="btn btn-accent text-base-100 shadow-lg transform transition-all duration-300 hover:scale-105 hover:shadow-xl hover:-translate-y-1">
 | 
			
		||||
              <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 24 24" fill="none"
 | 
			
		||||
                stroke="currentColor" stroke-width="2">
 | 
			
		||||
            <router-link
 | 
			
		||||
              to="/user"
 | 
			
		||||
              class="btn btn-accent text-base-100 shadow-lg transform transition-all duration-300 hover:scale-105 hover:shadow-xl hover:-translate-y-1"
 | 
			
		||||
            >
 | 
			
		||||
              <svg
 | 
			
		||||
                xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
                class="h-5 w-5 mr-2"
 | 
			
		||||
                viewBox="0 0 24 24"
 | 
			
		||||
                fill="none"
 | 
			
		||||
                stroke="currentColor"
 | 
			
		||||
                stroke-width="2"
 | 
			
		||||
              >
 | 
			
		||||
                <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path>
 | 
			
		||||
                <circle cx="12" cy="7" r="4"></circle>
 | 
			
		||||
              </svg>
 | 
			
		||||
              用户中心
 | 
			
		||||
            </router-link>
 | 
			
		||||
            <router-link to="/test"
 | 
			
		||||
              class="btn btn-info text-base-100 shadow-lg transform transition-all duration-300 hover:scale-105 hover:shadow-xl hover:-translate-y-1">
 | 
			
		||||
              <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 24 24" fill="none"
 | 
			
		||||
                stroke="currentColor" stroke-width="2">
 | 
			
		||||
                <path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"></path>
 | 
			
		||||
                <polyline points="17 21 17 13 7 13 7 21"></polyline>
 | 
			
		||||
                <polyline points="7 3 7 8 15 8"></polyline>
 | 
			
		||||
              </svg>
 | 
			
		||||
              测试功能
 | 
			
		||||
            </router-link>
 | 
			
		||||
 | 
			
		||||
            <router-link to="/admin"
 | 
			
		||||
              class="btn btn-error text-base-100 shadow-lg transform transition-all duration-300 hover:scale-105 hover:shadow-xl hover:-translate-y-1">
 | 
			
		||||
              <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 24 24" fill="none"
 | 
			
		||||
                stroke="currentColor" stroke-width="2">
 | 
			
		||||
                <path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5z">
 | 
			
		||||
                </path>
 | 
			
		||||
                <circle cx="12" cy="12" r="3"></circle>
 | 
			
		||||
              </svg>
 | 
			
		||||
              管理控制台
 | 
			
		||||
            </router-link>
 | 
			
		||||
          </div>
 | 
			
		||||
          <div
 | 
			
		||||
            class="mt-8 p-4 bg-base-300 rounded-lg shadow-inner opacity-80 transition-all duration-300 hover:opacity-100 hover:shadow-md">
 | 
			
		||||
            class="mt-8 p-4 bg-base-300 rounded-lg shadow-inner opacity-80 transition-all duration-300 hover:opacity-100 hover:shadow-md"
 | 
			
		||||
          >
 | 
			
		||||
            <p class="text-sm">
 | 
			
		||||
              <span class="font-semibold text-primary">提示:</span>
 | 
			
		||||
              您可以在工程界面中创建、编辑和测试您的FPGA项目,使用我们简洁直观的界面轻松进行硬件设计。
 | 
			
		||||
 
 | 
			
		||||
@@ -1,119 +1,193 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="bg-base-200 min-h-screen p-6">
 | 
			
		||||
    <div class="flex flex-row justify-between items-center">
 | 
			
		||||
      <h1 class="text-3xl font-bold mb-6">FPGA 设备管理</h1>
 | 
			
		||||
      <button class="btn btn-ghost text-error hover:underline" @click="
 | 
			
		||||
  <div class="flex flex-row justify-between items-center">
 | 
			
		||||
    <h1 class="text-3xl font-bold mb-6">FPGA 设备管理</h1>
 | 
			
		||||
    <button
 | 
			
		||||
      class="btn btn-ghost text-error hover:underline"
 | 
			
		||||
      @click="
 | 
			
		||||
        () => {
 | 
			
		||||
          isEditMode = !isEditMode;
 | 
			
		||||
        }
 | 
			
		||||
      ">
 | 
			
		||||
        编辑
 | 
			
		||||
      </button>
 | 
			
		||||
    </div>
 | 
			
		||||
      "
 | 
			
		||||
    >
 | 
			
		||||
      编辑
 | 
			
		||||
    </button>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
    <div class="card bg-base-100 shadow-xl">
 | 
			
		||||
      <div class="card-body">
 | 
			
		||||
        <div class="flex flex-row justify-between items-center">
 | 
			
		||||
          <h2 class="card-title mb-4">IP 地址列表</h2>
 | 
			
		||||
          <button class="btn btn-ghost" @click="">刷新</button>
 | 
			
		||||
        </div>
 | 
			
		||||
  <div class="card bg-base-100 shadow-xl">
 | 
			
		||||
    <div class="card-body">
 | 
			
		||||
      <div class="flex flex-row justify-between items-center mb-4">
 | 
			
		||||
        <h2 class="card-title">IP 地址列表</h2>
 | 
			
		||||
        <button class="btn btn-ghost" @click="refreshData">刷新</button>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
        <div class="overflow-x-auto">
 | 
			
		||||
          <table class="table w-full">
 | 
			
		||||
            <!-- 表头 -->
 | 
			
		||||
            <thead>
 | 
			
		||||
              <tr class="bg-base-300">
 | 
			
		||||
                <th class="w-50">IP 地址</th>
 | 
			
		||||
                <th class="w-30">版本号</th>
 | 
			
		||||
                <th class="w-50">默认启动位流</th>
 | 
			
		||||
                <th class="w-80">黄金位流</th>
 | 
			
		||||
                <th class="w-80">应用位流1</th>
 | 
			
		||||
                <th class="w-80">应用位流2</th>
 | 
			
		||||
                <th class="w-80">应用位流3</th>
 | 
			
		||||
                <th>操作</th>
 | 
			
		||||
              </tr>
 | 
			
		||||
            </thead>
 | 
			
		||||
      <!-- 搜索和列控制 -->
 | 
			
		||||
      <div class="flex items-center py-4 gap-4">
 | 
			
		||||
        <input
 | 
			
		||||
          type="text"
 | 
			
		||||
          placeholder="筛选 IP 地址..."
 | 
			
		||||
          class="input input-bordered max-w-sm"
 | 
			
		||||
          :value="table.getColumn('devAddr')?.getFilterValue() as string"
 | 
			
		||||
          @input="
 | 
			
		||||
            table
 | 
			
		||||
              .getColumn('devAddr')
 | 
			
		||||
              ?.setFilterValue(($event.target as HTMLInputElement).value)
 | 
			
		||||
          "
 | 
			
		||||
        />
 | 
			
		||||
 | 
			
		||||
            <!-- 表格内容 -->
 | 
			
		||||
            <tbody>
 | 
			
		||||
              <tr class="hover">
 | 
			
		||||
                <td class="font-medium">
 | 
			
		||||
                  <input v-if="isEditMode" type="text" placeholder="Type here" class="input m-0" v-model="devAddr" />
 | 
			
		||||
                  <span v-else>{{ devAddr }}</span>
 | 
			
		||||
                </td>
 | 
			
		||||
                <td>v1.2.3</td>
 | 
			
		||||
                <td>
 | 
			
		||||
                  <select class="select select-bordered w-full max-w-xs" v-model="selectBitstream">
 | 
			
		||||
                    <option selected>黄金位流</option>
 | 
			
		||||
                    <option>应用位流1</option>
 | 
			
		||||
                    <option>应用位流2</option>
 | 
			
		||||
                    <option>应用位流3</option>
 | 
			
		||||
                  </select>
 | 
			
		||||
                </td>
 | 
			
		||||
                <!-- 黄金位流上传区 -->
 | 
			
		||||
                <td>
 | 
			
		||||
                  <div class="flex flex-col items-center gap-2">
 | 
			
		||||
                    <input type="file" class="file-input file-input-primary" @change="handleFileChange($event, 0)" />
 | 
			
		||||
                  </div>
 | 
			
		||||
                </td>
 | 
			
		||||
                <!-- 应用位流1上传区 -->
 | 
			
		||||
                <td>
 | 
			
		||||
                  <div class="flex flex-col items-center gap-2">
 | 
			
		||||
                    <input type="file" class="file-input file-input-secondary" @change="handleFileChange($event, 1)" />
 | 
			
		||||
                  </div>
 | 
			
		||||
                </td>
 | 
			
		||||
                <!-- 应用位流2上传区 -->
 | 
			
		||||
                <td>
 | 
			
		||||
                  <div class="flex flex-col items-center gap-2">
 | 
			
		||||
                    <input type="file" class="file-input file-input-accent" @change="handleFileChange($event, 2)" />
 | 
			
		||||
                  </div>
 | 
			
		||||
                </td>
 | 
			
		||||
                <!-- 应用位流3上传区 -->
 | 
			
		||||
                <td>
 | 
			
		||||
                  <div class="flex flex-col items-center gap-2">
 | 
			
		||||
                    <input type="file" class="file-input file-input-info" @change="handleFileChange($event, 3)" />
 | 
			
		||||
                  </div>
 | 
			
		||||
                </td>
 | 
			
		||||
 | 
			
		||||
                <!-- 操作按钮 -->
 | 
			
		||||
                <td class="flex gap-2">
 | 
			
		||||
                  <button class="btn grow btn-warning" @click="
 | 
			
		||||
                    uploadAndDownloadBitstreams(
 | 
			
		||||
                      devAddr,
 | 
			
		||||
                      goldBitstreamFile,
 | 
			
		||||
                      appBitstream1File,
 | 
			
		||||
                      appBitstream2File,
 | 
			
		||||
                      appBitstream3File,
 | 
			
		||||
        <div class="dropdown dropdown-end ml-auto">
 | 
			
		||||
          <div tabindex="0" role="button" class="btn btn-outline">
 | 
			
		||||
            列显示
 | 
			
		||||
            <svg
 | 
			
		||||
              class="w-4 h-4 ml-2"
 | 
			
		||||
              fill="none"
 | 
			
		||||
              stroke="currentColor"
 | 
			
		||||
              viewBox="0 0 24 24"
 | 
			
		||||
            >
 | 
			
		||||
              <path
 | 
			
		||||
                stroke-linecap="round"
 | 
			
		||||
                stroke-linejoin="round"
 | 
			
		||||
                stroke-width="2"
 | 
			
		||||
                d="m19 9-7 7-7-7"
 | 
			
		||||
              ></path>
 | 
			
		||||
            </svg>
 | 
			
		||||
          </div>
 | 
			
		||||
          <ul
 | 
			
		||||
            tabindex="0"
 | 
			
		||||
            class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52"
 | 
			
		||||
          >
 | 
			
		||||
            <li
 | 
			
		||||
              v-for="column in table
 | 
			
		||||
                .getAllColumns()
 | 
			
		||||
                .filter((column) => column.getCanHide())"
 | 
			
		||||
              :key="column.id"
 | 
			
		||||
            >
 | 
			
		||||
              <label class="label cursor-pointer">
 | 
			
		||||
                <span class="label-text capitalize">{{ column.id }}</span>
 | 
			
		||||
                <input
 | 
			
		||||
                  type="checkbox"
 | 
			
		||||
                  class="checkbox checkbox-sm"
 | 
			
		||||
                  :checked="column.getIsVisible()"
 | 
			
		||||
                  @change="
 | 
			
		||||
                    column.toggleVisibility(
 | 
			
		||||
                      !!($event.target as HTMLInputElement).checked,
 | 
			
		||||
                    )
 | 
			
		||||
                    ">
 | 
			
		||||
                    固化
 | 
			
		||||
                  </button>
 | 
			
		||||
                  <button class="btn grow btn-success" @click="
 | 
			
		||||
                    hotresetBitstream(devAddr, getSelectedBitstreamNum())
 | 
			
		||||
                    ">
 | 
			
		||||
                    切换并热启动
 | 
			
		||||
                  </button>
 | 
			
		||||
                </td>
 | 
			
		||||
              </tr>
 | 
			
		||||
            </tbody>
 | 
			
		||||
          </table>
 | 
			
		||||
                  "
 | 
			
		||||
                />
 | 
			
		||||
              </label>
 | 
			
		||||
            </li>
 | 
			
		||||
          </ul>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
        <div class="mt-6 bg-base-300 p-4 rounded-lg">
 | 
			
		||||
          <p class="text-sm opacity-80">
 | 
			
		||||
            <span class="font-semibold text-error">提示:</span>
 | 
			
		||||
            请谨慎操作FPGA固化和热启动功能,确保上传的位流文件无误,以避免设备损坏。
 | 
			
		||||
          </p>
 | 
			
		||||
      <!-- 表格 -->
 | 
			
		||||
      <div class="overflow-x-auto border border-base-300 rounded-lg">
 | 
			
		||||
        <table class="table w-full">
 | 
			
		||||
          <thead>
 | 
			
		||||
            <tr
 | 
			
		||||
              v-for="headerGroup in table.getHeaderGroups()"
 | 
			
		||||
              :key="headerGroup.id"
 | 
			
		||||
              class="bg-base-300"
 | 
			
		||||
            >
 | 
			
		||||
              <th v-for="header in headerGroup.headers" :key="header.id">
 | 
			
		||||
                <FlexRender
 | 
			
		||||
                  v-if="!header.isPlaceholder"
 | 
			
		||||
                  :render="header.column.columnDef.header"
 | 
			
		||||
                  :props="header.getContext()"
 | 
			
		||||
                />
 | 
			
		||||
              </th>
 | 
			
		||||
            </tr>
 | 
			
		||||
          </thead>
 | 
			
		||||
          <tbody>
 | 
			
		||||
            <template v-if="table.getRowModel().rows?.length">
 | 
			
		||||
              <template v-for="row in table.getRowModel().rows" :key="row.id">
 | 
			
		||||
                <tr
 | 
			
		||||
                  class="hover"
 | 
			
		||||
                  :class="{ 'bg-primary/10': row.getIsSelected() }"
 | 
			
		||||
                >
 | 
			
		||||
                  <td v-for="cell in row.getVisibleCells()" :key="cell.id">
 | 
			
		||||
                    <FlexRender
 | 
			
		||||
                      :render="cell.column.columnDef.cell"
 | 
			
		||||
                      :props="cell.getContext()"
 | 
			
		||||
                    />
 | 
			
		||||
                  </td>
 | 
			
		||||
                </tr>
 | 
			
		||||
                <tr v-if="row.getIsExpanded()">
 | 
			
		||||
                  <td :colspan="row.getAllCells().length" class="bg-base-200">
 | 
			
		||||
                    <div class="p-4">
 | 
			
		||||
                      <pre class="text-sm">{{
 | 
			
		||||
                        JSON.stringify(row.original, null, 2)
 | 
			
		||||
                      }}</pre>
 | 
			
		||||
                    </div>
 | 
			
		||||
                  </td>
 | 
			
		||||
                </tr>
 | 
			
		||||
              </template>
 | 
			
		||||
            </template>
 | 
			
		||||
            <tr v-else>
 | 
			
		||||
              <td
 | 
			
		||||
                :colspan="columns.length"
 | 
			
		||||
                class="h-24 text-center text-base-content/60"
 | 
			
		||||
              >
 | 
			
		||||
                暂无数据
 | 
			
		||||
              </td>
 | 
			
		||||
            </tr>
 | 
			
		||||
          </tbody>
 | 
			
		||||
        </table>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <!-- 分页控制 -->
 | 
			
		||||
      <div class="flex items-center justify-between py-4">
 | 
			
		||||
        <div class="text-sm text-base-content/60">
 | 
			
		||||
          已选择 {{ table.getFilteredSelectedRowModel().rows.length }} /
 | 
			
		||||
          {{ table.getFilteredRowModel().rows.length }} 行
 | 
			
		||||
        </div>
 | 
			
		||||
        <div class="flex gap-2">
 | 
			
		||||
          <button
 | 
			
		||||
            class="btn btn-outline btn-sm"
 | 
			
		||||
            :disabled="!table.getCanPreviousPage()"
 | 
			
		||||
            @click="table.previousPage()"
 | 
			
		||||
          >
 | 
			
		||||
            上一页
 | 
			
		||||
          </button>
 | 
			
		||||
          <button
 | 
			
		||||
            class="btn btn-outline btn-sm"
 | 
			
		||||
            :disabled="!table.getCanNextPage()"
 | 
			
		||||
            @click="table.nextPage()"
 | 
			
		||||
          >
 | 
			
		||||
            下一页
 | 
			
		||||
          </button>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div class="mt-6 bg-base-300 p-4 rounded-lg">
 | 
			
		||||
        <p class="text-sm opacity-80">
 | 
			
		||||
          <span class="font-semibold text-error">提示:</span>
 | 
			
		||||
          请谨慎操作FPGA固化和热启动功能,确保上传的位流文件无误,以避免设备损坏。
 | 
			
		||||
        </p>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import type {
 | 
			
		||||
  ColumnDef,
 | 
			
		||||
  ColumnFiltersState,
 | 
			
		||||
  ExpandedState,
 | 
			
		||||
  SortingState,
 | 
			
		||||
  VisibilityState,
 | 
			
		||||
} from "@tanstack/vue-table";
 | 
			
		||||
import {
 | 
			
		||||
  FlexRender,
 | 
			
		||||
  getCoreRowModel,
 | 
			
		||||
  getExpandedRowModel,
 | 
			
		||||
  getFilteredRowModel,
 | 
			
		||||
  getPaginationRowModel,
 | 
			
		||||
  getSortedRowModel,
 | 
			
		||||
  useVueTable,
 | 
			
		||||
} from "@tanstack/vue-table";
 | 
			
		||||
import { isNull, isUndefined } from "lodash";
 | 
			
		||||
import { ref } from "vue";
 | 
			
		||||
import {  RemoteUpdateClient } from "@/APIClient";
 | 
			
		||||
import { h, ref } from "vue";
 | 
			
		||||
import { RemoteUpdateClient } from "@/APIClient";
 | 
			
		||||
import { useDialogStore } from "@/stores/dialog";
 | 
			
		||||
import { Common } from "@/utils/Common";
 | 
			
		||||
 | 
			
		||||
@@ -122,52 +196,285 @@ const dialog = useDialogStore();
 | 
			
		||||
// 编辑状态
 | 
			
		||||
const isEditMode = ref(false);
 | 
			
		||||
 | 
			
		||||
// 选择热切换的比特流
 | 
			
		||||
const selectBitstream = ref("黄金位流");
 | 
			
		||||
 | 
			
		||||
// 存储上传文件的信息
 | 
			
		||||
const goldBitstreamFile = ref<File>();
 | 
			
		||||
const appBitstream1File = ref<File>();
 | 
			
		||||
const appBitstream2File = ref<File>();
 | 
			
		||||
const appBitstream3File = ref<File>();
 | 
			
		||||
 | 
			
		||||
// 远程升级相关参数
 | 
			
		||||
const devAddr = ref("192.168.1.100");
 | 
			
		||||
const devPort = 1234;
 | 
			
		||||
const remoteUpdater = new RemoteUpdateClient();
 | 
			
		||||
 | 
			
		||||
// 设备数据接口
 | 
			
		||||
export interface DeviceData {
 | 
			
		||||
  id: string;
 | 
			
		||||
  devAddr: string;
 | 
			
		||||
  version: string;
 | 
			
		||||
  defaultBitstream: string;
 | 
			
		||||
  goldBitstreamFile?: File;
 | 
			
		||||
  appBitstream1File?: File;
 | 
			
		||||
  appBitstream2File?: File;
 | 
			
		||||
  appBitstream3File?: File;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 模拟数据,实际应该从API获取
 | 
			
		||||
const data = ref<DeviceData[]>([
 | 
			
		||||
  {
 | 
			
		||||
    id: "1",
 | 
			
		||||
    devAddr: "192.168.1.100",
 | 
			
		||||
    version: "v1.2.3",
 | 
			
		||||
    defaultBitstream: "黄金位流",
 | 
			
		||||
  },
 | 
			
		||||
]);
 | 
			
		||||
 | 
			
		||||
// 表格列定义
 | 
			
		||||
const columns: ColumnDef<DeviceData>[] = [
 | 
			
		||||
  {
 | 
			
		||||
    id: "select",
 | 
			
		||||
    header: ({ table }) =>
 | 
			
		||||
      h("input", {
 | 
			
		||||
        type: "checkbox",
 | 
			
		||||
        class: "checkbox",
 | 
			
		||||
        checked:
 | 
			
		||||
          table.getIsAllPageRowsSelected() ||
 | 
			
		||||
          (table.getIsSomePageRowsSelected() ? "indeterminate" : false),
 | 
			
		||||
        onChange: (event: Event) =>
 | 
			
		||||
          table.toggleAllPageRowsSelected(
 | 
			
		||||
            !!(event.target as HTMLInputElement).checked,
 | 
			
		||||
          ),
 | 
			
		||||
      }),
 | 
			
		||||
    cell: ({ row }) =>
 | 
			
		||||
      h("input", {
 | 
			
		||||
        type: "checkbox",
 | 
			
		||||
        class: "checkbox",
 | 
			
		||||
        checked: row.getIsSelected(),
 | 
			
		||||
        onChange: (event: Event) =>
 | 
			
		||||
          row.toggleSelected(!!(event.target as HTMLInputElement).checked),
 | 
			
		||||
      }),
 | 
			
		||||
    enableSorting: false,
 | 
			
		||||
    enableHiding: false,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    accessorKey: "devAddr",
 | 
			
		||||
    header: "IP 地址",
 | 
			
		||||
    cell: ({ row }) => {
 | 
			
		||||
      const device = row.original;
 | 
			
		||||
      return isEditMode.value
 | 
			
		||||
        ? h("input", {
 | 
			
		||||
            type: "text",
 | 
			
		||||
            class: "input input-sm w-full",
 | 
			
		||||
            value: device.devAddr,
 | 
			
		||||
            onInput: (e: Event) => {
 | 
			
		||||
              device.devAddr = (e.target as HTMLInputElement).value;
 | 
			
		||||
            },
 | 
			
		||||
          })
 | 
			
		||||
        : h("span", { class: "font-medium" }, device.devAddr);
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    accessorKey: "version",
 | 
			
		||||
    header: "版本号",
 | 
			
		||||
    cell: ({ row }) => row.getValue("version"),
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    accessorKey: "defaultBitstream",
 | 
			
		||||
    header: "默认启动位流",
 | 
			
		||||
    cell: ({ row }) => {
 | 
			
		||||
      const device = row.original;
 | 
			
		||||
      return h(
 | 
			
		||||
        "select",
 | 
			
		||||
        {
 | 
			
		||||
          class: "select select-bordered select-sm w-full",
 | 
			
		||||
          value: device.defaultBitstream,
 | 
			
		||||
          onChange: (e: Event) => {
 | 
			
		||||
            device.defaultBitstream = (e.target as HTMLSelectElement).value;
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
        [
 | 
			
		||||
          h("option", { value: "黄金位流" }, "黄金位流"),
 | 
			
		||||
          h("option", { value: "应用位流1" }, "应用位流1"),
 | 
			
		||||
          h("option", { value: "应用位流2" }, "应用位流2"),
 | 
			
		||||
          h("option", { value: "应用位流3" }, "应用位流3"),
 | 
			
		||||
        ],
 | 
			
		||||
      );
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    id: "goldBitstream",
 | 
			
		||||
    header: "黄金位流",
 | 
			
		||||
    cell: ({ row }) => {
 | 
			
		||||
      const device = row.original;
 | 
			
		||||
      return h("input", {
 | 
			
		||||
        type: "file",
 | 
			
		||||
        class: "file-input file-input-primary file-input-sm",
 | 
			
		||||
        onChange: (e: Event) =>
 | 
			
		||||
          handleFileChange(e, device, "goldBitstreamFile"),
 | 
			
		||||
      });
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    id: "appBitstream1",
 | 
			
		||||
    header: "应用位流1",
 | 
			
		||||
    cell: ({ row }) => {
 | 
			
		||||
      const device = row.original;
 | 
			
		||||
      return h("input", {
 | 
			
		||||
        type: "file",
 | 
			
		||||
        class: "file-input file-input-secondary file-input-sm",
 | 
			
		||||
        onChange: (e: Event) =>
 | 
			
		||||
          handleFileChange(e, device, "appBitstream1File"),
 | 
			
		||||
      });
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    id: "appBitstream2",
 | 
			
		||||
    header: "应用位流2",
 | 
			
		||||
    cell: ({ row }) => {
 | 
			
		||||
      const device = row.original;
 | 
			
		||||
      return h("input", {
 | 
			
		||||
        type: "file",
 | 
			
		||||
        class: "file-input file-input-accent file-input-sm",
 | 
			
		||||
        onChange: (e: Event) =>
 | 
			
		||||
          handleFileChange(e, device, "appBitstream2File"),
 | 
			
		||||
      });
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    id: "appBitstream3",
 | 
			
		||||
    header: "应用位流3",
 | 
			
		||||
    cell: ({ row }) => {
 | 
			
		||||
      const device = row.original;
 | 
			
		||||
      return h("input", {
 | 
			
		||||
        type: "file",
 | 
			
		||||
        class: "file-input file-input-info file-input-sm",
 | 
			
		||||
        onChange: (e: Event) =>
 | 
			
		||||
          handleFileChange(e, device, "appBitstream3File"),
 | 
			
		||||
      });
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    id: "actions",
 | 
			
		||||
    header: "操作",
 | 
			
		||||
    cell: ({ row }) => {
 | 
			
		||||
      const device = row.original;
 | 
			
		||||
      return h("div", { class: "flex gap-2 min-w-30" }, [
 | 
			
		||||
        h(
 | 
			
		||||
          "button",
 | 
			
		||||
          {
 | 
			
		||||
            class: "btn btn-warning btn-sm",
 | 
			
		||||
            onClick: () =>
 | 
			
		||||
              uploadAndDownloadBitstreams(
 | 
			
		||||
                device.devAddr,
 | 
			
		||||
                device.goldBitstreamFile,
 | 
			
		||||
                device.appBitstream1File,
 | 
			
		||||
                device.appBitstream2File,
 | 
			
		||||
                device.appBitstream3File,
 | 
			
		||||
              ),
 | 
			
		||||
          },
 | 
			
		||||
          "固化",
 | 
			
		||||
        ),
 | 
			
		||||
        h(
 | 
			
		||||
          "button",
 | 
			
		||||
          {
 | 
			
		||||
            class: "btn btn-success btn-sm",
 | 
			
		||||
            onClick: () =>
 | 
			
		||||
              hotresetBitstream(
 | 
			
		||||
                device.devAddr,
 | 
			
		||||
                getSelectedBitstreamNum(device.defaultBitstream),
 | 
			
		||||
              ),
 | 
			
		||||
          },
 | 
			
		||||
          "热启动",
 | 
			
		||||
        ),
 | 
			
		||||
      ]);
 | 
			
		||||
    },
 | 
			
		||||
    enableHiding: false,
 | 
			
		||||
  },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
// TanStack Table 状态
 | 
			
		||||
const sorting = ref<SortingState>([]);
 | 
			
		||||
const columnFilters = ref<ColumnFiltersState>([]);
 | 
			
		||||
const columnVisibility = ref<VisibilityState>({});
 | 
			
		||||
const rowSelection = ref({});
 | 
			
		||||
const expanded = ref<ExpandedState>({});
 | 
			
		||||
 | 
			
		||||
// 创建表格实例
 | 
			
		||||
const table = useVueTable({
 | 
			
		||||
  get data() {
 | 
			
		||||
    return data.value;
 | 
			
		||||
  },
 | 
			
		||||
  columns,
 | 
			
		||||
  getCoreRowModel: getCoreRowModel(),
 | 
			
		||||
  getPaginationRowModel: getPaginationRowModel(),
 | 
			
		||||
  getSortedRowModel: getSortedRowModel(),
 | 
			
		||||
  getFilteredRowModel: getFilteredRowModel(),
 | 
			
		||||
  getExpandedRowModel: getExpandedRowModel(),
 | 
			
		||||
  onSortingChange: (updaterOrValue) => {
 | 
			
		||||
    if (typeof updaterOrValue === "function") {
 | 
			
		||||
      sorting.value = updaterOrValue(sorting.value);
 | 
			
		||||
    } else {
 | 
			
		||||
      sorting.value = updaterOrValue;
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  onColumnFiltersChange: (updaterOrValue) => {
 | 
			
		||||
    if (typeof updaterOrValue === "function") {
 | 
			
		||||
      columnFilters.value = updaterOrValue(columnFilters.value);
 | 
			
		||||
    } else {
 | 
			
		||||
      columnFilters.value = updaterOrValue;
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  onColumnVisibilityChange: (updaterOrValue) => {
 | 
			
		||||
    if (typeof updaterOrValue === "function") {
 | 
			
		||||
      columnVisibility.value = updaterOrValue(columnVisibility.value);
 | 
			
		||||
    } else {
 | 
			
		||||
      columnVisibility.value = updaterOrValue;
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  onRowSelectionChange: (updaterOrValue) => {
 | 
			
		||||
    if (typeof updaterOrValue === "function") {
 | 
			
		||||
      rowSelection.value = updaterOrValue(rowSelection.value);
 | 
			
		||||
    } else {
 | 
			
		||||
      rowSelection.value = updaterOrValue;
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  onExpandedChange: (updaterOrValue) => {
 | 
			
		||||
    if (typeof updaterOrValue === "function") {
 | 
			
		||||
      expanded.value = updaterOrValue(expanded.value);
 | 
			
		||||
    } else {
 | 
			
		||||
      expanded.value = updaterOrValue;
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  state: {
 | 
			
		||||
    get sorting() {
 | 
			
		||||
      return sorting.value;
 | 
			
		||||
    },
 | 
			
		||||
    get columnFilters() {
 | 
			
		||||
      return columnFilters.value;
 | 
			
		||||
    },
 | 
			
		||||
    get columnVisibility() {
 | 
			
		||||
      return columnVisibility.value;
 | 
			
		||||
    },
 | 
			
		||||
    get rowSelection() {
 | 
			
		||||
      return rowSelection.value;
 | 
			
		||||
    },
 | 
			
		||||
    get expanded() {
 | 
			
		||||
      return expanded.value;
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// 处理文件上传
 | 
			
		||||
function handleFileChange(event: Event, bistreamNum: number) {
 | 
			
		||||
function handleFileChange(
 | 
			
		||||
  event: Event,
 | 
			
		||||
  device: DeviceData,
 | 
			
		||||
  fileKey: keyof DeviceData,
 | 
			
		||||
) {
 | 
			
		||||
  const target = event.target as HTMLInputElement;
 | 
			
		||||
  const file = target.files?.[0]; // 获取选中的第一个文件
 | 
			
		||||
 | 
			
		||||
  if (!file) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (bistreamNum === 0) {
 | 
			
		||||
    goldBitstreamFile.value = file;
 | 
			
		||||
  } else if (bistreamNum === 1) {
 | 
			
		||||
    appBitstream1File.value = file;
 | 
			
		||||
  } else if (bistreamNum === 2) {
 | 
			
		||||
    appBitstream2File.value = file;
 | 
			
		||||
  } else if (bistreamNum === 3) {
 | 
			
		||||
    appBitstream3File.value = file;
 | 
			
		||||
  } else {
 | 
			
		||||
    goldBitstreamFile.value = file;
 | 
			
		||||
  const file = target.files?.[0];
 | 
			
		||||
  if (file) {
 | 
			
		||||
    (device as any)[fileKey] = file;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getSelectedBitstreamNum(): number {
 | 
			
		||||
  if (selectBitstream.value == "黄金位流") {
 | 
			
		||||
    return 0;
 | 
			
		||||
  } else if (selectBitstream.value == "应用位流1") {
 | 
			
		||||
    return 1;
 | 
			
		||||
  } else if (selectBitstream.value == "应用位流2") {
 | 
			
		||||
    return 2;
 | 
			
		||||
  } else if (selectBitstream.value == "应用位流3") {
 | 
			
		||||
    return 3;
 | 
			
		||||
  }
 | 
			
		||||
function getSelectedBitstreamNum(bitstreamName: string): number {
 | 
			
		||||
  if (bitstreamName === "黄金位流") return 0;
 | 
			
		||||
  if (bitstreamName === "应用位流1") return 1;
 | 
			
		||||
  if (bitstreamName === "应用位流2") return 2;
 | 
			
		||||
  if (bitstreamName === "应用位流3") return 3;
 | 
			
		||||
  return 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -206,7 +513,7 @@ async function uploadAndDownloadBitstreams(
 | 
			
		||||
      const ret = await remoteUpdater.downloadMultiBitstreams(
 | 
			
		||||
        devAddr,
 | 
			
		||||
        devPort,
 | 
			
		||||
        getSelectedBitstreamNum(),
 | 
			
		||||
        getSelectedBitstreamNum(data.value[0].defaultBitstream),
 | 
			
		||||
      );
 | 
			
		||||
      if (ret != cnt) {
 | 
			
		||||
        dialog.warn("固化比特流出错");
 | 
			
		||||
@@ -240,7 +547,14 @@ async function hotresetBitstream(devAddr: string, bitstreamNum: number) {
 | 
			
		||||
 | 
			
		||||
async function refreshData() {
 | 
			
		||||
  try {
 | 
			
		||||
    const ret = await remoteUpdater.getFirmwareVersion(devAddr.value, devPort);
 | 
			
		||||
    const ret = await remoteUpdater.getFirmwareVersion(
 | 
			
		||||
      data.value[0].devAddr,
 | 
			
		||||
      devPort,
 | 
			
		||||
    );
 | 
			
		||||
    // 更新版本信息
 | 
			
		||||
    if (ret) {
 | 
			
		||||
      data.value[0].version = String(ret);
 | 
			
		||||
    }
 | 
			
		||||
  } catch (e) {
 | 
			
		||||
    dialog.error("获取数据失败");
 | 
			
		||||
    console.error(e);
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										0
									
								
								src/views/User/BoardManager.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								src/views/User/BoardManager.ts
									
									
									
									
									
										Normal file
									
								
							@@ -9,25 +9,40 @@
 | 
			
		||||
      <li id="2" @click="setActivePage">
 | 
			
		||||
        <a :class="{ 'menu-active': activePage === 2 }">Item 2</a>
 | 
			
		||||
      </li>
 | 
			
		||||
      <li id="100" @click="setActivePage">
 | 
			
		||||
      <li v-if="isAdmin" id="100" @click="setActivePage">
 | 
			
		||||
        <a :class="{ 'menu-active': activePage === 100 }">实验板控制台</a>
 | 
			
		||||
      </li>
 | 
			
		||||
    </ul>
 | 
			
		||||
    <div class="divider divider-horizontal h-full"></div>
 | 
			
		||||
    <div class="card bg-base-200 w-300"></div>
 | 
			
		||||
    <div class="card bg-base-200 w-300 rounded-2xl p-7">
 | 
			
		||||
      <div v-if="activePage === 1">
 | 
			
		||||
        <h2 class="card-title">用户信息</h2>
 | 
			
		||||
        <p>这里是用户信息页面的内容。</p>
 | 
			
		||||
      </div>
 | 
			
		||||
      <div v-else-if="activePage === 100">
 | 
			
		||||
        <BoardControl />
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import BoardControl from "./BoardControl.vue";
 | 
			
		||||
import { toNumber } from "lodash";
 | 
			
		||||
import { ref } from "vue";
 | 
			
		||||
import { onMounted, ref } from "vue";
 | 
			
		||||
import { AuthManager } from "@/utils/AuthManager";
 | 
			
		||||
 | 
			
		||||
const activePage = ref(1);
 | 
			
		||||
const isAdmin = ref(false);
 | 
			
		||||
 | 
			
		||||
function setActivePage(event: Event) {
 | 
			
		||||
  const target = event.currentTarget as HTMLLinkElement;
 | 
			
		||||
  activePage.value = toNumber(target.id);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
onMounted(async ()=>{
 | 
			
		||||
  isAdmin.value = await AuthManager.verifyAdminAuth();
 | 
			
		||||
})
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user