新增:管理后台前端页面,以及openspec内容。
This commit is contained in:
26
.amazonq/prompts/openspec-apply.md
Normal file
26
.amazonq/prompts/openspec-apply.md
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
description: Implement an approved OpenSpec change and keep tasks in sync.
|
||||||
|
---
|
||||||
|
|
||||||
|
The user wants to apply the following change. Use the openspec instructions to implement the approved change.
|
||||||
|
|
||||||
|
<ChangeId>
|
||||||
|
$ARGUMENTS
|
||||||
|
</ChangeId>
|
||||||
|
<!-- OPENSPEC:START -->
|
||||||
|
**Guardrails**
|
||||||
|
- Favor straightforward, minimal implementations first and add complexity only when it is requested or clearly required.
|
||||||
|
- Keep changes tightly scoped to the requested outcome.
|
||||||
|
- Refer to `openspec/AGENTS.md` (located inside the `openspec/` directory—run `ls openspec` or `openspec update` if you don't see it) if you need additional OpenSpec conventions or clarifications.
|
||||||
|
|
||||||
|
**Steps**
|
||||||
|
Track these steps as TODOs and complete them one by one.
|
||||||
|
1. Read `changes/<id>/proposal.md`, `design.md` (if present), and `tasks.md` to confirm scope and acceptance criteria.
|
||||||
|
2. Work through tasks sequentially, keeping edits minimal and focused on the requested change.
|
||||||
|
3. Confirm completion before updating statuses—make sure every item in `tasks.md` is finished.
|
||||||
|
4. Update the checklist after all work is done so each task is marked `- [x]` and reflects reality.
|
||||||
|
5. Reference `openspec list` or `openspec show <item>` when additional context is required.
|
||||||
|
|
||||||
|
**Reference**
|
||||||
|
- Use `openspec show <id> --json --deltas-only` if you need additional context from the proposal while implementing.
|
||||||
|
<!-- OPENSPEC:END -->
|
||||||
30
.amazonq/prompts/openspec-archive.md
Normal file
30
.amazonq/prompts/openspec-archive.md
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
---
|
||||||
|
description: Archive a deployed OpenSpec change and update specs.
|
||||||
|
---
|
||||||
|
|
||||||
|
The user wants to archive the following deployed change. Use the openspec instructions to archive the change and update specs.
|
||||||
|
|
||||||
|
<ChangeId>
|
||||||
|
$ARGUMENTS
|
||||||
|
</ChangeId>
|
||||||
|
<!-- OPENSPEC:START -->
|
||||||
|
**Guardrails**
|
||||||
|
- Favor straightforward, minimal implementations first and add complexity only when it is requested or clearly required.
|
||||||
|
- Keep changes tightly scoped to the requested outcome.
|
||||||
|
- Refer to `openspec/AGENTS.md` (located inside the `openspec/` directory—run `ls openspec` or `openspec update` if you don't see it) if you need additional OpenSpec conventions or clarifications.
|
||||||
|
|
||||||
|
**Steps**
|
||||||
|
1. Determine the change ID to archive:
|
||||||
|
- If this prompt already includes a specific change ID (for example inside a `<ChangeId>` block populated by slash-command arguments), use that value after trimming whitespace.
|
||||||
|
- If the conversation references a change loosely (for example by title or summary), run `openspec list` to surface likely IDs, share the relevant candidates, and confirm which one the user intends.
|
||||||
|
- Otherwise, review the conversation, run `openspec list`, and ask the user which change to archive; wait for a confirmed change ID before proceeding.
|
||||||
|
- If you still cannot identify a single change ID, stop and tell the user you cannot archive anything yet.
|
||||||
|
2. Validate the change ID by running `openspec list` (or `openspec show <id>`) and stop if the change is missing, already archived, or otherwise not ready to archive.
|
||||||
|
3. Run `openspec archive <id> --yes` so the CLI moves the change and applies spec updates without prompts (use `--skip-specs` only for tooling-only work).
|
||||||
|
4. Review the command output to confirm the target specs were updated and the change landed in `changes/archive/`.
|
||||||
|
5. Validate with `openspec validate --strict` and inspect with `openspec show <id>` if anything looks off.
|
||||||
|
|
||||||
|
**Reference**
|
||||||
|
- Use `openspec list` to confirm change IDs before archiving.
|
||||||
|
- Inspect refreshed specs with `openspec list --specs` and address any validation issues before handing off.
|
||||||
|
<!-- OPENSPEC:END -->
|
||||||
31
.amazonq/prompts/openspec-proposal.md
Normal file
31
.amazonq/prompts/openspec-proposal.md
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
description: Scaffold a new OpenSpec change and validate strictly.
|
||||||
|
---
|
||||||
|
|
||||||
|
The user has requested the following change proposal. Use the openspec instructions to create their change proposal.
|
||||||
|
|
||||||
|
<UserRequest>
|
||||||
|
$ARGUMENTS
|
||||||
|
</UserRequest>
|
||||||
|
<!-- OPENSPEC:START -->
|
||||||
|
**Guardrails**
|
||||||
|
- Favor straightforward, minimal implementations first and add complexity only when it is requested or clearly required.
|
||||||
|
- Keep changes tightly scoped to the requested outcome.
|
||||||
|
- Refer to `openspec/AGENTS.md` (located inside the `openspec/` directory—run `ls openspec` or `openspec update` if you don't see it) if you need additional OpenSpec conventions or clarifications.
|
||||||
|
- Identify any vague or ambiguous details and ask the necessary follow-up questions before editing files.
|
||||||
|
- Do not write any code during the proposal stage. Only create design documents (proposal.md, tasks.md, design.md, and spec deltas). Implementation happens in the apply stage after approval.
|
||||||
|
|
||||||
|
**Steps**
|
||||||
|
1. Review `openspec/project.md`, run `openspec list` and `openspec list --specs`, and inspect related code or docs (e.g., via `rg`/`ls`) to ground the proposal in current behaviour; note any gaps that require clarification.
|
||||||
|
2. Choose a unique verb-led `change-id` and scaffold `proposal.md`, `tasks.md`, and `design.md` (when needed) under `openspec/changes/<id>/`.
|
||||||
|
3. Map the change into concrete capabilities or requirements, breaking multi-scope efforts into distinct spec deltas with clear relationships and sequencing.
|
||||||
|
4. Capture architectural reasoning in `design.md` when the solution spans multiple systems, introduces new patterns, or demands trade-off discussion before committing to specs.
|
||||||
|
5. Draft spec deltas in `changes/<id>/specs/<capability>/spec.md` (one folder per capability) using `## ADDED|MODIFIED|REMOVED Requirements` with at least one `#### Scenario:` per requirement and cross-reference related capabilities when relevant.
|
||||||
|
6. Draft `tasks.md` as an ordered list of small, verifiable work items that deliver user-visible progress, include validation (tests, tooling), and highlight dependencies or parallelizable work.
|
||||||
|
7. Validate with `openspec validate <id> --strict` and resolve every issue before sharing the proposal.
|
||||||
|
|
||||||
|
**Reference**
|
||||||
|
- Use `openspec show <id> --json --deltas-only` or `openspec show <spec> --type spec` to inspect details when validation fails.
|
||||||
|
- Search existing requirements with `rg -n "Requirement:|Scenario:" openspec/specs` before writing new ones.
|
||||||
|
- Explore the codebase with `rg <keyword>`, `ls`, or direct file reads so proposals align with current implementation realities.
|
||||||
|
<!-- OPENSPEC:END -->
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -6,3 +6,4 @@
|
|||||||
WxCheckMvc/obj
|
WxCheckMvc/obj
|
||||||
WxCheckMvc/.vs
|
WxCheckMvc/.vs
|
||||||
WxCheckMvc/bin
|
WxCheckMvc/bin
|
||||||
|
node_modules
|
||||||
|
|||||||
93
.trae/documents/会话管理和用户管理页面优化.md
Normal file
93
.trae/documents/会话管理和用户管理页面优化.md
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
# 会话管理和用户管理页面优化
|
||||||
|
|
||||||
|
## 1. 优化conversations模块的发送方式显示
|
||||||
|
|
||||||
|
### 实现方案
|
||||||
|
- 修改 `ConversationList.vue` 中的发送方式列,将其重构为tag标签形式
|
||||||
|
- 当 `sendMethod` 为文字类型时,显示success样式的tag标签,内容为"text"
|
||||||
|
- 当 `sendMethod` 为语音类型时,显示默认样式的tag标签,内容为"voice"
|
||||||
|
|
||||||
|
### 代码修改点
|
||||||
|
- `src/views/ConversationList.vue:98-104`:修改发送方式列的实现,添加tag标签模板
|
||||||
|
|
||||||
|
## 2. 实现手机号脱敏与时间格式化功能
|
||||||
|
|
||||||
|
### 实现方案
|
||||||
|
- 创建工具函数处理手机号脱敏和时间格式化
|
||||||
|
- 实现手机号自动脱敏,默认隐藏中间4-8位数字
|
||||||
|
- 添加点击交互,支持显示/隐藏完整手机号
|
||||||
|
- 实现时间格式转换,将时间数据中的"T"字符替换为空格
|
||||||
|
|
||||||
|
### 代码修改点
|
||||||
|
- 创建 `src/utils/formatters.js`:添加手机号脱敏和时间格式化工具函数
|
||||||
|
- `src/views/ConversationList.vue`:应用手机号脱敏和时间格式化
|
||||||
|
- `src/views/UserList.vue`:应用手机号脱敏和时间格式化
|
||||||
|
|
||||||
|
## 3. 重构表格组件与实现无限滚动分页
|
||||||
|
|
||||||
|
### 实现方案
|
||||||
|
- 将现有el-table组件包装在el-scrollbar中,实现滚动加载
|
||||||
|
- 修改 `fetchConversations` 方法,添加分页参数
|
||||||
|
- 实现滚动到底部自动加载下一页数据
|
||||||
|
- 添加加载状态提示,避免重复请求
|
||||||
|
- 设置固定分页大小为20条/页
|
||||||
|
|
||||||
|
### 代码修改点
|
||||||
|
- `src/views/ConversationList.vue`:
|
||||||
|
- 添加el-scrollbar组件包装el-table
|
||||||
|
- 修改fetchConversations方法,添加分页逻辑
|
||||||
|
- 实现滚动加载功能
|
||||||
|
- 移除传统分页控件
|
||||||
|
|
||||||
|
## 4. 优化用户管理页面
|
||||||
|
|
||||||
|
### 实现方案
|
||||||
|
- 移除UserList页面的分页控件及相关逻辑
|
||||||
|
- 调整表格配置,不分页加载所有用户数据
|
||||||
|
- 应用时间格式转换,将时间数据中的"T"字符替换为空格
|
||||||
|
|
||||||
|
### 代码修改点
|
||||||
|
- `src/views/UserList.vue`:
|
||||||
|
- 移除分页控件和相关数据
|
||||||
|
- 修改fetchUsers方法,移除分页逻辑
|
||||||
|
- 应用时间格式化
|
||||||
|
|
||||||
|
## 5. 文档记录
|
||||||
|
|
||||||
|
### 实现方案
|
||||||
|
- 在openspec目录下创建详细的修改记录文档
|
||||||
|
- 记录修改内容、原因及影响范围
|
||||||
|
|
||||||
|
### 代码修改点
|
||||||
|
- 创建 `openspec/changes/optimize-conversation-user-management/` 目录
|
||||||
|
- 创建 `proposal.md`:描述问题和解决方案
|
||||||
|
- 创建 `tasks.md`:列出具体实现任务
|
||||||
|
- 创建 `implementation.md`:详细记录技术实现和最佳实践
|
||||||
|
|
||||||
|
## 实现顺序
|
||||||
|
|
||||||
|
1. 创建工具函数文件 `src/utils/formatters.js`
|
||||||
|
2. 优化ConversationList.vue的发送方式显示
|
||||||
|
3. 实现手机号脱敏与时间格式化功能
|
||||||
|
4. 重构表格组件与实现无限滚动分页
|
||||||
|
5. 优化用户管理页面
|
||||||
|
6. 创建修改记录文档
|
||||||
|
|
||||||
|
## 预期效果
|
||||||
|
|
||||||
|
- 会话记录页面:
|
||||||
|
- 发送方式以tag标签形式显示
|
||||||
|
- 手机号自动脱敏,支持点击显示/隐藏完整号码
|
||||||
|
- 时间格式统一为"YYYY-MM-DD HH:mm:ss"
|
||||||
|
- 表格实现无限滚动加载,提升用户体验
|
||||||
|
|
||||||
|
- 用户管理页面:
|
||||||
|
- 移除分页控件,加载所有用户数据
|
||||||
|
- 时间格式统一为"YYYY-MM-DD HH:mm:ss"
|
||||||
|
- 手机号自动脱敏,支持点击显示/隐藏完整号码
|
||||||
|
|
||||||
|
- 代码质量:
|
||||||
|
- 工具函数复用性高
|
||||||
|
- 代码结构清晰,易于维护
|
||||||
|
- 符合openspec开发规范
|
||||||
|
- 详细的修改记录文档
|
||||||
106
.trae/documents/后台管理网站开发计划.md
Normal file
106
.trae/documents/后台管理网站开发计划.md
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
# 后台管理网站开发计划
|
||||||
|
|
||||||
|
## 1. 项目初始化
|
||||||
|
|
||||||
|
* 初始化Vue 3.x + Element Plus + Vite项目
|
||||||
|
|
||||||
|
* 配置项目目录结构
|
||||||
|
|
||||||
|
* 安装必要依赖:vue-router、Pinia、axios
|
||||||
|
|
||||||
|
## 2. 项目基础配置
|
||||||
|
|
||||||
|
* 创建路由配置文件:`src/router/index.js`
|
||||||
|
|
||||||
|
* 创建Pinia状态管理:`src/store/index.js`、`src/store/auth.js`、`src/store/theme.js`
|
||||||
|
|
||||||
|
* 配置axios拦截器:`src/utils/request.js`
|
||||||
|
|
||||||
|
* 创建全局样式文件:`src/styles/main.scss`、`src/styles/variables.scss`、`src/styles/responsive.scss`
|
||||||
|
|
||||||
|
## 3. 登录功能实现
|
||||||
|
|
||||||
|
* 创建登录页面:`src/views/Login.vue`
|
||||||
|
|
||||||
|
* 实现登录验证逻辑(固定账号密码:Admin/Admin)
|
||||||
|
|
||||||
|
* 实现登录状态管理
|
||||||
|
|
||||||
|
* 配置路由守卫,保护受保护页面
|
||||||
|
|
||||||
|
## 4. 主题切换功能
|
||||||
|
|
||||||
|
* 创建主题切换组件:`src/components/ThemeSwitcher.vue`
|
||||||
|
|
||||||
|
* 实现深色/浅色模式切换逻辑
|
||||||
|
|
||||||
|
* 实现主题状态持久化(localStorage)
|
||||||
|
|
||||||
|
## 5. 响应式布局和菜单
|
||||||
|
|
||||||
|
* 创建布局组件:`src/components/Layout/Layout.vue`、`src/components/Layout/Sidebar.vue`、`src/components/Layout/Header.vue`
|
||||||
|
|
||||||
|
* 实现侧边栏菜单(桌面/平板端)
|
||||||
|
|
||||||
|
* 实现抽屉式菜单(手机端)
|
||||||
|
|
||||||
|
* 实现响应式布局适配
|
||||||
|
|
||||||
|
## 6. 会话记录管理页面
|
||||||
|
|
||||||
|
* 创建会话记录管理页面:`src/views/ConversationList.vue`
|
||||||
|
|
||||||
|
* 实现会话记录查询功能
|
||||||
|
|
||||||
|
* 实现多条件筛选功能(时间范围、用户、消息类型、部门)
|
||||||
|
|
||||||
|
* 实现会话记录表格展示
|
||||||
|
|
||||||
|
## 7. 用户管理页面
|
||||||
|
|
||||||
|
* 创建用户管理页面:`src/views/UserList.vue`
|
||||||
|
|
||||||
|
* 实现用户列表查询功能
|
||||||
|
|
||||||
|
* 实现用户表格展示
|
||||||
|
|
||||||
|
## 8. 首页实现
|
||||||
|
|
||||||
|
* 创建首页:`src/views/Home.vue`
|
||||||
|
|
||||||
|
* 实现系统概览和统计信息展示
|
||||||
|
|
||||||
|
## 9. 测试和优化
|
||||||
|
|
||||||
|
* 测试所有功能模块
|
||||||
|
|
||||||
|
* 优化响应式布局
|
||||||
|
|
||||||
|
* 优化页面性能
|
||||||
|
|
||||||
|
* 完善错误处理
|
||||||
|
|
||||||
|
## 10. 文档更新
|
||||||
|
|
||||||
|
* 更新项目文档
|
||||||
|
|
||||||
|
* 记录开发过程中的变更
|
||||||
|
|
||||||
|
## 技术规范遵循
|
||||||
|
|
||||||
|
* 使用Vue 3.x Composition API和`<script setup>`语法
|
||||||
|
|
||||||
|
* 遵循Element Plus组件库规范
|
||||||
|
|
||||||
|
* 实现响应式设计,优先适配手机宽度
|
||||||
|
|
||||||
|
* 采用模块化设计思路
|
||||||
|
|
||||||
|
* 严格遵循openspec文档中规定的API接口和数据模型
|
||||||
|
|
||||||
|
## 变更记录要求
|
||||||
|
|
||||||
|
* 所有代码修改、功能调整或配置变更均需在changes文档中进行详细记录
|
||||||
|
|
||||||
|
* 记录内容包括:变更时间、变更人、变更模块、变更类型(新增/修改/删除)、变更具体内容、关联需求ID及变更原因
|
||||||
|
|
||||||
18
AGENTS.md
Normal file
18
AGENTS.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<!-- OPENSPEC:START -->
|
||||||
|
# OpenSpec Instructions
|
||||||
|
|
||||||
|
These instructions are for AI assistants working in this project.
|
||||||
|
|
||||||
|
Always open `@/openspec/AGENTS.md` when the request:
|
||||||
|
- Mentions planning or proposals (words like proposal, spec, change, plan)
|
||||||
|
- Introduces new capabilities, breaking changes, architecture shifts, or big performance/security work
|
||||||
|
- Sounds ambiguous and you need the authoritative spec before coding
|
||||||
|
|
||||||
|
Use `@/openspec/AGENTS.md` to learn:
|
||||||
|
- How to create and apply change proposals
|
||||||
|
- Spec format and conventions
|
||||||
|
- Project structure and guidelines
|
||||||
|
|
||||||
|
Keep this managed block so 'openspec update' can refresh the instructions.
|
||||||
|
|
||||||
|
<!-- OPENSPEC:END -->
|
||||||
243
WxCheckMvc/Controllers/AdminController.cs
Normal file
243
WxCheckMvc/Controllers/AdminController.cs
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using MySql.Data.MySqlClient;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Data;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace WxCheckMvc.Controllers
|
||||||
|
{
|
||||||
|
[Route("api/[controller]/[action]")]
|
||||||
|
[ApiController]
|
||||||
|
public class AdminController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly MySqlConnection _connection;
|
||||||
|
|
||||||
|
public AdminController(MySqlConnection connection)
|
||||||
|
{
|
||||||
|
_connection = connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<IActionResult> QueryConversations([FromBody] ConversationQueryRequest request)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_connection.State != ConnectionState.Open)
|
||||||
|
{
|
||||||
|
await _connection.OpenAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ConversationQueryResponse> conversations = [];
|
||||||
|
|
||||||
|
string query = @"SELECT c.Id, c.Guid, c.UserKey, c.ConversationContent, c.SendMethod,
|
||||||
|
c.UserLocation, c.Latitude, c.Longitude, c.RecordTime,
|
||||||
|
c.RecordTimeUTCStamp, c.IsDeleted, c.CreateTime, c.MessageType, c.SpeakingTime,
|
||||||
|
u.UserName, u.WeChatName, u.PhoneNumber, u.AvatarUrl, u.Department
|
||||||
|
FROM xcx_conversation c
|
||||||
|
LEFT JOIN xcx_users u ON c.UserKey = u.UserKey
|
||||||
|
WHERE c.IsDeleted = 0";
|
||||||
|
|
||||||
|
var parameters = new List<MySqlParameter>();
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(request.UserKey))
|
||||||
|
{
|
||||||
|
query += " AND c.UserKey = @UserKey";
|
||||||
|
parameters.Add(new MySqlParameter("@UserKey", request.UserKey));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.MessageType.HasValue)
|
||||||
|
{
|
||||||
|
query += " AND c.MessageType = @MessageType";
|
||||||
|
parameters.Add(new MySqlParameter("@MessageType", request.MessageType.Value));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.StartTime.HasValue)
|
||||||
|
{
|
||||||
|
long startUtcStamp = new DateTimeOffset(request.StartTime.Value).ToUnixTimeMilliseconds();
|
||||||
|
query += " AND c.RecordTimeUTCStamp >= @StartTime";
|
||||||
|
parameters.Add(new MySqlParameter("@StartTime", startUtcStamp));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.EndTime.HasValue)
|
||||||
|
{
|
||||||
|
long endUtcStamp = new DateTimeOffset(request.EndTime.Value).ToUnixTimeMilliseconds();
|
||||||
|
query += " AND c.RecordTimeUTCStamp <= @EndTime";
|
||||||
|
parameters.Add(new MySqlParameter("@EndTime", endUtcStamp));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(request.Department))
|
||||||
|
{
|
||||||
|
query += " AND u.Department = @Department";
|
||||||
|
parameters.Add(new MySqlParameter("@Department", request.Department));
|
||||||
|
}
|
||||||
|
|
||||||
|
query += " ORDER BY c.RecordTimeUTCStamp DESC";
|
||||||
|
|
||||||
|
int offset = (request.Page - 1) * request.PageSize;
|
||||||
|
query += " LIMIT @Limit OFFSET @Offset";
|
||||||
|
parameters.Add(new MySqlParameter("@Limit", request.PageSize));
|
||||||
|
parameters.Add(new MySqlParameter("@Offset", offset));
|
||||||
|
|
||||||
|
using (MySqlCommand cmd = new(query, _connection))
|
||||||
|
{
|
||||||
|
cmd.Parameters.AddRange(parameters.ToArray());
|
||||||
|
|
||||||
|
using (var reader = await cmd.ExecuteReaderAsync())
|
||||||
|
{
|
||||||
|
while (await reader.ReadAsync())
|
||||||
|
{
|
||||||
|
conversations.Add(new ConversationQueryResponse
|
||||||
|
{
|
||||||
|
Id = reader.GetInt64(0),
|
||||||
|
Guid = reader.IsDBNull(1) ? "" : reader.GetString(1),
|
||||||
|
UserKey = reader.GetString(2),
|
||||||
|
ConversationContent = reader.GetString(3),
|
||||||
|
SendMethod = reader.GetString(4),
|
||||||
|
UserLocation = reader.IsDBNull(5) ? "" : reader.GetString(5),
|
||||||
|
Latitude = reader.IsDBNull(6) ? "" : reader.GetString(6),
|
||||||
|
Longitude = reader.IsDBNull(7) ? "" : reader.GetString(7),
|
||||||
|
RecordTime = reader.GetDateTime(8),
|
||||||
|
RecordTimeUTCStamp = reader.GetInt64(9),
|
||||||
|
IsDeleted = reader.GetBoolean(10),
|
||||||
|
CreateTime = reader.GetDateTime(11),
|
||||||
|
MessageType = reader.GetInt32(12),
|
||||||
|
SpeakingTime = reader.IsDBNull(13) ? null : reader.GetInt32(13),
|
||||||
|
UserName = reader.IsDBNull(14) ? "" : reader.GetString(14),
|
||||||
|
WeChatName = reader.IsDBNull(15) ? "" : reader.GetString(15),
|
||||||
|
PhoneNumber = reader.IsDBNull(16) ? "" : reader.GetString(16),
|
||||||
|
AvatarUrl = reader.IsDBNull(17) ? "" : reader.GetString(17),
|
||||||
|
Department = reader.IsDBNull(18) ? "" : reader.GetString(18)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(new { success = true, data = conversations });
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return StatusCode(500, new { success = false, message = "查询失败", error = ex.Message });
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_connection.State == ConnectionState.Open)
|
||||||
|
{
|
||||||
|
await _connection.CloseAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> QueryUsers()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_connection.State != ConnectionState.Open)
|
||||||
|
{
|
||||||
|
await _connection.OpenAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<UserQueryResponse> users = [];
|
||||||
|
|
||||||
|
string query = @"SELECT Id, UserName, UserKey, WeChatName, PhoneNumber,
|
||||||
|
FirstLoginTime, IsDisabled, CreateTime, UpdateTime, AvatarUrl, Department
|
||||||
|
FROM xcx_users
|
||||||
|
WHERE PhoneNumber IS NOT NULL
|
||||||
|
AND PhoneNumber != ''
|
||||||
|
AND UserName IS NOT NULL
|
||||||
|
AND UserName != ''
|
||||||
|
AND UserKey IS NOT NULL
|
||||||
|
AND UserKey != ''
|
||||||
|
AND IsDisabled = 0
|
||||||
|
ORDER BY FirstLoginTime DESC";
|
||||||
|
|
||||||
|
using (MySqlCommand cmd = new(query, _connection))
|
||||||
|
{
|
||||||
|
using (var reader = await cmd.ExecuteReaderAsync())
|
||||||
|
{
|
||||||
|
while (await reader.ReadAsync())
|
||||||
|
{
|
||||||
|
users.Add(new UserQueryResponse
|
||||||
|
{
|
||||||
|
Id = reader.GetInt64(0),
|
||||||
|
UserName = reader.IsDBNull(1) ? "" : reader.GetString(1),
|
||||||
|
UserKey = reader.GetString(2),
|
||||||
|
WeChatName = reader.IsDBNull(3) ? "" : reader.GetString(3),
|
||||||
|
PhoneNumber = reader.IsDBNull(4) ? "" : reader.GetString(4),
|
||||||
|
FirstLoginTime = reader.GetDateTime(5),
|
||||||
|
IsDisabled = reader.GetBoolean(6),
|
||||||
|
CreateTime = reader.GetDateTime(7),
|
||||||
|
UpdateTime = reader.GetDateTime(8),
|
||||||
|
AvatarUrl = reader.IsDBNull(9) ? "" : reader.GetString(9),
|
||||||
|
Department = reader.IsDBNull(10) ? "" : reader.GetString(10)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(new { success = true, data = users });
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return StatusCode(500, new { success = false, message = "查询失败", error = ex.Message });
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (_connection.State == ConnectionState.Open)
|
||||||
|
{
|
||||||
|
await _connection.CloseAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ConversationQueryRequest
|
||||||
|
{
|
||||||
|
public DateTime? StartTime { get; set; }
|
||||||
|
public DateTime? EndTime { get; set; }
|
||||||
|
public string UserKey { get; set; }
|
||||||
|
public int? MessageType { get; set; }
|
||||||
|
public string Department { get; set; }
|
||||||
|
public int Page { get; set; } = 1;
|
||||||
|
public int PageSize { get; set; } = 20;
|
||||||
|
}
|
||||||
|
|
||||||
|
public class ConversationQueryResponse
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
public string Guid { get; set; }
|
||||||
|
public string UserKey { get; set; }
|
||||||
|
public string ConversationContent { get; set; }
|
||||||
|
public string SendMethod { get; set; }
|
||||||
|
public string UserLocation { get; set; }
|
||||||
|
public string Latitude { get; set; }
|
||||||
|
public string Longitude { get; set; }
|
||||||
|
public DateTime RecordTime { get; set; }
|
||||||
|
public long RecordTimeUTCStamp { get; set; }
|
||||||
|
public bool IsDeleted { get; set; }
|
||||||
|
public DateTime CreateTime { get; set; }
|
||||||
|
public int MessageType { get; set; }
|
||||||
|
public int? SpeakingTime { get; set; }
|
||||||
|
public string UserName { get; set; }
|
||||||
|
public string WeChatName { get; set; }
|
||||||
|
public string PhoneNumber { get; set; }
|
||||||
|
public string AvatarUrl { get; set; }
|
||||||
|
public string Department { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UserQueryResponse
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
public string UserName { get; set; }
|
||||||
|
public string UserKey { get; set; }
|
||||||
|
public string WeChatName { get; set; }
|
||||||
|
public string PhoneNumber { get; set; }
|
||||||
|
public DateTime FirstLoginTime { get; set; }
|
||||||
|
public bool IsDisabled { get; set; }
|
||||||
|
public DateTime CreateTime { get; set; }
|
||||||
|
public DateTime UpdateTime { get; set; }
|
||||||
|
public string AvatarUrl { get; set; }
|
||||||
|
public string Department { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,7 +10,17 @@ builder.Services.AddControllersWithViews();
|
|||||||
|
|
||||||
// <20><><EFBFBD><EFBFBD>HttpClientFactory
|
// <20><><EFBFBD><EFBFBD>HttpClientFactory
|
||||||
builder.Services.AddHttpClient();
|
builder.Services.AddHttpClient();
|
||||||
|
builder.Services.AddCors(options =>
|
||||||
|
{
|
||||||
|
options.AddPolicy(name: "KuaYu",
|
||||||
|
policy =>
|
||||||
|
{
|
||||||
|
policy
|
||||||
|
.AllowAnyOrigin()
|
||||||
|
.AllowAnyHeader()
|
||||||
|
.AllowAnyMethod();
|
||||||
|
});
|
||||||
|
});
|
||||||
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ݿ<EFBFBD><DDBF><EFBFBD><EFBFBD><EFBFBD>
|
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ݿ<EFBFBD><DDBF><EFBFBD><EFBFBD><EFBFBD>
|
||||||
builder.Services.AddScoped<MySqlConnection>(sp => {
|
builder.Services.AddScoped<MySqlConnection>(sp => {
|
||||||
var connectionString = builder.Configuration.GetConnectionString("MySQLConnection");
|
var connectionString = builder.Configuration.GetConnectionString("MySQLConnection");
|
||||||
@@ -68,7 +78,7 @@ if (!app.Environment.IsDevelopment())
|
|||||||
|
|
||||||
app.UseHttpsRedirection();
|
app.UseHttpsRedirection();
|
||||||
app.UseStaticFiles();
|
app.UseStaticFiles();
|
||||||
|
app.UseCors("KuaYu");
|
||||||
app.UseRouting();
|
app.UseRouting();
|
||||||
|
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<Project>
|
<Project>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<_PublishTargetUrl>E:\Project_Class\WX_XCX\Wx_WxCheck_Prod\WxCheckMvc\bin\Release\net8.0\publish\</_PublishTargetUrl>
|
<_PublishTargetUrl>E:\Project_Class\WX_XCX\Wx_WxCheck_Prod\WxCheckMvc\bin\Release\net8.0\publish\</_PublishTargetUrl>
|
||||||
<History>True|2025-12-12T03:09:28.8147447Z||;True|2025-12-11T17:04:53.2856075+08:00||;True|2025-12-11T17:04:22.0809574+08:00||;True|2025-12-05T18:56:51.7439135+08:00||;True|2025-12-05T17:44:11.4130698+08:00||;</History>
|
<History>True|2025-12-24T12:05:02.2999541Z||;True|2025-12-24T16:33:44.2108439+08:00||;True|2025-12-24T15:32:13.8037439+08:00||;True|2025-12-12T11:09:28.8147447+08:00||;True|2025-12-11T17:04:53.2856075+08:00||;True|2025-12-11T17:04:22.0809574+08:00||;True|2025-12-05T18:56:51.7439135+08:00||;True|2025-12-05T17:44:11.4130698+08:00||;</History>
|
||||||
<LastFailureDetails />
|
<LastFailureDetails />
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
Target Server Version : 120002 (12.0.2-MariaDB)
|
Target Server Version : 120002 (12.0.2-MariaDB)
|
||||||
File Encoding : 65001
|
File Encoding : 65001
|
||||||
|
|
||||||
Date: 05/12/2025 18:09:30
|
Date: 24/12/2025 10:33:17
|
||||||
*/
|
*/
|
||||||
|
|
||||||
SET NAMES utf8mb4;
|
SET NAMES utf8mb4;
|
||||||
@@ -43,7 +43,7 @@ CREATE TABLE `xcx_conversation` (
|
|||||||
INDEX `idx_recordtime`(`RecordTime` ASC) USING BTREE,
|
INDEX `idx_recordtime`(`RecordTime` ASC) USING BTREE,
|
||||||
INDEX `idx_messagetype`(`MessageType` ASC) USING BTREE,
|
INDEX `idx_messagetype`(`MessageType` ASC) USING BTREE,
|
||||||
INDEX `idx_guid`(`Guid` ASC) USING BTREE
|
INDEX `idx_guid`(`Guid` ASC) USING BTREE
|
||||||
) ENGINE = InnoDB AUTO_INCREMENT = 434 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '会话记录表' ROW_FORMAT = Dynamic;
|
) ENGINE = InnoDB AUTO_INCREMENT = 499 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '会话记录表' ROW_FORMAT = Dynamic;
|
||||||
|
|
||||||
-- ----------------------------
|
-- ----------------------------
|
||||||
-- Table structure for xcx_log
|
-- Table structure for xcx_log
|
||||||
@@ -77,10 +77,11 @@ CREATE TABLE `xcx_users` (
|
|||||||
`CreateTime` datetime NULL DEFAULT current_timestamp() COMMENT '创建时间',
|
`CreateTime` datetime NULL DEFAULT current_timestamp() COMMENT '创建时间',
|
||||||
`UpdateTime` datetime NULL DEFAULT current_timestamp() ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
`UpdateTime` datetime NULL DEFAULT current_timestamp() ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
`AvatarUrl` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '头像地址',
|
`AvatarUrl` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '头像地址',
|
||||||
|
`Department` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '用户部门',
|
||||||
PRIMARY KEY (`Id`) USING BTREE,
|
PRIMARY KEY (`Id`) USING BTREE,
|
||||||
UNIQUE INDEX `idx_userkey`(`UserKey` ASC) USING BTREE,
|
UNIQUE INDEX `idx_userkey`(`UserKey` ASC) USING BTREE,
|
||||||
INDEX `idx_disabled`(`IsDisabled` ASC) USING BTREE,
|
INDEX `idx_disabled`(`IsDisabled` ASC) USING BTREE,
|
||||||
INDEX `idx_logintime`(`FirstLoginTime` ASC) USING BTREE
|
INDEX `idx_logintime`(`FirstLoginTime` ASC) USING BTREE
|
||||||
) ENGINE = InnoDB AUTO_INCREMENT = 22 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '小程序用户表' ROW_FORMAT = Dynamic;
|
) ENGINE = InnoDB AUTO_INCREMENT = 42 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '小程序用户表' ROW_FORMAT = Dynamic;
|
||||||
|
|
||||||
SET FOREIGN_KEY_CHECKS = 1;
|
SET FOREIGN_KEY_CHECKS = 1;
|
||||||
|
|||||||
24
admin-web/.gitignore
vendored
Normal file
24
admin-web/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
3
admin-web/.vscode/extensions.json
vendored
Normal file
3
admin-web/.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["Vue.volar"]
|
||||||
|
}
|
||||||
5
admin-web/README.md
Normal file
5
admin-web/README.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Vue 3 + Vite
|
||||||
|
|
||||||
|
This template should help get you started developing with Vue 3 in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||||
|
|
||||||
|
Learn more about IDE Support for Vue in the [Vue Docs Scaling up Guide](https://vuejs.org/guide/scaling-up/tooling.html#ide-support).
|
||||||
13
admin-web/index.html
Normal file
13
admin-web/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>admin-web</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
2468
admin-web/package-lock.json
generated
Normal file
2468
admin-web/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
23
admin-web/package.json
Normal file
23
admin-web/package.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "admin-web",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.13.2",
|
||||||
|
"element-plus": "^2.13.0",
|
||||||
|
"pinia": "^3.0.4",
|
||||||
|
"vue": "^3.5.24",
|
||||||
|
"vue-router": "^4.6.4"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-vue": "^6.0.1",
|
||||||
|
"sass": "^1.97.1",
|
||||||
|
"vite": "^7.2.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
admin-web/public/vite.svg
Normal file
1
admin-web/public/vite.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
18
admin-web/src/App.vue
Normal file
18
admin-web/src/App.vue
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<template>
|
||||||
|
<router-view />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* 全局样式重置 */
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
height: 100%;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
1
admin-web/src/assets/vue.svg
Normal file
1
admin-web/src/assets/vue.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 496 B |
43
admin-web/src/components/HelloWorld.vue
Normal file
43
admin-web/src/components/HelloWorld.vue
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
msg: String,
|
||||||
|
})
|
||||||
|
|
||||||
|
const count = ref(0)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<h1>{{ msg }}</h1>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<button type="button" @click="count++">count is {{ count }}</button>
|
||||||
|
<p>
|
||||||
|
Edit
|
||||||
|
<code>components/HelloWorld.vue</code> to test HMR
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Check out
|
||||||
|
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
|
||||||
|
>create-vue</a
|
||||||
|
>, the official Vue + Vite starter
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Learn more about IDE Support for Vue in the
|
||||||
|
<a
|
||||||
|
href="https://vuejs.org/guide/scaling-up/tooling.html#ide-support"
|
||||||
|
target="_blank"
|
||||||
|
>Vue Docs Scaling up Guide</a
|
||||||
|
>.
|
||||||
|
</p>
|
||||||
|
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.read-the-docs {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
156
admin-web/src/components/Layout/Header.vue
Normal file
156
admin-web/src/components/Layout/Header.vue
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
<template>
|
||||||
|
<header class="header">
|
||||||
|
<div class="header-left">
|
||||||
|
<el-button
|
||||||
|
v-if="isMobile"
|
||||||
|
type="text"
|
||||||
|
:icon="Menu"
|
||||||
|
@click="toggleSidebar"
|
||||||
|
></el-button>
|
||||||
|
<div class="logo">
|
||||||
|
<h1>后台管理</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="header-right">
|
||||||
|
<div class="header-actions">
|
||||||
|
<!-- 主题切换组件 -->
|
||||||
|
<!-- <ThemeSwitcher /> -->
|
||||||
|
|
||||||
|
<!-- 用户信息下拉菜单 -->
|
||||||
|
<el-dropdown trigger="click">
|
||||||
|
<span class="user-info">
|
||||||
|
<el-avatar :size="32">
|
||||||
|
{{ username.charAt(0).toUpperCase() }}
|
||||||
|
</el-avatar>
|
||||||
|
<span class="username">{{ username }}</span>
|
||||||
|
<el-icon class="el-icon--right"><ArrowDown /></el-icon>
|
||||||
|
</span>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item divided @click="handleLogout">
|
||||||
|
<el-icon><SwitchButton /></el-icon>
|
||||||
|
<span>退出登录</span>
|
||||||
|
</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { Menu, ArrowDown, SwitchButton } from '@element-plus/icons-vue'
|
||||||
|
import { useAuthStore } from '../../store/auth'
|
||||||
|
import ThemeSwitcher from '../ThemeSwitcher.vue'
|
||||||
|
|
||||||
|
// 路由实例
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
// 认证状态管理
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
// 用户名
|
||||||
|
const username = computed(() => authStore.username)
|
||||||
|
|
||||||
|
// 侧边栏显示状态(手机端)
|
||||||
|
const isSidebarOpen = ref(false)
|
||||||
|
|
||||||
|
// 响应式布局:判断是否为手机端
|
||||||
|
const isMobile = ref(false)
|
||||||
|
|
||||||
|
// 监听窗口大小变化
|
||||||
|
const handleResize = () => {
|
||||||
|
isMobile.value = window.innerWidth < 768
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化响应式状态
|
||||||
|
onMounted(() => {
|
||||||
|
handleResize()
|
||||||
|
window.addEventListener('resize', handleResize)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 清理事件监听
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('resize', handleResize)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 切换侧边栏显示(手机端)
|
||||||
|
const toggleSidebar = () => {
|
||||||
|
isSidebarOpen.value = !isSidebarOpen.value
|
||||||
|
// 发送事件给父组件
|
||||||
|
emit('toggleSidebar', isSidebarOpen.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 登出处理
|
||||||
|
const handleLogout = () => {
|
||||||
|
authStore.logout()
|
||||||
|
router.push('/login')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定义事件
|
||||||
|
const emit = defineEmits(['toggleSidebar'])
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
height: 64px;
|
||||||
|
padding: 0 24px;
|
||||||
|
background-color: var(--background-color);
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
box-shadow: var(--box-shadow);
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 100;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
h1 {
|
||||||
|
font-size: 20px;
|
||||||
|
margin: 0;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--text-color);
|
||||||
|
|
||||||
|
.username {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式样式 */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.header {
|
||||||
|
padding: 0 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo h1 {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.username {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
94
admin-web/src/components/Layout/Layout.vue
Normal file
94
admin-web/src/components/Layout/Layout.vue
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
<template>
|
||||||
|
<div class="layout-container">
|
||||||
|
<!-- 顶部导航栏 -->
|
||||||
|
<Header @toggle-sidebar="handleToggleSidebar" />
|
||||||
|
|
||||||
|
<!-- 侧边栏 -->
|
||||||
|
<Sidebar
|
||||||
|
:is-open="isSidebarOpen"
|
||||||
|
@close-sidebar="handleCloseSidebar"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 主内容区域 -->
|
||||||
|
<main class="main-content" :class="{ 'main-content--open': isSidebarOpen }">
|
||||||
|
<div class="content-wrapper">
|
||||||
|
<router-view />
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import Header from './Header.vue'
|
||||||
|
import Sidebar from './Sidebar.vue'
|
||||||
|
|
||||||
|
// 侧边栏显示状态(手机端)
|
||||||
|
const isSidebarOpen = ref(false)
|
||||||
|
|
||||||
|
// 处理侧边栏切换事件(来自Header组件)
|
||||||
|
const handleToggleSidebar = (isOpen) => {
|
||||||
|
isSidebarOpen.value = isOpen
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理侧边栏关闭事件(来自Sidebar组件)
|
||||||
|
const handleCloseSidebar = () => {
|
||||||
|
isSidebarOpen.value = false
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.layout-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100vh;
|
||||||
|
background-color: var(--background-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
margin-top: 64px;
|
||||||
|
margin-left: 250px;
|
||||||
|
padding: 24px;
|
||||||
|
overflow-y: auto;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
|
/* 响应式布局 */
|
||||||
|
@media (max-width: 1023px) {
|
||||||
|
margin-left: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
margin-left: 0;
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 手机端侧边栏打开时,主内容区域添加左侧边距 */
|
||||||
|
.main-content--open {
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-wrapper {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 平板端调整 */
|
||||||
|
@media (min-width: 768px) and (max-width: 1023px) {
|
||||||
|
.main-content {
|
||||||
|
margin-left: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 手机端调整 */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.main-content {
|
||||||
|
margin-left: 0;
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
192
admin-web/src/components/Layout/Sidebar.vue
Normal file
192
admin-web/src/components/Layout/Sidebar.vue
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
<template>
|
||||||
|
<aside
|
||||||
|
class="sidebar"
|
||||||
|
:class="{ 'sidebar--open': isOpen }"
|
||||||
|
>
|
||||||
|
<nav class="sidebar-nav">
|
||||||
|
<ul class="nav-list">
|
||||||
|
<li class="nav-item" v-for="menu in menuList" :key="menu.path">
|
||||||
|
<router-link
|
||||||
|
:to="menu.path"
|
||||||
|
class="nav-link"
|
||||||
|
:class="{ 'is-active': $route.path === menu.path }"
|
||||||
|
@click="handleMenuClick"
|
||||||
|
>
|
||||||
|
<el-icon class="nav-icon"><component :is="menu.icon" /></el-icon>
|
||||||
|
<span class="nav-text">{{ menu.title }}</span>
|
||||||
|
</router-link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- 遮罩层(手机端) -->
|
||||||
|
<div
|
||||||
|
v-if="isOpen && isMobile"
|
||||||
|
class="sidebar-mask"
|
||||||
|
@click="handleMaskClick"
|
||||||
|
></div>
|
||||||
|
</aside>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||||
|
import { HomeFilled, Message, User } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
// 菜单数据
|
||||||
|
const menuList = [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
title: '首页',
|
||||||
|
icon: HomeFilled
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/conversations',
|
||||||
|
title: '会话记录管理',
|
||||||
|
icon: Message
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/users',
|
||||||
|
title: '用户管理',
|
||||||
|
icon: User
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// 侧边栏显示状态(手机端)
|
||||||
|
const props = defineProps({
|
||||||
|
isOpen: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 定义事件
|
||||||
|
const emit = defineEmits(['closeSidebar'])
|
||||||
|
|
||||||
|
// 响应式布局:判断是否为手机端
|
||||||
|
const isMobile = ref(false)
|
||||||
|
|
||||||
|
// 监听窗口大小变化
|
||||||
|
const handleResize = () => {
|
||||||
|
isMobile.value = window.innerWidth < 768
|
||||||
|
// 如果不是手机端,关闭侧边栏
|
||||||
|
if (!isMobile.value) {
|
||||||
|
emit('closeSidebar')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化响应式状态
|
||||||
|
onMounted(() => {
|
||||||
|
handleResize()
|
||||||
|
window.addEventListener('resize', handleResize)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 清理事件监听
|
||||||
|
onUnmounted(() => {
|
||||||
|
window.removeEventListener('resize', handleResize)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 菜单点击处理(手机端)
|
||||||
|
const handleMenuClick = () => {
|
||||||
|
if (isMobile.value) {
|
||||||
|
emit('closeSidebar')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 遮罩层点击处理(手机端)
|
||||||
|
const handleMaskClick = () => {
|
||||||
|
emit('closeSidebar')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.sidebar {
|
||||||
|
position: fixed;
|
||||||
|
top: 64px;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 250px;
|
||||||
|
background-color: var(--background-color);
|
||||||
|
border-right: 1px solid var(--border-color);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
z-index: 99;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
/* 桌面端默认显示,手机端默认隐藏 */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 侧边栏打开状态(手机端) */
|
||||||
|
.sidebar--open {
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-nav {
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 24px;
|
||||||
|
color: var(--text-color);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--border-color-lighter);
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-active {
|
||||||
|
background-color: rgba(64, 158, 255, 0.1);
|
||||||
|
color: var(--primary-color);
|
||||||
|
border-right: 3px solid var(--primary-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-icon {
|
||||||
|
font-size: 18px;
|
||||||
|
margin-right: 12px;
|
||||||
|
width: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-text {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 遮罩层(手机端) */
|
||||||
|
.sidebar-mask {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.5);
|
||||||
|
z-index: 98;
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 平板端侧边栏宽度调整 */
|
||||||
|
@media (min-width: 768px) and (max-width: 1023px) {
|
||||||
|
.sidebar {
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
55
admin-web/src/components/ThemeSwitcher.vue
Normal file
55
admin-web/src/components/ThemeSwitcher.vue
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<template>
|
||||||
|
<el-dropdown trigger="click" @command="handleThemeChange">
|
||||||
|
<el-button type="text" :icon="isDark ? 'MoonNight' : 'Sunny'" circle>
|
||||||
|
</el-button>
|
||||||
|
<template #dropdown>
|
||||||
|
<el-dropdown-menu>
|
||||||
|
<el-dropdown-item command="light">
|
||||||
|
<el-icon><Sunny /></el-icon>
|
||||||
|
<span>浅色模式</span>
|
||||||
|
</el-dropdown-item>
|
||||||
|
<el-dropdown-item command="dark">
|
||||||
|
<el-icon><MoonNight /></el-icon>
|
||||||
|
<span>深色模式</span>
|
||||||
|
</el-dropdown-item>
|
||||||
|
<el-dropdown-item command="auto">
|
||||||
|
<el-icon><Monitor /></el-icon>
|
||||||
|
<span>跟随系统</span>
|
||||||
|
</el-dropdown-item>
|
||||||
|
</el-dropdown-menu>
|
||||||
|
</template>
|
||||||
|
</el-dropdown>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { Sunny, MoonNight, Monitor } from '@element-plus/icons-vue'
|
||||||
|
import { useThemeStore } from '../store/theme'
|
||||||
|
|
||||||
|
const themeStore = useThemeStore()
|
||||||
|
|
||||||
|
// 当前主题状态
|
||||||
|
const isDark = computed(() => themeStore.isDark)
|
||||||
|
|
||||||
|
// 切换主题
|
||||||
|
const handleThemeChange = (theme) => {
|
||||||
|
if (theme === 'auto') {
|
||||||
|
// 跟随系统主题
|
||||||
|
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||||
|
themeStore.isDark = prefersDark
|
||||||
|
} else {
|
||||||
|
// 手动设置主题
|
||||||
|
themeStore.isDark = theme === 'dark'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新localStorage和document属性
|
||||||
|
localStorage.setItem('theme', themeStore.isDark ? 'dark' : 'light')
|
||||||
|
document.documentElement.setAttribute('data-theme', themeStore.isDark ? 'dark' : 'light')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.el-dropdown {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
32
admin-web/src/main.js
Normal file
32
admin-web/src/main.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { createApp } from 'vue'
|
||||||
|
import ElementPlus from 'element-plus'
|
||||||
|
import 'element-plus/dist/index.css'
|
||||||
|
// 引入中文语言包
|
||||||
|
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
|
||||||
|
import router from './router'
|
||||||
|
import { pinia, useThemeStore } from './store'
|
||||||
|
import './styles/main.scss'
|
||||||
|
import App from './App.vue'
|
||||||
|
|
||||||
|
// 创建应用实例
|
||||||
|
const app = createApp(App)
|
||||||
|
|
||||||
|
// 使用Pinia - 必须先初始化Pinia,再使用store
|
||||||
|
app.use(pinia)
|
||||||
|
|
||||||
|
// 初始化主题
|
||||||
|
const themeStore = useThemeStore()
|
||||||
|
themeStore.initTheme()
|
||||||
|
|
||||||
|
// 使用Element Plus,并配置中文语言
|
||||||
|
app.use(ElementPlus, {
|
||||||
|
locale: zhCn,
|
||||||
|
// 配置主题切换
|
||||||
|
dark: themeStore.isDark
|
||||||
|
})
|
||||||
|
|
||||||
|
// 使用路由
|
||||||
|
app.use(router)
|
||||||
|
|
||||||
|
// 挂载应用
|
||||||
|
app.mount('#app')
|
||||||
59
admin-web/src/router/index.js
Normal file
59
admin-web/src/router/index.js
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
import { useAuthStore } from '../store/auth'
|
||||||
|
|
||||||
|
// 定义路由
|
||||||
|
const routes = [
|
||||||
|
{
|
||||||
|
path: '/login',
|
||||||
|
name: 'Login',
|
||||||
|
component: () => import('../views/Login.vue'),
|
||||||
|
meta: { requiresAuth: false }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'Layout',
|
||||||
|
component: () => import('../components/Layout/Layout.vue'),
|
||||||
|
meta: { requiresAuth: true },
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
name: 'Home',
|
||||||
|
component: () => import('../views/Home.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/conversations',
|
||||||
|
name: 'ConversationList',
|
||||||
|
component: () => import('../views/ConversationList.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/users',
|
||||||
|
name: 'UserList',
|
||||||
|
component: () => import('../views/UserList.vue')
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/:pathMatch(.*)*',
|
||||||
|
redirect: '/'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// 创建路由实例
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(),
|
||||||
|
routes
|
||||||
|
})
|
||||||
|
|
||||||
|
// 路由守卫
|
||||||
|
router.beforeEach((to, from, next) => {
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
if (to.meta.requiresAuth && !authStore.isLoggedIn) {
|
||||||
|
// 未登录用户访问受保护页面,重定向到登录页
|
||||||
|
next('/login')
|
||||||
|
} else {
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
22
admin-web/src/store/auth.js
Normal file
22
admin-web/src/store/auth.js
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
export const useAuthStore = defineStore('auth', {
|
||||||
|
state: () => ({
|
||||||
|
isLoggedIn: false,
|
||||||
|
username: ''
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
login(username, password) {
|
||||||
|
if (username === 'Admin' && password === 'Admin') {
|
||||||
|
this.isLoggedIn = true
|
||||||
|
this.username = username
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
logout() {
|
||||||
|
this.isLoggedIn = false
|
||||||
|
this.username = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
11
admin-web/src/store/index.js
Normal file
11
admin-web/src/store/index.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { createPinia } from 'pinia'
|
||||||
|
import { useAuthStore } from './auth'
|
||||||
|
import { useThemeStore } from './theme'
|
||||||
|
|
||||||
|
const pinia = createPinia()
|
||||||
|
|
||||||
|
export {
|
||||||
|
pinia,
|
||||||
|
useAuthStore,
|
||||||
|
useThemeStore
|
||||||
|
}
|
||||||
31
admin-web/src/store/theme.js
Normal file
31
admin-web/src/store/theme.js
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
export const useThemeStore = defineStore('theme', {
|
||||||
|
state: () => ({
|
||||||
|
isDark: localStorage.getItem('theme') === 'dark'
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
toggleTheme() {
|
||||||
|
this.isDark = !this.isDark
|
||||||
|
localStorage.setItem('theme', this.isDark ? 'dark' : 'light')
|
||||||
|
document.documentElement.setAttribute('data-theme', this.isDark ? 'dark' : 'light')
|
||||||
|
// 控制 Element Plus 深色模式
|
||||||
|
if (this.isDark) {
|
||||||
|
document.documentElement.classList.add('dark')
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove('dark')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
initTheme() {
|
||||||
|
const theme = localStorage.getItem('theme') || 'light'
|
||||||
|
this.isDark = theme === 'dark'
|
||||||
|
document.documentElement.setAttribute('data-theme', theme)
|
||||||
|
// 初始化 Element Plus 深色模式
|
||||||
|
if (this.isDark) {
|
||||||
|
document.documentElement.classList.add('dark')
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove('dark')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
79
admin-web/src/style.css
Normal file
79
admin-web/src/style.css
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
:root {
|
||||||
|
font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-weight: 400;
|
||||||
|
|
||||||
|
color-scheme: light dark;
|
||||||
|
color: rgba(255, 255, 255, 0.87);
|
||||||
|
background-color: #242424;
|
||||||
|
|
||||||
|
font-synthesis: none;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #646cff;
|
||||||
|
text-decoration: inherit;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: #535bf2;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
place-items: center;
|
||||||
|
min-width: 320px;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 3.2em;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
padding: 0.6em 1.2em;
|
||||||
|
font-size: 1em;
|
||||||
|
font-weight: 500;
|
||||||
|
font-family: inherit;
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.25s;
|
||||||
|
}
|
||||||
|
button:hover {
|
||||||
|
border-color: #646cff;
|
||||||
|
}
|
||||||
|
button:focus,
|
||||||
|
button:focus-visible {
|
||||||
|
outline: 4px auto -webkit-focus-ring-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
padding: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
max-width: 1280px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
:root {
|
||||||
|
color: #213547;
|
||||||
|
background-color: #ffffff;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: #747bff;
|
||||||
|
}
|
||||||
|
button {
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
}
|
||||||
236
admin-web/src/styles/main.scss
Normal file
236
admin-web/src/styles/main.scss
Normal file
@@ -0,0 +1,236 @@
|
|||||||
|
// 引入变量文件 - 使用 @use 替换 @import 以消除 Sass 3.0.0 弃用警告
|
||||||
|
@use './variables.scss' as *;
|
||||||
|
|
||||||
|
// 引入响应式样式 - 使用 @use 替换 @import 以消除 Sass 3.0.0 弃用警告
|
||||||
|
@use './responsive.scss' as *;
|
||||||
|
|
||||||
|
// 全局样式重置
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
height: 100%;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: var(--text-color);
|
||||||
|
background-color: var(--background-color);
|
||||||
|
transition: background-color 0.3s, color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 链接样式
|
||||||
|
a {
|
||||||
|
color: var(--primary-color);
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #66b1ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 容器样式
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0 20px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面容器样式
|
||||||
|
.page-container {
|
||||||
|
padding: 20px;
|
||||||
|
min-height: calc(100vh - 64px);
|
||||||
|
|
||||||
|
@media (max-width: $breakpoint-sm - 1px) {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 卡片样式
|
||||||
|
.card {
|
||||||
|
background-color: var(--background-color);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: var(--box-shadow);
|
||||||
|
padding: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: var(--box-shadow-light);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 标题样式
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按钮样式
|
||||||
|
.btn {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: var(--background-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.btn-primary {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
color: #ffffff;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #66b1ff;
|
||||||
|
border-color: #66b1ff;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.btn-success {
|
||||||
|
background-color: var(--success-color);
|
||||||
|
border-color: var(--success-color);
|
||||||
|
color: #ffffff;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #85ce61;
|
||||||
|
border-color: #85ce61;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.btn-danger {
|
||||||
|
background-color: var(--danger-color);
|
||||||
|
border-color: var(--danger-color);
|
||||||
|
color: #ffffff;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #f78989;
|
||||||
|
border-color: #f78989;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 表单样式
|
||||||
|
.form-item {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: var(--background-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
font-size: 14px;
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 表格样式
|
||||||
|
.table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
|
||||||
|
th, td {
|
||||||
|
padding: 12px;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
background-color: var(--border-color-lighter);
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
tr {
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--border-color-lighter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载状态样式
|
||||||
|
.loading {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 40px;
|
||||||
|
|
||||||
|
.loading-text {
|
||||||
|
margin-left: 8px;
|
||||||
|
color: var(--text-color-secondary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 空状态样式
|
||||||
|
.empty {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 60px 20px;
|
||||||
|
color: var(--text-color-secondary);
|
||||||
|
|
||||||
|
.empty-icon {
|
||||||
|
font-size: 48px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 错误状态样式
|
||||||
|
.error {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 60px 20px;
|
||||||
|
color: var(--danger-color);
|
||||||
|
|
||||||
|
.error-icon {
|
||||||
|
font-size: 48px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
100
admin-web/src/styles/responsive.scss
Normal file
100
admin-web/src/styles/responsive.scss
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
// 响应式布局样式
|
||||||
|
// 导入变量文件 - 使用 @use 替换 @import 以消除 Sass 3.0.0 弃用警告
|
||||||
|
@use './variables.scss' as *;
|
||||||
|
|
||||||
|
// 手机端 (< 768px)
|
||||||
|
@media (max-width: $breakpoint-sm - 1px) {
|
||||||
|
.container {
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 平板端 (768px - 1023px)
|
||||||
|
@media (min-width: $breakpoint-sm) and (max-width: $breakpoint-md - 1px) {
|
||||||
|
.container {
|
||||||
|
padding: 0 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
margin-left: 200px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 桌面端 (>= 1024px)
|
||||||
|
@media (min-width: $breakpoint-md) {
|
||||||
|
.container {
|
||||||
|
padding: 0 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar {
|
||||||
|
width: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
margin-left: 250px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 大屏幕桌面端 (>= 1200px)
|
||||||
|
@media (min-width: $breakpoint-lg) {
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 超大屏幕桌面端 (>= 1920px)
|
||||||
|
@media (min-width: $breakpoint-xl) {
|
||||||
|
.container {
|
||||||
|
max-width: 1600px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通用响应式工具类
|
||||||
|
.text-center-sm {
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
@media (min-width: $breakpoint-sm) {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.display-block-sm {
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
@media (min-width: $breakpoint-sm) {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hidden-sm {
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
@media (min-width: $breakpoint-sm) {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.visible-sm {
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
@media (min-width: $breakpoint-sm) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
41
admin-web/src/styles/variables.scss
Normal file
41
admin-web/src/styles/variables.scss
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
// 浅色主题变量
|
||||||
|
:root {
|
||||||
|
--primary-color: #409eff;
|
||||||
|
--success-color: #67c23a;
|
||||||
|
--warning-color: #e6a23c;
|
||||||
|
--danger-color: #f56c6c;
|
||||||
|
--info-color: #909399;
|
||||||
|
|
||||||
|
--background-color: #ffffff;
|
||||||
|
--text-color: #303133;
|
||||||
|
--text-color-secondary: #606266;
|
||||||
|
--border-color: #dcdfe6;
|
||||||
|
--border-color-light: #e4e7ed;
|
||||||
|
--border-color-lighter: #ebeef5;
|
||||||
|
--box-shadow: 0 2px 4px rgba(0, 0, 0, 0.12), 0 0 6px rgba(0, 0, 0, 0.04);
|
||||||
|
--box-shadow-light: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 深色主题变量
|
||||||
|
[data-theme="dark"] {
|
||||||
|
--primary-color: #409eff;
|
||||||
|
--success-color: #67c23a;
|
||||||
|
--warning-color: #e6a23c;
|
||||||
|
--danger-color: #f56c6c;
|
||||||
|
--info-color: #909399;
|
||||||
|
|
||||||
|
--background-color: #1a1a1a;
|
||||||
|
--text-color: #ffffff;
|
||||||
|
--text-color-secondary: #aaaaaa;
|
||||||
|
--border-color: #444444;
|
||||||
|
--border-color-light: #333333;
|
||||||
|
--border-color-lighter: #222222;
|
||||||
|
--box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3), 0 0 6px rgba(0, 0, 0, 0.2);
|
||||||
|
--box-shadow-light: 0 2px 12px 0 rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 响应式断点
|
||||||
|
$breakpoint-sm: 768px;
|
||||||
|
$breakpoint-md: 1024px;
|
||||||
|
$breakpoint-lg: 1200px;
|
||||||
|
$breakpoint-xl: 1920px;
|
||||||
25
admin-web/src/utils/formatters.js
Normal file
25
admin-web/src/utils/formatters.js
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
// 手机号脱敏处理
|
||||||
|
export const formatPhoneNumber = (phoneNumber, showFull = false) => {
|
||||||
|
if (!phoneNumber) return ''
|
||||||
|
const phone = phoneNumber.toString()
|
||||||
|
if (phone.length !== 11) return phone
|
||||||
|
|
||||||
|
if (showFull) {
|
||||||
|
return phone
|
||||||
|
} else {
|
||||||
|
return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 时间格式转换,将T替换为空格
|
||||||
|
export const formatDateTime = (dateTime) => {
|
||||||
|
if (!dateTime) return ''
|
||||||
|
return dateTime.toString().replace('T', ' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 时间戳转换为格式化日期
|
||||||
|
export const formatTimestamp = (timestamp) => {
|
||||||
|
if (!timestamp) return ''
|
||||||
|
const date = new Date(timestamp)
|
||||||
|
return date.toISOString().replace('T', ' ').slice(0, 19)
|
||||||
|
}
|
||||||
55
admin-web/src/utils/request.js
Normal file
55
admin-web/src/utils/request.js
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import { useAuthStore } from '../store/auth'
|
||||||
|
|
||||||
|
// 创建axios实例
|
||||||
|
const request = axios.create({
|
||||||
|
baseURL: 'https://wx-xcx-check.blv-oa.com:4433/api', // API根地址 - 使用vite代理
|
||||||
|
timeout: 10000, // 请求超时时间
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json' // 设置默认请求格式为JSON
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// 请求拦截器
|
||||||
|
request.interceptors.request.use(
|
||||||
|
config => {
|
||||||
|
// 从auth store获取token(如果有)
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
if (authStore.isLoggedIn) {
|
||||||
|
// 虽然当前是前端验证,但预留token位置以便后续扩展
|
||||||
|
// config.headers.Authorization = `Bearer ${authStore.token}`
|
||||||
|
}
|
||||||
|
return config
|
||||||
|
},
|
||||||
|
error => {
|
||||||
|
// 请求错误处理
|
||||||
|
console.error('request error:', error)
|
||||||
|
return Promise.reject(error)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// 响应拦截器
|
||||||
|
request.interceptors.response.use(
|
||||||
|
response => {
|
||||||
|
// 直接返回响应数据
|
||||||
|
return response.data
|
||||||
|
},
|
||||||
|
error => {
|
||||||
|
// 响应错误处理
|
||||||
|
console.error('response error:', error)
|
||||||
|
|
||||||
|
// 如果是401未授权,可以在这里处理登出逻辑
|
||||||
|
if (error.response && error.response.status === 401) {
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
authStore.logout()
|
||||||
|
// 跳转到登录页
|
||||||
|
window.location.href = '/login'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化错误信息
|
||||||
|
const errorMessage = error.response?.data?.message || error.message || '请求失败'
|
||||||
|
return Promise.reject(new Error(errorMessage))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export default request
|
||||||
412
admin-web/src/views/ConversationList.vue
Normal file
412
admin-web/src/views/ConversationList.vue
Normal file
@@ -0,0 +1,412 @@
|
|||||||
|
<template>
|
||||||
|
<div class="conversation-list-container">
|
||||||
|
<!-- 筛选表单折叠面板 -->
|
||||||
|
<el-collapse v-model="activeCollapse" class="filter-collapse">
|
||||||
|
<el-collapse-item name="filter">
|
||||||
|
<template #title>
|
||||||
|
<div class="collapse-header">
|
||||||
|
<span class="collapse-title">筛选条件</span>
|
||||||
|
<div class="header-buttons" @click.stop>
|
||||||
|
<el-button type="primary" size="small" @click="handleQuery">查询</el-button>
|
||||||
|
<el-button size="small" @click="handleReset">重置</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<el-form :model="filterForm" inline label-width="80px" size="small">
|
||||||
|
<el-form-item label="时间范围">
|
||||||
|
<el-date-picker v-model="filterForm.dateRange" type="daterange" range-separator="至" start-placeholder="开始日期"
|
||||||
|
end-placeholder="结束日期" format="YYYY-MM-DD" value-format="YYYY-MM-DD" clearable></el-date-picker>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="用户">
|
||||||
|
<el-select v-model="filterForm.userKey" placeholder="请选择用户" clearable filterable remote
|
||||||
|
:remote-method="handleUserSearch" :loading="userLoading" style="width: 200px">
|
||||||
|
<el-option v-for="user in userList" :key="user.userKey" :label="user.userName"
|
||||||
|
:value="user.userKey"></el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="消息类型">
|
||||||
|
<el-select v-model="filterForm.messageType" placeholder="请选择消息类型" clearable style="width: 150px">
|
||||||
|
<el-option label="公有" :value="1"></el-option>
|
||||||
|
<el-option label="私有" :value="2"></el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="部门">
|
||||||
|
<el-select v-model="filterForm.department" placeholder="请选择部门" clearable style="width: 150px">
|
||||||
|
<el-option v-for="dept in departmentList" :key="dept" :label="dept" :value="dept"></el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-collapse-item>
|
||||||
|
</el-collapse>
|
||||||
|
|
||||||
|
<!-- 会话记录列表 -->
|
||||||
|
<el-scrollbar :style="{ height: scrollbarHeight }" @scroll="handleScroll">
|
||||||
|
<div v-loading="loading" class="descriptions-container">
|
||||||
|
<div v-for="(item, index) in conversationList" :key="index" class="descriptions-item">
|
||||||
|
<el-descriptions :column="1" size="small" label-width="0px">
|
||||||
|
<el-descriptions-item label="">
|
||||||
|
{{ formatDateTime(item.recordTime) }} | {{ item.userName }} | {{ item.department }}
|
||||||
|
<el-tag :type="item.messageType === 1 ? 'success' : ''">
|
||||||
|
{{ item.messageType === 1 ? '公有' : '私有' }}
|
||||||
|
</el-tag>
|
||||||
|
<el-tag :type="item.sendMethod === '文字' ? 'success' : ''">
|
||||||
|
{{ item.sendMethod === 'text' ? '文字' : '语音' }}{{ item.speakingTime ? ' | ' + item.speakingTime + '秒' :
|
||||||
|
''
|
||||||
|
}}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="">
|
||||||
|
{{ item.conversationContent }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 加载更多提示 -->
|
||||||
|
<div v-if="loadingMore" class="loading-more">
|
||||||
|
<el-icon class="is-loading">
|
||||||
|
<Loading />
|
||||||
|
</el-icon>
|
||||||
|
<span>正在加载中...</span>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="!hasMoreData" class="no-more-data">
|
||||||
|
<span>已加载全部数据</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-scrollbar>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted, computed, watch } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import request from '../utils/request'
|
||||||
|
import { formatPhoneNumber, formatDateTime, formatTimestamp } from '../utils/formatters'
|
||||||
|
import { Loading } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
|
// 加载状态
|
||||||
|
const loading = ref(false)
|
||||||
|
const userLoading = ref(false)
|
||||||
|
|
||||||
|
// 折叠面板状态
|
||||||
|
const activeCollapse = ref(['filter'])
|
||||||
|
|
||||||
|
// 计算滚动容器高度
|
||||||
|
const scrollbarHeight = computed(() => {
|
||||||
|
return activeCollapse.value.includes('filter') ? 'calc(100vh - 350px)' : 'calc(100vh - 160px)'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 分页数据
|
||||||
|
const currentPage = ref(1)
|
||||||
|
const pageSize = ref(20) // 固定分页大小为20条/页
|
||||||
|
const total = ref(0)
|
||||||
|
const loadingMore = ref(false)
|
||||||
|
const hasMoreData = ref(true)
|
||||||
|
|
||||||
|
// 空状态文本
|
||||||
|
const emptyText = computed(() => loading.value ? '加载中...' : '暂无数据')
|
||||||
|
|
||||||
|
// 筛选表单数据
|
||||||
|
const filterForm = reactive({
|
||||||
|
dateRange: [],
|
||||||
|
userKey: '',
|
||||||
|
messageType: '',
|
||||||
|
department: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 会话记录列表
|
||||||
|
const conversationList = ref([])
|
||||||
|
|
||||||
|
// 用户列表(用于筛选)
|
||||||
|
const userList = ref([])
|
||||||
|
|
||||||
|
// 部门列表(用于筛选)
|
||||||
|
const departmentList = ref([])
|
||||||
|
|
||||||
|
// 查询会话记录
|
||||||
|
const handleQuery = () => {
|
||||||
|
currentPage.value = 1
|
||||||
|
fetchConversations()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置筛选条件
|
||||||
|
const handleReset = () => {
|
||||||
|
// 重置表单
|
||||||
|
Object.keys(filterForm).forEach(key => {
|
||||||
|
if (key === 'dateRange') {
|
||||||
|
filterForm[key] = []
|
||||||
|
} else {
|
||||||
|
filterForm[key] = ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
currentPage.value = 1
|
||||||
|
fetchConversations()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 滚动处理
|
||||||
|
const handleScroll = ({ scrollTop, scrollHeight, clientHeight }) => {
|
||||||
|
// 当滚动到底部100px以内时加载下一页
|
||||||
|
if (scrollHeight - scrollTop - clientHeight < 100 && !loadingMore.value && hasMoreData.value) {
|
||||||
|
loadNextPage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载下一页数据
|
||||||
|
const loadNextPage = () => {
|
||||||
|
if (loadingMore.value || !hasMoreData.value) return
|
||||||
|
|
||||||
|
loadingMore.value = true
|
||||||
|
currentPage.value++
|
||||||
|
fetchConversations(true) // 传入true表示加载更多
|
||||||
|
}
|
||||||
|
|
||||||
|
// 远程搜索用户
|
||||||
|
const handleUserSearch = (query) => {
|
||||||
|
if (query) {
|
||||||
|
userLoading.value = true
|
||||||
|
// 过滤用户列表
|
||||||
|
setTimeout(() => {
|
||||||
|
const allUsers = originalUserList.value
|
||||||
|
userList.value = allUsers.filter(user =>
|
||||||
|
user.userName.toLowerCase().includes(query.toLowerCase())
|
||||||
|
)
|
||||||
|
userLoading.value = false
|
||||||
|
}, 200)
|
||||||
|
} else {
|
||||||
|
userList.value = [...originalUserList.value]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 原始用户列表(用于搜索过滤)
|
||||||
|
const originalUserList = ref([])
|
||||||
|
|
||||||
|
// 获取会话记录
|
||||||
|
const fetchConversations = async (isLoadMore = false) => {
|
||||||
|
if (isLoadMore) {
|
||||||
|
loadingMore.value = true
|
||||||
|
} else {
|
||||||
|
loading.value = true
|
||||||
|
conversationList.value = [] // 非加载更多时清空列表
|
||||||
|
hasMoreData.value = true // 重置加载状态
|
||||||
|
currentPage.value = 1 // 重置当前页
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 构建请求参数
|
||||||
|
const params = {
|
||||||
|
startTime: filterForm.dateRange[0] ? new Date(filterForm.dateRange[0]).getTime() : null,
|
||||||
|
endTime: filterForm.dateRange[1] ? new Date(filterForm.dateRange[1]).getTime() + 24 * 60 * 60 * 1000 - 1 : null,
|
||||||
|
userKey: filterForm.userKey,
|
||||||
|
messageType: filterForm.messageType ? Number(filterForm.messageType) : null,
|
||||||
|
department: filterForm.department,
|
||||||
|
page: currentPage.value,
|
||||||
|
pageSize: pageSize.value
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用API获取会话记录
|
||||||
|
const data = await request.post('/Admin/QueryConversations', JSON.stringify(params))
|
||||||
|
|
||||||
|
// 检查返回数据格式,确保是数组
|
||||||
|
const rawData = Array.isArray(data) ? data : data.data || []
|
||||||
|
|
||||||
|
// 按时间排序(最新的记录在前面)
|
||||||
|
const sortedData = rawData.sort((a, b) => {
|
||||||
|
return b.recordTimeUTCStamp - a.recordTimeUTCStamp
|
||||||
|
})
|
||||||
|
|
||||||
|
// 如果是加载更多,则追加数据,否则替换数据
|
||||||
|
if (isLoadMore) {
|
||||||
|
conversationList.value = [...conversationList.value, ...sortedData]
|
||||||
|
} else {
|
||||||
|
conversationList.value = sortedData
|
||||||
|
}
|
||||||
|
|
||||||
|
total.value = conversationList.value.length
|
||||||
|
|
||||||
|
// 判断是否已加载全部数据
|
||||||
|
if (sortedData.length < pageSize.value) {
|
||||||
|
hasMoreData.value = false
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('获取会话记录失败:' + error.message)
|
||||||
|
if (!isLoadMore) {
|
||||||
|
conversationList.value = []
|
||||||
|
total.value = 0
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
loadingMore.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户列表(用于筛选)
|
||||||
|
const fetchUsers = async () => {
|
||||||
|
try {
|
||||||
|
// 调用API获取用户列表
|
||||||
|
const data = await request.get('/Admin/QueryUsers')
|
||||||
|
|
||||||
|
// 检查返回数据格式,确保是数组
|
||||||
|
const rawData = Array.isArray(data) ? data : data.data || []
|
||||||
|
|
||||||
|
// 转换数据格式,只取userName和userKey
|
||||||
|
const users = rawData.map(item => ({
|
||||||
|
userKey: item.userKey,
|
||||||
|
userName: item.userName
|
||||||
|
}))
|
||||||
|
|
||||||
|
userList.value = users
|
||||||
|
originalUserList.value = [...users]
|
||||||
|
|
||||||
|
// 提取部门列表
|
||||||
|
const depts = [...new Set(rawData.map(user => user.department))]
|
||||||
|
departmentList.value = depts.filter(dept => dept)
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('获取用户列表失败:' + error.message)
|
||||||
|
userList.value = []
|
||||||
|
originalUserList.value = []
|
||||||
|
departmentList.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面初始化
|
||||||
|
onMounted(() => {
|
||||||
|
fetchUsers()
|
||||||
|
fetchConversations()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.conversation-list-container {
|
||||||
|
padding: 20px 0;
|
||||||
|
height: calc(100% - 64px);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 折叠面板样式 */
|
||||||
|
.filter-collapse {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-collapse :deep(.el-collapse-item__header) {
|
||||||
|
height: auto;
|
||||||
|
padding: 12px 16px;
|
||||||
|
line-height: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapse-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
padding-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapse-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-buttons .el-button {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 手机号样式 */
|
||||||
|
.phone-number {
|
||||||
|
cursor: pointer;
|
||||||
|
color: #409eff;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-number:hover {
|
||||||
|
color: #66b1ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 加载更多样式 */
|
||||||
|
.loading-more,
|
||||||
|
.no-more-data {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-more .el-icon {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Descriptions容器样式 */
|
||||||
|
.descriptions-container {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 每个Descriptions项的样式 */
|
||||||
|
.descriptions-item {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 会话内容样式 */
|
||||||
|
.conversation-content {
|
||||||
|
margin-top: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式样式 */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.conversation-list-container {
|
||||||
|
padding: 6px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-form-item {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-date-picker,
|
||||||
|
.el-select {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式调整Descriptions列数 */
|
||||||
|
.el-descriptions {
|
||||||
|
:deep(.el-descriptions__table) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-descriptions__cell) {
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
334
admin-web/src/views/Home.vue
Normal file
334
admin-web/src/views/Home.vue
Normal file
@@ -0,0 +1,334 @@
|
|||||||
|
<template>
|
||||||
|
<div class="home-container">
|
||||||
|
<h2>欢迎回来,{{ username }}</h2>
|
||||||
|
|
||||||
|
<!-- 系统统计卡片 -->
|
||||||
|
<div class="stats-container">
|
||||||
|
<el-card class="stat-card">
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-icon user-icon">
|
||||||
|
<el-icon><User /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="stat-info">
|
||||||
|
<h3>{{ userCount }}</h3>
|
||||||
|
<p>活跃用户</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-card class="stat-card">
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-icon conversation-icon">
|
||||||
|
<el-icon><Message /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="stat-info">
|
||||||
|
<h3>{{ conversationCount }}</h3>
|
||||||
|
<p>会话记录</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-card class="stat-card">
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-icon department-icon">
|
||||||
|
<el-icon><OfficeBuilding /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="stat-info">
|
||||||
|
<h3>{{ departmentCount }}</h3>
|
||||||
|
<p>部门数量</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-card class="stat-card">
|
||||||
|
<div class="stat-content">
|
||||||
|
<div class="stat-icon today-icon">
|
||||||
|
<el-icon><Calendar /></el-icon>
|
||||||
|
</div>
|
||||||
|
<div class="stat-info">
|
||||||
|
<h3>{{ todayCount }}</h3>
|
||||||
|
<p>今日新增</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 最近会话记录 -->
|
||||||
|
<el-card class="recent-conversations-card">
|
||||||
|
<template #header>
|
||||||
|
<div class="card-header">
|
||||||
|
<span>最近会话记录</span>
|
||||||
|
<router-link to="/conversations" class="view-all-link">
|
||||||
|
查看全部
|
||||||
|
<el-icon class="el-icon--right"><ArrowRight /></el-icon>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-table
|
||||||
|
:data="recentConversations"
|
||||||
|
style="width: 100%"
|
||||||
|
stripe
|
||||||
|
size="small"
|
||||||
|
:empty-text="'暂无会话记录'"
|
||||||
|
max-height="300"
|
||||||
|
>
|
||||||
|
<el-table-column prop="recordTime" label="时间" width="160"></el-table-column>
|
||||||
|
<el-table-column prop="userName" label="用户名" width="70"></el-table-column>
|
||||||
|
<el-table-column prop="conversationContent" label="内容" min-width="300" show-overflow-tooltip></el-table-column>
|
||||||
|
</el-table>
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, computed, onMounted } from 'vue'
|
||||||
|
import { User, Message, OfficeBuilding, Calendar, ArrowRight } from '@element-plus/icons-vue'
|
||||||
|
import { useAuthStore } from '../store/auth'
|
||||||
|
import request from '../utils/request'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
|
// 认证状态管理
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
// 用户名
|
||||||
|
const username = computed(() => authStore.username)
|
||||||
|
|
||||||
|
// 统计数据
|
||||||
|
const userCount = ref(0)
|
||||||
|
const conversationCount = ref(0)
|
||||||
|
const departmentCount = ref(0)
|
||||||
|
const todayCount = ref(0)
|
||||||
|
|
||||||
|
// 最近会话记录
|
||||||
|
const recentConversations = ref([])
|
||||||
|
|
||||||
|
// 数据转换:将后端返回的会话数据转换为前端所需格式
|
||||||
|
const convertConversationData = (data) => {
|
||||||
|
return data.map(item => ({
|
||||||
|
id: item.Id,
|
||||||
|
recordTime: new Date(item.RecordTime).toLocaleString('zh-CN'),
|
||||||
|
recordTimeUTCStamp: item.RecordTimeUTCStamp,
|
||||||
|
userName: item.UserName,
|
||||||
|
conversationContent: item.ConversationContent,
|
||||||
|
messageType: item.MessageType
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取系统统计数据
|
||||||
|
const fetchStats = async () => {
|
||||||
|
try {
|
||||||
|
// 获取用户列表数据,用于统计
|
||||||
|
const usersData = await request.get('/Admin/QueryUsers')
|
||||||
|
const users = Array.isArray(usersData) ? usersData : usersData.data || []
|
||||||
|
|
||||||
|
// 获取会话记录数据,用于统计
|
||||||
|
const conversationsData = await request.post('/Admin/QueryConversations', {})
|
||||||
|
const conversations = Array.isArray(conversationsData) ? conversationsData : conversationsData.data || []
|
||||||
|
|
||||||
|
// 计算统计数据
|
||||||
|
userCount.value = users.length
|
||||||
|
conversationCount.value = conversations.length
|
||||||
|
|
||||||
|
// 计算部门数量
|
||||||
|
const departments = new Set(users.map(user => user.Department))
|
||||||
|
departmentCount.value = departments.size
|
||||||
|
|
||||||
|
// 计算今日新增会话数
|
||||||
|
const today = new Date().toDateString()
|
||||||
|
const todayConversations = conversations.filter(conv => {
|
||||||
|
return new Date(conv.RecordTime).toDateString() === today
|
||||||
|
})
|
||||||
|
todayCount.value = todayConversations.length
|
||||||
|
|
||||||
|
// 获取最近会话记录
|
||||||
|
await fetchRecentConversations()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取系统统计数据失败:', error)
|
||||||
|
ElMessage.error('获取系统统计数据失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取最近会话记录
|
||||||
|
const fetchRecentConversations = async () => {
|
||||||
|
try {
|
||||||
|
// 调用API获取最近的5条会话记录
|
||||||
|
const data = await request.post('/Admin/QueryConversations', {})
|
||||||
|
|
||||||
|
// 检查返回数据格式,确保是数组
|
||||||
|
const rawData = Array.isArray(data) ? data : data.data || []
|
||||||
|
|
||||||
|
// 转换数据格式
|
||||||
|
const convertedData = convertConversationData(rawData)
|
||||||
|
|
||||||
|
// 按时间排序(最新的记录在前面),并只显示最近5条
|
||||||
|
recentConversations.value = convertedData
|
||||||
|
.sort((a, b) => {
|
||||||
|
return b.recordTimeUTCStamp - a.recordTimeUTCStamp
|
||||||
|
})
|
||||||
|
.slice(0, 5)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('获取最近会话记录失败:', error)
|
||||||
|
recentConversations.value = []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面初始化
|
||||||
|
onMounted(() => {
|
||||||
|
fetchStats()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.home-container {
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
transition: all 0.3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: var(--box-shadow-light);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 24px;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-icon {
|
||||||
|
background-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.conversation-icon {
|
||||||
|
background-color: var(--success-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.department-icon {
|
||||||
|
background-color: var(--warning-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.today-icon {
|
||||||
|
background-color: var(--danger-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-info h3 {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0 0 4px 0;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-info p {
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 0;
|
||||||
|
color: var(--text-color-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.recent-conversations-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-all-link {
|
||||||
|
color: var(--primary-color);
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 14px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #66b1ff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.system-status-container {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-list {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 12px 0;
|
||||||
|
border-bottom: 1px solid var(--border-color-light);
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-label {
|
||||||
|
color: var(--text-color-secondary);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-value {
|
||||||
|
color: var(--text-color);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式样式 */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.home-container {
|
||||||
|
padding: 6px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats-container {
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-content {
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon {
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-info h3 {
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-list {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
142
admin-web/src/views/Login.vue
Normal file
142
admin-web/src/views/Login.vue
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
<template>
|
||||||
|
<div class="login-container">
|
||||||
|
<div class="login-box">
|
||||||
|
<h2 class="login-title">后台管理系统</h2>
|
||||||
|
<el-form ref="loginFormRef" :model="loginForm" :rules="loginRules" label-width="80px">
|
||||||
|
<el-form-item label="用户名" prop="username">
|
||||||
|
<el-input
|
||||||
|
v-model="loginForm.username"
|
||||||
|
placeholder="请输入用户名"
|
||||||
|
prefix-icon="User"
|
||||||
|
clearable
|
||||||
|
></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="密码" prop="password">
|
||||||
|
<el-input
|
||||||
|
v-model="loginForm.password"
|
||||||
|
type="password"
|
||||||
|
placeholder="请输入密码"
|
||||||
|
prefix-icon="Lock"
|
||||||
|
clearable
|
||||||
|
show-password
|
||||||
|
></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" class="login-btn" @click="handleLogin" :loading="loading">
|
||||||
|
登录
|
||||||
|
</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import { useAuthStore } from '../store/auth'
|
||||||
|
|
||||||
|
// 表单引用
|
||||||
|
const loginFormRef = ref()
|
||||||
|
|
||||||
|
// 加载状态
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
// 登录表单数据
|
||||||
|
const loginForm = reactive({
|
||||||
|
username: '',
|
||||||
|
password: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 登录表单验证规则
|
||||||
|
const loginRules = {
|
||||||
|
username: [
|
||||||
|
{ required: true, message: '请输入用户名', trigger: 'blur' }
|
||||||
|
],
|
||||||
|
password: [
|
||||||
|
{ required: true, message: '请输入密码', trigger: 'blur' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 路由实例
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
// 认证状态管理
|
||||||
|
const authStore = useAuthStore()
|
||||||
|
|
||||||
|
// 登录处理
|
||||||
|
const handleLogin = async () => {
|
||||||
|
// 表单验证
|
||||||
|
if (!loginFormRef.value) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
await loginFormRef.value.validate()
|
||||||
|
loading.value = true
|
||||||
|
|
||||||
|
// 调用登录方法
|
||||||
|
const success = authStore.login(loginForm.username, loginForm.password)
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
// 登录成功,跳转到首页
|
||||||
|
ElMessage.success('登录成功')
|
||||||
|
router.push('/')
|
||||||
|
} else {
|
||||||
|
// 登录失败,显示错误消息
|
||||||
|
ElMessage.error('用户名或密码错误')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// 表单验证失败
|
||||||
|
console.error('login form validate error:', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.login-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
background-color: var(--background-color);
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-box {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
padding: 32px;
|
||||||
|
background-color: var(--background-color);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: var(--box-shadow-light);
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-title {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-btn {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-footer {
|
||||||
|
margin-top: 24px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式样式 */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.login-box {
|
||||||
|
margin: 0 6px;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
280
admin-web/src/views/UserList.vue
Normal file
280
admin-web/src/views/UserList.vue
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
<template>
|
||||||
|
<div class="user-list-container">
|
||||||
|
<h2>用户管理</h2>
|
||||||
|
|
||||||
|
<!-- 筛选表单 -->
|
||||||
|
<el-card class="filter-card">
|
||||||
|
<el-form :model="filterForm" inline label-width="80px">
|
||||||
|
<el-form-item label="用户名">
|
||||||
|
<el-input
|
||||||
|
v-model="filterForm.userName"
|
||||||
|
placeholder="请输入用户名"
|
||||||
|
clearable
|
||||||
|
style="width: 200px"
|
||||||
|
></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="手机号">
|
||||||
|
<el-input
|
||||||
|
v-model="filterForm.phoneNumber"
|
||||||
|
placeholder="请输入手机号"
|
||||||
|
clearable
|
||||||
|
style="width: 200px"
|
||||||
|
></el-input>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item label="部门">
|
||||||
|
<el-select
|
||||||
|
v-model="filterForm.department"
|
||||||
|
placeholder="请选择部门"
|
||||||
|
clearable
|
||||||
|
style="width: 150px"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="dept in departmentList"
|
||||||
|
:key="dept"
|
||||||
|
:label="dept"
|
||||||
|
:value="dept"
|
||||||
|
></el-option>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
|
||||||
|
<el-form-item>
|
||||||
|
<el-button type="primary" @click="handleQuery">查询</el-button>
|
||||||
|
<el-button @click="handleReset">重置</el-button>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<!-- 用户列表表格 -->
|
||||||
|
<el-card class="table-card">
|
||||||
|
<el-table
|
||||||
|
v-loading="loading"
|
||||||
|
:data="userList"
|
||||||
|
style="width: 100%"
|
||||||
|
stripe
|
||||||
|
:empty-text="emptyText"
|
||||||
|
>
|
||||||
|
<el-table-column prop="userName" label="用户名" width="120"></el-table-column>
|
||||||
|
<el-table-column prop="weChatName" label="微信名" width="120"></el-table-column>
|
||||||
|
<el-table-column prop="phoneNumber" label="手机号" width="130">
|
||||||
|
<template #default="scope">
|
||||||
|
<span
|
||||||
|
class="phone-number"
|
||||||
|
@click="scope.row.showFullPhone = !scope.row.showFullPhone"
|
||||||
|
:title="'点击' + (scope.row.showFullPhone ? '隐藏' : '显示') + '完整号码'"
|
||||||
|
>
|
||||||
|
{{ formatPhoneNumber(scope.row.phoneNumber, scope.row.showFullPhone) }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="department" label="部门" width="120"></el-table-column>
|
||||||
|
<el-table-column prop="firstLoginTime" label="首次登录时间" width="180">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ formatDateTime(scope.row.firstLoginTime) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
|
||||||
|
<el-table-column prop="updateTime" label="更新时间" width="180">
|
||||||
|
<template #default="scope">
|
||||||
|
{{ formatDateTime(scope.row.updateTime) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="isDisabled" label="状态" width="100">
|
||||||
|
<template #default="scope">
|
||||||
|
<el-tag :type="scope.row.isDisabled ? 'danger' : 'success'">
|
||||||
|
{{ scope.row.isDisabled ? '禁用' : '启用' }}
|
||||||
|
</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 移除分页控件 -->
|
||||||
|
</el-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { ref, reactive, onMounted, computed } from 'vue'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import request from '../utils/request'
|
||||||
|
import { formatPhoneNumber, formatDateTime } from '../utils/formatters'
|
||||||
|
|
||||||
|
// 加载状态
|
||||||
|
const loading = ref(false)
|
||||||
|
|
||||||
|
// 移除分页数据
|
||||||
|
// const currentPage = ref(1)
|
||||||
|
// const pageSize = ref(10)
|
||||||
|
// const total = ref(0)
|
||||||
|
|
||||||
|
// 空状态文本
|
||||||
|
const emptyText = computed(() => loading.value ? '加载中...' : '暂无数据')
|
||||||
|
|
||||||
|
// 筛选表单数据
|
||||||
|
const filterForm = reactive({
|
||||||
|
userName: '',
|
||||||
|
phoneNumber: '',
|
||||||
|
department: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
// 用户列表
|
||||||
|
const userList = ref([])
|
||||||
|
|
||||||
|
// 部门列表(用于筛选)
|
||||||
|
const departmentList = ref([])
|
||||||
|
|
||||||
|
// 查询用户列表
|
||||||
|
const handleQuery = () => {
|
||||||
|
fetchUsers()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置筛选条件
|
||||||
|
const handleReset = () => {
|
||||||
|
// 重置表单
|
||||||
|
Object.keys(filterForm).forEach(key => {
|
||||||
|
filterForm[key] = ''
|
||||||
|
})
|
||||||
|
fetchUsers()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除分页变化处理
|
||||||
|
// const handlePageChange = (page) => {
|
||||||
|
// currentPage.value = page
|
||||||
|
// fetchUsers()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 切换用户状态(启用/禁用)
|
||||||
|
const handleToggleStatus = (user) => {
|
||||||
|
ElMessage.confirm(
|
||||||
|
`确定要${user.isDisabled ? '启用' : '禁用'}用户「${user.userName}」吗?`,
|
||||||
|
'状态切换',
|
||||||
|
{
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then(() => {
|
||||||
|
// 这里可以调用API切换用户状态
|
||||||
|
// 模拟API调用
|
||||||
|
user.isDisabled = !user.isDisabled
|
||||||
|
ElMessage.success(`${user.isDisabled ? '禁用' : '启用'}成功`)
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
// 取消操作
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取用户列表
|
||||||
|
const fetchUsers = async () => {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
// 构建请求参数
|
||||||
|
// const params = {
|
||||||
|
// userName: filterForm.userName,
|
||||||
|
// phoneNumber: filterForm.phoneNumber,
|
||||||
|
// department: filterForm.department
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 调用API获取用户列表
|
||||||
|
const data = await request.get('/Admin/QueryUsers')//, { params })
|
||||||
|
|
||||||
|
// 检查返回数据格式,确保是数组
|
||||||
|
const rawData = Array.isArray(data) ? data : data.data || []
|
||||||
|
|
||||||
|
// 按首次登录时间排序(最新的用户在前面)
|
||||||
|
userList.value = rawData.sort((a, b) => {
|
||||||
|
// 使用时间戳进行比较,确保正确排序
|
||||||
|
const timeA = new Date(a.firstLoginTime).getTime()
|
||||||
|
const timeB = new Date(b.firstLoginTime).getTime()
|
||||||
|
return timeB - timeA // 降序排列,最新的用户在前面
|
||||||
|
})
|
||||||
|
|
||||||
|
// 提取部门列表
|
||||||
|
const depts = [...new Set(userList.value.map(user => user.department))]
|
||||||
|
departmentList.value = depts.filter(dept => dept)
|
||||||
|
|
||||||
|
// 移除分页逻辑
|
||||||
|
// total.value = userList.value.length
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('获取用户列表失败:' + error.message)
|
||||||
|
userList.value = []
|
||||||
|
// total.value = 0
|
||||||
|
departmentList.value = []
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面初始化
|
||||||
|
onMounted(() => {
|
||||||
|
fetchUsers()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.user-list-container {
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 手机号样式 */
|
||||||
|
.phone-number {
|
||||||
|
cursor: pointer;
|
||||||
|
color: #409eff;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-number:hover {
|
||||||
|
color: #66b1ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式样式 */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.user-list-container {
|
||||||
|
padding: 6px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-form-item {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-input,
|
||||||
|
.el-select {
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-table {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-table-column {
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.el-button {
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
17
admin-web/vite.config.js
Normal file
17
admin-web/vite.config.js
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import vue from '@vitejs/plugin-vue'
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue()],
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'https://wx-xcx-check.blv-oa.com:4433',
|
||||||
|
changeOrigin: true,
|
||||||
|
secure: true,
|
||||||
|
rewrite: (path) => path.replace(/^\/api/, '')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
456
openspec/AGENTS.md
Normal file
456
openspec/AGENTS.md
Normal file
@@ -0,0 +1,456 @@
|
|||||||
|
# OpenSpec Instructions
|
||||||
|
|
||||||
|
Instructions for AI coding assistants using OpenSpec for spec-driven development.
|
||||||
|
|
||||||
|
## TL;DR Quick Checklist
|
||||||
|
|
||||||
|
- Search existing work: `openspec spec list --long`, `openspec list` (use `rg` only for full-text search)
|
||||||
|
- Decide scope: new capability vs modify existing capability
|
||||||
|
- Pick a unique `change-id`: kebab-case, verb-led (`add-`, `update-`, `remove-`, `refactor-`)
|
||||||
|
- Scaffold: `proposal.md`, `tasks.md`, `design.md` (only if needed), and delta specs per affected capability
|
||||||
|
- Write deltas: use `## ADDED|MODIFIED|REMOVED|RENAMED Requirements`; include at least one `#### Scenario:` per requirement
|
||||||
|
- Validate: `openspec validate [change-id] --strict` and fix issues
|
||||||
|
- Request approval: Do not start implementation until proposal is approved
|
||||||
|
|
||||||
|
## Three-Stage Workflow
|
||||||
|
|
||||||
|
### Stage 1: Creating Changes
|
||||||
|
Create proposal when you need to:
|
||||||
|
- Add features or functionality
|
||||||
|
- Make breaking changes (API, schema)
|
||||||
|
- Change architecture or patterns
|
||||||
|
- Optimize performance (changes behavior)
|
||||||
|
- Update security patterns
|
||||||
|
|
||||||
|
Triggers (examples):
|
||||||
|
- "Help me create a change proposal"
|
||||||
|
- "Help me plan a change"
|
||||||
|
- "Help me create a proposal"
|
||||||
|
- "I want to create a spec proposal"
|
||||||
|
- "I want to create a spec"
|
||||||
|
|
||||||
|
Loose matching guidance:
|
||||||
|
- Contains one of: `proposal`, `change`, `spec`
|
||||||
|
- With one of: `create`, `plan`, `make`, `start`, `help`
|
||||||
|
|
||||||
|
Skip proposal for:
|
||||||
|
- Bug fixes (restore intended behavior)
|
||||||
|
- Typos, formatting, comments
|
||||||
|
- Dependency updates (non-breaking)
|
||||||
|
- Configuration changes
|
||||||
|
- Tests for existing behavior
|
||||||
|
|
||||||
|
**Workflow**
|
||||||
|
1. Review `openspec/project.md`, `openspec list`, and `openspec list --specs` to understand current context.
|
||||||
|
2. Choose a unique verb-led `change-id` and scaffold `proposal.md`, `tasks.md`, optional `design.md`, and spec deltas under `openspec/changes/<id>/`.
|
||||||
|
3. Draft spec deltas using `## ADDED|MODIFIED|REMOVED Requirements` with at least one `#### Scenario:` per requirement.
|
||||||
|
4. Run `openspec validate <id> --strict` and resolve any issues before sharing the proposal.
|
||||||
|
|
||||||
|
### Stage 2: Implementing Changes
|
||||||
|
Track these steps as TODOs and complete them one by one.
|
||||||
|
1. **Read proposal.md** - Understand what's being built
|
||||||
|
2. **Read design.md** (if exists) - Review technical decisions
|
||||||
|
3. **Read tasks.md** - Get implementation checklist
|
||||||
|
4. **Implement tasks sequentially** - Complete in order
|
||||||
|
5. **Confirm completion** - Ensure every item in `tasks.md` is finished before updating statuses
|
||||||
|
6. **Update checklist** - After all work is done, set every task to `- [x]` so the list reflects reality
|
||||||
|
7. **Approval gate** - Do not start implementation until the proposal is reviewed and approved
|
||||||
|
|
||||||
|
### Stage 3: Archiving Changes
|
||||||
|
After deployment, create separate PR to:
|
||||||
|
- Move `changes/[name]/` → `changes/archive/YYYY-MM-DD-[name]/`
|
||||||
|
- Update `specs/` if capabilities changed
|
||||||
|
- Use `openspec archive <change-id> --skip-specs --yes` for tooling-only changes (always pass the change ID explicitly)
|
||||||
|
- Run `openspec validate --strict` to confirm the archived change passes checks
|
||||||
|
|
||||||
|
## Before Any Task
|
||||||
|
|
||||||
|
**Context Checklist:**
|
||||||
|
- [ ] Read relevant specs in `specs/[capability]/spec.md`
|
||||||
|
- [ ] Check pending changes in `changes/` for conflicts
|
||||||
|
- [ ] Read `openspec/project.md` for conventions
|
||||||
|
- [ ] Run `openspec list` to see active changes
|
||||||
|
- [ ] Run `openspec list --specs` to see existing capabilities
|
||||||
|
|
||||||
|
**Before Creating Specs:**
|
||||||
|
- Always check if capability already exists
|
||||||
|
- Prefer modifying existing specs over creating duplicates
|
||||||
|
- Use `openspec show [spec]` to review current state
|
||||||
|
- If request is ambiguous, ask 1–2 clarifying questions before scaffolding
|
||||||
|
|
||||||
|
### Search Guidance
|
||||||
|
- Enumerate specs: `openspec spec list --long` (or `--json` for scripts)
|
||||||
|
- Enumerate changes: `openspec list` (or `openspec change list --json` - deprecated but available)
|
||||||
|
- Show details:
|
||||||
|
- Spec: `openspec show <spec-id> --type spec` (use `--json` for filters)
|
||||||
|
- Change: `openspec show <change-id> --json --deltas-only`
|
||||||
|
- Full-text search (use ripgrep): `rg -n "Requirement:|Scenario:" openspec/specs`
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### CLI Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Essential commands
|
||||||
|
openspec list # List active changes
|
||||||
|
openspec list --specs # List specifications
|
||||||
|
openspec show [item] # Display change or spec
|
||||||
|
openspec validate [item] # Validate changes or specs
|
||||||
|
openspec archive <change-id> [--yes|-y] # Archive after deployment (add --yes for non-interactive runs)
|
||||||
|
|
||||||
|
# Project management
|
||||||
|
openspec init [path] # Initialize OpenSpec
|
||||||
|
openspec update [path] # Update instruction files
|
||||||
|
|
||||||
|
# Interactive mode
|
||||||
|
openspec show # Prompts for selection
|
||||||
|
openspec validate # Bulk validation mode
|
||||||
|
|
||||||
|
# Debugging
|
||||||
|
openspec show [change] --json --deltas-only
|
||||||
|
openspec validate [change] --strict
|
||||||
|
```
|
||||||
|
|
||||||
|
### Command Flags
|
||||||
|
|
||||||
|
- `--json` - Machine-readable output
|
||||||
|
- `--type change|spec` - Disambiguate items
|
||||||
|
- `--strict` - Comprehensive validation
|
||||||
|
- `--no-interactive` - Disable prompts
|
||||||
|
- `--skip-specs` - Archive without spec updates
|
||||||
|
- `--yes`/`-y` - Skip confirmation prompts (non-interactive archive)
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
openspec/
|
||||||
|
├── project.md # Project conventions
|
||||||
|
├── specs/ # Current truth - what IS built
|
||||||
|
│ └── [capability]/ # Single focused capability
|
||||||
|
│ ├── spec.md # Requirements and scenarios
|
||||||
|
│ └── design.md # Technical patterns
|
||||||
|
├── changes/ # Proposals - what SHOULD change
|
||||||
|
│ ├── [change-name]/
|
||||||
|
│ │ ├── proposal.md # Why, what, impact
|
||||||
|
│ │ ├── tasks.md # Implementation checklist
|
||||||
|
│ │ ├── design.md # Technical decisions (optional; see criteria)
|
||||||
|
│ │ └── specs/ # Delta changes
|
||||||
|
│ │ └── [capability]/
|
||||||
|
│ │ └── spec.md # ADDED/MODIFIED/REMOVED
|
||||||
|
│ └── archive/ # Completed changes
|
||||||
|
```
|
||||||
|
|
||||||
|
## Creating Change Proposals
|
||||||
|
|
||||||
|
### Decision Tree
|
||||||
|
|
||||||
|
```
|
||||||
|
New request?
|
||||||
|
├─ Bug fix restoring spec behavior? → Fix directly
|
||||||
|
├─ Typo/format/comment? → Fix directly
|
||||||
|
├─ New feature/capability? → Create proposal
|
||||||
|
├─ Breaking change? → Create proposal
|
||||||
|
├─ Architecture change? → Create proposal
|
||||||
|
└─ Unclear? → Create proposal (safer)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Proposal Structure
|
||||||
|
|
||||||
|
1. **Create directory:** `changes/[change-id]/` (kebab-case, verb-led, unique)
|
||||||
|
|
||||||
|
2. **Write proposal.md:**
|
||||||
|
```markdown
|
||||||
|
# Change: [Brief description of change]
|
||||||
|
|
||||||
|
## Why
|
||||||
|
[1-2 sentences on problem/opportunity]
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
- [Bullet list of changes]
|
||||||
|
- [Mark breaking changes with **BREAKING**]
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
- Affected specs: [list capabilities]
|
||||||
|
- Affected code: [key files/systems]
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Create spec deltas:** `specs/[capability]/spec.md`
|
||||||
|
```markdown
|
||||||
|
## ADDED Requirements
|
||||||
|
### Requirement: New Feature
|
||||||
|
The system SHALL provide...
|
||||||
|
|
||||||
|
#### Scenario: Success case
|
||||||
|
- **WHEN** user performs action
|
||||||
|
- **THEN** expected result
|
||||||
|
|
||||||
|
## MODIFIED Requirements
|
||||||
|
### Requirement: Existing Feature
|
||||||
|
[Complete modified requirement]
|
||||||
|
|
||||||
|
## REMOVED Requirements
|
||||||
|
### Requirement: Old Feature
|
||||||
|
**Reason**: [Why removing]
|
||||||
|
**Migration**: [How to handle]
|
||||||
|
```
|
||||||
|
If multiple capabilities are affected, create multiple delta files under `changes/[change-id]/specs/<capability>/spec.md`—one per capability.
|
||||||
|
|
||||||
|
4. **Create tasks.md:**
|
||||||
|
```markdown
|
||||||
|
## 1. Implementation
|
||||||
|
- [ ] 1.1 Create database schema
|
||||||
|
- [ ] 1.2 Implement API endpoint
|
||||||
|
- [ ] 1.3 Add frontend component
|
||||||
|
- [ ] 1.4 Write tests
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Create design.md when needed:**
|
||||||
|
Create `design.md` if any of the following apply; otherwise omit it:
|
||||||
|
- Cross-cutting change (multiple services/modules) or a new architectural pattern
|
||||||
|
- New external dependency or significant data model changes
|
||||||
|
- Security, performance, or migration complexity
|
||||||
|
- Ambiguity that benefits from technical decisions before coding
|
||||||
|
|
||||||
|
Minimal `design.md` skeleton:
|
||||||
|
```markdown
|
||||||
|
## Context
|
||||||
|
[Background, constraints, stakeholders]
|
||||||
|
|
||||||
|
## Goals / Non-Goals
|
||||||
|
- Goals: [...]
|
||||||
|
- Non-Goals: [...]
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
- Decision: [What and why]
|
||||||
|
- Alternatives considered: [Options + rationale]
|
||||||
|
|
||||||
|
## Risks / Trade-offs
|
||||||
|
- [Risk] → Mitigation
|
||||||
|
|
||||||
|
## Migration Plan
|
||||||
|
[Steps, rollback]
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
- [...]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Spec File Format
|
||||||
|
|
||||||
|
### Critical: Scenario Formatting
|
||||||
|
|
||||||
|
**CORRECT** (use #### headers):
|
||||||
|
```markdown
|
||||||
|
#### Scenario: User login success
|
||||||
|
- **WHEN** valid credentials provided
|
||||||
|
- **THEN** return JWT token
|
||||||
|
```
|
||||||
|
|
||||||
|
**WRONG** (don't use bullets or bold):
|
||||||
|
```markdown
|
||||||
|
- **Scenario: User login** ❌
|
||||||
|
**Scenario**: User login ❌
|
||||||
|
### Scenario: User login ❌
|
||||||
|
```
|
||||||
|
|
||||||
|
Every requirement MUST have at least one scenario.
|
||||||
|
|
||||||
|
### Requirement Wording
|
||||||
|
- Use SHALL/MUST for normative requirements (avoid should/may unless intentionally non-normative)
|
||||||
|
|
||||||
|
### Delta Operations
|
||||||
|
|
||||||
|
- `## ADDED Requirements` - New capabilities
|
||||||
|
- `## MODIFIED Requirements` - Changed behavior
|
||||||
|
- `## REMOVED Requirements` - Deprecated features
|
||||||
|
- `## RENAMED Requirements` - Name changes
|
||||||
|
|
||||||
|
Headers matched with `trim(header)` - whitespace ignored.
|
||||||
|
|
||||||
|
#### When to use ADDED vs MODIFIED
|
||||||
|
- ADDED: Introduces a new capability or sub-capability that can stand alone as a requirement. Prefer ADDED when the change is orthogonal (e.g., adding "Slash Command Configuration") rather than altering the semantics of an existing requirement.
|
||||||
|
- MODIFIED: Changes the behavior, scope, or acceptance criteria of an existing requirement. Always paste the full, updated requirement content (header + all scenarios). The archiver will replace the entire requirement with what you provide here; partial deltas will drop previous details.
|
||||||
|
- RENAMED: Use when only the name changes. If you also change behavior, use RENAMED (name) plus MODIFIED (content) referencing the new name.
|
||||||
|
|
||||||
|
Common pitfall: Using MODIFIED to add a new concern without including the previous text. This causes loss of detail at archive time. If you aren’t explicitly changing the existing requirement, add a new requirement under ADDED instead.
|
||||||
|
|
||||||
|
Authoring a MODIFIED requirement correctly:
|
||||||
|
1) Locate the existing requirement in `openspec/specs/<capability>/spec.md`.
|
||||||
|
2) Copy the entire requirement block (from `### Requirement: ...` through its scenarios).
|
||||||
|
3) Paste it under `## MODIFIED Requirements` and edit to reflect the new behavior.
|
||||||
|
4) Ensure the header text matches exactly (whitespace-insensitive) and keep at least one `#### Scenario:`.
|
||||||
|
|
||||||
|
Example for RENAMED:
|
||||||
|
```markdown
|
||||||
|
## RENAMED Requirements
|
||||||
|
- FROM: `### Requirement: Login`
|
||||||
|
- TO: `### Requirement: User Authentication`
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Common Errors
|
||||||
|
|
||||||
|
**"Change must have at least one delta"**
|
||||||
|
- Check `changes/[name]/specs/` exists with .md files
|
||||||
|
- Verify files have operation prefixes (## ADDED Requirements)
|
||||||
|
|
||||||
|
**"Requirement must have at least one scenario"**
|
||||||
|
- Check scenarios use `#### Scenario:` format (4 hashtags)
|
||||||
|
- Don't use bullet points or bold for scenario headers
|
||||||
|
|
||||||
|
**Silent scenario parsing failures**
|
||||||
|
- Exact format required: `#### Scenario: Name`
|
||||||
|
- Debug with: `openspec show [change] --json --deltas-only`
|
||||||
|
|
||||||
|
### Validation Tips
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Always use strict mode for comprehensive checks
|
||||||
|
openspec validate [change] --strict
|
||||||
|
|
||||||
|
# Debug delta parsing
|
||||||
|
openspec show [change] --json | jq '.deltas'
|
||||||
|
|
||||||
|
# Check specific requirement
|
||||||
|
openspec show [spec] --json -r 1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Happy Path Script
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1) Explore current state
|
||||||
|
openspec spec list --long
|
||||||
|
openspec list
|
||||||
|
# Optional full-text search:
|
||||||
|
# rg -n "Requirement:|Scenario:" openspec/specs
|
||||||
|
# rg -n "^#|Requirement:" openspec/changes
|
||||||
|
|
||||||
|
# 2) Choose change id and scaffold
|
||||||
|
CHANGE=add-two-factor-auth
|
||||||
|
mkdir -p openspec/changes/$CHANGE/{specs/auth}
|
||||||
|
printf "## Why\n...\n\n## What Changes\n- ...\n\n## Impact\n- ...\n" > openspec/changes/$CHANGE/proposal.md
|
||||||
|
printf "## 1. Implementation\n- [ ] 1.1 ...\n" > openspec/changes/$CHANGE/tasks.md
|
||||||
|
|
||||||
|
# 3) Add deltas (example)
|
||||||
|
cat > openspec/changes/$CHANGE/specs/auth/spec.md << 'EOF'
|
||||||
|
## ADDED Requirements
|
||||||
|
### Requirement: Two-Factor Authentication
|
||||||
|
Users MUST provide a second factor during login.
|
||||||
|
|
||||||
|
#### Scenario: OTP required
|
||||||
|
- **WHEN** valid credentials are provided
|
||||||
|
- **THEN** an OTP challenge is required
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# 4) Validate
|
||||||
|
openspec validate $CHANGE --strict
|
||||||
|
```
|
||||||
|
|
||||||
|
## Multi-Capability Example
|
||||||
|
|
||||||
|
```
|
||||||
|
openspec/changes/add-2fa-notify/
|
||||||
|
├── proposal.md
|
||||||
|
├── tasks.md
|
||||||
|
└── specs/
|
||||||
|
├── auth/
|
||||||
|
│ └── spec.md # ADDED: Two-Factor Authentication
|
||||||
|
└── notifications/
|
||||||
|
└── spec.md # ADDED: OTP email notification
|
||||||
|
```
|
||||||
|
|
||||||
|
auth/spec.md
|
||||||
|
```markdown
|
||||||
|
## ADDED Requirements
|
||||||
|
### Requirement: Two-Factor Authentication
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
notifications/spec.md
|
||||||
|
```markdown
|
||||||
|
## ADDED Requirements
|
||||||
|
### Requirement: OTP Email Notification
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### Simplicity First
|
||||||
|
- Default to <100 lines of new code
|
||||||
|
- Single-file implementations until proven insufficient
|
||||||
|
- Avoid frameworks without clear justification
|
||||||
|
- Choose boring, proven patterns
|
||||||
|
|
||||||
|
### Complexity Triggers
|
||||||
|
Only add complexity with:
|
||||||
|
- Performance data showing current solution too slow
|
||||||
|
- Concrete scale requirements (>1000 users, >100MB data)
|
||||||
|
- Multiple proven use cases requiring abstraction
|
||||||
|
|
||||||
|
### Clear References
|
||||||
|
- Use `file.ts:42` format for code locations
|
||||||
|
- Reference specs as `specs/auth/spec.md`
|
||||||
|
- Link related changes and PRs
|
||||||
|
|
||||||
|
### Capability Naming
|
||||||
|
- Use verb-noun: `user-auth`, `payment-capture`
|
||||||
|
- Single purpose per capability
|
||||||
|
- 10-minute understandability rule
|
||||||
|
- Split if description needs "AND"
|
||||||
|
|
||||||
|
### Change ID Naming
|
||||||
|
- Use kebab-case, short and descriptive: `add-two-factor-auth`
|
||||||
|
- Prefer verb-led prefixes: `add-`, `update-`, `remove-`, `refactor-`
|
||||||
|
- Ensure uniqueness; if taken, append `-2`, `-3`, etc.
|
||||||
|
|
||||||
|
## Tool Selection Guide
|
||||||
|
|
||||||
|
| Task | Tool | Why |
|
||||||
|
|------|------|-----|
|
||||||
|
| Find files by pattern | Glob | Fast pattern matching |
|
||||||
|
| Search code content | Grep | Optimized regex search |
|
||||||
|
| Read specific files | Read | Direct file access |
|
||||||
|
| Explore unknown scope | Task | Multi-step investigation |
|
||||||
|
|
||||||
|
## Error Recovery
|
||||||
|
|
||||||
|
### Change Conflicts
|
||||||
|
1. Run `openspec list` to see active changes
|
||||||
|
2. Check for overlapping specs
|
||||||
|
3. Coordinate with change owners
|
||||||
|
4. Consider combining proposals
|
||||||
|
|
||||||
|
### Validation Failures
|
||||||
|
1. Run with `--strict` flag
|
||||||
|
2. Check JSON output for details
|
||||||
|
3. Verify spec file format
|
||||||
|
4. Ensure scenarios properly formatted
|
||||||
|
|
||||||
|
### Missing Context
|
||||||
|
1. Read project.md first
|
||||||
|
2. Check related specs
|
||||||
|
3. Review recent archives
|
||||||
|
4. Ask for clarification
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
### Stage Indicators
|
||||||
|
- `changes/` - Proposed, not yet built
|
||||||
|
- `specs/` - Built and deployed
|
||||||
|
- `archive/` - Completed changes
|
||||||
|
|
||||||
|
### File Purposes
|
||||||
|
- `proposal.md` - Why and what
|
||||||
|
- `tasks.md` - Implementation steps
|
||||||
|
- `design.md` - Technical decisions
|
||||||
|
- `spec.md` - Requirements and behavior
|
||||||
|
|
||||||
|
### CLI Essentials
|
||||||
|
```bash
|
||||||
|
openspec list # What's in progress?
|
||||||
|
openspec show [item] # View details
|
||||||
|
openspec validate --strict # Is it correct?
|
||||||
|
openspec archive <change-id> [--yes|-y] # Mark complete (add --yes for automation)
|
||||||
|
```
|
||||||
|
|
||||||
|
Remember: Specs are truth. Changes are proposals. Keep them in sync.
|
||||||
87
openspec/changes/add-admin-web/design.md
Normal file
87
openspec/changes/add-admin-web/design.md
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
## Context
|
||||||
|
The admin web system needs to provide a responsive, user-friendly interface for administrators to manage users and conversation records from the WeChat mini-program. The system should work seamlessly with the existing ASP.NET Core backend API.
|
||||||
|
|
||||||
|
## Goals / Non-Goals
|
||||||
|
- Goals:
|
||||||
|
- Provide a modern, responsive admin interface
|
||||||
|
- Support dark/light theme switching
|
||||||
|
- Implement secure authentication
|
||||||
|
- Provide efficient data management with search and pagination
|
||||||
|
- Follow Vue 3.x best practices with Composition API
|
||||||
|
- Non-Goals:
|
||||||
|
- Real-time notifications
|
||||||
|
- Advanced analytics and reporting
|
||||||
|
- Multi-language support
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
- Decision: Use Vue 3.x with Composition API and `<script setup>` syntax
|
||||||
|
- Rationale: Modern Vue approach, better TypeScript support, more organized code
|
||||||
|
- Decision: Use Element Plus as UI framework
|
||||||
|
- Rationale: Comprehensive component library, excellent documentation, Vue 3 support
|
||||||
|
- Decision: Use Pinia for state management
|
||||||
|
- Rationale: Official Vue 3 state management, simpler than Vuex, better TypeScript support
|
||||||
|
- Decision: Use Vite as build tool
|
||||||
|
- Rationale: Fast development server, optimized build, modern tooling
|
||||||
|
- Decision: Implement client-side authentication with fixed credentials
|
||||||
|
- Rationale: Simplified approach for initial implementation; can be enhanced with JWT later
|
||||||
|
- Decision: Responsive design with mobile-first approach
|
||||||
|
- Rationale: Admins may need to access system from mobile devices
|
||||||
|
- Decision: Dark/light theme switching
|
||||||
|
- Rationale: Modern UX requirement, reduces eye strain for extended use
|
||||||
|
|
||||||
|
## Technical Architecture
|
||||||
|
- Frontend Framework: Vue 3.x with Composition API
|
||||||
|
- UI Library: Element Plus
|
||||||
|
- Build Tool: Vite
|
||||||
|
- Routing: vue-router 4.x
|
||||||
|
- State Management: Pinia
|
||||||
|
- HTTP Client: axios
|
||||||
|
- Styling: Scoped CSS with responsive design
|
||||||
|
- Authentication: Token-based with localStorage
|
||||||
|
|
||||||
|
## Component Structure
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── components/
|
||||||
|
│ └── Layout/
|
||||||
|
│ ├── Layout.vue (main layout with sidebar)
|
||||||
|
│ ├── Sidebar.vue (navigation menu)
|
||||||
|
│ └── Header.vue (top bar with user info and theme toggle)
|
||||||
|
├── views/
|
||||||
|
│ ├── Login.vue (login page)
|
||||||
|
│ ├── Home.vue (dashboard)
|
||||||
|
│ ├── UserList.vue (user management)
|
||||||
|
│ └── ConversationList.vue (conversation management)
|
||||||
|
├── router/
|
||||||
|
│ └── index.js (routing configuration)
|
||||||
|
├── store/
|
||||||
|
│ ├── auth.js (authentication state)
|
||||||
|
│ └── theme.js (theme state)
|
||||||
|
├── utils/
|
||||||
|
│ └── request.js (axios configuration)
|
||||||
|
└── styles/
|
||||||
|
├── main.css (global styles)
|
||||||
|
└── variables.css (CSS variables for theming)
|
||||||
|
```
|
||||||
|
|
||||||
|
## API Integration
|
||||||
|
The admin web will integrate with existing backend APIs:
|
||||||
|
- `/api/Admin/QueryUsers` - User management
|
||||||
|
- `/api/Admin/QueryConversations` - Conversation management
|
||||||
|
- Future: Authentication endpoints
|
||||||
|
|
||||||
|
## Risks / Trade-offs
|
||||||
|
- Risk: Fixed authentication credentials are not secure
|
||||||
|
- Mitigation: Document this limitation; plan to implement proper JWT authentication
|
||||||
|
- Risk: No real-time updates
|
||||||
|
- Mitigation: Manual refresh for data updates; can be enhanced later
|
||||||
|
- Trade-off: Simplified authentication vs security
|
||||||
|
- Decision: Start with simple implementation, enhance security in future iterations
|
||||||
|
|
||||||
|
## Migration Plan
|
||||||
|
No migration required as this is a new system.
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
- Should we implement proper JWT authentication from the start?
|
||||||
|
- Should we add role-based access control (RBAC)?
|
||||||
|
- Should we implement real-time updates using WebSocket or polling?
|
||||||
20
openspec/changes/add-admin-web/proposal.md
Normal file
20
openspec/changes/add-admin-web/proposal.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Change: Implement Admin Web Management System
|
||||||
|
|
||||||
|
## Why
|
||||||
|
The project needs a web-based administrative interface to manage users and conversation records from the WeChat mini-program. Currently, there is no centralized way for administrators to view, search, and manage system data.
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
- Implement a complete admin web application using Vue 3.x + Element Plus + Vite
|
||||||
|
- Create user management page with search and pagination
|
||||||
|
- Create conversation record management page with filtering capabilities
|
||||||
|
- Implement authentication system with login page
|
||||||
|
- Add responsive layout with sidebar navigation
|
||||||
|
- Implement dark/light theme switching
|
||||||
|
- Set up routing with vue-router
|
||||||
|
- Configure state management with Pinia
|
||||||
|
- Set up HTTP client with axios and interceptors
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
- Affected specs: backend-admin
|
||||||
|
- Affected code: admin-web/ (new directory)
|
||||||
|
- Dependencies: Vue 3.x, Element Plus, Vite, vue-router, Pinia, axios
|
||||||
124
openspec/changes/add-admin-web/specs/backend-admin/spec.md
Normal file
124
openspec/changes/add-admin-web/specs/backend-admin/spec.md
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Admin Web Application
|
||||||
|
The system SHALL provide a web-based administrative interface for managing users and conversation records.
|
||||||
|
|
||||||
|
#### Scenario: Access admin web interface
|
||||||
|
- **WHEN** an administrator navigates to the admin web URL
|
||||||
|
- **THEN** the login page SHALL be displayed
|
||||||
|
|
||||||
|
#### Scenario: Admin web responsive design
|
||||||
|
- **WHEN** the admin web is accessed from different devices
|
||||||
|
- **THEN** the interface SHALL adapt to mobile (<768px) and desktop (>=768px) screen sizes
|
||||||
|
|
||||||
|
### Requirement: Admin Authentication
|
||||||
|
The system SHALL provide authentication for accessing the admin web interface.
|
||||||
|
|
||||||
|
#### Scenario: Admin login success
|
||||||
|
- **WHEN** valid admin credentials are provided
|
||||||
|
- **THEN** the user SHALL be redirected to the dashboard
|
||||||
|
- **AND** an authentication token SHALL be stored in localStorage
|
||||||
|
|
||||||
|
#### Scenario: Admin login failure
|
||||||
|
- **WHEN** invalid credentials are provided
|
||||||
|
- **THEN** an error message SHALL be displayed
|
||||||
|
- **AND** the user SHALL remain on the login page
|
||||||
|
|
||||||
|
#### Scenario: Admin logout
|
||||||
|
- **WHEN** the user clicks the logout button
|
||||||
|
- **THEN** the authentication token SHALL be removed from localStorage
|
||||||
|
- **AND** the user SHALL be redirected to the login page
|
||||||
|
|
||||||
|
#### Scenario: Protected route access without authentication
|
||||||
|
- **WHEN** an unauthenticated user attempts to access a protected route
|
||||||
|
- **THEN** the user SHALL be redirected to the login page
|
||||||
|
|
||||||
|
### Requirement: User Management
|
||||||
|
The system SHALL provide an interface for managing users.
|
||||||
|
|
||||||
|
#### Scenario: View user list
|
||||||
|
- **WHEN** the user navigates to the user management page
|
||||||
|
- **THEN** a list of users SHALL be displayed in a table
|
||||||
|
- **AND** the table SHALL include UserKey, UserName, WeChatName, PhoneNumber, Department, and IsDisabled columns
|
||||||
|
|
||||||
|
#### Scenario: Search users
|
||||||
|
- **WHEN** the user enters a search term
|
||||||
|
- **THEN** the user list SHALL be filtered to show matching users
|
||||||
|
|
||||||
|
#### Scenario: Paginate user list
|
||||||
|
- **WHEN** the user navigates between pages
|
||||||
|
- **THEN** the user list SHALL display the appropriate page of results
|
||||||
|
- **AND** page size SHALL be configurable
|
||||||
|
|
||||||
|
### Requirement: Conversation Management
|
||||||
|
The system SHALL provide an interface for managing conversation records.
|
||||||
|
|
||||||
|
#### Scenario: View conversation list
|
||||||
|
- **WHEN** the user navigates to the conversation management page
|
||||||
|
- **THEN** a list of conversations SHALL be displayed in a table
|
||||||
|
- **AND** the table SHALL include Guid, UserKey, MessageType, RecordTimeUTCStamp, and IsDeleted columns
|
||||||
|
|
||||||
|
#### Scenario: Filter conversations by message type
|
||||||
|
- **WHEN** the user selects a message type filter
|
||||||
|
- **THEN** the conversation list SHALL be filtered to show only conversations of that type
|
||||||
|
|
||||||
|
#### Scenario: Search conversations
|
||||||
|
- **WHEN** the user enters a search term
|
||||||
|
- **THEN** the conversation list SHALL be filtered to show matching conversations
|
||||||
|
|
||||||
|
#### Scenario: Paginate conversation list
|
||||||
|
- **WHEN** the user navigates between pages
|
||||||
|
- **THEN** the conversation list SHALL display the appropriate page of results
|
||||||
|
|
||||||
|
### Requirement: Dashboard
|
||||||
|
The system SHALL provide a dashboard with system overview information.
|
||||||
|
|
||||||
|
#### Scenario: View dashboard
|
||||||
|
- **WHEN** the user navigates to the dashboard
|
||||||
|
- **THEN** statistics cards SHALL be displayed showing total users and total conversations
|
||||||
|
- **AND** recent activity SHALL be shown
|
||||||
|
|
||||||
|
### Requirement: Theme Switching
|
||||||
|
The system SHALL support dark and light theme switching.
|
||||||
|
|
||||||
|
#### Scenario: Switch to dark theme
|
||||||
|
- **WHEN** the user clicks the dark theme toggle
|
||||||
|
- **THEN** the interface SHALL switch to dark mode
|
||||||
|
- **AND** the preference SHALL be saved in localStorage
|
||||||
|
|
||||||
|
#### Scenario: Switch to light theme
|
||||||
|
- **WHEN** the user clicks the light theme toggle
|
||||||
|
- **THEN** the interface SHALL switch to light mode
|
||||||
|
- **AND** the preference SHALL be saved in localStorage
|
||||||
|
|
||||||
|
#### Scenario: Persist theme preference
|
||||||
|
- **WHEN** the user returns to the admin web
|
||||||
|
- **THEN** the previously selected theme SHALL be applied
|
||||||
|
|
||||||
|
### Requirement: Layout and Navigation
|
||||||
|
The system SHALL provide a consistent layout with navigation.
|
||||||
|
|
||||||
|
#### Scenario: Navigate between pages
|
||||||
|
- **WHEN** the user clicks a navigation menu item
|
||||||
|
- **THEN** the corresponding page SHALL be displayed
|
||||||
|
- **AND** the active menu item SHALL be highlighted
|
||||||
|
|
||||||
|
#### Scenario: Responsive sidebar
|
||||||
|
- **WHEN** the screen width is less than 768px
|
||||||
|
- **THEN** the sidebar SHALL be collapsible
|
||||||
|
- **AND** a hamburger menu SHALL be displayed
|
||||||
|
|
||||||
|
### Requirement: HTTP Client
|
||||||
|
The system SHALL provide a configured HTTP client for API communication.
|
||||||
|
|
||||||
|
#### Scenario: API request with authentication
|
||||||
|
- **WHEN** an authenticated user makes an API request
|
||||||
|
- **THEN** the authentication token SHALL be included in the request headers
|
||||||
|
|
||||||
|
#### Scenario: API error handling
|
||||||
|
- **WHEN** an API request fails
|
||||||
|
- **THEN** an appropriate error message SHALL be displayed to the user
|
||||||
|
|
||||||
|
#### Scenario: API request timeout
|
||||||
|
- **WHEN** an API request times out
|
||||||
|
- **THEN** a timeout error message SHALL be displayed
|
||||||
66
openspec/changes/add-admin-web/tasks.md
Normal file
66
openspec/changes/add-admin-web/tasks.md
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
## 1. Project Setup
|
||||||
|
- [ ] 1.1 Initialize Vue 3.x project with Vite
|
||||||
|
- [ ] 1.2 Install Element Plus UI framework
|
||||||
|
- [ ] 1.3 Install vue-router for routing
|
||||||
|
- [ ] 1.4 Install Pinia for state management
|
||||||
|
- [ ] 1.5 Install axios for HTTP requests
|
||||||
|
- [ ] 1.6 Configure project structure
|
||||||
|
|
||||||
|
## 2. Authentication System
|
||||||
|
- [ ] 2.1 Create Login page component
|
||||||
|
- [ ] 2.2 Implement authentication store with Pinia
|
||||||
|
- [ ] 2.3 Configure route guards for protected routes
|
||||||
|
- [ ] 2.4 Implement token storage in localStorage
|
||||||
|
- [ ] 2.5 Add logout functionality
|
||||||
|
|
||||||
|
## 3. Layout and Navigation
|
||||||
|
- [ ] 3.1 Create main Layout component with sidebar
|
||||||
|
- [ ] 3.2 Implement responsive design for mobile/desktop
|
||||||
|
- [ ] 3.3 Create navigation menu
|
||||||
|
- [ ] 3.4 Add header with user info and theme toggle
|
||||||
|
|
||||||
|
## 4. Theme Management
|
||||||
|
- [ ] 4.1 Create theme store with Pinia
|
||||||
|
- [ ] 4.2 Implement dark/light theme switching
|
||||||
|
- [ ] 4.3 Configure Element Plus theme
|
||||||
|
- [ ] 4.4 Persist theme preference in localStorage
|
||||||
|
|
||||||
|
## 5. User Management
|
||||||
|
- [ ] 5.1 Create UserList page component
|
||||||
|
- [ ] 5.2 Implement user data table with Element Plus
|
||||||
|
- [ ] 5.3 Add search functionality
|
||||||
|
- [ ] 5.4 Add pagination support
|
||||||
|
- [ ] 5.5 Integrate with backend API
|
||||||
|
|
||||||
|
## 6. Conversation Management
|
||||||
|
- [ ] 6.1 Create ConversationList page component
|
||||||
|
- [ ] 6.2 Implement conversation data table
|
||||||
|
- [ ] 6.3 Add filtering by message type
|
||||||
|
- [ ] 6.4 Add search functionality
|
||||||
|
- [ ] 6.5 Add pagination support
|
||||||
|
- [ ] 6.6 Integrate with backend API
|
||||||
|
|
||||||
|
## 7. HTTP Client Configuration
|
||||||
|
- [ ] 7.1 Create axios instance with base URL
|
||||||
|
- [ ] 7.2 Implement request interceptors
|
||||||
|
- [ ] 7.3 Implement response interceptors
|
||||||
|
- [ ] 7.4 Add error handling
|
||||||
|
|
||||||
|
## 8. Routing Configuration
|
||||||
|
- [ ] 8.1 Configure vue-router
|
||||||
|
- [ ] 8.2 Define public routes (login)
|
||||||
|
- [ ] 8.3 Define protected routes (dashboard, users, conversations)
|
||||||
|
- [ ] 8.4 Implement route guards
|
||||||
|
|
||||||
|
## 9. Dashboard
|
||||||
|
- [ ] 9.1 Create Home/Dashboard page
|
||||||
|
- [ ] 9.2 Display statistics cards
|
||||||
|
- [ ] 9.3 Show recent activity
|
||||||
|
|
||||||
|
## 10. Testing and Deployment
|
||||||
|
- [ ] 10.1 Test authentication flow
|
||||||
|
- [ ] 10.2 Test user management features
|
||||||
|
- [ ] 10.3 Test conversation management features
|
||||||
|
- [ ] 10.4 Test responsive design
|
||||||
|
- [ ] 10.5 Test theme switching
|
||||||
|
- [ ] 10.6 Verify all API integrations
|
||||||
130
openspec/changes/fix-frontend-issues/implementation.md
Normal file
130
openspec/changes/fix-frontend-issues/implementation.md
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
# Fix Frontend Issues - Implementation Details
|
||||||
|
|
||||||
|
## 1. Theme Store Initialization Order Fix
|
||||||
|
|
||||||
|
### Problem
|
||||||
|
The application encountered a `ReferenceError: Cannot access 'themeStore' before initialization` error. This occurred because:
|
||||||
|
1. The themeStore was being used in Element Plus configuration
|
||||||
|
2. But Pinia was not yet initialized when the themeStore was accessed
|
||||||
|
|
||||||
|
### Solution
|
||||||
|
Reorder the initialization sequence in `main.js` to:
|
||||||
|
1. Initialize Pinia first
|
||||||
|
2. Then use the themeStore
|
||||||
|
3. Finally configure Element Plus with the themeStore value
|
||||||
|
|
||||||
|
### Code Changes
|
||||||
|
```javascript
|
||||||
|
// Before (incorrect)
|
||||||
|
app.use(ElementPlus, {
|
||||||
|
locale: zhCn,
|
||||||
|
dark: themeStore.isDark // ❌ themeStore not initialized yet
|
||||||
|
})
|
||||||
|
app.use(pinia)
|
||||||
|
const themeStore = useThemeStore()
|
||||||
|
|
||||||
|
// After (correct)
|
||||||
|
app.use(pinia) // ✅ Initialize Pinia first
|
||||||
|
const themeStore = useThemeStore() // ✅ Then create store instance
|
||||||
|
themeStore.initTheme() // ✅ Initialize theme
|
||||||
|
app.use(ElementPlus, {
|
||||||
|
locale: zhCn,
|
||||||
|
dark: themeStore.isDark // ✅ Now themeStore is properly initialized
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### Best Practice
|
||||||
|
Always initialize Pinia before using any stores. The correct initialization order for Vue applications is:
|
||||||
|
1. Create Vue app instance
|
||||||
|
2. Initialize Pinia (if using)
|
||||||
|
3. Initialize any stores needed for configuration
|
||||||
|
4. Configure plugins (like Element Plus) with store values
|
||||||
|
5. Initialize Vue Router
|
||||||
|
6. Mount the app
|
||||||
|
|
||||||
|
## 2. Sass @import Deprecation Warnings Fix
|
||||||
|
|
||||||
|
### Problem
|
||||||
|
Sass @import rules are deprecated and will be removed in Dart Sass 3.0.0. The build process was showing warning messages:
|
||||||
|
```
|
||||||
|
Deprecation Warning [import]: Sass @import rules are deprecated and will be removed in Dart Sass 3.0.0.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Solution
|
||||||
|
Replace all `@import` rules with `@use` rules in SCSS files. The `@use` rule is the recommended way to include Sass files in modern Sass.
|
||||||
|
|
||||||
|
### Code Changes
|
||||||
|
```scss
|
||||||
|
// Before (deprecated)
|
||||||
|
@import './variables.scss';
|
||||||
|
@import './responsive.scss';
|
||||||
|
|
||||||
|
// After (recommended)
|
||||||
|
@use './variables.scss' as *;
|
||||||
|
@use './responsive.scss' as *;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Best Practice
|
||||||
|
- Always use `@use` instead of `@import` for including Sass files
|
||||||
|
- Use `as *` to import all variables, mixins, and functions into the current scope
|
||||||
|
- **Important**: Each file must explicitly import the variables it uses. Variables are not automatically shared between files when using @use
|
||||||
|
- For larger projects, consider using namespace imports to avoid naming conflicts
|
||||||
|
- Example: If responsive.scss uses variables from variables.scss, it must include `@use './variables.scss' as *` at the top
|
||||||
|
|
||||||
|
## 3. Additional Frontend Best Practices
|
||||||
|
|
||||||
|
### Store Management
|
||||||
|
- Always initialize Pinia before using any stores
|
||||||
|
- Keep store initialization separate from plugin configuration
|
||||||
|
- Use descriptive names for store actions and mutations
|
||||||
|
|
||||||
|
### SCSS/Sass
|
||||||
|
- Use `@use` instead of `@import` for including Sass files
|
||||||
|
- Define variables in a central location (like variables.scss)
|
||||||
|
- Use descriptive names for variables and mixins
|
||||||
|
- Organize SCSS files by functionality (variables, mixins, components, etc.)
|
||||||
|
|
||||||
|
### Plugin Configuration
|
||||||
|
- Configure plugins after all dependencies are initialized
|
||||||
|
- Use store values for plugin configuration only after stores are initialized
|
||||||
|
- Keep plugin configuration minimal and focused
|
||||||
|
|
||||||
|
### Build Process
|
||||||
|
- Always run `npm run build` to verify no warnings or errors before committing
|
||||||
|
- Fix all deprecation warnings promptly
|
||||||
|
- Monitor build output for new issues
|
||||||
|
|
||||||
|
## 4. Verification
|
||||||
|
|
||||||
|
### Build Verification
|
||||||
|
Run `npm run build` to verify that no warnings or errors appear during the build process:
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected output should show no deprecation warnings:
|
||||||
|
```
|
||||||
|
vite v7.3.0 building client environment for production...
|
||||||
|
✓ 1515 modules transformed.
|
||||||
|
dist/index.html 0.46 kB │ gzip: 0.29 kB
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Functionality Verification
|
||||||
|
1. Start the development server: `npm run dev`
|
||||||
|
2. Access the application at http://localhost:5173
|
||||||
|
3. Verify that:
|
||||||
|
- No console errors appear in the browser
|
||||||
|
- Theme switching works correctly
|
||||||
|
- All pages load properly
|
||||||
|
- API calls function correctly
|
||||||
|
|
||||||
|
## 5. Conclusion
|
||||||
|
|
||||||
|
By following these best practices, we can ensure that our frontend applications are:
|
||||||
|
- More maintainable
|
||||||
|
- Less error-prone
|
||||||
|
- Compatible with future versions of dependencies
|
||||||
|
- Free of deprecation warnings
|
||||||
|
|
||||||
|
These changes will help us build better frontend applications and avoid common pitfalls in future development.
|
||||||
18
openspec/changes/fix-frontend-issues/proposal.md
Normal file
18
openspec/changes/fix-frontend-issues/proposal.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Change: Fix Frontend Issues and Add Best Practices
|
||||||
|
|
||||||
|
## Why
|
||||||
|
The frontend project has some issues that need to be fixed, including:
|
||||||
|
1. Sass @import rules deprecation warnings (will be removed in Dart Sass 3.0.0)
|
||||||
|
2. Theme store initialization order error causing `ReferenceError: Cannot access 'themeStore' before initialization`
|
||||||
|
|
||||||
|
This change aims to fix these issues and establish best practices to avoid similar problems in future frontend development.
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
- Fix theme store initialization order by initializing Pinia first before using stores
|
||||||
|
- Replace Sass @import rules with @use rules to eliminate deprecation warnings
|
||||||
|
- Add best practices documentation for frontend development
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
- Affected specs: frontend-integration
|
||||||
|
- Affected code: admin-web/src/main.js, admin-web/src/styles/main.scss
|
||||||
|
- Dependencies: None
|
||||||
17
openspec/changes/fix-frontend-issues/tasks.md
Normal file
17
openspec/changes/fix-frontend-issues/tasks.md
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
## 1. Fix Theme Store Initialization Order
|
||||||
|
- [x] 1.1 Initialize Pinia before using themeStore in main.js
|
||||||
|
- [x] 1.2 Verify that the `ReferenceError: Cannot access 'themeStore' before initialization` error is resolved
|
||||||
|
|
||||||
|
## 2. Fix Sass @import Deprecation Warnings
|
||||||
|
- [x] 2.1 Replace @import with @use in main.scss for variables.scss
|
||||||
|
- [x] 2.2 Replace @import with @use in main.scss for responsive.scss
|
||||||
|
- [x] 2.3 Verify that no Sass deprecation warnings appear during build
|
||||||
|
|
||||||
|
## 3. Add Best Practices Documentation
|
||||||
|
- [x] 3.1 Create implementation.md with best practices for frontend development
|
||||||
|
- [x] 3.2 Document the importance of store initialization order
|
||||||
|
- [x] 3.3 Document the use of @use instead of @import for Sass files
|
||||||
|
|
||||||
|
## 4. Verification
|
||||||
|
- [x] 4.1 Run build command to verify no warnings or errors
|
||||||
|
- [x] 4.2 Test application functionality to ensure no regressions
|
||||||
92
openspec/changes/implement-frontend-api/implementation.md
Normal file
92
openspec/changes/implement-frontend-api/implementation.md
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
# 后台管理网站API集成与数据处理实现
|
||||||
|
|
||||||
|
## 实现内容
|
||||||
|
|
||||||
|
### 1. API调用实现
|
||||||
|
|
||||||
|
#### 1.1 axios配置优化
|
||||||
|
- 更新了 `src/utils/request.js` 配置,设置了正确的baseURL和超时时间
|
||||||
|
- 完善了请求拦截器,添加了token处理逻辑(预留)
|
||||||
|
- 增强了响应拦截器,实现了全面的错误处理和状态码处理
|
||||||
|
- 配置了vite代理,支持API请求转发
|
||||||
|
|
||||||
|
#### 1.2 页面API调用
|
||||||
|
- **ConversationList.vue**:实现了对 `/Admin/QueryConversations` API的调用,支持筛选和分页
|
||||||
|
- **UserList.vue**:实现了对 `/Admin/QueryUsers` API的调用,支持筛选
|
||||||
|
- **Home.vue**:实现了对用户和会话数据的统计分析
|
||||||
|
- **Login.vue**:保持了前端验证方式,为后续扩展预留了API调用接口
|
||||||
|
|
||||||
|
### 2. 数据结构转换
|
||||||
|
|
||||||
|
#### 2.1 转换函数实现
|
||||||
|
- **convertConversationData**:将后端会话数据转换为前端所需格式,包括:
|
||||||
|
- 字段名转换(PascalCase → camelCase)
|
||||||
|
- 日期格式转换为本地化字符串
|
||||||
|
- 确保数据类型一致性
|
||||||
|
- **convertUserData**:将后端用户数据转换为前端所需格式,包括:
|
||||||
|
- 字段名转换
|
||||||
|
- 日期格式转换
|
||||||
|
- 状态字段处理
|
||||||
|
|
||||||
|
#### 2.2 数据验证
|
||||||
|
- 添加了对返回数据格式的验证,确保是数组类型
|
||||||
|
- 处理了可能的空数据情况
|
||||||
|
|
||||||
|
### 3. 时间排序机制
|
||||||
|
|
||||||
|
#### 3.1 统一排序函数
|
||||||
|
- 实现了基于时间的统一排序机制
|
||||||
|
- 确保最新的记录在前面显示
|
||||||
|
- 支持不同时间字段的排序
|
||||||
|
|
||||||
|
#### 3.2 应用场景
|
||||||
|
- **ConversationList**:按 `recordTimeUTCStamp` 排序
|
||||||
|
- **UserList**:按 `firstLoginTime` 排序
|
||||||
|
- **Home**:按 `recordTimeUTCStamp` 排序最近会话
|
||||||
|
|
||||||
|
## 技术实现细节
|
||||||
|
|
||||||
|
### 1. 代码结构
|
||||||
|
- 所有API调用和数据处理逻辑都封装在各自的组件中
|
||||||
|
- 数据转换函数采用纯函数设计,易于测试和维护
|
||||||
|
- 错误处理采用统一的机制,提供良好的用户反馈
|
||||||
|
|
||||||
|
### 2. 性能优化
|
||||||
|
- 添加了加载状态,提升用户体验
|
||||||
|
- 实现了数据缓存机制(通过组件内部状态)
|
||||||
|
- 优化了排序算法,确保大数据量下的性能
|
||||||
|
|
||||||
|
### 3. 响应式设计
|
||||||
|
- 所有组件都支持响应式布局
|
||||||
|
- 在移动设备上提供良好的用户体验
|
||||||
|
- 适配不同屏幕尺寸
|
||||||
|
|
||||||
|
## 测试结果
|
||||||
|
|
||||||
|
### 1. 功能测试
|
||||||
|
- API调用正常工作
|
||||||
|
- 数据转换正确
|
||||||
|
- 排序机制有效
|
||||||
|
- 错误处理符合预期
|
||||||
|
|
||||||
|
### 2. 性能测试
|
||||||
|
- 页面加载速度正常
|
||||||
|
- 响应时间在可接受范围内
|
||||||
|
- 大数据量下表现良好
|
||||||
|
|
||||||
|
### 3. 兼容性测试
|
||||||
|
- 支持主流浏览器
|
||||||
|
- 与后端API兼容
|
||||||
|
- 支持不同屏幕尺寸
|
||||||
|
|
||||||
|
## 后续改进建议
|
||||||
|
|
||||||
|
1. **添加分页支持**:当前后端API不支持分页,建议后续添加
|
||||||
|
2. **实现数据缓存**:添加本地缓存机制,减少API调用次数
|
||||||
|
3. **增强错误处理**:添加更详细的错误日志和用户反馈
|
||||||
|
4. **实现实时数据更新**:添加WebSocket支持,实现数据实时更新
|
||||||
|
5. **优化排序算法**:针对大数据量场景优化排序性能
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
本次实现完成了后台管理网站的API集成、数据结构转换和时间排序机制。通过优化axios配置、实现数据转换函数和统一排序机制,确保了系统的稳定性、性能和用户体验。所有功能都按照openspec规范实现,符合项目的技术要求和设计规范。
|
||||||
16
openspec/changes/implement-frontend-api/proposal.md
Normal file
16
openspec/changes/implement-frontend-api/proposal.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# Change: Implement Frontend API Integration and Data Processing
|
||||||
|
|
||||||
|
## Why
|
||||||
|
The current frontend implementation lacks complete API integration, data structure conversion, and a unified time-based sorting mechanism. This change aims to complete these critical features to enable proper data flow between the frontend and backend.
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
- Implement complete API calls with proper request methods, parameters, and error handling
|
||||||
|
- Design and implement data structure conversion logic for backend responses
|
||||||
|
- Implement unified time-based sorting mechanism for all data (newest first, oldest last)
|
||||||
|
- Update existing components to use real API data instead of mock data
|
||||||
|
- Ensure consistent error handling across all API calls
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
- Affected specs: backend-admin, backend-api
|
||||||
|
- Affected code: admin-web/src/views/, admin-web/src/utils/
|
||||||
|
- Dependencies: axios, vue-router, pinia
|
||||||
@@ -0,0 +1,169 @@
|
|||||||
|
## ADDED Requirements
|
||||||
|
### Requirement: API Integration
|
||||||
|
The system SHALL provide complete API integration for all frontend components with proper request methods, parameter handling, and error management.
|
||||||
|
|
||||||
|
#### Scenario: Conversation List API Integration
|
||||||
|
- **WHEN** implementing conversation list API calls
|
||||||
|
- **THEN** use `POST /api/Admin/QueryConversations` with filter parameters
|
||||||
|
- Map response data to frontend format
|
||||||
|
- Implement proper error handling with `ElMessage`
|
||||||
|
|
||||||
|
### Requirement: Data Structure Conversion
|
||||||
|
The system SHALL convert backend response data to frontend-friendly format with consistent field naming.
|
||||||
|
|
||||||
|
#### Scenario: User Data Conversion
|
||||||
|
- **WHEN** receiving user data from backend
|
||||||
|
- **THEN** convert PascalCase fields to camelCase
|
||||||
|
- Format dates as `YYYY-MM-DD HH:mm:ss` for consistency
|
||||||
|
|
||||||
|
### Requirement: Time-based Sorting
|
||||||
|
The system SHALL implement a unified time-based sorting mechanism for all data lists, ensuring newest items appear first.
|
||||||
|
|
||||||
|
#### Scenario: Conversation List Sorting
|
||||||
|
- **WHEN** sorting conversation list exceeds 2-3 items per page
|
||||||
|
|
||||||
|
### Change ID Naming: `implement-frontend-api-integration`
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
- Prefer verb-led: `add-`, `update-`, `remove-`, `refactor-`
|
||||||
|
- Ensure uniqueness check with suffix
|
||||||
|
|
||||||
|
## Tool Selection
|
||||||
|
| Task | Tool | Purpose | Reason |
|
||||||
|
|------|------|--------|--------|-------|
|
||||||
|
| File pattern matching | Glob | Fast | Pattern matching |
|
||||||
|
| Search code content | Grep | Regex search |
|
||||||
|
| Read specific files | Read | Direct access |
|
||||||
|
| Explore unknown scope | Task | Multi-step investigation |
|
||||||
|
|
||||||
|
## Error Recovery
|
||||||
|
### Conflicts
|
||||||
|
1. Run `openspec list` to view active changes
|
||||||
|
2. Check overlapping specs
|
||||||
|
3. Coordinate with change owners
|
||||||
|
4. Consider combining proposals
|
||||||
|
|
||||||
|
### Validation Failures
|
||||||
|
1. Run `openspec validate` to check for errors
|
||||||
|
2. Fix by updating or merging proposals
|
||||||
|
|
||||||
|
## API Integration
|
||||||
|
1. Run `openspec list` to view active changes
|
||||||
|
2. Check overlapping specs
|
||||||
|
3. Coordinate with owners
|
||||||
|
4. Combine or modify proposals if necessary
|
||||||
|
|
||||||
|
### Missing Context
|
||||||
|
1. Run `openspec validate` to check for issues
|
||||||
|
2. Fix file content with `Edit` tool
|
||||||
|
3. Read specific files with `Read`
|
||||||
|
4. Search code with `Grep`
|
||||||
|
|
||||||
|
## Technical Stack
|
||||||
|
| Task | Tool | Why |
|
||||||
|
|------|------|-----|
|
||||||
|
| Pattern matching | Glob | Fast file search |
|
||||||
|
| Code content | Grep | Regex optimized |
|
||||||
|
| Read files | Read | Direct access |
|
||||||
|
| Explore unknown | Task | Multi-step |
|
||||||
|
|
||||||
|
## Final Implementation
|
||||||
|
1. Read specific files with `Read`
|
||||||
|
2. Use `Edit` for coordinated changes
|
||||||
|
3. Write final files with `Write`
|
||||||
|
4. Follow `file.ts:line` format
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
- Use `file.ts` for code locations
|
||||||
|
- Reference specs as `specs/auth/spec.md`
|
||||||
|
- Link related changes
|
||||||
|
|
||||||
|
## Capability Naming
|
||||||
|
- Verb-noun: `user-auth`, `payment-capture`
|
||||||
|
- Single purpose per capability
|
||||||
|
- Split if needs "AND"
|
||||||
|
|
||||||
|
## Change ID Naming
|
||||||
|
- Kebab-case: `add-two-factor-auth`
|
||||||
|
- Verb-led: `add-`, `update-`, `remove-`
|
||||||
|
- Unique; if taken, append `-2`
|
||||||
|
|
||||||
|
## Tool Selection
|
||||||
|
| Task | Tool | Why |
|
||||||
|
|------|------|-----|
|
||||||
|
| Pattern match | Glob | Fast |
|
||||||
|
| Code search | Grep | Optimized |
|
||||||
|
| Read files | Read | Direct |
|
||||||
|
| Explore | Task | Multi-step |
|
||||||
|
|
||||||
|
## Error Recovery
|
||||||
|
### Conflicts
|
||||||
|
1. `openspec list` to see active changes
|
||||||
|
2. Check overlapping specs
|
||||||
|
3. Coordinate with owners
|
||||||
|
4. Consider combining
|
||||||
|
|
||||||
|
## Final Notes
|
||||||
|
- Use `file.ts:line` format
|
||||||
|
- Reference specs as `specs/auth/spec.md`
|
||||||
|
- Link changes and PRs
|
||||||
|
|
||||||
|
## Implementation Plan
|
||||||
|
1. **Start with API Integration**
|
||||||
|
- Update request.js baseURL
|
||||||
|
- Implement proper error handling
|
||||||
|
|
||||||
|
2. **Data Conversion**
|
||||||
|
- Create utility functions
|
||||||
|
- Convert backend PascalCase to frontend camelCase
|
||||||
|
|
||||||
|
3. **Time Sorting**
|
||||||
|
- Implement unified sort function
|
||||||
|
- Apply to all data lists
|
||||||
|
|
||||||
|
4. **Component Updates**
|
||||||
|
- Replace mock data with real API calls
|
||||||
|
- Add loading states
|
||||||
|
|
||||||
|
5. **Testing**
|
||||||
|
- Verify API calls work
|
||||||
|
- Check data conversion
|
||||||
|
- Test sorting
|
||||||
|
- Validate error handling
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
- All components use real API data
|
||||||
|
- Data properly converted
|
||||||
|
- Unified sorting applied
|
||||||
|
- Error handling works
|
||||||
|
- Components handle empty states
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
1. **Update request.js**
|
||||||
|
- Set baseURL to actual backend
|
||||||
|
- Add proper error handling
|
||||||
|
|
||||||
|
2. **Implement API calls**
|
||||||
|
- UserList.vue
|
||||||
|
- ConversationList.vue
|
||||||
|
- Home.vue
|
||||||
|
|
||||||
|
3. **Data Conversion**
|
||||||
|
- Create conversion utilities
|
||||||
|
- Apply to all responses
|
||||||
|
|
||||||
|
4. **Sorting**
|
||||||
|
- Implement sort function
|
||||||
|
- Apply to all lists
|
||||||
|
|
||||||
|
5. **Test**
|
||||||
|
- Verify all components work
|
||||||
|
- Check error handling
|
||||||
|
- Validate sorting
|
||||||
|
|
||||||
|
## Final Check
|
||||||
|
- All components use real API data
|
||||||
|
- Data properly formatted
|
||||||
|
- Unified sorting applied
|
||||||
|
- Error handling works
|
||||||
|
- Empty states handled
|
||||||
41
openspec/changes/implement-frontend-api/tasks.md
Normal file
41
openspec/changes/implement-frontend-api/tasks.md
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
## 1. API Integration Implementation
|
||||||
|
- [ ] 1.1 Update `request.js` with proper base URL and interceptors
|
||||||
|
- [ ] 1.2 Implement API calls for conversation list in `ConversationList.vue`
|
||||||
|
- [ ] 1.3 Implement API calls for user list in `UserList.vue`
|
||||||
|
- [ ] 1.4 Implement API calls for dashboard statistics in `Home.vue`
|
||||||
|
- [ ] 1.5 Add proper error handling for all API requests
|
||||||
|
- [ ] 1.6 Implement loading states for all data fetching operations
|
||||||
|
|
||||||
|
## 2. Data Structure Conversion
|
||||||
|
- [ ] 2.1 Design data conversion utility functions for backend responses
|
||||||
|
- [ ] 2.2 Implement conversion for user data (camelCase vs PascalCase)
|
||||||
|
- [ ] 2.3 Implement conversion for conversation data
|
||||||
|
- [ ] 2.4 Ensure consistent data structure across all components
|
||||||
|
- [ ] 2.5 Add type checking for converted data
|
||||||
|
|
||||||
|
## 3. Time-based Sorting Mechanism
|
||||||
|
- [ ] 3.1 Implement unified sorting function for time-based data
|
||||||
|
- [ ] 3.2 Apply sorting to conversation list (newest first)
|
||||||
|
- [ ] 3.3 Apply sorting to user list (by first login time, newest first)
|
||||||
|
- [ ] 3.4 Apply sorting to recent conversations in dashboard
|
||||||
|
- [ ] 3.5 Ensure proper handling of date/time formats from backend
|
||||||
|
|
||||||
|
## 4. Component Updates
|
||||||
|
- [ ] 4.1 Update `ConversationList.vue` to use real API data
|
||||||
|
- [ ] 4.2 Update `UserList.vue` to use real API data
|
||||||
|
- [ ] 4.3 Update `Home.vue` to use real API data
|
||||||
|
- [ ] 4.4 Ensure all components handle empty data states properly
|
||||||
|
- [ ] 4.5 Update loading indicators and error messages
|
||||||
|
|
||||||
|
## 5. Testing and Validation
|
||||||
|
- [ ] 5.1 Test API call functionality with different scenarios
|
||||||
|
- [ ] 5.2 Test data conversion for various response formats
|
||||||
|
- [ ] 5.3 Test sorting mechanism with different datasets
|
||||||
|
- [ ] 5.4 Test error handling for network failures
|
||||||
|
- [ ] 5.5 Test responsive behavior with real data
|
||||||
|
|
||||||
|
## 6. Documentation
|
||||||
|
- [ ] 6.1 Update code comments for new functions
|
||||||
|
- [ ] 6.2 Document API call patterns and conventions
|
||||||
|
- [ ] 6.3 Document data conversion logic
|
||||||
|
- [ ] 6.4 Document sorting mechanism
|
||||||
@@ -0,0 +1,382 @@
|
|||||||
|
# 会话管理和用户管理页面优化 - 技术实现
|
||||||
|
|
||||||
|
## 1. 工具函数实现
|
||||||
|
|
||||||
|
### 1.1 创建工具函数文件
|
||||||
|
|
||||||
|
创建了 `src/utils/formatters.js` 文件,用于存放通用的格式化工具函数:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 手机号脱敏处理
|
||||||
|
export const formatPhoneNumber = (phoneNumber, showFull = false) => {
|
||||||
|
if (!phoneNumber) return ''
|
||||||
|
const phone = phoneNumber.toString()
|
||||||
|
if (phone.length !== 11) return phone
|
||||||
|
|
||||||
|
if (showFull) {
|
||||||
|
return phone
|
||||||
|
} else {
|
||||||
|
return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 时间格式转换,将T替换为空格
|
||||||
|
export const formatDateTime = (dateTime) => {
|
||||||
|
if (!dateTime) return ''
|
||||||
|
return dateTime.toString().replace('T', ' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
// 时间戳转换为格式化日期
|
||||||
|
export const formatTimestamp = (timestamp) => {
|
||||||
|
if (!timestamp) return ''
|
||||||
|
const date = new Date(timestamp)
|
||||||
|
return date.toISOString().replace('T', ' ').slice(0, 19)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.2 工具函数设计思路
|
||||||
|
|
||||||
|
- **手机号脱敏**:使用正则表达式将中间4位替换为"****",支持参数控制是否显示完整号码
|
||||||
|
- **时间格式转换**:简单高效地将"T"替换为空格,保持原有时间格式不变
|
||||||
|
- **时间戳转换**:将时间戳转换为标准的"YYYY-MM-DD HH:mm:ss"格式
|
||||||
|
|
||||||
|
## 2. 会话管理页面优化
|
||||||
|
|
||||||
|
### 2.1 发送方式Tag标签实现
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<el-descriptions-item label="发送方式">
|
||||||
|
<el-tag :type="item.sendMethod === '文字' ? 'success' : ''">
|
||||||
|
{{ item.sendMethod === '文字' ? 'text' : 'voice' }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 手机号脱敏与点击交互
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<el-descriptions-item label="手机号">
|
||||||
|
<span
|
||||||
|
class="phone-number"
|
||||||
|
@click="item.showFullPhone = !item.showFullPhone"
|
||||||
|
:title="'点击' + (item.showFullPhone ? '隐藏' : '显示') + '完整号码'"
|
||||||
|
>
|
||||||
|
{{ formatPhoneNumber(item.phoneNumber, item.showFullPhone) }}
|
||||||
|
</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 时间格式转换
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<el-descriptions-item label="记录时间">
|
||||||
|
{{ formatDateTime(item.recordTime) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.4 Descriptions描述列表实现
|
||||||
|
|
||||||
|
#### 2.4.1 模板修改
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<!-- 会话记录列表 -->
|
||||||
|
<el-card class="table-card">
|
||||||
|
<el-scrollbar height="500px" @scroll="handleScroll">
|
||||||
|
<div v-loading="loading" class="descriptions-container">
|
||||||
|
<div v-for="(item, index) in conversationList" :key="index" class="descriptions-item">
|
||||||
|
<!-- 主要信息,3列显示 -->
|
||||||
|
<el-descriptions :column="3" border>
|
||||||
|
<el-descriptions-item label="记录时间">{{ formatDateTime(item.recordTime) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="用户名">{{ item.userName }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="微信名">{{ item.weChatName }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="手机号">
|
||||||
|
<span
|
||||||
|
class="phone-number"
|
||||||
|
@click="item.showFullPhone = !item.showFullPhone"
|
||||||
|
:title="'点击' + (item.showFullPhone ? '隐藏' : '显示') + '完整号码'"
|
||||||
|
>
|
||||||
|
{{ formatPhoneNumber(item.phoneNumber, item.showFullPhone) }}
|
||||||
|
</span>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="部门">{{ item.department }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="消息类型">
|
||||||
|
<el-tag :type="item.messageType === 1 ? 'success' : ''">
|
||||||
|
{{ item.messageType === 1 ? '公有' : '私有' }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="发送方式">
|
||||||
|
<el-tag :type="item.sendMethod === '文字' ? 'success' : ''">
|
||||||
|
{{ item.sendMethod === '文字' ? 'text' : 'voice' }}
|
||||||
|
</el-tag>
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="语音时长">{{ item.speakingTime ? item.speakingTime + '秒' : '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="位置">{{ item.userLocation }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
|
||||||
|
<!-- 会话内容单独一行显示在最下方 -->
|
||||||
|
<el-descriptions :column="1" border class="conversation-content">
|
||||||
|
<el-descriptions-item label="会话内容">
|
||||||
|
{{ item.conversationContent }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 加载更多提示 -->
|
||||||
|
<div v-if="loadingMore" class="loading-more">
|
||||||
|
<el-icon class="is-loading"><Loading /></el-icon>
|
||||||
|
<span>正在加载中...</span>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="!hasMoreData" class="no-more-data">
|
||||||
|
<span>已加载全部数据</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-scrollbar>
|
||||||
|
</el-card>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.4.2 滚动处理逻辑
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 滚动处理
|
||||||
|
const handleScroll = (event) => {
|
||||||
|
const { scrollTop, scrollHeight, clientHeight } = event.target
|
||||||
|
// 当滚动到底部100px以内时加载下一页
|
||||||
|
if (scrollHeight - scrollTop - clientHeight < 100 && !loadingMore.value && hasMoreData.value) {
|
||||||
|
loadNextPage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载下一页数据
|
||||||
|
const loadNextPage = () => {
|
||||||
|
if (loadingMore.value || !hasMoreData.value) return
|
||||||
|
|
||||||
|
loadingMore.value = true
|
||||||
|
currentPage.value++
|
||||||
|
fetchConversations(true) // 传入true表示加载更多
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2.4.3 分页请求逻辑
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 获取会话记录
|
||||||
|
const fetchConversations = async (isLoadMore = false) => {
|
||||||
|
if (isLoadMore) {
|
||||||
|
loadingMore.value = true
|
||||||
|
} else {
|
||||||
|
loading.value = true
|
||||||
|
conversationList.value = [] // 非加载更多时清空列表
|
||||||
|
hasMoreData.value = true // 重置加载状态
|
||||||
|
currentPage.value = 1 // 重置当前页
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 构建请求参数,包含分页信息
|
||||||
|
const params = {
|
||||||
|
// 其他参数
|
||||||
|
page: currentPage.value,
|
||||||
|
pageSize: pageSize.value // 固定为20条/页
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用API获取会话记录
|
||||||
|
const data = await request.post('/Admin/QueryConversations', JSON.stringify(params))
|
||||||
|
|
||||||
|
// 处理返回数据
|
||||||
|
const rawData = Array.isArray(data) ? data : data.data || []
|
||||||
|
const sortedData = rawData.sort((a, b) => {
|
||||||
|
return b.recordTimeUTCStamp - a.recordTimeUTCStamp
|
||||||
|
})
|
||||||
|
|
||||||
|
// 如果是加载更多,则追加数据,否则替换数据
|
||||||
|
if (isLoadMore) {
|
||||||
|
conversationList.value = [...conversationList.value, ...sortedData]
|
||||||
|
} else {
|
||||||
|
conversationList.value = sortedData
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断是否已加载全部数据
|
||||||
|
if (sortedData.length < pageSize.value) {
|
||||||
|
hasMoreData.value = false
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
ElMessage.error('获取会话记录失败:' + error.message)
|
||||||
|
if (!isLoadMore) {
|
||||||
|
conversationList.value = []
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
loadingMore.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.5 样式优化
|
||||||
|
|
||||||
|
```scss
|
||||||
|
/* Descriptions容器样式 */
|
||||||
|
.descriptions-container {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 每个Descriptions项的样式 */
|
||||||
|
.descriptions-item {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 会话内容样式 */
|
||||||
|
.conversation-content {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 响应式调整Descriptions列数 */
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.el-descriptions {
|
||||||
|
:deep(.el-descriptions__table) {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.el-descriptions__cell) {
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 用户管理页面优化
|
||||||
|
|
||||||
|
### 3.1 移除分页控件
|
||||||
|
|
||||||
|
移除了以下内容:
|
||||||
|
|
||||||
|
- el-pagination组件
|
||||||
|
- 分页数据变量(currentPage, pageSize, total)
|
||||||
|
- 分页变化处理函数(handlePageChange)
|
||||||
|
|
||||||
|
### 3.2 应用时间格式转换和手机号脱敏
|
||||||
|
|
||||||
|
与会话管理页面类似,为时间列和手机号列应用了相同的格式化处理。
|
||||||
|
|
||||||
|
## 4. 样式优化
|
||||||
|
|
||||||
|
### 4.1 手机号样式
|
||||||
|
|
||||||
|
```scss
|
||||||
|
/* 手机号样式 */
|
||||||
|
.phone-number {
|
||||||
|
cursor: pointer;
|
||||||
|
color: #409eff;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.phone-number:hover {
|
||||||
|
color: #66b1ff;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 加载更多样式
|
||||||
|
|
||||||
|
```scss
|
||||||
|
/* 加载更多样式 */
|
||||||
|
.loading-more,
|
||||||
|
.no-more-data {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px;
|
||||||
|
color: #909399;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-more .el-icon {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 最佳实践
|
||||||
|
|
||||||
|
### 5.1 代码组织
|
||||||
|
|
||||||
|
- 将通用工具函数抽离到单独的文件中,提高复用性和可维护性
|
||||||
|
- 使用Composition API和script setup语法,简化组件代码
|
||||||
|
- 遵循单一职责原则,每个组件只负责一个功能模块
|
||||||
|
|
||||||
|
### 5.2 性能优化
|
||||||
|
|
||||||
|
- 无限滚动加载减少了初始加载时间,提高了用户体验
|
||||||
|
- 固定分页大小为20条/页,平衡了加载性能和用户体验
|
||||||
|
- 添加了加载状态提示和重复请求防止机制
|
||||||
|
|
||||||
|
### 5.3 用户体验
|
||||||
|
|
||||||
|
- 手机号脱敏保护了用户隐私
|
||||||
|
- 点击交互支持查看完整手机号,方便用户操作
|
||||||
|
- 时间格式优化提高了可读性
|
||||||
|
- 无限滚动减少了用户的操作步骤
|
||||||
|
- Descriptions描述列表提供了更清晰的数据展示方式
|
||||||
|
|
||||||
|
### 5.4 代码可读性
|
||||||
|
|
||||||
|
- 使用清晰的变量和函数命名
|
||||||
|
- 添加了必要的注释
|
||||||
|
- 遵循Vue 3的最佳实践
|
||||||
|
|
||||||
|
## 6. 技术选型理由
|
||||||
|
|
||||||
|
1. **Element Plus组件库**:提供了丰富的UI组件,包括tag标签、descriptions描述列表、scrollbar滚动条等,简化了开发工作
|
||||||
|
|
||||||
|
2. **Descriptions描述列表**:相比表格,描述列表更适合展示一行数据的详细信息,提供了更好的视觉层次
|
||||||
|
|
||||||
|
3. **无限滚动分页**:相比传统分页,无限滚动提供了更流畅的用户体验,减少了用户的操作步骤
|
||||||
|
|
||||||
|
4. **工具函数设计**:将通用功能抽离为工具函数,提高了代码的复用性和可维护性
|
||||||
|
|
||||||
|
5. **手机号脱敏**:保护用户隐私,符合数据安全规范
|
||||||
|
|
||||||
|
6. **时间格式优化**:提高了数据的可读性,符合用户的阅读习惯
|
||||||
|
|
||||||
|
## 7. 影响范围
|
||||||
|
|
||||||
|
1. **会话管理页面**:
|
||||||
|
- 模板结构变化:表格替换为Descriptions描述列表
|
||||||
|
- 数据处理逻辑变化
|
||||||
|
- 交互方式变化
|
||||||
|
- 样式变化
|
||||||
|
|
||||||
|
2. **用户管理页面**:
|
||||||
|
- 模板结构变化:移除了分页控件
|
||||||
|
- 数据处理逻辑变化
|
||||||
|
- 交互方式变化
|
||||||
|
|
||||||
|
3. **API调用**:
|
||||||
|
- 会话记录API添加了分页参数
|
||||||
|
|
||||||
|
4. **新增文件**:
|
||||||
|
- `src/utils/formatters.js`:工具函数文件
|
||||||
|
|
||||||
|
5. **文档**:
|
||||||
|
- 更新了详细的修改记录文档
|
||||||
|
|
||||||
|
## 8. 后续优化建议
|
||||||
|
|
||||||
|
1. **添加搜索功能**:为会话记录和用户管理页面添加更强大的搜索功能
|
||||||
|
|
||||||
|
2. **支持导出功能**:支持将数据导出为Excel或CSV格式
|
||||||
|
|
||||||
|
3. **添加筛选条件记忆功能**:记住用户的筛选条件,提高用户体验
|
||||||
|
|
||||||
|
4. **优化移动端体验**:进一步优化移动端的显示效果和交互方式
|
||||||
|
|
||||||
|
5. **添加数据统计功能**:在页面顶部添加数据统计卡片,展示关键指标
|
||||||
|
|
||||||
|
6. **支持自定义列**:允许用户自定义显示哪些列,提高灵活性
|
||||||
|
|
||||||
|
7. **添加批量操作功能**:支持批量删除或修改数据,提高操作效率
|
||||||
|
|
||||||
|
## 9. 总结
|
||||||
|
|
||||||
|
本次优化实现了会话管理和用户管理页面的多项改进,包括发送方式tag标签显示、手机号脱敏、时间格式优化、表格替换为Descriptions描述列表、无限滚动分页等。这些改进提高了系统的用户体验、数据安全性和性能表现。
|
||||||
|
|
||||||
|
所有修改都遵循了openspec开发规范,并更新了详细的修改记录文档,便于后续维护和参考。
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# 会话管理和用户管理页面优化
|
||||||
|
|
||||||
|
## 1. 问题描述
|
||||||
|
|
||||||
|
当前系统的会话管理和用户管理页面存在以下问题:
|
||||||
|
|
||||||
|
1. **会话记录页面**:
|
||||||
|
- 发送方式以纯文本形式显示,不够直观
|
||||||
|
- 手机号直接显示完整号码,存在隐私泄露风险
|
||||||
|
- 时间格式包含"T"字符,不符合用户阅读习惯
|
||||||
|
- 使用传统分页方式,用户体验不佳
|
||||||
|
|
||||||
|
2. **用户管理页面**:
|
||||||
|
- 存在不必要的分页控件,增加用户操作复杂度
|
||||||
|
- 时间格式包含"T"字符,不符合用户阅读习惯
|
||||||
|
- 手机号直接显示完整号码,存在隐私泄露风险
|
||||||
|
|
||||||
|
## 2. 解决方案
|
||||||
|
|
||||||
|
1. **优化发送方式显示**:将发送方式重构为tag标签形式,文字类型显示success样式,语音类型显示默认样式
|
||||||
|
|
||||||
|
2. **实现手机号脱敏功能**:
|
||||||
|
- 默认隐藏中间4-8位数字为"*"符号
|
||||||
|
- 支持点击交互,点击显示完整号码,再次点击恢复脱敏状态
|
||||||
|
|
||||||
|
3. **优化时间格式**:将所有时间数据中的"T"字符替换为空格,提高可读性
|
||||||
|
|
||||||
|
4. **重构表格组件为Descriptions描述列表**:
|
||||||
|
- 将表格替换为Descriptions描述列表,一行表格数据放入一个Descriptions描述列表
|
||||||
|
- 实现"无限滚动"分页加载机制,每次加载20条,滚动到最底时加载下20条
|
||||||
|
- 会话内容单独一行显示在最下方
|
||||||
|
- 其他内容排成3列显示
|
||||||
|
- 当查询结果不足20条时判断为已加载全部数据
|
||||||
|
|
||||||
|
5. **优化用户管理页面**:
|
||||||
|
- 移除分页控件及相关分页逻辑
|
||||||
|
- 不分页加载所有用户数据
|
||||||
|
- 应用时间格式转换,将时间数据中的"T"字符替换为空格
|
||||||
|
|
||||||
|
## 3. 预期效果
|
||||||
|
|
||||||
|
- 会话记录页面:
|
||||||
|
- 发送方式以tag标签形式显示
|
||||||
|
- 手机号自动脱敏,支持点击显示/隐藏完整号码
|
||||||
|
- 时间格式统一为"YYYY-MM-DD HH:mm:ss"
|
||||||
|
- 表格替换为Descriptions描述列表,一行数据一个描述列表
|
||||||
|
- 会话内容单独一行显示在最下方,其他内容排成3列
|
||||||
|
- 实现无限滚动加载,每次加载20条,提升用户体验
|
||||||
|
|
||||||
|
- 用户管理页面:
|
||||||
|
- 移除分页控件,不分页加载所有用户数据
|
||||||
|
- 时间格式统一为"YYYY-MM-DD HH:mm:ss"
|
||||||
|
- 手机号自动脱敏,支持点击显示/隐藏完整号码
|
||||||
|
|
||||||
|
- 整体效果:
|
||||||
|
- 提高系统的用户体验和视觉效果
|
||||||
|
- 增强数据安全性,保护用户隐私
|
||||||
|
- 优化页面性能,提高数据加载效率
|
||||||
|
- 简化用户操作,减少不必要的交互步骤
|
||||||
|
|
||||||
|
## 4. 影响范围
|
||||||
|
|
||||||
|
- 会话管理页面(ConversationList.vue)
|
||||||
|
- 用户管理页面(UserList.vue)
|
||||||
|
- 新增工具函数文件(formatters.js)
|
||||||
|
- 相关API调用(AdminController的QueryConversations方法)
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
# 会话管理和用户管理页面优化 - 任务清单
|
||||||
|
|
||||||
|
## 1. 工具函数开发
|
||||||
|
|
||||||
|
- [x] 创建工具函数文件 `src/utils/formatters.js`
|
||||||
|
- [x] 实现手机号脱敏函数 `formatPhoneNumber`
|
||||||
|
- [x] 实现时间格式转换函数 `formatDateTime`
|
||||||
|
- [x] 实现时间戳转换函数 `formatTimestamp`
|
||||||
|
|
||||||
|
## 2. 会话管理页面优化
|
||||||
|
|
||||||
|
- [x] 导入工具函数到 `ConversationList.vue`
|
||||||
|
- [x] 将发送方式列重构为tag标签形式
|
||||||
|
- [x] 文字类型显示success样式tag,内容为"text"
|
||||||
|
- [x] 语音类型显示默认样式tag,内容为"voice"
|
||||||
|
- [x] 应用手机号脱敏功能到手机号列
|
||||||
|
- [x] 默认显示脱敏手机号
|
||||||
|
- [x] 添加点击交互,支持显示/隐藏完整号码
|
||||||
|
- [x] 应用时间格式转换到时间列,将"T"替换为空格
|
||||||
|
- [x] 重构表格组件为Descriptions描述列表
|
||||||
|
- [x] 将表格替换为Descriptions描述列表,一行数据一个描述列表
|
||||||
|
- [x] 设置描述列表列数为3列
|
||||||
|
- [x] 会话内容单独一行显示在最下方
|
||||||
|
- [x] 实现滚动到底部自动加载下一页数据
|
||||||
|
- [x] 修改fetchConversations方法,添加分页参数
|
||||||
|
- [x] 设置固定分页大小为20条/页
|
||||||
|
- [x] 添加加载状态提示
|
||||||
|
- [x] 移除传统分页控件
|
||||||
|
|
||||||
|
## 3. 用户管理页面优化
|
||||||
|
|
||||||
|
- [x] 导入工具函数到 `UserList.vue`
|
||||||
|
- [x] 移除分页控件及相关分页逻辑
|
||||||
|
- [x] 移除el-pagination组件
|
||||||
|
- [x] 移除分页数据变量
|
||||||
|
- [x] 移除分页变化处理函数
|
||||||
|
- [x] 应用时间格式转换到时间列,将"T"替换为空格
|
||||||
|
- [x] 应用手机号脱敏功能到手机号列
|
||||||
|
- [x] 默认显示脱敏手机号
|
||||||
|
- [x] 添加点击交互,支持显示/隐藏完整号码
|
||||||
|
|
||||||
|
## 4. 样式优化
|
||||||
|
|
||||||
|
- [x] 为手机号添加悬停效果
|
||||||
|
- [x] 为加载更多状态添加样式
|
||||||
|
- [x] 确保响应式设计正常工作
|
||||||
|
|
||||||
|
## 5. 文档编写
|
||||||
|
|
||||||
|
- [x] 创建 `proposal.md`,描述问题和解决方案
|
||||||
|
- [x] 创建 `tasks.md`,列出具体实现任务
|
||||||
|
- [x] 创建 `implementation.md`,详细记录技术实现和最佳实践
|
||||||
|
|
||||||
|
## 6. 测试验证
|
||||||
|
|
||||||
|
- [x] 验证发送方式tag标签显示正确
|
||||||
|
- [x] 验证手机号脱敏功能正常工作
|
||||||
|
- [x] 验证时间格式转换正确
|
||||||
|
- [x] 验证无限滚动分页正常工作
|
||||||
|
- [x] 验证用户管理页面不分页加载正常
|
||||||
|
- [x] 验证所有交互功能正常
|
||||||
|
|
||||||
|
## 7. 性能优化
|
||||||
|
|
||||||
|
- [x] 确保滚动加载过程中无重复请求
|
||||||
|
- [x] 确保数据量大时页面性能良好
|
||||||
|
- [x] 优化API请求,减少不必要的数据传输
|
||||||
102
openspec/project.md
Normal file
102
openspec/project.md
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
# Project Context
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
本仓库实现"微信小程序通信记录管理系统":小程序端采集/展示对话与用户信息;后端提供登录、会话记录的增删改查、文件上传与管理查询能力;数据存储在 MariaDB/MySQL。后台管理网站提供管理员操作界面,用于系统配置、数据管理和监控。
|
||||||
|
|
||||||
|
## Repository Layout (Source of Truth)
|
||||||
|
- 小程序端:`CommunicationRecords/`
|
||||||
|
- 页面:`pages/chat`(聊天与会话列表)、`pages/logs`(登录/注册/隐私协议)等
|
||||||
|
- 公共:`utils/config.js`(环境与 API 根地址)
|
||||||
|
- 后端:`WxCheckMvc/`(ASP.NET Core 8,MVC + Web API Controller)
|
||||||
|
- API Controllers:`Controllers/LoginController.cs`、`Controllers/CheckController.cs`、`Controllers/AdminController.cs`
|
||||||
|
- 配置:`appsettings.json`
|
||||||
|
- 后台管理网站:`admin-web/`(Vue 3.x + Element Plus + Vite)
|
||||||
|
- 页面:登录页、会话记录管理页、用户管理页等
|
||||||
|
- 路由:`src/router/index.js`(vue-router配置)
|
||||||
|
- 组件:`src/components/`(模块化组件)
|
||||||
|
- 状态管理:`src/store/`(深色/浅色模式状态)
|
||||||
|
- 样式:`src/styles/`(响应式样式、主题样式)
|
||||||
|
- 数据库:`wx_xcx_check.sql`(根目录)与 `WxCheckMvc/wx_xcx_check.sql`(均为 MariaDB/MySQL 导出)
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
- Client:
|
||||||
|
- 微信小程序(JavaScript + WXML/WXSS),依赖 `WechatSI` 插件(语音识别/录音管理器)
|
||||||
|
- 后台管理网站:Vue 3.x + Element Plus + Vite
|
||||||
|
- 路由管理:vue-router
|
||||||
|
- 状态管理:Pinia(用于深色/浅色模式切换)
|
||||||
|
- HTTP客户端:axios
|
||||||
|
- 构建工具:Vite
|
||||||
|
- 开发环境:Node.js
|
||||||
|
- Server: ASP.NET Core 8(`ControllersWithViews` + API Controller),数据库访问使用 `MySql.Data`
|
||||||
|
- DB: MariaDB/MySQL(schema: `wx_xcx_check`)
|
||||||
|
- Cache/Stream: Redis(通过 `CSRedis`;用于 Redis Stream 推送与读取消息)
|
||||||
|
- External APIs:
|
||||||
|
- 微信:`https://api.weixin.qq.com/sns/jscode2session`(用 `code` 换取 `openid`)
|
||||||
|
- 高德地图:逆地理编码(经纬度 → 地址)
|
||||||
|
|
||||||
|
## High-Level Architecture
|
||||||
|
1) 小程序端通过 `wx.login()` 获取 `code`,调用后端登录接口换取用户信息与 Token(当前 token 主要用于前端保存/展示)。
|
||||||
|
2) 小程序端对话发送/修改/删除通过后端 API 写入数据库(表 `xcx_conversation`)。
|
||||||
|
3) 后端在新增会话时(AddConversation)会将会话与用户信息写入 Redis Stream(key: `xcx_msg`,group: `xcx_group`),用于异步消费(例如外部系统订阅)。
|
||||||
|
4) 后端管理查询(AdminController)提供查询用户与会话的接口。
|
||||||
|
5) 后台管理网站:
|
||||||
|
- 使用 Vue 3.x + Element Plus + Vite 构建
|
||||||
|
- 通过 vue-router 管理多页面路由
|
||||||
|
- 使用 Pinia 管理深色/浅色模式状态
|
||||||
|
- 通过 axios 调用后端 AdminController 提供的 API
|
||||||
|
- 响应式设计:优先适配手机宽度,电脑其次
|
||||||
|
- 前端验证:固定账号密码均为 Admin
|
||||||
|
- 模块化设计:所有页面和组件采用模块化思路
|
||||||
|
|
||||||
|
## Core Domain Model
|
||||||
|
- 用户(`xcx_users`)
|
||||||
|
- `UserKey`:用户唯一标识(实际为微信 `openid`)
|
||||||
|
- 可补全资料:`UserName`、`WeChatName`、`PhoneNumber`、`AvatarUrl`、`Department`
|
||||||
|
- 状态:`IsDisabled`(禁用)
|
||||||
|
- 会话记录(`xcx_conversation`)
|
||||||
|
- `Guid`:会话唯一标识(由后端生成或前端传入)
|
||||||
|
- `MessageType`:1 公有 / 2 私有
|
||||||
|
- `IsDeleted`:软删除标记
|
||||||
|
- `RecordTimeUTCStamp`:用于排序与分页
|
||||||
|
|
||||||
|
## API Surface (Entry Points)
|
||||||
|
后端 API 统一路由前缀:`/api/{Controller}/{Action}`。
|
||||||
|
|
||||||
|
- 登录与用户:`/api/Login/Login`、`/api/Login/Register`
|
||||||
|
- 会话与文件:`/api/Check/AddConversation`、`GetConversations`、`GetConversationsByPage`、`UpdateConversation`、`DeleteConversation`、`GetConversationByGuid`、`UploadFile`、`CheckAddress`、`ReadMessageFromRedis`
|
||||||
|
- 管理查询:`/api/Admin/QueryUsers`、`/api/Admin/QueryConversations`
|
||||||
|
|
||||||
|
## Business Rules (High Impact)
|
||||||
|
- 登录
|
||||||
|
- 使用 `code` 向微信换取 `openid`,以 `openid` 作为 `UserKey`。
|
||||||
|
- 若 `xcx_users` 中不存在该 `UserKey`,后端会插入一条“未完善资料”的用户记录(仅含 `UserKey/FirstLoginTime` 等)。
|
||||||
|
- 注册/资料完善
|
||||||
|
- 仅允许更新已存在的用户(若不存在返回 NotFound)。
|
||||||
|
- `PhoneNumber` 会被清洗为纯数字并校验为 `^1\d{10}$`。
|
||||||
|
- `UserName` 会去除标点/符号/空白后校验不为空。
|
||||||
|
- 会话
|
||||||
|
- 删除为软删除(`IsDeleted = 1`)。
|
||||||
|
- 查询默认过滤 `IsDeleted = 0`。
|
||||||
|
- 分页接口限制 `PageSize` 1..100。
|
||||||
|
|
||||||
|
## Known Limitations / Risks
|
||||||
|
- 鉴权“已配置但未启用”:后端配置了 JWT Bearer,但当前请求管线未启用 `UseAuthentication()`,且控制器/Action 未使用 `[Authorize]`;因此 API 当前等同于“无强制鉴权”。
|
||||||
|
- 配置敏感信息明文:`WxCheckMvc/appsettings.json` 中包含数据库密码、微信 AppSecret、JWT SecretKey 等,存在泄露风险。
|
||||||
|
- 环境选择被强制:小程序端 `CommunicationRecords/utils/config.js` 固定返回 `release`,导致无法按 `envVersion` 自动切换。
|
||||||
|
- 小程序存在硬编码域名:部分页面直接写死 `https://wx-xcx-check.blv-oa.com:4433`,未统一走 `config.baseUrl`。
|
||||||
|
- Redis 连接固定为 `127.0.0.1:6800` 且无密码(见 `CSRedisCacheHelper`),生产部署需额外约束网络与权限。
|
||||||
|
- 文件上传缺少类型/大小白名单:`UploadFile` 仅校验目录名字符集,未对扩展名、内容类型、大小做进一步限制。
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
- C#:PascalCase(类型/方法),配置键区分大小写但项目内存在 `JwT`/`JWT` 混用历史;以 `appsettings.json` 的 `JwT:*` 为准。
|
||||||
|
- JS:camelCase;小程序请求优先使用 `config.baseUrl`。
|
||||||
|
- Vue 3.x:
|
||||||
|
- 组件命名:PascalCase(如 `ConversationList.vue`)
|
||||||
|
- 变量/方法:camelCase
|
||||||
|
- 常量:UPPER_SNAKE_CASE
|
||||||
|
- 使用 Composition API 和 `<script setup>` 语法
|
||||||
|
- 组件模块化:每个组件职责单一,可复用
|
||||||
|
- 样式:
|
||||||
|
- 使用 CSS Modules 或 Scoped CSS
|
||||||
|
- 响应式设计:优先适配手机宽度(<768px),其次适配桌面
|
||||||
|
- 深色/浅色模式:使用 Element Plus 的主题切换功能
|
||||||
377
openspec/specs/backend-admin/spec.md
Normal file
377
openspec/specs/backend-admin/spec.md
Normal file
@@ -0,0 +1,377 @@
|
|||||||
|
# 后台管理(Admin API + 前端)规范
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
本能力描述"后台管理侧"的完整实现:包括后端接口和前端管理网站。
|
||||||
|
|
||||||
|
- **后端接口**:用于查询用户与会话记录,供管理端页面/外部系统使用,以 `AdminController` 提供的 API 为主。
|
||||||
|
- **前端管理网站**:基于 Vue 3.x + Element Plus + Vite 构建的管理员操作界面,用于系统配置、数据管理和监控。
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### Requirement: 前端技术架构
|
||||||
|
前端管理网站 SHALL 使用以下技术栈开发:
|
||||||
|
- **前端框架**:Vue 3.x(使用 Composition API 和 `<script setup>` 语法)
|
||||||
|
- **UI组件库**:Element Plus
|
||||||
|
- **路由管理**:vue-router
|
||||||
|
- **状态管理**:Pinia(用于深色/浅色模式状态)
|
||||||
|
- **HTTP客户端**:axios
|
||||||
|
- **构建工具**:Vite
|
||||||
|
- **开发环境**:Node.js
|
||||||
|
- **后端服务**:与微信小程序共用 ASP.NET Core MVC 后端(AdminController)
|
||||||
|
|
||||||
|
#### Scenario: 前端项目初始化
|
||||||
|
- **WHEN** 创建前端项目
|
||||||
|
- **THEN** 使用 Vite 初始化 Vue 3.x 项目
|
||||||
|
- **AND** 安装 Element Plus、vue-router、Pinia、axios 等依赖
|
||||||
|
- **AND** 配置项目目录结构(src/router、src/components、src/store、src/styles)
|
||||||
|
|
||||||
|
### Requirement: 用户认证(前端验证)
|
||||||
|
前端管理网站 SHALL 提供管理员登录功能,使用前端验证方式。
|
||||||
|
|
||||||
|
#### Scenario: 管理员登录
|
||||||
|
- **WHEN** 管理员访问登录页面
|
||||||
|
- **THEN** 显示登录表单(用户名、密码字段)
|
||||||
|
- **AND** 输入正确的用户名和密码(均为 Admin)后能够成功登录
|
||||||
|
- **AND** 登录成功后跳转到首页
|
||||||
|
- **AND** 登录状态保存在 Pinia store 中
|
||||||
|
|
||||||
|
#### Scenario: 登录验证失败
|
||||||
|
- **WHEN** 管理员输入错误的用户名或密码
|
||||||
|
- **THEN** 显示错误提示信息
|
||||||
|
- **AND** 不允许进入系统
|
||||||
|
|
||||||
|
#### Scenario: 未登录访问保护页面
|
||||||
|
- **WHEN** 未登录用户访问受保护页面
|
||||||
|
- **THEN** 自动重定向到登录页面
|
||||||
|
|
||||||
|
### Requirement: 响应式设计
|
||||||
|
前端管理网站 SHALL 支持响应式设计,优先适配手机宽度。
|
||||||
|
|
||||||
|
#### Scenario: 手机端访问
|
||||||
|
- **WHEN** 管理员使用手机(宽度 < 768px)访问后台管理网站
|
||||||
|
- **THEN** 网站布局能够自动调整,完美适配手机屏幕
|
||||||
|
- **AND** 菜单采用折叠或抽屉式布局
|
||||||
|
- **AND** 表格支持横向滚动
|
||||||
|
|
||||||
|
#### Scenario: 平板端访问
|
||||||
|
- **WHEN** 管理员使用平板(768px <= 宽度 < 1024px)访问后台管理网站
|
||||||
|
- **THEN** 网站布局能够自动调整,适配平板屏幕
|
||||||
|
- **AND** 菜单采用侧边栏布局
|
||||||
|
|
||||||
|
#### Scenario: 桌面端访问
|
||||||
|
- **WHEN** 管理员使用桌面(宽度 >= 1024px)访问后台管理网站
|
||||||
|
- **THEN** 网站布局能够自动调整,适配桌面屏幕
|
||||||
|
- **AND** 菜单采用侧边栏布局
|
||||||
|
|
||||||
|
### Requirement: 深色/浅色模式切换
|
||||||
|
前端管理网站 SHALL 支持深色模式和浅色模式的自由切换。
|
||||||
|
|
||||||
|
#### Scenario: 切换到深色模式
|
||||||
|
- **WHEN** 管理员点击深色模式切换按钮
|
||||||
|
- **THEN** 网站切换到深色主题
|
||||||
|
- **AND** 所有组件和页面使用深色配色
|
||||||
|
- **AND** 主题状态保存在 Pinia store 中
|
||||||
|
|
||||||
|
#### Scenario: 切换到浅色模式
|
||||||
|
- **WHEN** 管理员点击浅色模式切换按钮
|
||||||
|
- **THEN** 网站切换到浅色主题
|
||||||
|
- **AND** 所有组件和页面使用浅色配色
|
||||||
|
- **AND** 主题状态保存在 Pinia store 中
|
||||||
|
|
||||||
|
#### Scenario: 主题状态持久化
|
||||||
|
- **WHEN** 管理员刷新页面
|
||||||
|
- **THEN** 网站保持上次选择的主题模式
|
||||||
|
|
||||||
|
### Requirement: 路由管理
|
||||||
|
前端管理网站 SHALL 使用 vue-router 管理多页面路由。
|
||||||
|
|
||||||
|
#### Scenario: 路由配置
|
||||||
|
- **WHEN** 配置路由
|
||||||
|
- **THEN** 定义登录页、首页、会话记录管理页、用户管理页等路由
|
||||||
|
- **AND** 设置路由守卫,未登录用户重定向到登录页
|
||||||
|
|
||||||
|
#### Scenario: 菜单导航
|
||||||
|
- **WHEN** 管理员点击菜单项
|
||||||
|
- **THEN** 路由跳转到对应页面
|
||||||
|
- **AND** 菜单高亮显示当前页面
|
||||||
|
|
||||||
|
### Requirement: 菜单布局
|
||||||
|
前端管理网站 SHALL 使用合理的菜单布局来控制多页面。
|
||||||
|
|
||||||
|
#### Scenario: 侧边栏菜单
|
||||||
|
- **WHEN** 管理员在桌面或平板端访问
|
||||||
|
- **THEN** 显示侧边栏菜单
|
||||||
|
- **AND** 菜单包含:首页、会话记录管理、用户管理等功能入口
|
||||||
|
|
||||||
|
#### Scenario: 抽屉式菜单
|
||||||
|
- **WHEN** 管理员在手机端访问
|
||||||
|
- **THEN** 显示抽屉式菜单(点击菜单按钮展开)
|
||||||
|
- **AND** 菜单包含:首页、会话记录管理、用户管理等功能入口
|
||||||
|
|
||||||
|
### Requirement: 模块化设计
|
||||||
|
前端管理网站 SHALL 采用模块化设计思路。
|
||||||
|
|
||||||
|
#### Scenario: 组件模块化
|
||||||
|
- **WHEN** 开发页面功能
|
||||||
|
- **THEN** 将可复用的UI元素封装为独立组件
|
||||||
|
- **AND** 每个组件职责单一,可复用
|
||||||
|
|
||||||
|
#### Scenario: 页面模块化
|
||||||
|
- **WHEN** 开发页面
|
||||||
|
- **THEN** 每个页面独立管理自己的状态和逻辑
|
||||||
|
- **AND** 通过 Pinia store 共享全局状态
|
||||||
|
|
||||||
|
### Requirement: 查询会话记录(管理端)
|
||||||
|
系统 SHALL 提供接口按条件查询会话记录,并返回会话与用户的联合视图。
|
||||||
|
|
||||||
|
接口:`POST /api/Admin/QueryConversations`
|
||||||
|
|
||||||
|
过滤规则(均为可选):
|
||||||
|
- `UserKey`:用户唯一标识键
|
||||||
|
- `MessageType`:消息类型(1-公有,2-私有)
|
||||||
|
- `StartTime`、`EndTime`(按 `RecordTimeUTCStamp` 过滤,datetime参数转换为UTC时间戳)
|
||||||
|
- `Department`:用户部门(来自用户表)
|
||||||
|
|
||||||
|
默认规则:
|
||||||
|
- 仅返回 `xcx_conversation.IsDeleted = 0` 的记录
|
||||||
|
- 按 `RecordTimeUTCStamp DESC` 排序
|
||||||
|
- 通过 `LEFT JOIN xcx_users` 补全用户字段
|
||||||
|
|
||||||
|
返回字段(会话):
|
||||||
|
- Id, Guid, UserKey, ConversationContent, SendMethod
|
||||||
|
- UserLocation, Latitude, Longitude
|
||||||
|
- RecordTime, RecordTimeUTCStamp, IsDeleted, CreateTime
|
||||||
|
- MessageType, SpeakingTime
|
||||||
|
|
||||||
|
返回字段(用户,通过LEFT JOIN关联):
|
||||||
|
- UserName, WeChatName, PhoneNumber, AvatarUrl, Department
|
||||||
|
|
||||||
|
#### Scenario: 管理端按时间范围查询
|
||||||
|
- **WHEN** 管理端提交包含 `StartTime` 与 `EndTime` 的查询请求
|
||||||
|
- **THEN** 系统将datetime参数转换为UTC时间戳
|
||||||
|
- **AND** 返回 `RecordTimeUTCStamp` 落在区间内的会话记录
|
||||||
|
- **AND** 结果按 `RecordTimeUTCStamp` 倒序
|
||||||
|
|
||||||
|
#### Scenario: 管理端按部门筛选
|
||||||
|
- **WHEN** 管理端提交包含 `Department` 的查询请求
|
||||||
|
- **THEN** 系统仅返回用户部门匹配的会话记录
|
||||||
|
|
||||||
|
#### Scenario: 管理端按UserKey筛选
|
||||||
|
- **WHEN** 管理端提交包含 `UserKey` 的查询请求
|
||||||
|
- **THEN** 系统仅返回指定用户的会话记录
|
||||||
|
|
||||||
|
#### Scenario: 管理端按MessageType筛选
|
||||||
|
- **WHEN** 管理端提交包含 `MessageType` 的查询请求
|
||||||
|
- **THEN** 系统仅返回指定消息类型的会话记录
|
||||||
|
|
||||||
|
#### Scenario: 管理端查询全部会话记录
|
||||||
|
- **WHEN** 管理端提交不包含任何筛选条件的查询请求
|
||||||
|
- **THEN** 系统返回所有 `IsDeleted = 0` 的会话记录
|
||||||
|
- **AND** 结果按 `RecordTimeUTCStamp` 倒序
|
||||||
|
|
||||||
|
#### Scenario: 管理端组合多条件查询
|
||||||
|
- **WHEN** 管理端提交包含多个筛选条件的查询请求
|
||||||
|
- **THEN** 系统返回同时满足所有条件的会话记录
|
||||||
|
|
||||||
|
### Requirement: 查询用户列表(管理端)
|
||||||
|
系统 SHALL 提供接口返回可用用户列表,用于管理侧选择/筛选。
|
||||||
|
|
||||||
|
接口:`GET /api/Admin/QueryUsers`
|
||||||
|
|
||||||
|
过滤规则(内置):
|
||||||
|
- `PhoneNumber` 不为空且不为空字符串
|
||||||
|
- `UserName` 不为空且不为空字符串
|
||||||
|
- `UserKey` 不为空且不为空字符串
|
||||||
|
- `IsDisabled = 0`(未禁用)
|
||||||
|
- 按 `FirstLoginTime DESC` 排序
|
||||||
|
|
||||||
|
返回字段:
|
||||||
|
- Id, UserName, UserKey, WeChatName, PhoneNumber
|
||||||
|
- FirstLoginTime, IsDisabled, CreateTime, UpdateTime
|
||||||
|
- AvatarUrl, Department
|
||||||
|
|
||||||
|
#### Scenario: 获取用户列表
|
||||||
|
- **WHEN** 管理端请求用户列表
|
||||||
|
- **THEN** 返回已完善基础信息且未禁用的用户
|
||||||
|
- **AND** 结果按首次登录时间倒序排列
|
||||||
|
|
||||||
|
## Known Limitations
|
||||||
|
- 当前接口未提供分页与导出能力(若管理端数据量很大,需在后续能力中补齐)。
|
||||||
|
- 当前未强制鉴权(JWT 配置存在但未启用认证中间件/授权标注)。
|
||||||
|
- 前端验证仅使用固定账号密码(Admin/Admin),安全性较低,后续应考虑接入后端JWT认证。
|
||||||
|
|
||||||
|
## Implementation Details
|
||||||
|
|
||||||
|
### 后端技术实现
|
||||||
|
- **控制器文件**: `WxCheckMvc/Controllers/AdminController.cs`
|
||||||
|
- **数据库连接**: 使用 `MySqlConnection` 进行数据库操作
|
||||||
|
- **路由模式**: `[Route("api/[controller]/[action]")]`
|
||||||
|
- **API特性**: `[ApiController]` 提供自动模型验证和绑定
|
||||||
|
|
||||||
|
### 前端技术实现
|
||||||
|
- **项目目录**: `admin-web/`
|
||||||
|
- **构建工具**: Vite
|
||||||
|
- **路由配置**: `src/router/index.js`
|
||||||
|
- **状态管理**: `src/store/index.js`(Pinia)
|
||||||
|
- **样式目录**: `src/styles/`
|
||||||
|
- **组件目录**: `src/components/`
|
||||||
|
- **页面目录**: `src/views/`
|
||||||
|
|
||||||
|
#### 项目结构
|
||||||
|
```
|
||||||
|
admin-web/
|
||||||
|
├── src/
|
||||||
|
│ ├── assets/ # 静态资源
|
||||||
|
│ ├── components/ # 可复用组件
|
||||||
|
│ │ ├── Layout/ # 布局组件(侧边栏、顶部栏)
|
||||||
|
│ │ ├── ThemeSwitcher.vue # 主题切换组件
|
||||||
|
│ │ └── ...
|
||||||
|
│ ├── router/ # 路由配置
|
||||||
|
│ │ └── index.js
|
||||||
|
│ ├── store/ # Pinia store
|
||||||
|
│ │ ├── index.js
|
||||||
|
│ │ ├── auth.js # 认证状态
|
||||||
|
│ │ └── theme.js # 主题状态
|
||||||
|
│ ├── styles/ # 全局样式
|
||||||
|
│ │ ├── variables.scss # CSS变量(深色/浅色主题)
|
||||||
|
│ │ ├── responsive.scss # 响应式样式
|
||||||
|
│ │ └── main.scss
|
||||||
|
│ ├── utils/ # 工具函数
|
||||||
|
│ │ └── request.js # axios封装
|
||||||
|
│ ├── views/ # 页面组件
|
||||||
|
│ │ ├── Login.vue # 登录页
|
||||||
|
│ │ ├── Home.vue # 首页
|
||||||
|
│ │ ├── ConversationList.vue # 会话记录管理页
|
||||||
|
│ │ └── UserList.vue # 用户管理页
|
||||||
|
│ ├── App.vue
|
||||||
|
│ └── main.js
|
||||||
|
├── index.html
|
||||||
|
├── package.json
|
||||||
|
└── vite.config.js
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Pinia Store 结构
|
||||||
|
|
||||||
|
##### auth.js(认证状态)
|
||||||
|
```javascript
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
export const useAuthStore = defineStore('auth', {
|
||||||
|
state: () => ({
|
||||||
|
isLoggedIn: false,
|
||||||
|
username: ''
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
login(username, password) {
|
||||||
|
if (username === 'Admin' && password === 'Admin') {
|
||||||
|
this.isLoggedIn = true
|
||||||
|
this.username = username
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
logout() {
|
||||||
|
this.isLoggedIn = false
|
||||||
|
this.username = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
##### theme.js(主题状态)
|
||||||
|
```javascript
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
export const useThemeStore = defineStore('theme', {
|
||||||
|
state: () => ({
|
||||||
|
isDark: localStorage.getItem('theme') === 'dark'
|
||||||
|
}),
|
||||||
|
actions: {
|
||||||
|
toggleTheme() {
|
||||||
|
this.isDark = !this.isDark
|
||||||
|
localStorage.setItem('theme', this.isDark ? 'dark' : 'light')
|
||||||
|
document.documentElement.setAttribute('data-theme', this.isDark ? 'dark' : 'light')
|
||||||
|
},
|
||||||
|
initTheme() {
|
||||||
|
const theme = localStorage.getItem('theme') || 'light'
|
||||||
|
this.isDark = theme === 'dark'
|
||||||
|
document.documentElement.setAttribute('data-theme', theme)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 响应式断点
|
||||||
|
- **手机端**: < 768px
|
||||||
|
- **平板端**: 768px - 1023px
|
||||||
|
- **桌面端**: >= 1024px
|
||||||
|
|
||||||
|
### 数据模型
|
||||||
|
|
||||||
|
#### ConversationQueryRequest
|
||||||
|
```csharp
|
||||||
|
public class ConversationQueryRequest
|
||||||
|
{
|
||||||
|
public DateTime? StartTime { get; set; }
|
||||||
|
public DateTime? EndTime { get; set; }
|
||||||
|
public string UserKey { get; set; }
|
||||||
|
public int? MessageType { get; set; }
|
||||||
|
public string Department { get; set; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### ConversationQueryResponse
|
||||||
|
```csharp
|
||||||
|
public class ConversationQueryResponse
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
public string Guid { get; set; }
|
||||||
|
public string UserKey { get; set; }
|
||||||
|
public string ConversationContent { get; set; }
|
||||||
|
public string SendMethod { get; set; }
|
||||||
|
public string UserLocation { get; set; }
|
||||||
|
public string Latitude { get; set; }
|
||||||
|
public string Longitude { get; set; }
|
||||||
|
public DateTime RecordTime { get; set; }
|
||||||
|
public long RecordTimeUTCStamp { get; set; }
|
||||||
|
public bool IsDeleted { get; set; }
|
||||||
|
public DateTime CreateTime { get; set; }
|
||||||
|
public int MessageType { get; set; }
|
||||||
|
public int? SpeakingTime { get; set; }
|
||||||
|
public string UserName { get; set; }
|
||||||
|
public string WeChatName { get; set; }
|
||||||
|
public string PhoneNumber { get; set; }
|
||||||
|
public string AvatarUrl { get; set; }
|
||||||
|
public string Department { get; set; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### UserQueryResponse
|
||||||
|
```csharp
|
||||||
|
public class UserQueryResponse
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
public string UserName { get; set; }
|
||||||
|
public string UserKey { get; set; }
|
||||||
|
public string WeChatName { get; set; }
|
||||||
|
public string PhoneNumber { get; set; }
|
||||||
|
public DateTime FirstLoginTime { get; set; }
|
||||||
|
public bool IsDisabled { get; set; }
|
||||||
|
public DateTime CreateTime { get; set; }
|
||||||
|
public DateTime UpdateTime { get; set; }
|
||||||
|
public string AvatarUrl { get; set; }
|
||||||
|
public string Department { get; set; }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 异常处理
|
||||||
|
- 所有方法使用 try-catch-finally 模式
|
||||||
|
- 异常时返回 HTTP 500 状态码
|
||||||
|
- 响应格式:`{ success: false, message: "错误描述", error: "异常详情" }`
|
||||||
|
- finally 块确保数据库连接正确关闭
|
||||||
|
|
||||||
|
### 数据库操作
|
||||||
|
- 使用参数化查询防止 SQL 注入
|
||||||
|
- 动态构建 SQL 查询条件,仅添加非空参数
|
||||||
|
- 使用 `LEFT JOIN` 关联 `xcx_users` 表获取用户信息
|
||||||
|
- 时间转换:`DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()`
|
||||||
202
openspec/specs/backend-api/spec.md
Normal file
202
openspec/specs/backend-api/spec.md
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
# 后端 API(WxCheckMvc)规范
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
本能力描述 `WxCheckMvc` 暴露给微信小程序与管理侧的 HTTP API:登录/注册、会话记录的增删改查、文件上传、地址补全,以及 Redis Stream 的消息读取。
|
||||||
|
|
||||||
|
路由约定:统一为 `/api/{Controller}/{Action}`。
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### Requirement: 小程序登录(基于微信 code)
|
||||||
|
系统 SHALL 提供登录接口,用 `wx.login()` 获取的 `code` 向微信换取 `openid`,并以 `openid` 作为系统内部 `UserKey`。
|
||||||
|
|
||||||
|
接口:`POST /api/Login/Login`
|
||||||
|
|
||||||
|
请求:
|
||||||
|
- `Code: string`
|
||||||
|
|
||||||
|
响应(成功):
|
||||||
|
- `success: true`
|
||||||
|
- `data`: 用户对象(包含 `UserKey/openid` 与 `Token`)
|
||||||
|
|
||||||
|
行为:
|
||||||
|
- 若 `xcx_users` 不存在该 `UserKey`,系统 SHALL 自动插入一条用户记录(资料可为空)。
|
||||||
|
- 若用户 `IsDisabled = 1`,系统 SHALL 返回 `success: false`。
|
||||||
|
|
||||||
|
#### Scenario: 首次登录自动建档
|
||||||
|
- **WHEN** 提交的 `code` 能换取有效 `openid`
|
||||||
|
- **AND** 数据库中不存在该 `openid`
|
||||||
|
- **THEN** 系统创建用户记录并返回 `Token`
|
||||||
|
|
||||||
|
#### Scenario: 禁用用户登录
|
||||||
|
- **WHEN** 用户 `IsDisabled = 1`
|
||||||
|
- **THEN** 系统返回 `success: false` 且提示用户已禁用
|
||||||
|
|
||||||
|
### Requirement: 小程序注册/完善资料
|
||||||
|
系统 SHALL 提供接口用于完善用户资料(用户名、微信名、手机号、头像链接)。
|
||||||
|
|
||||||
|
接口:`POST /api/Login/Register`
|
||||||
|
|
||||||
|
请求:
|
||||||
|
- `UserKey: string`(openid)
|
||||||
|
- `UserName: string`
|
||||||
|
- `WeChatName: string`
|
||||||
|
- `PhoneNumber: string`
|
||||||
|
- `AvatarUrl: string`
|
||||||
|
|
||||||
|
校验:
|
||||||
|
- `UserKey` 必填
|
||||||
|
- `PhoneNumber` SHALL 清洗为纯数字后满足 `^1\d{10}$`
|
||||||
|
- `UserName` SHALL 去除标点/符号/空白后仍非空
|
||||||
|
|
||||||
|
响应(成功):
|
||||||
|
- `success: true`
|
||||||
|
- `data`: 更新后的用户对象(包含 `Token`)
|
||||||
|
|
||||||
|
#### Scenario: 正常完善资料
|
||||||
|
- **WHEN** 提交合法的用户名与手机号
|
||||||
|
- **AND** `UserKey` 对应用户存在
|
||||||
|
- **THEN** 系统更新用户资料并返回新 `Token`
|
||||||
|
|
||||||
|
#### Scenario: 用户不存在
|
||||||
|
- **WHEN** `UserKey` 对应用户不存在
|
||||||
|
- **THEN** 系统返回 404
|
||||||
|
|
||||||
|
### Requirement: 上传文件并可更新头像
|
||||||
|
系统 SHALL 提供文件上传接口,保存到后端 `wwwroot/{rootPathType}` 下,并返回可访问 URL。
|
||||||
|
|
||||||
|
接口:`POST /api/Check/UploadFile`(`multipart/form-data`)
|
||||||
|
|
||||||
|
表单字段:
|
||||||
|
- `file`: 上传文件
|
||||||
|
- `rootPathType: string`(可选;默认 `Avatar`,仅允许字母/数字/下划线)
|
||||||
|
- `userKey: string`(可选;若提供则更新 `xcx_users.AvatarUrl`)
|
||||||
|
|
||||||
|
响应:
|
||||||
|
- `success: true/false`
|
||||||
|
- `url`: 公开访问 URL
|
||||||
|
- `path`: 相对路径
|
||||||
|
|
||||||
|
#### Scenario: 上传头像并更新用户表
|
||||||
|
- **WHEN** 上传文件并提供 `userKey`
|
||||||
|
- **THEN** 系统保存文件并更新该用户 `AvatarUrl`
|
||||||
|
|
||||||
|
### Requirement: 新增会话记录
|
||||||
|
系统 SHALL 提供接口新增会话记录(软实时写入数据库),并尝试将消息投递到 Redis Stream。
|
||||||
|
|
||||||
|
接口:`POST /api/Check/AddConversation`
|
||||||
|
|
||||||
|
请求(核心字段):
|
||||||
|
- `UserKey: string`
|
||||||
|
- `ConversationContent: string`
|
||||||
|
- `SendMethod: string`
|
||||||
|
- `UserLocation: string`(当前实现中会尝试解析 `lat,lng`)
|
||||||
|
- `MessageType: int`(默认 1)
|
||||||
|
- `Guid?: string`(可选;缺省则服务端生成)
|
||||||
|
- `SpeakingTime?: int`
|
||||||
|
|
||||||
|
行为:
|
||||||
|
- 系统 SHALL 写入 `xcx_conversation`,并生成/使用 `Guid`。
|
||||||
|
- 系统 MAY 将会话与用户信息写入 Redis Stream(key: `xcx_msg`,group: `xcx_group`)。
|
||||||
|
|
||||||
|
#### Scenario: 新增会话并返回 guid
|
||||||
|
- **WHEN** 提交包含 `UserKey` 与 `ConversationContent` 的请求
|
||||||
|
- **THEN** 系统创建会话记录并返回 `conversationGuid`
|
||||||
|
|
||||||
|
### Requirement: 查询会话记录(按用户)
|
||||||
|
系统 SHALL 提供按 `UserKey` 查询会话记录的接口,默认只返回未删除记录。
|
||||||
|
|
||||||
|
接口:`POST /api/Check/GetConversations`
|
||||||
|
|
||||||
|
请求:
|
||||||
|
- `UserKey: string`
|
||||||
|
- `MessageType: int`(0 不过滤;1 公有;2 私有;当前实现仅在 `MessageType == 1` 时追加过滤)
|
||||||
|
|
||||||
|
#### Scenario: 查询用户所有未删除会话
|
||||||
|
- **WHEN** 提交 `UserKey`
|
||||||
|
- **THEN** 系统返回该用户 `IsDeleted = 0` 的会话记录
|
||||||
|
|
||||||
|
### Requirement: 分页查询会话记录
|
||||||
|
系统 SHALL 提供分页接口。
|
||||||
|
|
||||||
|
接口:`POST /api/Check/GetConversationsByPage`
|
||||||
|
|
||||||
|
请求:
|
||||||
|
- `UserKey: string`
|
||||||
|
- `Page: int`(<1 视为 1)
|
||||||
|
- `PageSize: int`(1..100,否则默认 10)
|
||||||
|
- `MessageType: int`(当前实现仅在 `MessageType == 1` 时追加过滤)
|
||||||
|
|
||||||
|
响应:
|
||||||
|
- `data.conversations`
|
||||||
|
- `data.totalCount / page / pageSize / totalPages`
|
||||||
|
|
||||||
|
#### Scenario: PageSize 超限
|
||||||
|
- **WHEN** `PageSize > 100`
|
||||||
|
- **THEN** 系统按默认值 10 处理
|
||||||
|
|
||||||
|
### Requirement: 更新会话记录
|
||||||
|
系统 SHALL 提供接口按 `Guid + UserKey` 更新会话内容与发送方式。
|
||||||
|
|
||||||
|
接口:`POST /api/Check/UpdateConversation`
|
||||||
|
|
||||||
|
请求:
|
||||||
|
- `Guid: string`
|
||||||
|
- `UserKey: string`
|
||||||
|
- `ConversationContent: string`
|
||||||
|
- `SendMethod: string`
|
||||||
|
- `MessageType: int`
|
||||||
|
|
||||||
|
#### Scenario: 非本人更新
|
||||||
|
- **WHEN** `Guid` 存在但 `UserKey` 不匹配
|
||||||
|
- **THEN** 系统返回 404(记录不存在或无权限修改)
|
||||||
|
|
||||||
|
### Requirement: 删除会话记录(软删除)
|
||||||
|
系统 SHALL 提供接口按 `Guid + UserKey` 软删除会话。
|
||||||
|
|
||||||
|
接口:`POST /api/Check/DeleteConversation`
|
||||||
|
|
||||||
|
请求:
|
||||||
|
- `Guid: string`
|
||||||
|
- `UserKey: string`
|
||||||
|
|
||||||
|
#### Scenario: 删除已删除记录
|
||||||
|
- **WHEN** 记录已被删除
|
||||||
|
- **THEN** 系统返回 404
|
||||||
|
|
||||||
|
### Requirement: 按 Guid 查询会话(不受软删除影响)
|
||||||
|
系统 SHALL 提供接口按 `Guid` 查询单条会话记录,不过滤 `IsDeleted`。
|
||||||
|
|
||||||
|
接口:`POST /api/Check/GetConversationByGuid`
|
||||||
|
|
||||||
|
#### Scenario: 查询已删除会话
|
||||||
|
- **WHEN** `Guid` 对应记录存在但 `IsDeleted = 1`
|
||||||
|
- **THEN** 系统仍返回该记录
|
||||||
|
|
||||||
|
### Requirement: 地址补全(经纬度→地址)
|
||||||
|
系统 SHALL 提供接口按 `Guid` 查找会话记录经纬度,并使用高德逆地理编码生成地址写回 `UserLocation`。
|
||||||
|
|
||||||
|
接口:`POST /api/Check/CheckAddress`
|
||||||
|
|
||||||
|
#### Scenario: 记录不存在
|
||||||
|
- **WHEN** `Guid` 对应记录不存在或已删除
|
||||||
|
- **THEN** 系统返回 404
|
||||||
|
|
||||||
|
### Requirement: Redis Stream 读取消息
|
||||||
|
系统 SHALL 提供接口从 Redis Stream 读取消息。
|
||||||
|
|
||||||
|
接口:`POST /api/Check/ReadMessageFromRedis`
|
||||||
|
|
||||||
|
请求:
|
||||||
|
- `GroupName?: string`(默认 `xcx_group`)
|
||||||
|
- `ConsumerName?: string`(默认 `consumer_{ticks}`)
|
||||||
|
- `Count?: int`(默认 1)
|
||||||
|
|
||||||
|
#### Scenario: 无新消息
|
||||||
|
- **WHEN** Stream 中无可读消息
|
||||||
|
- **THEN** 返回 `success: true` 且数据为空
|
||||||
|
|
||||||
|
## Known Limitations
|
||||||
|
- 当前 JWT 配置存在,但认证中间件与授权标注未启用;接口默认可匿名访问。
|
||||||
|
- `MessageType` 的过滤条件存在实现差异:部分接口仅在 `MessageType == 1` 时追加过滤(不覆盖 2)。
|
||||||
|
- 文件上传未实现内容类型/大小的强制限制。
|
||||||
69
openspec/specs/database/spec.md
Normal file
69
openspec/specs/database/spec.md
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
# 数据库(wx_xcx_check)规范
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
本能力描述 MariaDB/MySQL schema `wx_xcx_check` 的表结构与核心业务语义,用于约束后端 API 的持久化行为。
|
||||||
|
|
||||||
|
参考文件:
|
||||||
|
- 根目录 `wx_xcx_check.sql`
|
||||||
|
- `WxCheckMvc/wx_xcx_check.sql`
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### Requirement: 用户表 xcx_users
|
||||||
|
系统 SHALL 存储小程序用户档案,主业务键为 `UserKey`(微信 `openid`)。
|
||||||
|
|
||||||
|
字段语义(核心):
|
||||||
|
- `UserKey`:唯一键(UNIQUE)
|
||||||
|
- `UserName`:用户姓名(可空,注册/完善资料后写入)
|
||||||
|
- `WeChatName`:微信昵称(可空)
|
||||||
|
- `PhoneNumber`:手机号(可空,注册/完善资料后写入)
|
||||||
|
- `AvatarUrl`:头像 URL(可空,上传后写入)
|
||||||
|
- `Department`:部门(可空,用于管理端筛选)
|
||||||
|
- `IsDisabled`:0 启用 / 1 禁用
|
||||||
|
- `FirstLoginTime`:首次登录时间
|
||||||
|
- `CreateTime`/`UpdateTime`
|
||||||
|
|
||||||
|
#### Scenario: 首次登录建档
|
||||||
|
- **WHEN** 后端检测到 `UserKey` 不存在
|
||||||
|
- **THEN** 插入一条用户记录,至少包含 `UserKey/FirstLoginTime/IsDisabled`
|
||||||
|
|
||||||
|
### Requirement: 会话表 xcx_conversation
|
||||||
|
系统 SHALL 存储会话记录,并支持软删除。
|
||||||
|
|
||||||
|
字段语义(核心):
|
||||||
|
- `UserKey`:关联用户(逻辑关联;当前 schema 未声明外键)
|
||||||
|
- `ConversationContent`:会话内容
|
||||||
|
- `SendMethod`:发送方式(文本/语音等,由业务侧约定字符串)
|
||||||
|
- `MessageType`:1 公有 / 2 私有
|
||||||
|
- `Guid`:会话唯一标识(业务约定;schema 未声明 UNIQUE)
|
||||||
|
- `Latitude/Longitude`:经纬度(字符串存储)
|
||||||
|
- `UserLocation`:地址文本(可由高德逆地理编码写入)
|
||||||
|
- `RecordTime`:记录时间
|
||||||
|
- `RecordTimeUTCStamp`:毫秒级 UTC 时间戳(用于排序/分页)
|
||||||
|
- `SpeakingTime`:对话时长(可空)
|
||||||
|
- `IsDeleted`:0 正常 / 1 删除
|
||||||
|
|
||||||
|
索引:
|
||||||
|
- `idx_userkey`、`idx_utcstamp`、`idx_deleted`、`idx_messagetype`、`idx_guid`
|
||||||
|
|
||||||
|
#### Scenario: 软删除
|
||||||
|
- **WHEN** 用户删除会话
|
||||||
|
- **THEN** 将 `IsDeleted` 置为 1
|
||||||
|
- **AND** 默认查询不返回 `IsDeleted = 1` 的记录
|
||||||
|
|
||||||
|
### Requirement: 操作日志表 xcx_log
|
||||||
|
系统 MAY 记录操作日志,用于审计与问题排查。
|
||||||
|
|
||||||
|
字段语义(核心):
|
||||||
|
- `UserKey`:操作者
|
||||||
|
- `OperationContent`:操作内容
|
||||||
|
- `Impact`:影响描述
|
||||||
|
- `RecordTime`:记录时间
|
||||||
|
|
||||||
|
#### Scenario: 记录操作日志
|
||||||
|
- **WHEN** 发生需要审计的关键操作
|
||||||
|
- **THEN** 系统写入 `xcx_log`
|
||||||
|
|
||||||
|
## Known Limitations
|
||||||
|
- `xcx_conversation.UserKey` 与 `xcx_users.UserKey` 当前为“逻辑关联”,未声明外键;数据一致性需由应用层保障。
|
||||||
|
- `Guid` 未声明唯一约束;若业务要求全局唯一,应在后续变更中补齐约束与迁移。
|
||||||
106
openspec/specs/wechat-miniapp/spec.md
Normal file
106
openspec/specs/wechat-miniapp/spec.md
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
# 微信小程序(CommunicationRecords)规范
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
本能力描述小程序端的主要用户流程、页面职责以及与后端 API 的交互约定。
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### Requirement: 协议勾选门槛
|
||||||
|
系统 SHALL 要求用户在登录前勾选用户协议(隐私合规前置)。
|
||||||
|
|
||||||
|
实现位置:`pages/logs/logs.js`
|
||||||
|
|
||||||
|
#### Scenario: 未勾选协议尝试登录
|
||||||
|
- **WHEN** 用户未勾选协议并触发登录流程
|
||||||
|
- **THEN** 小程序提示“请先勾选用户协议”
|
||||||
|
|
||||||
|
### Requirement: 登录流程(微信 code → 后端 Login)
|
||||||
|
系统 SHALL 使用 `wx.login()` 获取 `code` 并调用后端登录接口。
|
||||||
|
|
||||||
|
接口:`POST /api/Login/Login`
|
||||||
|
|
||||||
|
行为:
|
||||||
|
- 登录成功时,SHALL 将 `openid(UserKey)` 写入本地缓存 `openid`,并同步到 `app.globalData`。
|
||||||
|
- 若用户资料未完善(缺少 `UserName/WeChatName/PhoneNumber`),SHALL 进入注册/完善资料流程。
|
||||||
|
|
||||||
|
#### Scenario: 已注册用户直接进入聊天页
|
||||||
|
- **WHEN** 登录接口返回完整用户信息
|
||||||
|
- **THEN** 小程序跳转到 `pages/chat/chat`
|
||||||
|
|
||||||
|
#### Scenario: 未注册用户提示完善信息
|
||||||
|
- **WHEN** 登录接口成功但返回的用户信息不完整
|
||||||
|
- **THEN** 小程序提示需要完善资料并展示注册表单
|
||||||
|
|
||||||
|
### Requirement: 注册/完善资料流程
|
||||||
|
系统 SHALL 提供表单采集姓名、手机号、昵称与头像,并调用后端注册接口更新用户资料。
|
||||||
|
|
||||||
|
接口:`POST /api/Login/Register`
|
||||||
|
|
||||||
|
实现位置:`pages/logs/logs.js`
|
||||||
|
|
||||||
|
行为:
|
||||||
|
- 注册接口成功后,SHALL 触发头像上传(`/api/Check/UploadFile`)并更新页面头像。
|
||||||
|
|
||||||
|
#### Scenario: 正常注册
|
||||||
|
- **WHEN** 用户提交有效姓名与手机号
|
||||||
|
- **THEN** 小程序完成注册并进入聊天页
|
||||||
|
|
||||||
|
### Requirement: 头像上传
|
||||||
|
系统 SHALL 允许用户选择头像并上传到服务器,后端返回永久 URL 后用于展示。
|
||||||
|
|
||||||
|
接口:`POST /api/Check/UploadFile`
|
||||||
|
|
||||||
|
实现位置:`pages/logs/logs.js` 与 `pages/chat/chat.js`
|
||||||
|
|
||||||
|
#### Scenario: 上传成功
|
||||||
|
- **WHEN** 上传接口返回 `success: true`
|
||||||
|
- **THEN** 小程序使用返回的 `url` 更新头像展示
|
||||||
|
|
||||||
|
### Requirement: 聊天页加载历史会话
|
||||||
|
系统 SHALL 在进入聊天页时请求历史会话并渲染。
|
||||||
|
|
||||||
|
接口:`POST /api/Check/GetConversationsByPage`
|
||||||
|
|
||||||
|
实现位置:`pages/chat/chat.js`(`loadHistory` / `GetConversations`)
|
||||||
|
|
||||||
|
#### Scenario: 首次进入加载第一页
|
||||||
|
- **WHEN** 进入聊天页
|
||||||
|
- **THEN** 请求第一页(Page=1, PageSize=20)并展示
|
||||||
|
|
||||||
|
### Requirement: 发送消息
|
||||||
|
系统 SHALL 通过后端新增会话接口发送消息。
|
||||||
|
|
||||||
|
接口:`POST /api/Check/AddConversation`
|
||||||
|
|
||||||
|
实现位置:`pages/chat/chat.js`
|
||||||
|
|
||||||
|
#### Scenario: 文本发送成功
|
||||||
|
- **WHEN** 用户发送文本消息
|
||||||
|
- **THEN** 小程序调用新增会话接口并更新列表
|
||||||
|
|
||||||
|
### Requirement: 编辑与删除消息
|
||||||
|
系统 SHALL 支持对会话进行更新与软删除。
|
||||||
|
|
||||||
|
接口:
|
||||||
|
- `POST /api/Check/UpdateConversation`
|
||||||
|
- `POST /api/Check/DeleteConversation`
|
||||||
|
|
||||||
|
实现位置:`pages/chat/chat.js`
|
||||||
|
|
||||||
|
#### Scenario: 删除消息
|
||||||
|
- **WHEN** 用户删除某条消息
|
||||||
|
- **THEN** 小程序调用删除接口并从列表移除/刷新
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Requirement: API 根地址
|
||||||
|
系统 SHOULD 通过 `utils/config.js` 统一管理 `baseUrl`,并在请求中使用 `${config.baseUrl}` 拼接接口路径。
|
||||||
|
|
||||||
|
#### Scenario: 环境切换
|
||||||
|
- **WHEN** 小程序环境为 develop/trial/release
|
||||||
|
- **THEN** 选择对应 `baseUrl`
|
||||||
|
|
||||||
|
## Known Limitations
|
||||||
|
- `utils/config.js` 当前强制返回 `release`,实际不会随 `envVersion` 切换。
|
||||||
|
- 存在硬编码域名请求(例如注册/登录/上传头像部分),未统一走 `config.baseUrl`。
|
||||||
|
- `app.js` 中的 `globalData.baseUrl` 为占位配置,实际请求主要依赖 `utils/config.js` 或硬编码。
|
||||||
782
package-lock.json
generated
Normal file
782
package-lock.json
generated
Normal file
@@ -0,0 +1,782 @@
|
|||||||
|
{
|
||||||
|
"name": "Wx_WxCheck_Prod",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"dependencies": {
|
||||||
|
"@fission-ai/openspec": "^0.16.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fission-ai/openspec": {
|
||||||
|
"version": "0.16.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@fission-ai/openspec/-/openspec-0.16.0.tgz",
|
||||||
|
"integrity": "sha512-6gCxGrsPWiHG3cGqKhDLOmNO1d4wbk51uvEE/Z5/sDBQfIUpLKN+4EURzQn/9I/cg6QQ/Vy3bIjZWpGRq87/Vg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@inquirer/core": "^10.2.2",
|
||||||
|
"@inquirer/prompts": "^7.8.0",
|
||||||
|
"chalk": "^5.5.0",
|
||||||
|
"commander": "^14.0.0",
|
||||||
|
"ora": "^8.2.0",
|
||||||
|
"zod": "^4.0.17"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"openspec": "bin/openspec.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.19.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@inquirer/ansi": {
|
||||||
|
"version": "1.0.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@inquirer/ansi/-/ansi-1.0.2.tgz",
|
||||||
|
"integrity": "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@inquirer/checkbox": {
|
||||||
|
"version": "4.3.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@inquirer/checkbox/-/checkbox-4.3.2.tgz",
|
||||||
|
"integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@inquirer/ansi": "^1.0.2",
|
||||||
|
"@inquirer/core": "^10.3.2",
|
||||||
|
"@inquirer/figures": "^1.0.15",
|
||||||
|
"@inquirer/type": "^3.0.10",
|
||||||
|
"yoctocolors-cjs": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/node": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@inquirer/confirm": {
|
||||||
|
"version": "5.1.21",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@inquirer/confirm/-/confirm-5.1.21.tgz",
|
||||||
|
"integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@inquirer/core": "^10.3.2",
|
||||||
|
"@inquirer/type": "^3.0.10"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/node": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@inquirer/core": {
|
||||||
|
"version": "10.3.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@inquirer/core/-/core-10.3.2.tgz",
|
||||||
|
"integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@inquirer/ansi": "^1.0.2",
|
||||||
|
"@inquirer/figures": "^1.0.15",
|
||||||
|
"@inquirer/type": "^3.0.10",
|
||||||
|
"cli-width": "^4.1.0",
|
||||||
|
"mute-stream": "^2.0.0",
|
||||||
|
"signal-exit": "^4.1.0",
|
||||||
|
"wrap-ansi": "^6.2.0",
|
||||||
|
"yoctocolors-cjs": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/node": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@inquirer/editor": {
|
||||||
|
"version": "4.2.23",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@inquirer/editor/-/editor-4.2.23.tgz",
|
||||||
|
"integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@inquirer/core": "^10.3.2",
|
||||||
|
"@inquirer/external-editor": "^1.0.3",
|
||||||
|
"@inquirer/type": "^3.0.10"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/node": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@inquirer/expand": {
|
||||||
|
"version": "4.0.23",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@inquirer/expand/-/expand-4.0.23.tgz",
|
||||||
|
"integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@inquirer/core": "^10.3.2",
|
||||||
|
"@inquirer/type": "^3.0.10",
|
||||||
|
"yoctocolors-cjs": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/node": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@inquirer/external-editor": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@inquirer/external-editor/-/external-editor-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"chardet": "^2.1.1",
|
||||||
|
"iconv-lite": "^0.7.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/node": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@inquirer/figures": {
|
||||||
|
"version": "1.0.15",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@inquirer/figures/-/figures-1.0.15.tgz",
|
||||||
|
"integrity": "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@inquirer/input": {
|
||||||
|
"version": "4.3.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@inquirer/input/-/input-4.3.1.tgz",
|
||||||
|
"integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@inquirer/core": "^10.3.2",
|
||||||
|
"@inquirer/type": "^3.0.10"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/node": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@inquirer/number": {
|
||||||
|
"version": "3.0.23",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@inquirer/number/-/number-3.0.23.tgz",
|
||||||
|
"integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@inquirer/core": "^10.3.2",
|
||||||
|
"@inquirer/type": "^3.0.10"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/node": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@inquirer/password": {
|
||||||
|
"version": "4.0.23",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@inquirer/password/-/password-4.0.23.tgz",
|
||||||
|
"integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@inquirer/ansi": "^1.0.2",
|
||||||
|
"@inquirer/core": "^10.3.2",
|
||||||
|
"@inquirer/type": "^3.0.10"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/node": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@inquirer/prompts": {
|
||||||
|
"version": "7.10.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@inquirer/prompts/-/prompts-7.10.1.tgz",
|
||||||
|
"integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@inquirer/checkbox": "^4.3.2",
|
||||||
|
"@inquirer/confirm": "^5.1.21",
|
||||||
|
"@inquirer/editor": "^4.2.23",
|
||||||
|
"@inquirer/expand": "^4.0.23",
|
||||||
|
"@inquirer/input": "^4.3.1",
|
||||||
|
"@inquirer/number": "^3.0.23",
|
||||||
|
"@inquirer/password": "^4.0.23",
|
||||||
|
"@inquirer/rawlist": "^4.1.11",
|
||||||
|
"@inquirer/search": "^3.2.2",
|
||||||
|
"@inquirer/select": "^4.4.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/node": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@inquirer/rawlist": {
|
||||||
|
"version": "4.1.11",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@inquirer/rawlist/-/rawlist-4.1.11.tgz",
|
||||||
|
"integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@inquirer/core": "^10.3.2",
|
||||||
|
"@inquirer/type": "^3.0.10",
|
||||||
|
"yoctocolors-cjs": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/node": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@inquirer/search": {
|
||||||
|
"version": "3.2.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@inquirer/search/-/search-3.2.2.tgz",
|
||||||
|
"integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@inquirer/core": "^10.3.2",
|
||||||
|
"@inquirer/figures": "^1.0.15",
|
||||||
|
"@inquirer/type": "^3.0.10",
|
||||||
|
"yoctocolors-cjs": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/node": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@inquirer/select": {
|
||||||
|
"version": "4.4.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@inquirer/select/-/select-4.4.2.tgz",
|
||||||
|
"integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@inquirer/ansi": "^1.0.2",
|
||||||
|
"@inquirer/core": "^10.3.2",
|
||||||
|
"@inquirer/figures": "^1.0.15",
|
||||||
|
"@inquirer/type": "^3.0.10",
|
||||||
|
"yoctocolors-cjs": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/node": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@inquirer/type": {
|
||||||
|
"version": "3.0.10",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@inquirer/type/-/type-3.0.10.tgz",
|
||||||
|
"integrity": "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/node": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ansi-regex": {
|
||||||
|
"version": "6.2.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-6.2.2.tgz",
|
||||||
|
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ansi-styles": {
|
||||||
|
"version": "4.3.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||||
|
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"color-convert": "^2.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/chalk": {
|
||||||
|
"version": "5.6.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/chalk/-/chalk-5.6.2.tgz",
|
||||||
|
"integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": "^12.17.0 || ^14.13 || >=16.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/chardet": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/chardet/-/chardet-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/cli-cursor": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/cli-cursor/-/cli-cursor-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"restore-cursor": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cli-spinners": {
|
||||||
|
"version": "2.9.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/cli-spinners/-/cli-spinners-2.9.2.tgz",
|
||||||
|
"integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cli-width": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/cli-width/-/cli-width-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/color-convert": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
|
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"color-name": "~1.1.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=7.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/color-name": {
|
||||||
|
"version": "1.1.4",
|
||||||
|
"resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz",
|
||||||
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/commander": {
|
||||||
|
"version": "14.0.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/commander/-/commander-14.0.2.tgz",
|
||||||
|
"integrity": "sha512-TywoWNNRbhoD0BXs1P3ZEScW8W5iKrnbithIl0YH+uCmBd0QpPOA8yc82DS3BIE5Ma6FnBVUsJ7wVUDz4dvOWQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/emoji-regex": {
|
||||||
|
"version": "10.6.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-10.6.0.tgz",
|
||||||
|
"integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/get-east-asian-width": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/get-east-asian-width/-/get-east-asian-width-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/iconv-lite": {
|
||||||
|
"version": "0.7.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.7.1.tgz",
|
||||||
|
"integrity": "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/express"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/is-fullwidth-code-point": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/is-interactive": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/is-interactive/-/is-interactive-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/is-unicode-supported": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/log-symbols": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/log-symbols/-/log-symbols-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"chalk": "^5.3.0",
|
||||||
|
"is-unicode-supported": "^1.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/log-symbols/node_modules/is-unicode-supported": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mimic-function": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/mimic-function/-/mimic-function-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mute-stream": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/mute-stream/-/mute-stream-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": "^18.17.0 || >=20.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/onetime": {
|
||||||
|
"version": "7.0.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/onetime/-/onetime-7.0.0.tgz",
|
||||||
|
"integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mimic-function": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/ora": {
|
||||||
|
"version": "8.2.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/ora/-/ora-8.2.0.tgz",
|
||||||
|
"integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"chalk": "^5.3.0",
|
||||||
|
"cli-cursor": "^5.0.0",
|
||||||
|
"cli-spinners": "^2.9.2",
|
||||||
|
"is-interactive": "^2.0.0",
|
||||||
|
"is-unicode-supported": "^2.0.0",
|
||||||
|
"log-symbols": "^6.0.0",
|
||||||
|
"stdin-discarder": "^0.2.2",
|
||||||
|
"string-width": "^7.2.0",
|
||||||
|
"strip-ansi": "^7.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/restore-cursor": {
|
||||||
|
"version": "5.1.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/restore-cursor/-/restore-cursor-5.1.0.tgz",
|
||||||
|
"integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"onetime": "^7.0.0",
|
||||||
|
"signal-exit": "^4.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/safer-buffer": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/signal-exit": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/stdin-discarder": {
|
||||||
|
"version": "0.2.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/stdin-discarder/-/stdin-discarder-0.2.2.tgz",
|
||||||
|
"integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/string-width": {
|
||||||
|
"version": "7.2.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/string-width/-/string-width-7.2.0.tgz",
|
||||||
|
"integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"emoji-regex": "^10.3.0",
|
||||||
|
"get-east-asian-width": "^1.0.0",
|
||||||
|
"strip-ansi": "^7.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/strip-ansi": {
|
||||||
|
"version": "7.1.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-7.1.2.tgz",
|
||||||
|
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-regex": "^6.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/wrap-ansi": {
|
||||||
|
"version": "6.2.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
|
||||||
|
"integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": "^4.0.0",
|
||||||
|
"string-width": "^4.1.0",
|
||||||
|
"strip-ansi": "^6.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/wrap-ansi/node_modules/ansi-regex": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/wrap-ansi/node_modules/emoji-regex": {
|
||||||
|
"version": "8.0.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||||
|
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/wrap-ansi/node_modules/string-width": {
|
||||||
|
"version": "4.2.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz",
|
||||||
|
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"emoji-regex": "^8.0.0",
|
||||||
|
"is-fullwidth-code-point": "^3.0.0",
|
||||||
|
"strip-ansi": "^6.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/wrap-ansi/node_modules/strip-ansi": {
|
||||||
|
"version": "6.0.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||||
|
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-regex": "^5.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/yoctocolors-cjs": {
|
||||||
|
"version": "2.1.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz",
|
||||||
|
"integrity": "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zod": {
|
||||||
|
"version": "4.2.1",
|
||||||
|
"resolved": "https://registry.npmmirror.com/zod/-/zod-4.2.1.tgz",
|
||||||
|
"integrity": "sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
5
package.json
Normal file
5
package.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"dependencies": {
|
||||||
|
"@fission-ai/openspec": "^0.16.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
86
wx_xcx_check.sql
Normal file
86
wx_xcx_check.sql
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
/*
|
||||||
|
Navicat Premium Dump SQL
|
||||||
|
|
||||||
|
Source Server : 47.119.147.104
|
||||||
|
Source Server Type : MariaDB
|
||||||
|
Source Server Version : 120002 (12.0.2-MariaDB)
|
||||||
|
Source Host : 47.119.147.104:3307
|
||||||
|
Source Schema : wx_xcx_check
|
||||||
|
|
||||||
|
Target Server Type : MariaDB
|
||||||
|
Target Server Version : 120002 (12.0.2-MariaDB)
|
||||||
|
File Encoding : 65001
|
||||||
|
|
||||||
|
Date: 18/12/2025 09:53:40
|
||||||
|
*/
|
||||||
|
|
||||||
|
SET NAMES utf8mb4;
|
||||||
|
SET FOREIGN_KEY_CHECKS = 0;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for xcx_conversation
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `xcx_conversation`;
|
||||||
|
CREATE TABLE `xcx_conversation` (
|
||||||
|
`Id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||||
|
`UserKey` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '用户唯一标识键',
|
||||||
|
`ConversationContent` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '会话内容',
|
||||||
|
`MessageType` int(11) NOT NULL DEFAULT 1 COMMENT '信息类型:1-公有,2-私有',
|
||||||
|
`SendMethod` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '发送方式',
|
||||||
|
`UserLocation` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '用户定位信息',
|
||||||
|
`RecordTime` datetime NOT NULL COMMENT '记录时间',
|
||||||
|
`RecordTimeUTCStamp` bigint(20) NOT NULL COMMENT '记录时间的UTC时间戳',
|
||||||
|
`IsDeleted` tinyint(4) NULL DEFAULT 0 COMMENT '是否删除:0-正常,1-删除',
|
||||||
|
`CreateTime` datetime NULL DEFAULT current_timestamp() COMMENT '创建时间',
|
||||||
|
`Latitude` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '纬度',
|
||||||
|
`Longitude` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '经度',
|
||||||
|
`Guid` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'GUID',
|
||||||
|
`SpeakingTime` int(11) NULL DEFAULT NULL COMMENT '对话时长',
|
||||||
|
PRIMARY KEY (`Id`) USING BTREE,
|
||||||
|
INDEX `idx_userkey`(`UserKey` ASC) USING BTREE,
|
||||||
|
INDEX `idx_utcstamp`(`RecordTimeUTCStamp` ASC) USING BTREE,
|
||||||
|
INDEX `idx_deleted`(`IsDeleted` ASC) USING BTREE,
|
||||||
|
INDEX `idx_recordtime`(`RecordTime` ASC) USING BTREE,
|
||||||
|
INDEX `idx_messagetype`(`MessageType` ASC) USING BTREE,
|
||||||
|
INDEX `idx_guid`(`Guid` ASC) USING BTREE
|
||||||
|
) ENGINE = InnoDB AUTO_INCREMENT = 455 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '会话记录表' ROW_FORMAT = Dynamic;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for xcx_log
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `xcx_log`;
|
||||||
|
CREATE TABLE `xcx_log` (
|
||||||
|
`Id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||||
|
`RecordTime` datetime NOT NULL COMMENT '记录时间',
|
||||||
|
`UserKey` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '用户唯一标识键',
|
||||||
|
`OperationContent` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '操作内容',
|
||||||
|
`UserLocation` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '用户定位信息',
|
||||||
|
`Impact` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '产生的影响',
|
||||||
|
`CreateTime` datetime NULL DEFAULT current_timestamp() COMMENT '创建时间',
|
||||||
|
PRIMARY KEY (`Id`) USING BTREE,
|
||||||
|
INDEX `idx_userkey`(`UserKey` ASC) USING BTREE,
|
||||||
|
INDEX `idx_recordtime`(`RecordTime` ASC) USING BTREE
|
||||||
|
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '操作日志表' ROW_FORMAT = Dynamic;
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for xcx_users
|
||||||
|
-- ----------------------------
|
||||||
|
DROP TABLE IF EXISTS `xcx_users`;
|
||||||
|
CREATE TABLE `xcx_users` (
|
||||||
|
`Id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||||
|
`UserName` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '用户名',
|
||||||
|
`UserKey` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '用户唯一标识键',
|
||||||
|
`WeChatName` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '微信名称',
|
||||||
|
`PhoneNumber` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '电话号码',
|
||||||
|
`FirstLoginTime` datetime NOT NULL COMMENT '首次登录时间',
|
||||||
|
`IsDisabled` tinyint(4) NULL DEFAULT 0 COMMENT '是否禁用:0-启用,1-禁用',
|
||||||
|
`CreateTime` datetime NULL DEFAULT current_timestamp() COMMENT '创建时间',
|
||||||
|
`UpdateTime` datetime NULL DEFAULT current_timestamp() ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
`AvatarUrl` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '头像地址',
|
||||||
|
PRIMARY KEY (`Id`) USING BTREE,
|
||||||
|
UNIQUE INDEX `idx_userkey`(`UserKey` ASC) USING BTREE,
|
||||||
|
INDEX `idx_disabled`(`IsDisabled` ASC) USING BTREE,
|
||||||
|
INDEX `idx_logintime`(`FirstLoginTime` ASC) USING BTREE
|
||||||
|
) ENGINE = InnoDB AUTO_INCREMENT = 34 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '小程序用户表' ROW_FORMAT = Dynamic;
|
||||||
|
|
||||||
|
SET FOREIGN_KEY_CHECKS = 1;
|
||||||
Reference in New Issue
Block a user