Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 48bc1effca | |||
| ee1d93d4f6 | |||
| 6a67c53e9b | |||
| 4de5f42dc8 |
@@ -0,0 +1,21 @@
|
|||||||
|
# role-admin Agent Skills
|
||||||
|
|
||||||
|
本目录存放 `role-admin` 的项目级 Agent skill。规则参考 SeaCloud 项目的 agent skill 组织方式,并按当前 Vue 3、Vite、TypeScript、Element Plus 和后台管理场景保留最小必要规范。
|
||||||
|
|
||||||
|
## 使用入口
|
||||||
|
|
||||||
|
1. 进入仓库后先读 `AGENTS.md` 和 `RTK.md`。
|
||||||
|
2. 创建或修改代码文件时,使用 `header-comment-sync` 保持中文文件头、导出声明和复杂逻辑注释同步。
|
||||||
|
3. 提交或推送代码时,使用 `chinese-commit-message` 生成英文 type 前缀加中文摘要的 commit message。
|
||||||
|
4. 所有 skill 的触达文件补齐规则只处理本次任务相关文件,不要求为了单次任务全仓扫描。
|
||||||
|
|
||||||
|
## 当前项目事实
|
||||||
|
|
||||||
|
- 应用类型:门店员工权限管理后台。
|
||||||
|
- 技术栈:Vue 3、Vite、TypeScript、Element Plus、Pinia、Vue Router。
|
||||||
|
- 关键边界:业务接口集中在 `src/api/access.ts`,页面层不要直接拼接后端 URL。
|
||||||
|
|
||||||
|
## Skill 索引
|
||||||
|
|
||||||
|
- `header-comment-sync`:中文文件头、导出声明、复杂逻辑和风险边界注释。
|
||||||
|
- `chinese-commit-message`:中文提交信息格式。
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
---
|
||||||
|
name: chinese-commit-message
|
||||||
|
description: 在本仓库执行 git commit、整理提交说明或准备推送时使用,保持提交信息使用英文类型前缀加中文描述。
|
||||||
|
---
|
||||||
|
|
||||||
|
# 中文提交信息
|
||||||
|
|
||||||
|
## 何时使用
|
||||||
|
|
||||||
|
- 用户要求提交、推送或整理 commit message。
|
||||||
|
- 你准备执行 `git commit`。
|
||||||
|
- 需要总结当前 diff 的提交说明。
|
||||||
|
|
||||||
|
## 核心要求
|
||||||
|
|
||||||
|
1. 提交信息必须使用英文 type 加中文摘要,例如 `feat: 补齐后台 agent skill 规范`。
|
||||||
|
2. 常用 type 优先使用 `feat`、`fix`、`refactor`、`docs`、`chore`。
|
||||||
|
3. 不强制 scope;只有模块边界非常清楚时才使用 `feat(router): ...`。
|
||||||
|
4. 标题部分使用简洁中文,直接说明改动,不写“修改一下”“更新代码”这类空泛描述。
|
||||||
|
5. 非 trivial 改动建议补中文正文,用扁平 bullet 按文件或内容块说明关键动作。
|
||||||
|
6. 正文与标题之间空一行;正文每条 bullet 对应一个独立文件或明确内容块。
|
||||||
|
7. 如果用户指定提交信息,优先尊重用户原意,只做必要格式整理。
|
||||||
|
|
||||||
|
## 触达文件补齐
|
||||||
|
|
||||||
|
- 不要求为了提交信息单独全仓扫描。
|
||||||
|
- 提交前如果 diff 中的触达文件明显违反对应 skill 的触达补齐要求,应先修正当前相关链路,再整理 commit message。
|
||||||
|
- commit 正文应概括本次触达补齐,例如“补齐触达文件注释”“同步 README 目录说明”。
|
||||||
|
|
||||||
|
## 推荐结构
|
||||||
|
|
||||||
|
```text
|
||||||
|
feat: 补齐后台 agent skill 规范
|
||||||
|
|
||||||
|
- .agents/README.md: 新增项目级 skill 索引和使用入口
|
||||||
|
- .agents/skills: 补充中文提交与注释同步规则
|
||||||
|
- AGENTS.md: 增加本地 skill 入口
|
||||||
|
```
|
||||||
|
|
||||||
|
## 不推荐写法
|
||||||
|
|
||||||
|
- `新增规则`
|
||||||
|
- `feat: add skills`
|
||||||
|
- `fix bug`
|
||||||
|
- `update`
|
||||||
|
- 复杂改动只有标题没有正文。
|
||||||
|
- 正文写成长段流水账或嵌套列表。
|
||||||
|
|
||||||
|
## 落地检查
|
||||||
|
|
||||||
|
- type 是否合理。
|
||||||
|
- 中文摘要是否覆盖主改动。
|
||||||
|
- 是否需要正文。
|
||||||
|
- 正文是否按文件或内容块归纳。
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
---
|
||||||
|
name: header-comment-sync
|
||||||
|
description: 在本仓库创建或修改 ts、tsx、js、jsx、mjs、cjs、vue、astro 文件时使用,保持中文文件头、导出声明、复杂逻辑和风险边界注释准确。
|
||||||
|
---
|
||||||
|
|
||||||
|
# 注释规范与同步
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
让下一次进入文件的维护者能快速理解当前职责、关键约束和风险边界。注释解释“为什么”和“边界”,不复述代码表面行为。
|
||||||
|
|
||||||
|
## 适用场景
|
||||||
|
|
||||||
|
- 新增或修改页面、组件、Hook、store、service、utils、router、类型、配置或脚本文件。
|
||||||
|
- 修改鉴权、权限菜单、后端协议转换、复杂表格列、批量操作或状态流转逻辑。
|
||||||
|
- 发现旧注释与当前代码行为不一致。
|
||||||
|
|
||||||
|
## 核心原则
|
||||||
|
|
||||||
|
- 注释使用中文,描述当前事实。
|
||||||
|
- 简单局部变量不强行注释。
|
||||||
|
- 文件头说明文件当前职责,1 到 2 行即可。
|
||||||
|
- 导出的函数、组件、Hook、类型、配置对象、store action、service method 应有用途说明。
|
||||||
|
- 复杂兼容逻辑、后端协议、竞态锁、轮询清理、权限映射和金额精度需要短注释。
|
||||||
|
- TODO/FIXME 必须说明触发条件、剩余动作和可删除条件。
|
||||||
|
|
||||||
|
## 文件头规则
|
||||||
|
|
||||||
|
- 每个适用文件都要有准确文件头。
|
||||||
|
- 如果文件必须以 `"use client"` 或 `"use server"` 开头,文件头注释放在指令之后、导入之前。
|
||||||
|
- Vue 文件不为了补头注释重排模板结构;在 `<script>` 或 `<script setup>` 顶部补当前职责说明。
|
||||||
|
- 文件头不要写“本文件用于...”这类空话,直接说明业务角色。
|
||||||
|
|
||||||
|
```ts
|
||||||
|
/**
|
||||||
|
* 员工管理页面配置,集中维护筛选、表格列、弹窗状态和员工操作入口。
|
||||||
|
*/
|
||||||
|
```
|
||||||
|
|
||||||
|
## JSDoc 与局部注释
|
||||||
|
|
||||||
|
- 导出的函数、类、组件、Hook、类型、常量和配置对象优先使用 JSDoc。
|
||||||
|
- 非导出但复杂的解析、格式化、请求构造、状态派生、回调工厂也要补。
|
||||||
|
- 只在局部逻辑确实有约束时使用行内注释,例如竞态锁、兼容字段、后端协议和临时迁移。
|
||||||
|
- 不要在每一行、每个模板片段、每个短生命周期变量上堆注释。
|
||||||
|
|
||||||
|
## 触达文件补齐
|
||||||
|
|
||||||
|
- 不要求为了注释规范单独全仓扫描。
|
||||||
|
- 只要本次任务修改了适用文件,就顺手补齐明显缺失或过时的文件头、导出声明、共享类型、复杂回调和协议注释。
|
||||||
|
- 大文件可按触达区域优先,但同文件内裸露的顶层导出和共享类型应一起补。
|
||||||
|
- 如果一次补齐整个大文件会明显超出需求范围,至少补齐当前需求链路和顶层声明,并在交付说明里说明剩余范围。
|
||||||
|
|
||||||
|
## 不推荐的写法
|
||||||
|
|
||||||
|
- 注释只写“处理数据”“点击事件”这种无信息内容。
|
||||||
|
- 注释描述旧方案,和当前代码矛盾。
|
||||||
|
- 为每个模板片段、简单赋值、短生命周期变量写注释。
|
||||||
|
- 用英文口号、emoji 或情绪化标记替代项目内中文说明。
|
||||||
|
|
||||||
|
## 落地检查
|
||||||
|
|
||||||
|
- 修改后的文件头是否准确。
|
||||||
|
- 新增/修改的导出声明是否有必要说明。
|
||||||
|
- 复杂逻辑是否解释了约束而不是复述代码。
|
||||||
|
- 旧注释是否仍然可信。
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# develop 分支测试环境构建配置
|
||||||
|
VITE_APP_ENV = "develop"
|
||||||
|
VITE_APP_ENV_LABEL = "测试环境"
|
||||||
|
|
||||||
|
VITE_PUBLIC_PATH = /
|
||||||
|
|
||||||
|
VITE_ROUTER_HISTORY = "hash"
|
||||||
|
|
||||||
|
VITE_CDN = false
|
||||||
|
|
||||||
|
VITE_COMPRESSION = "none"
|
||||||
|
|
||||||
|
# 本地调试 develop mode 时的后端代理目标;线上测试环境由 Nginx 将 /api 代理到测试后端。
|
||||||
|
VITE_API_PROXY_TARGET = "http://localhost:3501"
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
# 平台本地运行端口号
|
# 平台本地运行端口号
|
||||||
VITE_PORT = 8848
|
VITE_PORT = 8848
|
||||||
|
|
||||||
|
VITE_APP_ENV = "local"
|
||||||
|
VITE_APP_ENV_LABEL = "本地环境"
|
||||||
|
|
||||||
# 开发环境读取配置文件路径
|
# 开发环境读取配置文件路径
|
||||||
VITE_PUBLIC_PATH = /
|
VITE_PUBLIC_PATH = /
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
# 线上环境平台打包路径
|
# 线上环境平台打包路径
|
||||||
|
VITE_APP_ENV = "production"
|
||||||
VITE_APP_ENV_LABEL = "生产环境"
|
VITE_APP_ENV_LABEL = "生产环境"
|
||||||
|
|
||||||
VITE_PUBLIC_PATH = /
|
VITE_PUBLIC_PATH = /
|
||||||
|
|||||||
+4
-1
@@ -1,7 +1,10 @@
|
|||||||
# 预发布也需要生产环境的行为
|
# 预发布当前按 develop/测试环境标识
|
||||||
# https://cn.vitejs.dev/guide/env-and-mode.html#modes
|
# https://cn.vitejs.dev/guide/env-and-mode.html#modes
|
||||||
# NODE_ENV = development
|
# NODE_ENV = development
|
||||||
|
|
||||||
|
VITE_APP_ENV = "develop"
|
||||||
|
VITE_APP_ENV_LABEL = "测试环境"
|
||||||
|
|
||||||
VITE_PUBLIC_PATH = /
|
VITE_PUBLIC_PATH = /
|
||||||
|
|
||||||
# 预发布环境路由历史模式(Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数")
|
# 预发布环境路由历史模式(Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数")
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
# 测试环境构建配置
|
# 测试环境构建配置
|
||||||
|
VITE_APP_ENV = "develop"
|
||||||
VITE_APP_ENV_LABEL = "测试环境"
|
VITE_APP_ENV_LABEL = "测试环境"
|
||||||
|
|
||||||
VITE_PUBLIC_PATH = /
|
VITE_PUBLIC_PATH = /
|
||||||
|
|||||||
@@ -1 +1,13 @@
|
|||||||
@RTK.md
|
@RTK.md
|
||||||
|
|
||||||
|
# role-admin Agent 入口
|
||||||
|
|
||||||
|
## 必用 Skill
|
||||||
|
|
||||||
|
- 创建或修改 `ts`、`tsx`、`js`、`jsx`、`mjs`、`cjs`、`vue`、`astro` 文件时,使用 `./.agents/skills/header-comment-sync/SKILL.md`。
|
||||||
|
- 执行 `git commit`、整理提交说明或准备推送时,使用 `./.agents/skills/chinese-commit-message/SKILL.md`。
|
||||||
|
|
||||||
|
## 本地规则
|
||||||
|
|
||||||
|
- 先读 `RTK.md`,再按任务读取 `.agents/README.md` 中匹配的 skill。
|
||||||
|
- skill 的触达文件补齐只处理本次修改相关文件,不为单次任务全仓扫描。
|
||||||
|
|||||||
Vendored
-122
@@ -1,122 +0,0 @@
|
|||||||
def isProductionDeploy() {
|
|
||||||
return params.DEPLOY_ENV == "production"
|
|
||||||
}
|
|
||||||
|
|
||||||
def isTestDeploy() {
|
|
||||||
def branch = env.BRANCH_NAME ?: ""
|
|
||||||
return params.DEPLOY_ENV == "test" && !params.SKIP_DEPLOY && branch == env.TEST_BRANCH
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonCPS
|
|
||||||
boolean isUserTriggeredBuild() {
|
|
||||||
return currentBuild.rawBuild.getCauses().any { cause ->
|
|
||||||
cause.class.name == "hudson.model.Cause\$UserIdCause"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pipeline {
|
|
||||||
agent any
|
|
||||||
|
|
||||||
options {
|
|
||||||
timestamps()
|
|
||||||
disableConcurrentBuilds()
|
|
||||||
buildDiscarder(logRotator(numToKeepStr: "30", artifactNumToKeepStr: "10"))
|
|
||||||
}
|
|
||||||
|
|
||||||
parameters {
|
|
||||||
choice(
|
|
||||||
name: "DEPLOY_ENV",
|
|
||||||
choices: ["test", "production"],
|
|
||||||
description: "test: develop 合并后自动部署测试环境;production: 仅允许手动输入 Tag 部署"
|
|
||||||
)
|
|
||||||
string(name: "RELEASE_TAG", defaultValue: "", description: "生产部署必填,必须是 Gitea 中已存在的 Tag")
|
|
||||||
booleanParam(name: "SKIP_DEPLOY", defaultValue: false, description: "只构建检查,不执行部署")
|
|
||||||
}
|
|
||||||
|
|
||||||
environment {
|
|
||||||
PROJECT_NAME = "role-admin"
|
|
||||||
TEST_BRANCH = "develop"
|
|
||||||
DEPLOY_BASE_DIR = "/srv/www"
|
|
||||||
}
|
|
||||||
|
|
||||||
stages {
|
|
||||||
stage("Validate deploy policy") {
|
|
||||||
steps {
|
|
||||||
script {
|
|
||||||
if (isProductionDeploy()) {
|
|
||||||
if (!isUserTriggeredBuild()) {
|
|
||||||
error("生产环境禁止自动触发,只能在 Jenkins 手动 Build With Parameters。")
|
|
||||||
}
|
|
||||||
if (!params.RELEASE_TAG?.trim()) {
|
|
||||||
error("生产环境部署必须填写 RELEASE_TAG,且只能从项目 Tag 部署。")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (params.DEPLOY_ENV == "test" && env.BRANCH_NAME && env.BRANCH_NAME != env.TEST_BRANCH) {
|
|
||||||
echo "当前分支 ${env.BRANCH_NAME} 不是 ${env.TEST_BRANCH},本次只构建检查,不自动部署测试环境。"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stage("Checkout production tag") {
|
|
||||||
when {
|
|
||||||
expression { isProductionDeploy() }
|
|
||||||
}
|
|
||||||
steps {
|
|
||||||
sh '''
|
|
||||||
set -euo pipefail
|
|
||||||
git fetch --tags --force origin '+refs/tags/*:refs/tags/*'
|
|
||||||
tag_commit="$(git rev-parse -q --verify "refs/tags/${RELEASE_TAG}^{commit}")"
|
|
||||||
if [ -z "${tag_commit}" ]; then
|
|
||||||
echo "Tag not found: ${RELEASE_TAG}" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
git checkout -f "${tag_commit}"
|
|
||||||
git log -1 --oneline
|
|
||||||
'''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stage("Install") {
|
|
||||||
steps {
|
|
||||||
sh '''
|
|
||||||
corepack enable || true
|
|
||||||
pnpm install --frozen-lockfile
|
|
||||||
'''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stage("Verify") {
|
|
||||||
steps {
|
|
||||||
sh "pnpm typecheck"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stage("Build") {
|
|
||||||
steps {
|
|
||||||
script {
|
|
||||||
sh isProductionDeploy() ? "pnpm build:prod" : "pnpm build:test"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stage("Deploy test") {
|
|
||||||
when {
|
|
||||||
expression { isTestDeploy() }
|
|
||||||
}
|
|
||||||
steps {
|
|
||||||
sh "bash deploy/jenkins/deploy-static.sh test dist"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stage("Deploy production") {
|
|
||||||
when {
|
|
||||||
expression { isProductionDeploy() && !params.SKIP_DEPLOY }
|
|
||||||
}
|
|
||||||
steps {
|
|
||||||
sh "bash deploy/jenkins/deploy-static.sh production dist"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
pipeline {
|
|
||||||
agent any
|
|
||||||
|
|
||||||
parameters {
|
|
||||||
gitParameter(
|
|
||||||
name: 'RELEASE_TAG',
|
|
||||||
type: 'PT_TAG',
|
|
||||||
tagFilter: 'v*',
|
|
||||||
branchFilter: 'origin/(.*)',
|
|
||||||
sortMode: 'DESCENDING_SMART',
|
|
||||||
selectedValue: 'TOP',
|
|
||||||
useRepository: 'http://127.0.0.1:3001/my-project/role-admin.git',
|
|
||||||
quickFilterEnabled: false,
|
|
||||||
listSize: '10',
|
|
||||||
requiredParameter: true,
|
|
||||||
description: '请选择要部署到生产环境的 Git Tag。列表自动来自当前项目仓库,生产只能从 Tag 发布。'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
environment {
|
|
||||||
CI = 'true'
|
|
||||||
}
|
|
||||||
|
|
||||||
options {
|
|
||||||
timestamps()
|
|
||||||
disableConcurrentBuilds()
|
|
||||||
buildDiscarder(logRotator(numToKeepStr: '10'))
|
|
||||||
}
|
|
||||||
|
|
||||||
stages {
|
|
||||||
stage('Checkout Tag') {
|
|
||||||
steps {
|
|
||||||
checkout([
|
|
||||||
$class: 'GitSCM',
|
|
||||||
branches: [[name: '*/master']],
|
|
||||||
userRemoteConfigs: [[
|
|
||||||
url: 'http://127.0.0.1:3001/my-project/role-admin.git'
|
|
||||||
]]
|
|
||||||
])
|
|
||||||
sh '''
|
|
||||||
set -eu
|
|
||||||
test -n "$RELEASE_TAG"
|
|
||||||
NORMALIZED_RELEASE_TAG="$(printf '%s' "$RELEASE_TAG" | sed 's/\\^{}$//')"
|
|
||||||
case "$NORMALIZED_RELEASE_TAG" in
|
|
||||||
v[0-9A-Za-z._-]*) ;;
|
|
||||||
*) echo "Invalid release tag: $NORMALIZED_RELEASE_TAG"; exit 2 ;;
|
|
||||||
esac
|
|
||||||
if ! git ls-remote --exit-code --tags origin "refs/tags/$NORMALIZED_RELEASE_TAG" >/dev/null; then
|
|
||||||
echo "Release tag does not exist in repository: $NORMALIZED_RELEASE_TAG"
|
|
||||||
exit 3
|
|
||||||
fi
|
|
||||||
echo "Deploying production tag: $NORMALIZED_RELEASE_TAG"
|
|
||||||
git fetch --tags --force
|
|
||||||
git checkout -f "refs/tags/$NORMALIZED_RELEASE_TAG"
|
|
||||||
git describe --tags --exact-match HEAD
|
|
||||||
'''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stage('Env') {
|
|
||||||
steps {
|
|
||||||
sh '''
|
|
||||||
git --version
|
|
||||||
node -v
|
|
||||||
corepack --version
|
|
||||||
corepack enable
|
|
||||||
corepack prepare pnpm@11.5.0 --activate
|
|
||||||
pnpm -v
|
|
||||||
'''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stage('Install') {
|
|
||||||
steps {
|
|
||||||
sh '''
|
|
||||||
pnpm install --frozen-lockfile
|
|
||||||
'''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stage('Check') {
|
|
||||||
steps {
|
|
||||||
sh '''
|
|
||||||
pnpm typecheck
|
|
||||||
'''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stage('Build') {
|
|
||||||
steps {
|
|
||||||
sh '''
|
|
||||||
pnpm build:prod
|
|
||||||
test -f dist/index.html
|
|
||||||
'''
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stage('Deploy Production') {
|
|
||||||
steps {
|
|
||||||
sh 'sudo /usr/local/bin/deploy-role-admin-from-jenkins "$WORKSPACE"'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -47,16 +47,13 @@ http://localhost:8848/
|
|||||||
|
|
||||||
```text
|
```text
|
||||||
.
|
.
|
||||||
├── .codex/skills/ # 仓库内协作 skill,约束结构变更后同步 README
|
├── .agents/ # 当前项目 Agent skill 索引与中文提交、注释同步规则
|
||||||
|
├── .codex/skills/ # 旧版仓库内协作 skill,保留 README 同步规则
|
||||||
├── .husky/ # Git hooks,提交前执行 lint-staged,提交信息走 commitlint
|
├── .husky/ # Git hooks,提交前执行 lint-staged,提交信息走 commitlint
|
||||||
├── .vscode/ # VS Code 推荐配置与 Vue 代码片段
|
├── .vscode/ # VS Code 推荐配置与 Vue 代码片段
|
||||||
├── build/ # Vite 插件、CDN、压缩、构建信息与工具函数
|
├── build/ # Vite 插件、CDN、压缩、构建信息与工具函数
|
||||||
├── docs/ # 项目需求和后台扩展说明
|
├── docs/ # 项目需求和后台扩展说明
|
||||||
│ ├── C_APP_ADMIN_REQUIREMENTS.md # C 端正式版管理后台需求文档
|
│ └── C_APP_ADMIN_REQUIREMENTS.md # C 端正式版管理后台需求文档
|
||||||
│ └── ENVIRONMENT_DEPLOYMENT.md # 测试/生产环境拆分与 Jenkins 规则
|
|
||||||
├── deploy/ # Jenkins 部署脚本
|
|
||||||
│ └── jenkins/
|
|
||||||
│ └── deploy-static.sh # 静态产物部署脚本
|
|
||||||
├── public/ # 静态资源与运行时平台配置
|
├── public/ # 静态资源与运行时平台配置
|
||||||
├── src/ # 前端源码
|
├── src/ # 前端源码
|
||||||
│ ├── api/ # HTTP API 封装,业务接口集中在 access.ts
|
│ ├── api/ # HTTP API 封装,业务接口集中在 access.ts
|
||||||
@@ -72,9 +69,8 @@ http://localhost:8848/
|
|||||||
│ ├── utils/ # 鉴权、HTTP、缓存、消息、进度条、树处理等工具
|
│ ├── utils/ # 鉴权、HTTP、缓存、消息、进度条、树处理等工具
|
||||||
│ └── views/ # 页面视图,当前业务页在 employees、roles、stores、permissions 和员工工作台运营模块
|
│ └── views/ # 页面视图,当前业务页在 employees、roles、stores、permissions 和员工工作台运营模块
|
||||||
├── types/ # 全局类型声明、组件声明、路由声明和 Vue shim
|
├── types/ # 全局类型声明、组件声明、路由声明和 Vue shim
|
||||||
├── AGENTS.md # Codex/Agent 入口规则,转到 RTK.md
|
├── AGENTS.md # Codex/Agent 入口规则,转到 RTK.md 并登记本地 skill
|
||||||
├── RTK.md # 本仓库协作规则与 README 同步要求
|
├── RTK.md # 本仓库协作规则与 README 同步要求
|
||||||
├── Jenkinsfile # Jenkins 测试自动部署、生产手动 Tag 部署规则
|
|
||||||
├── Dockerfile # 容器构建入口
|
├── Dockerfile # 容器构建入口
|
||||||
├── package.json # 依赖、脚本、engines 与 pnpm preinstall 限制
|
├── package.json # 依赖、脚本、engines 与 pnpm preinstall 限制
|
||||||
├── pnpm-lock.yaml # pnpm 锁文件
|
├── pnpm-lock.yaml # pnpm 锁文件
|
||||||
@@ -91,15 +87,16 @@ http://localhost:8848/
|
|||||||
| `pnpm dev` | 启动 Vite 开发服务,默认端口来自 `.env.development` 的 `VITE_PORT=8848` | 本地开发 |
|
| `pnpm dev` | 启动 Vite 开发服务,默认端口来自 `.env.development` 的 `VITE_PORT=8848` | 本地开发 |
|
||||||
| `pnpm serve` | `pnpm dev` 的别名 | 兼容习惯命令 |
|
| `pnpm serve` | `pnpm dev` 的别名 | 兼容习惯命令 |
|
||||||
| `pnpm build` | 清理 `dist` 后执行生产构建,内存上限设为 8192MB | 发布前打包 |
|
| `pnpm build` | 清理 `dist` 后执行生产构建,内存上限设为 8192MB | 发布前打包 |
|
||||||
| `pnpm build:test` | 使用 `test` mode 构建测试环境静态产物 | Jenkins 测试部署 |
|
| `pnpm build:develop` | 使用 `develop` mode 构建测试环境静态产物 | 测试环境构建 |
|
||||||
| `pnpm build:prod` | `pnpm build` 的生产构建别名 | Jenkins 生产部署 |
|
| `pnpm build:test` | `pnpm build:develop` 的兼容命令 | 旧测试构建入口 |
|
||||||
|
| `pnpm build:prod` | `pnpm build` 的生产构建别名 | 生产环境构建 |
|
||||||
| `pnpm build:staging` | 使用 `staging` mode 构建 | 预发布环境验证 |
|
| `pnpm build:staging` | 使用 `staging` mode 构建 | 预发布环境验证 |
|
||||||
| `pnpm report` | 构建并打开 Rollup 可视化分析报告 | 分析包体积 |
|
| `pnpm report` | 构建并打开 Rollup 可视化分析报告 | 分析包体积 |
|
||||||
| `pnpm preview` | 预览已有 `dist` 构建产物 | 构建后本地验收 |
|
| `pnpm preview` | 预览已有 `dist` 构建产物 | 构建后本地验收 |
|
||||||
| `pnpm preview:build` | 先构建再预览 | 一步完成打包验收 |
|
| `pnpm preview:build` | 先构建再预览 | 一步完成打包验收 |
|
||||||
| `pnpm typecheck` | 执行 `tsc --noEmit` 和 `vue-tsc --noEmit --skipLibCheck` | 类型检查 |
|
| `pnpm typecheck` | 执行 `tsc --noEmit` 和 `vue-tsc --noEmit --skipLibCheck` | 类型检查 |
|
||||||
| `pnpm lint` | 依次执行 ESLint、Prettier、Stylelint 自动修复 | 提交前整理代码 |
|
| `pnpm lint` | 依次执行 ESLint、Prettier、Stylelint 自动修复 | 提交前整理代码 |
|
||||||
| `pnpm lint:eslint` | 对 `src`、`build` 下 JS/TS/Vue 做 ESLint 修复 | 脚本或逻辑代码调整后 |
|
| `pnpm lint:eslint` | 对 `src`、`build` 下 JS/TS/Vue 做 ESLint 修复 | 脚本或逻辑代码调整后 |
|
||||||
| `pnpm lint:prettier` | 格式化 `src` 下常见源码与文档格式 | 样式统一 |
|
| `pnpm lint:prettier` | 格式化 `src` 下常见源码与文档格式 | 样式统一 |
|
||||||
| `pnpm lint:stylelint` | 修复 HTML/Vue/CSS/SCSS 样式规则 | 样式文件调整后 |
|
| `pnpm lint:stylelint` | 修复 HTML/Vue/CSS/SCSS 样式规则 | 样式文件调整后 |
|
||||||
| `pnpm svgo` | 递归压缩 SVG | SVG 资源变更后 |
|
| `pnpm svgo` | 递归压缩 SVG | SVG 资源变更后 |
|
||||||
@@ -123,9 +120,10 @@ http://localhost:8848/
|
|||||||
|
|
||||||
## 后端对接
|
## 后端对接
|
||||||
|
|
||||||
开发环境通过 Vite 代理把 `/api` 转发到 `VITE_API_PROXY_TARGET`,默认值为 `http://localhost:3500`。相关配置位于:
|
开发环境通过 Vite 代理把 `/api` 转发到 `VITE_API_PROXY_TARGET`,默认值为 `http://localhost:3500`。页面右上角环境标识按 `VITE_APP_ENV=local/develop/production` 显示“本地环境”“测试环境”“生产环境”。相关配置位于:
|
||||||
|
|
||||||
- `.env.development`
|
- `.env.development`
|
||||||
|
- `.env.develop`
|
||||||
- `vite.config.ts`
|
- `vite.config.ts`
|
||||||
|
|
||||||
当前已对接接口:
|
当前已对接接口:
|
||||||
@@ -183,20 +181,16 @@ http://localhost:8848/
|
|||||||
## 配置说明
|
## 配置说明
|
||||||
|
|
||||||
- `.env`: 全局默认配置,目前包含端口和是否隐藏首页。
|
- `.env`: 全局默认配置,目前包含端口和是否隐藏首页。
|
||||||
- `.env.development`: 开发环境端口、路由模式、API 代理目标。
|
- `.env.development`: 本地开发环境端口、路由模式、API 代理目标,`VITE_APP_ENV=local`,页面标识为“本地环境”。
|
||||||
- `.env.test`: 测试环境构建配置,Jenkins 测试部署使用;线上测试站点的 `/api` 应由 Nginx 代理到测试后端。
|
- `.env.develop`: develop/测试环境构建配置,`VITE_APP_ENV=develop`,页面标识为“测试环境”。
|
||||||
- `.env.staging`: 预发布构建配置,可开启 CDN。
|
- `.env.test`: 兼容旧测试 mode 的构建配置,语义同 develop;线上测试站点的 `/api` 应由 Nginx 代理到测试后端。
|
||||||
- `.env.production`: 生产构建配置。
|
- `.env.staging`: 预发布构建配置,当前按 develop/测试环境标识,可开启 CDN。
|
||||||
|
- `.env.production`: 生产构建配置,`VITE_APP_ENV=production`,页面标识为“生产环境”。
|
||||||
- `public/platform-config.json`: pure-admin 运行时平台配置,例如标题、布局、主题、标签页和菜单搜索历史。
|
- `public/platform-config.json`: pure-admin 运行时平台配置,例如标题、布局、主题、标签页和菜单搜索历史。
|
||||||
|
|
||||||
## Jenkins 环境规则
|
## 环境构建约定
|
||||||
|
|
||||||
流水线规则见 [docs/ENVIRONMENT_DEPLOYMENT.md](./docs/ENVIRONMENT_DEPLOYMENT.md)。
|
仓库只维护项目自身的构建脚本和环境变量文件,不维护流水线文件。测试、生产的触发规则和部署路径由外部 CI/CD 平台配置;测试、生产站点的 `/api` 必须分别代理到对应的 `access-manage`,避免环境串线。
|
||||||
|
|
||||||
- 测试环境:`develop` 合并后自动触发,执行 `pnpm build:test`,部署到 `/srv/www/test/role-admin/current`。
|
|
||||||
- 生产环境:禁止代码合并自动触发,只能在 Jenkins 手动选择 `DEPLOY_ENV=production` 并填写 Gitea 项目 Tag。
|
|
||||||
- 生产部署会先 checkout 到 `RELEASE_TAG` 对应的提交,再执行 `pnpm build:prod` 和部署。
|
|
||||||
- 测试、生产站点的 `/api` 必须分别代理到测试、生产 `access-manage`,避免环境串线。
|
|
||||||
|
|
||||||
## 协作与文档同步
|
## 协作与文档同步
|
||||||
|
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
deploy_env="${1:?Usage: deploy-static.sh test|production [artifact_dir]}"
|
|
||||||
artifact_dir="${2:-dist}"
|
|
||||||
|
|
||||||
case "${deploy_env}" in
|
|
||||||
test|production)
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo "Unknown deploy environment: ${deploy_env}" >&2
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
if [[ ! -d "${artifact_dir}" ]]; then
|
|
||||||
echo "Artifact directory not found: ${artifact_dir}" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
project_name="${PROJECT_NAME:-role-admin}"
|
|
||||||
base_dir="${DEPLOY_BASE_DIR:-/srv/www}"
|
|
||||||
target_dir="${DEPLOY_TARGET_DIR:-${base_dir}/${deploy_env}/${project_name}}"
|
|
||||||
deploy_remote="${DEPLOY_REMOTE:-}"
|
|
||||||
release_id="${BUILD_NUMBER:-manual}-$(git rev-parse --short=12 HEAD 2>/dev/null || date +%Y%m%d%H%M%S)"
|
|
||||||
|
|
||||||
remote_shell() {
|
|
||||||
if [[ -n "${deploy_remote}" ]]; then
|
|
||||||
ssh "${deploy_remote}" "$@"
|
|
||||||
else
|
|
||||||
bash -lc "$*"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
remote_copy() {
|
|
||||||
local source_dir="$1"
|
|
||||||
local dest_dir="$2"
|
|
||||||
|
|
||||||
if [[ -n "${deploy_remote}" ]]; then
|
|
||||||
rsync -az --delete "${source_dir}/" "${deploy_remote}:${dest_dir}/"
|
|
||||||
else
|
|
||||||
mkdir -p "${dest_dir}"
|
|
||||||
rsync -az --delete "${source_dir}/" "${dest_dir}/"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
release_dir="${target_dir}/releases/${release_id}"
|
|
||||||
|
|
||||||
remote_shell "mkdir -p '${release_dir}' '${target_dir}/logs'"
|
|
||||||
remote_copy "${artifact_dir}" "${release_dir}"
|
|
||||||
remote_shell "ln -sfn '${release_dir}' '${target_dir}/current'"
|
|
||||||
|
|
||||||
if [[ -n "${DEPLOY_POST_DEPLOY_CMD:-}" ]]; then
|
|
||||||
remote_shell "cd '${target_dir}/current' && ${DEPLOY_POST_DEPLOY_CMD}"
|
|
||||||
else
|
|
||||||
echo "Deployed ${project_name} ${deploy_env} to ${target_dir}/current"
|
|
||||||
fi
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
# 环境拆分与流水线规则
|
|
||||||
|
|
||||||
`role-admin` 是静态前端项目。测试环境和生产环境通过独立构建模式、独立部署目录和独立 Nginx `/api` 代理拆分。
|
|
||||||
|
|
||||||
## 环境约定
|
|
||||||
|
|
||||||
| 环境 | 触发方式 | 代码依据 | 构建命令 | 默认部署目录 |
|
|
||||||
| --- | --- | --- | --- | --- |
|
|
||||||
| 测试环境 | `develop` 合并后自动触发 Jenkins | `develop` 最新提交 | `pnpm build:test` | `/srv/www/test/role-admin/current` |
|
|
||||||
| 生产环境 | Jenkins 手动触发 | Gitea 项目 Tag | `pnpm build:prod` | `/srv/www/production/role-admin/current` |
|
|
||||||
|
|
||||||
生产环境禁止因代码合并自动部署。生产部署时必须在 Jenkins 参数中选择 `DEPLOY_ENV=production` 并填写已存在的 `RELEASE_TAG`。
|
|
||||||
|
|
||||||
## Jenkins 参数
|
|
||||||
|
|
||||||
| 参数 | 说明 |
|
|
||||||
| --- | --- |
|
|
||||||
| `DEPLOY_ENV` | `test` 或 `production`。默认 `test`。 |
|
|
||||||
| `RELEASE_TAG` | 生产环境必填,必须是 Gitea 仓库中已存在的 Tag。 |
|
|
||||||
| `SKIP_DEPLOY` | 为 `true` 时只执行安装、检查、构建,不部署。 |
|
|
||||||
|
|
||||||
`Jenkinsfile` 会强制校验:
|
|
||||||
|
|
||||||
- 测试环境只在 `develop` 分支自动部署。
|
|
||||||
- 生产环境必须手动触发。
|
|
||||||
- 生产环境必须填写 `RELEASE_TAG`。
|
|
||||||
- 生产环境构建会先 checkout 到该 Tag 对应的提交,再部署。
|
|
||||||
|
|
||||||
## Nginx 拆分要求
|
|
||||||
|
|
||||||
静态资源部署目录默认如下:
|
|
||||||
|
|
||||||
```text
|
|
||||||
/srv/www/test/role-admin/current
|
|
||||||
/srv/www/production/role-admin/current
|
|
||||||
```
|
|
||||||
|
|
||||||
两套站点的 `/api` 必须分别代理到对应后端:
|
|
||||||
|
|
||||||
```text
|
|
||||||
测试 role-admin /api -> 测试 access-manage
|
|
||||||
生产 role-admin /api -> 生产 access-manage
|
|
||||||
```
|
|
||||||
|
|
||||||
不要让测试前端代理到生产后端,也不要让生产前端代理到测试后端。
|
|
||||||
|
|
||||||
## 生产发布流程
|
|
||||||
|
|
||||||
1. 在需要发布的提交上创建 Tag。
|
|
||||||
2. 推送 Tag 到 Gitea。
|
|
||||||
3. Jenkins 手动 Build With Parameters。
|
|
||||||
4. 选择 `DEPLOY_ENV=production`。
|
|
||||||
5. 填写 `RELEASE_TAG`。
|
|
||||||
6. 执行构建部署。
|
|
||||||
|
|
||||||
示例:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git tag -a v2026.06.05-1 -m "role-admin production release 2026-06-05"
|
|
||||||
git push origin v2026.06.05-1
|
|
||||||
```
|
|
||||||
|
|
||||||
## 部署脚本
|
|
||||||
|
|
||||||
`deploy/jenkins/deploy-static.sh` 会把 `dist/` 发布到:
|
|
||||||
|
|
||||||
```text
|
|
||||||
${DEPLOY_BASE_DIR}/${DEPLOY_ENV}/role-admin/releases/<build>-<commit>
|
|
||||||
```
|
|
||||||
|
|
||||||
并更新:
|
|
||||||
|
|
||||||
```text
|
|
||||||
${DEPLOY_BASE_DIR}/${DEPLOY_ENV}/role-admin/current
|
|
||||||
```
|
|
||||||
|
|
||||||
如果 Jenkins 不在目标服务器上运行,可以配置:
|
|
||||||
|
|
||||||
```text
|
|
||||||
DEPLOY_REMOTE=user@server
|
|
||||||
```
|
|
||||||
+2
-1
@@ -7,7 +7,8 @@
|
|||||||
"dev": "NODE_OPTIONS=--max-old-space-size=4096 vite",
|
"dev": "NODE_OPTIONS=--max-old-space-size=4096 vite",
|
||||||
"serve": "pnpm dev",
|
"serve": "pnpm dev",
|
||||||
"build": "rimraf dist && NODE_OPTIONS=--max-old-space-size=8192 vite build",
|
"build": "rimraf dist && NODE_OPTIONS=--max-old-space-size=8192 vite build",
|
||||||
"build:test": "rimraf dist && vite build --mode test",
|
"build:develop": "rimraf dist && vite build --mode develop",
|
||||||
|
"build:test": "pnpm build:develop",
|
||||||
"build:prod": "pnpm build",
|
"build:prod": "pnpm build",
|
||||||
"build:staging": "rimraf dist && vite build --mode staging",
|
"build:staging": "rimraf dist && vite build --mode staging",
|
||||||
"report": "rimraf dist && vite build",
|
"report": "rimraf dist && vite build",
|
||||||
|
|||||||
+49
@@ -1,5 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-config-provider :locale="currentLocale">
|
<el-config-provider :locale="currentLocale">
|
||||||
|
<div class="environment-badge" :aria-label="`当前环境:${appEnvLabel}`">
|
||||||
|
{{ appEnvLabel }}
|
||||||
|
</div>
|
||||||
<router-view />
|
<router-view />
|
||||||
<ReDialog />
|
<ReDialog />
|
||||||
</el-config-provider>
|
</el-config-provider>
|
||||||
@@ -17,6 +20,27 @@ export default defineComponent({
|
|||||||
[ElConfigProvider.name]: ElConfigProvider,
|
[ElConfigProvider.name]: ElConfigProvider,
|
||||||
ReDialog
|
ReDialog
|
||||||
},
|
},
|
||||||
|
data() {
|
||||||
|
const appEnv =
|
||||||
|
import.meta.env.VITE_APP_ENV ||
|
||||||
|
(import.meta.env.MODE === "development"
|
||||||
|
? "local"
|
||||||
|
: import.meta.env.MODE === "production"
|
||||||
|
? "production"
|
||||||
|
: "develop");
|
||||||
|
const defaultLabelMap = {
|
||||||
|
local: "本地环境",
|
||||||
|
develop: "测试环境",
|
||||||
|
production: "生产环境"
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
return {
|
||||||
|
appEnvLabel:
|
||||||
|
import.meta.env.VITE_APP_ENV_LABEL ||
|
||||||
|
defaultLabelMap[appEnv as keyof typeof defaultLabelMap] ||
|
||||||
|
"本地环境"
|
||||||
|
};
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
currentLocale() {
|
currentLocale() {
|
||||||
return zhCn;
|
return zhCn;
|
||||||
@@ -24,3 +48,28 @@ export default defineComponent({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.environment-badge {
|
||||||
|
position: fixed;
|
||||||
|
top: 72px;
|
||||||
|
right: 16px;
|
||||||
|
z-index: 1000;
|
||||||
|
padding: 8px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 750;
|
||||||
|
line-height: 1;
|
||||||
|
color: #fff;
|
||||||
|
letter-spacing: 0;
|
||||||
|
background: rgb(20 20 24 / 86%);
|
||||||
|
border-radius: 999px;
|
||||||
|
box-shadow: 0 10px 24px rgb(0 0 0 / 16%);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (width <= 768px) {
|
||||||
|
.environment-badge {
|
||||||
|
top: 16px;
|
||||||
|
right: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -9,8 +9,6 @@ import LaySidebarTopCollapse from "../lay-sidebar/components/SidebarTopCollapse.
|
|||||||
import LogoutCircleRLine from "~icons/ri/logout-circle-r-line";
|
import LogoutCircleRLine from "~icons/ri/logout-circle-r-line";
|
||||||
import Setting from "~icons/ri/settings-3-line";
|
import Setting from "~icons/ri/settings-3-line";
|
||||||
|
|
||||||
const appEnvLabel = import.meta.env.VITE_APP_ENV_LABEL || "生产环境";
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
layout,
|
layout,
|
||||||
device,
|
device,
|
||||||
@@ -41,9 +39,6 @@ const {
|
|||||||
<LayNavMix v-if="layout === 'mix'" />
|
<LayNavMix v-if="layout === 'mix'" />
|
||||||
|
|
||||||
<div v-if="layout === 'vertical'" class="vertical-header-right">
|
<div v-if="layout === 'vertical'" class="vertical-header-right">
|
||||||
<span class="environment-badge" :title="`当前环境:${appEnvLabel}`">
|
|
||||||
{{ appEnvLabel }}
|
|
||||||
</span>
|
|
||||||
<!-- 菜单搜索 -->
|
<!-- 菜单搜索 -->
|
||||||
<LaySearch id="header-search" />
|
<LaySearch id="header-search" />
|
||||||
<!-- 全屏 -->
|
<!-- 全屏 -->
|
||||||
@@ -98,20 +93,6 @@ const {
|
|||||||
height: 48px;
|
height: 48px;
|
||||||
color: #000000d9;
|
color: #000000d9;
|
||||||
|
|
||||||
.environment-badge {
|
|
||||||
padding: 5px 9px;
|
|
||||||
margin-right: 8px;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 1;
|
|
||||||
color: #ffffff;
|
|
||||||
letter-spacing: 0;
|
|
||||||
white-space: nowrap;
|
|
||||||
background: rgb(24 24 27 / 86%);
|
|
||||||
border-radius: 999px;
|
|
||||||
box-shadow: 0 8px 20px rgb(0 0 0 / 14%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-dropdown-link {
|
.el-dropdown-link {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
Vendored
+1
@@ -70,6 +70,7 @@ declare global {
|
|||||||
VITE_CDN: boolean;
|
VITE_CDN: boolean;
|
||||||
VITE_HIDE_HOME: string;
|
VITE_HIDE_HOME: string;
|
||||||
VITE_COMPRESSION: ViteCompression;
|
VITE_COMPRESSION: ViteCompression;
|
||||||
|
VITE_APP_ENV?: "local" | "develop" | "production";
|
||||||
VITE_APP_ENV_LABEL?: string;
|
VITE_APP_ENV_LABEL?: string;
|
||||||
VITE_API_PROXY_TARGET?: string;
|
VITE_API_PROXY_TARGET?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user