Compare commits
78
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93e1c1b8c9 | ||
|
|
05b977cc33 | ||
|
|
5741ebbf17 | ||
|
|
836600b811 | ||
|
|
4841b0bc24 | ||
|
|
e1e0055915 | ||
|
|
cff864d582 | ||
|
|
c71a12f6f4 | ||
|
|
7d39e3c684 | ||
|
|
6b3620c3c4 | ||
|
|
8e1a02ace0 | ||
|
|
982346045d | ||
|
|
48b09ee8cf | ||
|
|
ceb7905697 | ||
|
|
979a502373 | ||
|
|
3e779e4efd | ||
|
|
481c231dd9 | ||
|
|
9e427d485f | ||
|
|
58380170b0 | ||
|
|
e583bfed5f | ||
|
|
923a350cde | ||
|
|
0710ea55f3 | ||
|
|
50f2b177b1 | ||
|
|
0e814840b1 | ||
|
|
c1e03f94be | ||
|
|
c8fe571be1 | ||
|
|
45507dee8d | ||
|
|
1737f37d82 | ||
|
|
d283d223c0 | ||
|
|
f78960fcce | ||
|
|
8e9435ee7d | ||
|
|
83e535d12a | ||
|
|
2b5dda7c71 | ||
|
|
685d4fd1bd | ||
|
|
f281dcf1b3 | ||
|
|
5db565b15d | ||
|
|
8a84f24baf | ||
|
|
046400ae4a | ||
|
|
122aa21323 | ||
|
|
9f92d113f2 | ||
|
|
f6f4d264ee | ||
|
|
f023c7d416 | ||
|
|
bb03a5ad84 | ||
|
|
73da0bbd85 | ||
|
|
c8bf4a8fd4 | ||
|
|
6f522303a0 | ||
|
|
f5b399a52f | ||
|
|
e77db70278 | ||
|
|
fe1ae954e3 | ||
|
|
40fbd6ec7b | ||
|
|
ee3bb1c286 | ||
|
|
898e439b84 | ||
|
|
805d7ddc52 | ||
|
|
ba90bf9bfb | ||
|
|
3e8312ea58 | ||
|
|
a97b8cc026 | ||
|
|
e5c5101450 | ||
|
|
87970487af | ||
|
|
ada741f53d | ||
|
|
5494190330 | ||
|
|
29465ab4ed | ||
|
|
8f3447e528 | ||
|
|
97fbeda508 | ||
|
|
5c087f4db9 | ||
|
|
54db082ffb | ||
|
|
3ec9ea6717 | ||
|
|
db313f4aa6 | ||
|
|
1148458ca1 | ||
|
|
4564aa894d | ||
|
|
1fd0b8b100 | ||
|
|
2fa1416c3e | ||
|
|
4509323ba8 | ||
|
|
8ce43ccd4c | ||
|
|
bac070b60b | ||
|
|
685c912313 | ||
|
|
0a2e3e0b81 | ||
|
|
f037d243cd | ||
|
|
44c2c46a68 |
No files matched your search
@@ -8,13 +8,13 @@ VITE_API_BASE_PATH=
|
|||||||
VITE_BASE_PATH=/
|
VITE_BASE_PATH=/
|
||||||
|
|
||||||
# 标题
|
# 标题
|
||||||
VITE_APP_TITLE=ReelShort
|
VITE_APP_TITLE=FruitShorts
|
||||||
|
|
||||||
# 是否全量引入element-plus样式
|
# 是否全量引入element-plus样式
|
||||||
VITE_USE_ALL_ELEMENT_PLUS_STYLE=true
|
VITE_USE_ALL_ELEMENT_PLUS_STYLE=true
|
||||||
|
|
||||||
# 是否开启mock
|
# 是否开启mock
|
||||||
VITE_USE_MOCK=true
|
VITE_USE_MOCK=false
|
||||||
|
|
||||||
# 是否使用在线图标
|
# 是否使用在线图标
|
||||||
VITE_USE_ONLINE_ICON=true
|
VITE_USE_ONLINE_ICON=false
|
||||||
@@ -20,7 +20,7 @@ VITE_SOURCEMAP=true
|
|||||||
VITE_OUT_DIR=dist-dev
|
VITE_OUT_DIR=dist-dev
|
||||||
|
|
||||||
# 标题
|
# 标题
|
||||||
VITE_APP_TITLE=ElementAdmin
|
VITE_APP_TITLE=FruitShorts
|
||||||
|
|
||||||
# 是否包分析
|
# 是否包分析
|
||||||
VITE_USE_BUNDLE_ANALYZER=false
|
VITE_USE_BUNDLE_ANALYZER=false
|
||||||
@@ -29,10 +29,10 @@ VITE_USE_BUNDLE_ANALYZER=false
|
|||||||
VITE_USE_ALL_ELEMENT_PLUS_STYLE=false
|
VITE_USE_ALL_ELEMENT_PLUS_STYLE=false
|
||||||
|
|
||||||
# 是否开启mock
|
# 是否开启mock
|
||||||
VITE_USE_MOCK=true
|
VITE_USE_MOCK=false
|
||||||
|
|
||||||
# 是否切割css
|
# 是否切割css
|
||||||
VITE_USE_CSS_SPLIT=true
|
VITE_USE_CSS_SPLIT=true
|
||||||
|
|
||||||
# 是否使用在线图标
|
# 是否使用在线图标
|
||||||
VITE_USE_ONLINE_ICON=true
|
VITE_USE_ONLINE_ICON=false
|
||||||
@@ -20,7 +20,7 @@ VITE_SOURCEMAP=false
|
|||||||
VITE_OUT_DIR=dist-pro
|
VITE_OUT_DIR=dist-pro
|
||||||
|
|
||||||
# 标题
|
# 标题
|
||||||
VITE_APP_TITLE=ElementAdmin
|
VITE_APP_TITLE=FruitShorts
|
||||||
|
|
||||||
# 是否包分析
|
# 是否包分析
|
||||||
VITE_USE_BUNDLE_ANALYZER=true
|
VITE_USE_BUNDLE_ANALYZER=true
|
||||||
@@ -29,10 +29,10 @@ VITE_USE_BUNDLE_ANALYZER=true
|
|||||||
VITE_USE_ALL_ELEMENT_PLUS_STYLE=false
|
VITE_USE_ALL_ELEMENT_PLUS_STYLE=false
|
||||||
|
|
||||||
# 是否开启mock
|
# 是否开启mock
|
||||||
VITE_USE_MOCK=true
|
VITE_USE_MOCK=false
|
||||||
|
|
||||||
# 是否切割css
|
# 是否切割css
|
||||||
VITE_USE_CSS_SPLIT=true
|
VITE_USE_CSS_SPLIT=true
|
||||||
|
|
||||||
# 是否使用在线图标
|
# 是否使用在线图标
|
||||||
VITE_USE_ONLINE_ICON=true
|
VITE_USE_ONLINE_ICON=false
|
||||||
@@ -20,7 +20,7 @@ VITE_SOURCEMAP=true
|
|||||||
VITE_OUT_DIR=dist-test
|
VITE_OUT_DIR=dist-test
|
||||||
|
|
||||||
# 标题
|
# 标题
|
||||||
VITE_APP_TITLE=ElementAdmin
|
VITE_APP_TITLE=FruitShorts
|
||||||
|
|
||||||
# 是否包分析
|
# 是否包分析
|
||||||
VITE_USE_BUNDLE_ANALYZER=false
|
VITE_USE_BUNDLE_ANALYZER=false
|
||||||
@@ -29,10 +29,10 @@ VITE_USE_BUNDLE_ANALYZER=false
|
|||||||
VITE_USE_ALL_ELEMENT_PLUS_STYLE=false
|
VITE_USE_ALL_ELEMENT_PLUS_STYLE=false
|
||||||
|
|
||||||
# 是否开启mock
|
# 是否开启mock
|
||||||
VITE_USE_MOCK=true
|
VITE_USE_MOCK=false
|
||||||
|
|
||||||
# 是否切割css
|
# 是否切割css
|
||||||
VITE_USE_CSS_SPLIT=false
|
VITE_USE_CSS_SPLIT=false
|
||||||
|
|
||||||
# 是否使用在线图标
|
# 是否使用在线图标
|
||||||
VITE_USE_ONLINE_ICON=true
|
VITE_USE_ONLINE_ICON=false
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
// @ts-check
|
// @ts-check
|
||||||
const { defineConfig } = require('eslint-define-config')
|
const { defineConfig } = require('eslint-define-config')
|
||||||
module.exports = defineConfig({
|
module.exports = defineConfig({
|
||||||
|
|||||||
@@ -7,3 +7,9 @@ dist-ssr
|
|||||||
*-lock.*
|
*-lock.*
|
||||||
pnpm-debug
|
pnpm-debug
|
||||||
stats.html
|
stats.html
|
||||||
|
*.apk
|
||||||
|
|
||||||
|
# 本机运行时产物(start-dev.bat / vite 日志)
|
||||||
|
start-dev.bat
|
||||||
|
dev.out.log
|
||||||
|
dev.err.log
|
||||||
@@ -1,4 +1,9 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
. "$(dirname "$0")/_/husky.sh"
|
. "$(dirname "$0")/_/husky.sh"
|
||||||
|
|
||||||
|
if grep -qiE "(co-authored-by|generated with|assisted by).*(cursor|claude|codex|gpt|copilot|ai)" "$1"; then
|
||||||
|
echo "commit message contains AI attribution, remove it before committing" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
pnpm commitlint --edit "$1"
|
pnpm commitlint --edit "$1"
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
'*.{js,jsx,ts,tsx}': ['eslint --fix', 'prettier --write'],
|
'*.{js,jsx,ts,tsx}': ['eslint --fix', 'prettier --write'],
|
||||||
'{!(package)*.json,*.code-snippets,.!(browserslist)*rc}': ['prettier --write--parser json'],
|
'{!(package)*.json,*.code-snippets,.!(browserslist)*rc}': ['prettier --write--parser json'],
|
||||||
|
|||||||
@@ -1,29 +1,38 @@
|
|||||||
# ReelShort 内容管理后台
|
# FruitShorts 内容管理后台
|
||||||
|
|
||||||
短剧内容管理后台。界面和工程结构使用 [vue-element-plus-admin](https://element-plus-admin-doc.cn/) 的 `main` 分支,在上面接入了短剧、剧集、角色、菜单和管理员页面。
|
短剧内容管理后台。界面与工程结构基于 [vue-element-plus-admin](https://element-plus-admin-doc.cn/),在其上接入了短剧、剧集、App 用户、角色、菜单和管理员页面,数据全部来自 Java 后端服务。
|
||||||
|
|
||||||
|
界面名称由 `VITE_APP_TITLE` 控制(`.env.base` 及 dev / test / pro 三个环境文件),当前值为 `FruitShorts`,同时作用于浏览器标题、侧边栏品牌与登录页。
|
||||||
|
|
||||||
|
后端工程在同级目录 `../FruitServer`,接口与启动方式见 [../FruitServer/README.md](../FruitServer/README.md)。
|
||||||
|
|
||||||
## 环境
|
## 环境
|
||||||
|
|
||||||
- Node.js 18 或更高版本
|
- Node.js 18 或更高版本
|
||||||
- pnpm 8 或更高版本
|
- pnpm 8 或更高版本
|
||||||
|
- 已启动的后端服务(默认 `http://127.0.0.1:8080`)
|
||||||
|
|
||||||
## 开始
|
## 开始
|
||||||
|
|
||||||
|
先启动后端(见 `../FruitServer/README.md`),再启动前端:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
pnpm install
|
pnpm install
|
||||||
pnpm dev
|
pnpm dev
|
||||||
```
|
```
|
||||||
|
|
||||||
开发服务地址是 [http://localhost:3005](http://localhost:3005)。同一局域网的设备可以使用终端里的 Network 地址打开。
|
开发服务地址是 http://localhost:3005 。`vite.config.mts` 已把 `/admin` 代理到 `http://127.0.0.1:8080`、`/files` 代理到后端本地媒资目录,因此前端不需要额外配置跨域。
|
||||||
|
|
||||||
演示账号:
|
演示账号(密码均为 `admin123`,由后端 `sql/data.sql` 初始化):
|
||||||
|
|
||||||
| 账号 | 密码 |
|
| 账号 | 角色 | 说明 |
|
||||||
| ------- | ---------- |
|
| -------------------- | ---------- | ------------------------------ |
|
||||||
| `admin` | `admin123` |
|
| `admin` | 超级管理员 | 拥有全部菜单与操作权限 |
|
||||||
| `test` | `test` |
|
| `content.editor01` | 内容运营 | 只能看到内容管理,且无审核权限 |
|
||||||
|
| `content.reviewer01` | 审核专员 | 可审核发布 |
|
||||||
|
| `data.analyst01` | 数据分析师 | 只读内容数据 |
|
||||||
|
|
||||||
`admin` 拥有全部菜单。`test` 只用于对照框架自带的权限示例。
|
用不同账号登录可以直观看到菜单与按钮随权限变化。
|
||||||
|
|
||||||
## 命令
|
## 命令
|
||||||
|
|
||||||
@@ -39,22 +48,26 @@ pnpm dev
|
|||||||
|
|
||||||
登录后菜单包括:
|
登录后菜单包括:
|
||||||
|
|
||||||
- 工作台
|
- 工作台:短剧总数、累计播放量、待审核数、已上线数与最近更新
|
||||||
- 内容管理:短剧管理、剧集管理
|
- 内容管理:短剧管理(含封面分片上传、提交审核/通过/驳回/上线/下线)、剧集管理(含视频分片上传、自动读取时长、播放顺序调整)、轮播图管理(含图片上传、跳转配置、排序与上下线)、类型管理(短剧类型的增删改)
|
||||||
|
- 用户管理 → App 用户:按 uid / 昵称 / 手机号 / 邮箱检索 C 端用户,查看资料、观看流水与处置记录,支持封禁 / 限制 / 解封(处置后该用户全部会话立即失效)
|
||||||
- 后台管理:角色权限配置、管理员用户列表、菜单管理、角色分类、系统设置
|
- 后台管理:角色权限配置、管理员用户列表、菜单管理、角色分类、系统设置
|
||||||
|
- 后台管理 → 操作日志:查看后台写操作的请求与响应、操作人、耗时
|
||||||
|
|
||||||
这些页面的数据仍是前端演示数据,没有接后端。登录和菜单由框架的 mock 接口提供。
|
左侧菜单由后端 `GET /admin/auth/menus` 下发,属于服务端动态路由;在菜单管理页改动菜单后,重新登录即可看到新的菜单结构。
|
||||||
|
|
||||||
## 目录
|
## 目录
|
||||||
|
|
||||||
框架原有代码保持 [vue-element-plus-admin](https://github.com/kailong321200875/vue-element-plus-admin/tree/mini) 的结构。本项目新增的业务代码在:
|
框架原有代码保持 vue-element-plus-admin 的结构。本项目新增的业务代码:
|
||||||
|
|
||||||
|
- `src/api/admin/`:管理端接口封装(短剧、剧集、角色、菜单、管理员、配置)
|
||||||
- `src/views/common/DashboardView.vue`
|
- `src/views/common/DashboardView.vue`
|
||||||
- `src/views/contentManagement/`
|
- `src/views/contentManagement/`
|
||||||
- `src/views/permission/`
|
- `src/views/permission/`、`src/views/permission/composables/`
|
||||||
- `src/views/userManagement/`
|
- `src/views/userManagement/`、`src/views/userManagement/composables/`
|
||||||
- `src/views/systemManagement/`
|
- `src/views/systemManagement/`
|
||||||
- `src/data/`、`src/permissions/`、`src/stores/`
|
- `src/stores/rbac.js`:基于登录下发权限码的按钮级权限判断
|
||||||
|
- `src/utils/status.js`、`src/utils/format.js`
|
||||||
- `src/styles/business/`
|
- `src/styles/business/`
|
||||||
|
|
||||||
路由写在 `src/router/index.ts` 的 `asyncRouterMap`。
|
路由写在 `src/router/index.ts` 的 `asyncRouterMap`(服务端动态路由不可用时的兜底)。
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
extends: ['@commitlint/config-conventional'],
|
extends: ['@commitlint/config-conventional'],
|
||||||
rules: {
|
rules: {
|
||||||
|
|||||||
+8
-25
@@ -2,7 +2,7 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" type="image/png" href="/brand-mark.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title><%= title %></title>
|
<title><%= title %></title>
|
||||||
</head>
|
</head>
|
||||||
@@ -16,7 +16,8 @@
|
|||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background: #f0f2f5;
|
color: #f8f1ea;
|
||||||
|
background: #100c0a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-loading .app-loading-wrap {
|
.app-loading .app-loading-wrap {
|
||||||
@@ -38,8 +39,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.app-loading .app-loading-logo {
|
.app-loading .app-loading-logo {
|
||||||
width: 100px;
|
width: 72px;
|
||||||
margin: 0 auto 15px;
|
margin: 0 auto 15px;
|
||||||
|
border-radius: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-loading .app-loading-item {
|
.app-loading .app-loading-item {
|
||||||
@@ -55,7 +57,7 @@
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border: 4px solid #2d8cf0;
|
border: 4px solid #c4322a;
|
||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
border-left-color: transparent;
|
border-left-color: transparent;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
@@ -68,7 +70,7 @@
|
|||||||
left: calc(50% - 20px);
|
left: calc(50% - 20px);
|
||||||
width: 40px;
|
width: 40px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
border: 4px solid #87bdff;
|
border: 4px solid #ff8a72;
|
||||||
border-right: 0;
|
border-right: 0;
|
||||||
border-top-color: transparent;
|
border-top-color: transparent;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
@@ -85,16 +87,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes loader-outter {
|
|
||||||
0% {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes loader-inner {
|
@keyframes loader-inner {
|
||||||
0% {
|
0% {
|
||||||
transform: rotate(0deg);
|
transform: rotate(0deg);
|
||||||
@@ -105,20 +97,11 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes loader-inner {
|
|
||||||
0% {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
transform: rotate(-360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
<div class="app-loading">
|
<div class="app-loading">
|
||||||
<div class="app-loading-wrap">
|
<div class="app-loading-wrap">
|
||||||
<div class="app-loading-title">
|
<div class="app-loading-title">
|
||||||
<img src="/logo.png" class="app-loading-logo" alt="Logo" />
|
<img src="/brand-mark.png" class="app-loading-logo" alt="Logo" />
|
||||||
<div class="app-loading-title"><%= title %></div>
|
<div class="app-loading-title"><%= title %></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="app-loading-item">
|
<div class="app-loading-item">
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
import { createProdMockServer } from 'vite-plugin-mock/es/createProdMockServer'
|
|
||||||
|
|
||||||
const modules = import.meta.glob('./**/*.mock.ts', {
|
|
||||||
import: 'default',
|
|
||||||
eager: true
|
|
||||||
})
|
|
||||||
|
|
||||||
const mockModules: any[] = []
|
|
||||||
Object.keys(modules).forEach(async (key) => {
|
|
||||||
if (key.includes('_')) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
mockModules.push(...(modules[key] as any))
|
|
||||||
})
|
|
||||||
|
|
||||||
export function setupProdMockServer() {
|
|
||||||
createProdMockServer(mockModules)
|
|
||||||
}
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
import { MockMethod } from 'vite-plugin-mock'
|
|
||||||
import { SUCCESS_CODE } from '@/constants'
|
|
||||||
|
|
||||||
const timeout = 1000
|
|
||||||
|
|
||||||
const adminList = [
|
|
||||||
{
|
|
||||||
path: '/dashboard',
|
|
||||||
component: '#',
|
|
||||||
redirect: '/dashboard/index',
|
|
||||||
name: 'Dashboard',
|
|
||||||
meta: {
|
|
||||||
title: '工作台',
|
|
||||||
icon: 'ant-design:dashboard-filled'
|
|
||||||
},
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: 'index',
|
|
||||||
name: 'dashboard',
|
|
||||||
component: 'views/common/DashboardView',
|
|
||||||
meta: {
|
|
||||||
title: '工作台',
|
|
||||||
icon: 'ant-design:dashboard-filled',
|
|
||||||
affix: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/content',
|
|
||||||
component: '#',
|
|
||||||
redirect: '/content/dramas',
|
|
||||||
name: 'Content',
|
|
||||||
meta: {
|
|
||||||
title: '内容管理',
|
|
||||||
icon: 'ep:video-camera',
|
|
||||||
alwaysShow: true
|
|
||||||
},
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: 'dramas',
|
|
||||||
name: 'dramas',
|
|
||||||
component: 'views/contentManagement/DramaManagementView',
|
|
||||||
meta: {
|
|
||||||
title: '短剧管理'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'episodes',
|
|
||||||
name: 'episodes',
|
|
||||||
component: 'views/contentManagement/EpisodeManagementView',
|
|
||||||
meta: {
|
|
||||||
title: '剧集管理'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: '/system',
|
|
||||||
component: '#',
|
|
||||||
redirect: '/system/access',
|
|
||||||
name: 'System',
|
|
||||||
meta: {
|
|
||||||
title: '后台管理',
|
|
||||||
icon: 'ep:setting',
|
|
||||||
alwaysShow: true
|
|
||||||
},
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
path: 'access',
|
|
||||||
name: 'access',
|
|
||||||
component: 'views/permission/RoleManagementView',
|
|
||||||
meta: {
|
|
||||||
title: '角色权限配置'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'admin-users',
|
|
||||||
name: 'admin-users',
|
|
||||||
component: 'views/userManagement/AdminUsersView',
|
|
||||||
meta: {
|
|
||||||
title: '管理员用户列表'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'menus',
|
|
||||||
name: 'menus',
|
|
||||||
component: 'views/permission/MenuManagementView',
|
|
||||||
meta: {
|
|
||||||
title: '菜单管理'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'role-categories',
|
|
||||||
name: 'role-categories',
|
|
||||||
component: 'views/permission/RoleCategoriesView',
|
|
||||||
meta: {
|
|
||||||
title: '角色分类'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'settings',
|
|
||||||
name: 'settings',
|
|
||||||
component: 'views/systemManagement/SettingsView',
|
|
||||||
meta: {
|
|
||||||
title: '系统设置'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const testList = adminList.filter((route) => route.path !== '/system')
|
|
||||||
|
|
||||||
export default [
|
|
||||||
// 列表接口
|
|
||||||
{
|
|
||||||
url: '/mock/role/list',
|
|
||||||
method: 'get',
|
|
||||||
timeout,
|
|
||||||
response: ({ query }) => {
|
|
||||||
const { roleName } = query
|
|
||||||
return {
|
|
||||||
code: SUCCESS_CODE,
|
|
||||||
data: roleName === 'admin' ? adminList : testList
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
] as MockMethod[]
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
import { SUCCESS_CODE } from '@/constants'
|
|
||||||
|
|
||||||
const timeout = 1000
|
|
||||||
|
|
||||||
const List: {
|
|
||||||
username: string
|
|
||||||
password: string
|
|
||||||
role: string
|
|
||||||
roleId: string
|
|
||||||
permissions: string | string[]
|
|
||||||
}[] = [
|
|
||||||
{
|
|
||||||
username: 'admin',
|
|
||||||
password: 'admin123',
|
|
||||||
role: 'admin',
|
|
||||||
roleId: '1',
|
|
||||||
permissions: ['*.*.*']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
username: 'test',
|
|
||||||
password: 'test',
|
|
||||||
role: 'test',
|
|
||||||
roleId: '2',
|
|
||||||
permissions: ['example:dialog:create', 'example:dialog:delete']
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
export default [
|
|
||||||
// 列表接口
|
|
||||||
{
|
|
||||||
url: '/mock/user/list',
|
|
||||||
method: 'get',
|
|
||||||
response: ({ query }) => {
|
|
||||||
const { username, pageIndex, pageSize } = query
|
|
||||||
|
|
||||||
const mockList = List.filter((item) => {
|
|
||||||
if (username && item.username.indexOf(username) < 0) return false
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
const pageList = mockList.filter(
|
|
||||||
(_, index) => index < pageSize * pageIndex && index >= pageSize * (pageIndex - 1)
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
code: SUCCESS_CODE,
|
|
||||||
data: {
|
|
||||||
total: mockList.length,
|
|
||||||
list: pageList
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// 登录接口
|
|
||||||
{
|
|
||||||
url: '/mock/user/login',
|
|
||||||
method: 'post',
|
|
||||||
timeout,
|
|
||||||
response: ({ body }) => {
|
|
||||||
const data = body
|
|
||||||
let hasUser = false
|
|
||||||
for (const user of List) {
|
|
||||||
if (user.username === data.username && user.password === data.password) {
|
|
||||||
hasUser = true
|
|
||||||
return {
|
|
||||||
code: SUCCESS_CODE,
|
|
||||||
data: user
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!hasUser) {
|
|
||||||
return {
|
|
||||||
code: 500,
|
|
||||||
message: '账号或密码错误'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// 退出接口
|
|
||||||
{
|
|
||||||
url: '/mock/user/loginOut',
|
|
||||||
method: 'get',
|
|
||||||
timeout,
|
|
||||||
response: () => {
|
|
||||||
return {
|
|
||||||
code: SUCCESS_CODE,
|
|
||||||
data: null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
+3
-6
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "reelshort-admin",
|
"name": "fruitshorts-admin",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "ReelShort 内容管理后台,基于 vue-element-plus-admin。",
|
"description": "FruitShorts 内容管理后台,基于 vue-element-plus-admin。",
|
||||||
"author": "Archer <502431556@qq.com>",
|
"author": "Archer <502431556@qq.com>",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"i": "pnpm install",
|
"i": "pnpm install",
|
||||||
"dev": "VITE_CJS_IGNORE_WARNING=1 vite --mode base",
|
"dev": "vite --mode base",
|
||||||
"ts:check": "pnpm vue-tsc --noEmit --skipLibCheck",
|
"ts:check": "pnpm vue-tsc --noEmit --skipLibCheck",
|
||||||
"build:pro": "VITE_CJS_IGNORE_WARNING=1 vite build --mode pro",
|
"build:pro": "VITE_CJS_IGNORE_WARNING=1 vite build --mode pro",
|
||||||
"build:dev": "VITE_CJS_IGNORE_WARNING=1 vite build --mode dev",
|
"build:dev": "VITE_CJS_IGNORE_WARNING=1 vite build --mode dev",
|
||||||
@@ -24,8 +24,6 @@
|
|||||||
"prepare": "husky install"
|
"prepare": "husky install"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@element-plus/icons-vue": "^2.3.2",
|
|
||||||
"@faker-js/faker": "^8.4.0",
|
|
||||||
"@iconify/iconify": "^3.1.1",
|
"@iconify/iconify": "^3.1.1",
|
||||||
"@iconify/vue": "^4.1.1",
|
"@iconify/vue": "^4.1.1",
|
||||||
"@vueuse/core": "^10.7.2",
|
"@vueuse/core": "^10.7.2",
|
||||||
@@ -103,7 +101,6 @@
|
|||||||
"vite": "5.0.12",
|
"vite": "5.0.12",
|
||||||
"vite-plugin-ejs": "^1.7.0",
|
"vite-plugin-ejs": "^1.7.0",
|
||||||
"vite-plugin-eslint": "^1.8.1",
|
"vite-plugin-eslint": "^1.8.1",
|
||||||
"vite-plugin-mock": "2.9.6",
|
|
||||||
"vite-plugin-progress": "^0.0.7",
|
"vite-plugin-progress": "^0.0.7",
|
||||||
"vite-plugin-purge-icons": "^0.10.0",
|
"vite-plugin-purge-icons": "^0.10.0",
|
||||||
"vite-plugin-style-import": "2.0.0",
|
"vite-plugin-style-import": "2.0.0",
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: {
|
plugins: {
|
||||||
autoprefixer: {}
|
autoprefixer: {}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
printWidth: 100,
|
printWidth: 100,
|
||||||
tabWidth: 2,
|
tabWidth: 2,
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 5.9 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 9.0 KiB |
@@ -1,3 +1,5 @@
|
|||||||
|
<!-- Author: fruit.com.cn -->
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useAppStore } from '@/store/modules/app'
|
import { useAppStore } from '@/store/modules/app'
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
|
import request from '@/axios'
|
||||||
|
import type { AppUserBanRecord, AppUserItem, PageResult, WatchHistoryItem } from './types'
|
||||||
|
|
||||||
|
export interface AppUserPageParams {
|
||||||
|
pageIndex?: number
|
||||||
|
pageSize?: number
|
||||||
|
/** uid / 昵称 / 手机号 / 邮箱 */
|
||||||
|
keyword?: string
|
||||||
|
status?: string
|
||||||
|
countryCode?: string
|
||||||
|
registerSource?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WatchHistoryPageParams {
|
||||||
|
pageIndex?: number
|
||||||
|
pageSize?: number
|
||||||
|
keyword?: string
|
||||||
|
userId?: number
|
||||||
|
dramaTitle?: string
|
||||||
|
dramaId?: number
|
||||||
|
episodeId?: number
|
||||||
|
source?: string
|
||||||
|
finished?: boolean
|
||||||
|
startTime?: string
|
||||||
|
endTime?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const pageAppUsersApi = (params: AppUserPageParams) => {
|
||||||
|
return request.get<PageResult<AppUserItem>>({ url: '/admin/app-users/page', params })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getAppUserApi = (id: number) => {
|
||||||
|
return request.get<AppUserItem>({ url: `/admin/app-users/${id}` })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const listAppUserBanRecordsApi = (id: number) => {
|
||||||
|
return request.get<AppUserBanRecord[]>({ url: `/admin/app-users/${id}/ban-records` })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const listAppUserWatchHistoryApi = (id: number, limit = 50) => {
|
||||||
|
return request.get<WatchHistoryItem[]>({
|
||||||
|
url: `/admin/app-users/${id}/watch-history`,
|
||||||
|
params: { limit }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const pageWatchHistoryApi = (params: WatchHistoryPageParams) => {
|
||||||
|
return request.get<PageResult<WatchHistoryItem>>({
|
||||||
|
url: '/admin/watch-history/page',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** normal / limited / banned,封禁与限制会立即吊销该用户全部会话 */
|
||||||
|
export const updateAppUserStatusApi = (id: number, status: string, reason?: string) => {
|
||||||
|
return request.put<void>({
|
||||||
|
url: `/admin/app-users/${id}/status`,
|
||||||
|
params: { status, reason }
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
|
import request from '@/axios'
|
||||||
|
import type { BannerItem, BannerSaveData, PageResult } from './types'
|
||||||
|
|
||||||
|
export interface BannerPageParams {
|
||||||
|
pageIndex?: number
|
||||||
|
pageSize?: number
|
||||||
|
position?: string
|
||||||
|
status?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const pageBannersApi = (params: BannerPageParams) => {
|
||||||
|
return request.get<PageResult<BannerItem>>({ url: '/admin/banners/page', params })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getBannerApi = (id: number) => {
|
||||||
|
return request.get<BannerItem>({ url: `/admin/banners/${id}` })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createBannerApi = (data: BannerSaveData) => {
|
||||||
|
return request.post<number>({ url: '/admin/banners', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateBannerApi = (id: number, data: BannerSaveData) => {
|
||||||
|
return request.put<void>({ url: `/admin/banners/${id}`, data })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteBannerApi = (id: number) => {
|
||||||
|
return request.delete<void>({ url: `/admin/banners/${id}` })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateBannerStatusApi = (id: number, status: number) => {
|
||||||
|
return request.put<void>({ url: `/admin/banners/${id}/status`, params: { status } })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sortBannersApi = (ids: number[]) => {
|
||||||
|
return request.put<void>({ url: '/admin/banners/sort', data: { ids } })
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
|
import request from '@/axios'
|
||||||
|
import type { PageResult } from './types'
|
||||||
|
|
||||||
|
export interface OrderItem {
|
||||||
|
id: number
|
||||||
|
orderNo: string
|
||||||
|
userId: number
|
||||||
|
productCode: string
|
||||||
|
productName: string
|
||||||
|
type: string
|
||||||
|
platform: string
|
||||||
|
amount: number
|
||||||
|
currency: string
|
||||||
|
coinAmount: number
|
||||||
|
/** CREATED / PAID / DELIVERED / REFUNDED / CLOSED */
|
||||||
|
status: string
|
||||||
|
paidAt?: string
|
||||||
|
deliveredAt?: string
|
||||||
|
refundReason: string
|
||||||
|
failReason: string
|
||||||
|
createdAt?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OrderEventItem {
|
||||||
|
id: number
|
||||||
|
orderNo: string
|
||||||
|
event: string
|
||||||
|
fromStatus: string
|
||||||
|
toStatus: string
|
||||||
|
source: string
|
||||||
|
payload: string
|
||||||
|
operatorId: number
|
||||||
|
createdAt?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EntitlementItem {
|
||||||
|
id: number
|
||||||
|
userId: number
|
||||||
|
dramaId: number
|
||||||
|
episodeId: number
|
||||||
|
/** EPISODE / DRAMA */
|
||||||
|
type: string
|
||||||
|
/** ORDER / ADMIN / ACTIVITY */
|
||||||
|
source: string
|
||||||
|
/** ACTIVE / REVOKED / EXPIRED */
|
||||||
|
status: string
|
||||||
|
orderNo: string
|
||||||
|
startAt?: string
|
||||||
|
revokeReason: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LedgerItem {
|
||||||
|
id: number
|
||||||
|
userId: number
|
||||||
|
changeAmount: number
|
||||||
|
balanceAfter: number
|
||||||
|
bizType: string
|
||||||
|
bizId: string
|
||||||
|
remark: string
|
||||||
|
operatorId: number
|
||||||
|
createdAt?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReconcileItem {
|
||||||
|
id: number
|
||||||
|
bizDate: string
|
||||||
|
platform: string
|
||||||
|
totalCount: number
|
||||||
|
matchedCount: number
|
||||||
|
diffCount: number
|
||||||
|
amountSum: number
|
||||||
|
status: string
|
||||||
|
detail: string
|
||||||
|
finishedAt?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const pageOrdersApi = (params: Record<string, unknown>) => {
|
||||||
|
return request.get<PageResult<OrderItem>>({ url: '/admin/orders/page', params })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const listOrderEventsApi = (orderNo: string) => {
|
||||||
|
return request.get<OrderEventItem[]>({ url: `/admin/orders/${orderNo}/events` })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const compensateOrderApi = (orderNo: string) => {
|
||||||
|
return request.post<OrderItem>({ url: `/admin/orders/${orderNo}/compensate` })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const refundOrderApi = (orderNo: string, reason: string) => {
|
||||||
|
return request.post<OrderItem>({ url: `/admin/orders/${orderNo}/refund`, data: { reason } })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const pageEntitlementsApi = (params: Record<string, unknown>) => {
|
||||||
|
return request.get<PageResult<EntitlementItem>>({ url: '/admin/entitlements/page', params })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const grantEntitlementApi = (data: Record<string, unknown>) => {
|
||||||
|
return request.post<EntitlementItem>({ url: '/admin/entitlements', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const revokeEntitlementApi = (id: number, reason: string) => {
|
||||||
|
return request.put<void>({ url: `/admin/entitlements/${id}/revoke`, params: { reason } })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const listWalletLedgerApi = (userId: number, limit = 20) => {
|
||||||
|
return request.get<LedgerItem[]>({
|
||||||
|
url: `/admin/wallets/${userId}/ledger`,
|
||||||
|
params: { limit }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const adjustCoinsApi = (data: Record<string, unknown>) => {
|
||||||
|
return request.post<LedgerItem>({ url: '/admin/wallets/coins', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const pageReconcileApi = (params: Record<string, unknown>) => {
|
||||||
|
return request.get<PageResult<ReconcileItem>>({ url: '/admin/reconcile/page', params })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const runReconcileApi = (params: Record<string, unknown>) => {
|
||||||
|
return request.post<ReconcileItem>({ url: '/admin/reconcile/run', params })
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
|
import request from '@/axios'
|
||||||
|
import type { ConfigItem } from './types'
|
||||||
|
|
||||||
|
export const listConfigsApi = () => {
|
||||||
|
return request.get<ConfigItem[]>({ url: '/admin/configs' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateConfigsApi = (values: Record<string, string>) => {
|
||||||
|
return request.put<void>({ url: '/admin/configs', data: values })
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
|
import request from '@/axios'
|
||||||
|
import type { PageResult } from './types'
|
||||||
|
|
||||||
|
export interface CreatorItem {
|
||||||
|
id: number
|
||||||
|
userId: number
|
||||||
|
code: string
|
||||||
|
name: string
|
||||||
|
/** INDIVIDUAL / COMPANY */
|
||||||
|
type: string
|
||||||
|
contactName: string
|
||||||
|
contactMobile: string
|
||||||
|
contactEmail: string
|
||||||
|
countryCode: string
|
||||||
|
/** pending / approved / rejected / frozen */
|
||||||
|
status: string
|
||||||
|
auditNote: string
|
||||||
|
auditBy: number
|
||||||
|
auditAt?: string
|
||||||
|
settlementCurrency: string
|
||||||
|
createdAt?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreatorDocumentItem {
|
||||||
|
id: number
|
||||||
|
creatorId: number
|
||||||
|
/** ID_CARD / BUSINESS_LICENSE / CONTRACT */
|
||||||
|
type: string
|
||||||
|
fileUrl: string
|
||||||
|
status: string
|
||||||
|
auditNote: string
|
||||||
|
createdAt?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubmissionItem {
|
||||||
|
id: number
|
||||||
|
creatorId: number
|
||||||
|
dramaId: number
|
||||||
|
/** CREATE / UPDATE / PUBLISH / OFFLINE */
|
||||||
|
type: string
|
||||||
|
/** DRAFT / SUBMITTED / REVIEWING / APPROVED / REJECTED / WITHDRAWN */
|
||||||
|
status: string
|
||||||
|
version: number
|
||||||
|
submitAt?: string
|
||||||
|
reviewerId: number
|
||||||
|
reviewAt?: string
|
||||||
|
reviewNote: string
|
||||||
|
rejectCode: string
|
||||||
|
createdAt?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const pageCreatorsApi = (params: Record<string, unknown>) => {
|
||||||
|
return request.get<PageResult<CreatorItem>>({ url: '/admin/creators/page', params })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const listCreatorDocumentsApi = (id: number) => {
|
||||||
|
return request.get<CreatorDocumentItem[]>({ url: `/admin/creators/${id}/documents` })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** action: APPROVE / REJECT / FREEZE */
|
||||||
|
export const auditCreatorApi = (id: number, data: Record<string, unknown>) => {
|
||||||
|
return request.put<CreatorItem>({ url: `/admin/creators/${id}/audit`, data })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 手工录入创作者主体(C 端申请入驻未实现前的入口) */
|
||||||
|
export const createCreatorApi = (data: Record<string, unknown>) => {
|
||||||
|
return request.post<number>({ url: '/admin/creators', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreatorAccountResult {
|
||||||
|
creatorId: number
|
||||||
|
adminUserId: number
|
||||||
|
username: string
|
||||||
|
/** 一次性明文密码:只在开通 / 重置的这次响应里出现,之后拿不回来 */
|
||||||
|
password: string | null
|
||||||
|
created: boolean
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 开通创作者账号(仅审核通过可开;已开通则幂等返回) */
|
||||||
|
export const openCreatorAccountApi = (id: number, username?: string) => {
|
||||||
|
return request.post<CreatorAccountResult>({
|
||||||
|
url: `/admin/creators/${id}/account`,
|
||||||
|
data: { username }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 重置创作者账号密码(一次性明文返回) */
|
||||||
|
export const resetCreatorPasswordApi = (id: number) => {
|
||||||
|
return request.post<CreatorAccountResult>({
|
||||||
|
url: `/admin/creators/${id}/account/reset-password`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const pageSubmissionsApi = (params: Record<string, unknown>) => {
|
||||||
|
return request.get<PageResult<SubmissionItem>>({ url: '/admin/submissions/page', params })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** action: APPROVE / REJECT / WITHDRAW */
|
||||||
|
export const auditSubmissionApi = (id: number, data: Record<string, unknown>) => {
|
||||||
|
return request.put<SubmissionItem>({ url: `/admin/submissions/${id}/audit`, data })
|
||||||
|
}
|
||||||
@@ -0,0 +1,248 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
|
import request from '@/axios'
|
||||||
|
import type { PageResult } from './types'
|
||||||
|
|
||||||
|
/** 创作者主体:登录账号绑定的创作者信息 */
|
||||||
|
export interface CreatorProfile {
|
||||||
|
id: number
|
||||||
|
code: string
|
||||||
|
name: string
|
||||||
|
/** INDIVIDUAL / COMPANY */
|
||||||
|
type: string
|
||||||
|
/** pending / approved / rejected / frozen */
|
||||||
|
status: string
|
||||||
|
statusText: string
|
||||||
|
auditNote: string
|
||||||
|
auditAt?: string
|
||||||
|
contactName: string
|
||||||
|
contactEmail: string
|
||||||
|
countryCode: string
|
||||||
|
dramaTotal: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 代看目标下拉项(后端只给 id / 名称 / 状态) */
|
||||||
|
export interface CreatorCenterTarget {
|
||||||
|
id: number
|
||||||
|
code: string
|
||||||
|
name: string
|
||||||
|
status: string
|
||||||
|
statusText: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 我的作品 + 最近一次提交的审核进度 */
|
||||||
|
export interface CreatorDrama {
|
||||||
|
id: number
|
||||||
|
code: string
|
||||||
|
title: string
|
||||||
|
subtitle?: string
|
||||||
|
genre?: string
|
||||||
|
description?: string
|
||||||
|
cover: string
|
||||||
|
trailerUrl?: string
|
||||||
|
language?: string
|
||||||
|
status: string
|
||||||
|
statusText: string
|
||||||
|
episodes: number
|
||||||
|
views: number
|
||||||
|
updatedAt?: string
|
||||||
|
submissionId?: number | null
|
||||||
|
submissionType?: string | null
|
||||||
|
submissionStatus?: string | null
|
||||||
|
submissionStatusText?: string | null
|
||||||
|
reviewNote?: string | null
|
||||||
|
rejectCode?: string | null
|
||||||
|
submitAt?: string | null
|
||||||
|
reviewAt?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreatorNotice {
|
||||||
|
id: number
|
||||||
|
/** CREATOR(入驻审核)/ SUBMISSION(内容提交审核) */
|
||||||
|
bizType: string
|
||||||
|
title: string
|
||||||
|
content: string
|
||||||
|
result: string
|
||||||
|
time?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreatorSubmission {
|
||||||
|
id: number
|
||||||
|
creatorId: number
|
||||||
|
dramaId: number
|
||||||
|
/** CREATE / UPDATE / PUBLISH / OFFLINE */
|
||||||
|
type: string
|
||||||
|
/** DRAFT / SUBMITTED / REVIEWING / APPROVED / REJECTED / WITHDRAWN */
|
||||||
|
status: string
|
||||||
|
version: number
|
||||||
|
submitAt?: string
|
||||||
|
reviewAt?: string
|
||||||
|
reviewNote: string
|
||||||
|
rejectCode: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreatorEpisode {
|
||||||
|
id: number
|
||||||
|
dramaId: number
|
||||||
|
number: number
|
||||||
|
title: string
|
||||||
|
duration: string
|
||||||
|
durationSec: number
|
||||||
|
cover: string
|
||||||
|
videoUrl: string
|
||||||
|
isFree: boolean
|
||||||
|
unlockCoin: number
|
||||||
|
status: string
|
||||||
|
statusText: string
|
||||||
|
views: number
|
||||||
|
sort: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreatorDramaEditData {
|
||||||
|
title: string
|
||||||
|
subtitle?: string
|
||||||
|
categoryId?: number
|
||||||
|
genre?: string
|
||||||
|
description?: string
|
||||||
|
cover?: string
|
||||||
|
trailerUrl?: string
|
||||||
|
language?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PriceEpisodeConfig {
|
||||||
|
episodeId: number
|
||||||
|
number: number
|
||||||
|
title: string
|
||||||
|
free: boolean
|
||||||
|
unlockCoin: number
|
||||||
|
previewSeconds: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PriceConfig {
|
||||||
|
dramaId: number
|
||||||
|
freeEpisodes: number
|
||||||
|
unlockCoin: number
|
||||||
|
episodeCount: number
|
||||||
|
changedCount: number
|
||||||
|
episodes: PriceEpisodeConfig[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreatorDashboard {
|
||||||
|
generatedAt: string
|
||||||
|
drama: Record<string, number>
|
||||||
|
playback: Record<string, number>
|
||||||
|
interaction: Record<string, number>
|
||||||
|
/** paidOrderAmount 取不到时为 null */
|
||||||
|
revenue: Record<string, number | null>
|
||||||
|
/** 按天趋势:creator_stat_daily 无产出,恒为 null */
|
||||||
|
daily: null
|
||||||
|
notes: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getCreatorProfileApi = () => {
|
||||||
|
return request.get<CreatorProfile>({ url: '/admin/creator-center/profile' })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 静默探测当前账号的创作者身份:不弹全局提示,直接把 Result 交给调用方判断。
|
||||||
|
* 用于「已绑定 / 可代看但没选目标 / 未绑定」三态分流,渲染引导页。
|
||||||
|
*/
|
||||||
|
export const probeCreatorAccessApi = () => {
|
||||||
|
return request.get<CreatorProfile>({ url: '/admin/creator-center/profile', silent: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 代看目标下拉的数据源(仅 impersonate 权限可调) */
|
||||||
|
export const listImpersonationTargetsApi = () => {
|
||||||
|
return request.get<CreatorCenterTarget[]>({ url: '/admin/creator-center/impersonation/targets' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const pageMyDramasApi = (params: Record<string, unknown>) => {
|
||||||
|
return request.get<PageResult<CreatorDrama>>({ url: '/admin/creator-center/dramas/page', params })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getMyDramaApi = (id: number) => {
|
||||||
|
return request.get<CreatorDrama>({ url: `/admin/creator-center/dramas/${id}` })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const listCreatorNoticesApi = () => {
|
||||||
|
return request.get<CreatorNotice[]>({ url: '/admin/creator-center/notices' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getCreatorDashboardApi = () => {
|
||||||
|
return request.get<CreatorDashboard>({ url: '/admin/creator-center/dashboard' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createDramaApi = (data: CreatorDramaEditData) => {
|
||||||
|
return request.post<number>({ url: '/admin/creator-center/dramas', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateDramaApi = (id: number, data: CreatorDramaEditData) => {
|
||||||
|
return request.put<void>({ url: `/admin/creator-center/dramas/${id}`, data })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const listEpisodesApi = (dramaId: number) => {
|
||||||
|
return request.get<CreatorEpisode[]>({ url: `/admin/creator-center/dramas/${dramaId}/episodes` })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const addEpisodeApi = (dramaId: number, data: Record<string, unknown>) => {
|
||||||
|
return request.post<number>({ url: `/admin/creator-center/dramas/${dramaId}/episodes`, data })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateEpisodeApi = (
|
||||||
|
dramaId: number,
|
||||||
|
episodeId: number,
|
||||||
|
data: Record<string, unknown>
|
||||||
|
) => {
|
||||||
|
return request.put<void>({
|
||||||
|
url: `/admin/creator-center/dramas/${dramaId}/episodes/${episodeId}`,
|
||||||
|
data
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteEpisodeApi = (dramaId: number, episodeId: number) => {
|
||||||
|
return request.delete<void>({
|
||||||
|
url: `/admin/creator-center/dramas/${dramaId}/episodes/${episodeId}`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sortEpisodesApi = (dramaId: number, ids: number[]) => {
|
||||||
|
return request.put<void>({
|
||||||
|
url: `/admin/creator-center/dramas/${dramaId}/episodes/sort`,
|
||||||
|
data: { ids }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 把上传好的媒资挂到分集(产物必须是本人上传的) */
|
||||||
|
export const attachAssetApi = (dramaId: number, episodeId: number, assetId: number) => {
|
||||||
|
return request.post<void>({
|
||||||
|
url: `/admin/creator-center/dramas/${dramaId}/episodes/${episodeId}/attach`,
|
||||||
|
data: { assetId }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getPricesApi = (dramaId: number) => {
|
||||||
|
return request.get<PriceConfig>({ url: `/admin/creator-center/dramas/${dramaId}/prices` })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const savePricesApi = (dramaId: number, data: Record<string, unknown>) => {
|
||||||
|
return request.put<PriceConfig>({ url: `/admin/creator-center/dramas/${dramaId}/prices`, data })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const submitDramaApi = (dramaId: number, type: string) => {
|
||||||
|
return request.post<CreatorSubmission>({
|
||||||
|
url: `/admin/creator-center/dramas/${dramaId}/submit`,
|
||||||
|
data: { type }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const withdrawSubmissionApi = (submissionId: number) => {
|
||||||
|
return request.post<CreatorSubmission>({
|
||||||
|
url: `/admin/creator-center/submissions/${submissionId}/withdraw`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const listSubmissionsApi = (dramaId: number) => {
|
||||||
|
return request.get<CreatorSubmission[]>({
|
||||||
|
url: `/admin/creator-center/dramas/${dramaId}/submissions`
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
|
import request from '@/axios'
|
||||||
|
|
||||||
|
/** 指标口径见后端 DashboardService 注释与 docs/api-phase1.md「2.8 数据看板」 */
|
||||||
|
export interface DashboardOverview {
|
||||||
|
generatedAt: string
|
||||||
|
user: Record<string, number>
|
||||||
|
content: Record<string, number>
|
||||||
|
playback: Record<string, number>
|
||||||
|
revenue: Record<string, number>
|
||||||
|
cost: Record<string, number | null>
|
||||||
|
notes: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TranscodeBoard {
|
||||||
|
generatedAt: string
|
||||||
|
queue: Record<string, number>
|
||||||
|
failedTasks: {
|
||||||
|
id: number
|
||||||
|
episodeId: number
|
||||||
|
status: string
|
||||||
|
retryCount: number
|
||||||
|
errorMsg: string
|
||||||
|
createdAt?: string
|
||||||
|
}[]
|
||||||
|
notes: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getDashboardOverviewApi = () => {
|
||||||
|
return request.get<DashboardOverview>({ url: '/admin/dashboard/overview' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getTranscodeBoardApi = () => {
|
||||||
|
return request.get<TranscodeBoard>({ url: '/admin/dashboard/transcode' })
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
|
import request from '@/axios'
|
||||||
|
import type {
|
||||||
|
DramaCategory,
|
||||||
|
DramaItem,
|
||||||
|
DramaReviewAction,
|
||||||
|
DramaSaveData,
|
||||||
|
PageResult
|
||||||
|
} from './types'
|
||||||
|
|
||||||
|
export interface DramaPageParams {
|
||||||
|
pageIndex?: number
|
||||||
|
pageSize?: number
|
||||||
|
keyword?: string
|
||||||
|
status?: string
|
||||||
|
categoryId?: number
|
||||||
|
categoryIds?: number[]
|
||||||
|
isHot?: boolean
|
||||||
|
dramaTypes?: Array<'ANIME' | 'LIVE'>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const pageDramasApi = (params: DramaPageParams) => {
|
||||||
|
return request.get<PageResult<DramaItem>>({ url: '/admin/dramas/page', params })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getDramaApi = (id: number) => {
|
||||||
|
return request.get<DramaItem>({ url: `/admin/dramas/${id}` })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createDramaApi = (data: DramaSaveData) => {
|
||||||
|
return request.post<number>({ url: '/admin/dramas', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateDramaApi = (id: number, data: DramaSaveData) => {
|
||||||
|
return request.put<void>({ url: `/admin/dramas/${id}`, data })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteDramaApi = (id: number) => {
|
||||||
|
return request.delete<void>({ url: `/admin/dramas/${id}` })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 审核与发布:submit / approve / reject / publish / offline */
|
||||||
|
export const changeDramaStatusApi = (id: number, action: DramaReviewAction, reason?: string) => {
|
||||||
|
return request.post<void>({ url: `/admin/dramas/${id}/${action}`, data: { reason } })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 下拉选项(短剧表单用),只返回 id/name */
|
||||||
|
export const listDramaCategoryOptionsApi = () => {
|
||||||
|
return request.get<DramaCategory[]>({ url: '/admin/dicts/drama-category' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DramaCategorySaveData {
|
||||||
|
name: string
|
||||||
|
sort?: number
|
||||||
|
status?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 类型管理列表,带短剧数量 */
|
||||||
|
export const listDramaCategoriesApi = () => {
|
||||||
|
return request.get<DramaCategory[]>({ url: '/admin/drama-categories' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createDramaCategoryApi = (data: DramaCategorySaveData) => {
|
||||||
|
return request.post<number>({ url: '/admin/drama-categories', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateDramaCategoryApi = (id: number, data: DramaCategorySaveData) => {
|
||||||
|
return request.put<void>({ url: `/admin/drama-categories/${id}`, data })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteDramaCategoryApi = (id: number) => {
|
||||||
|
return request.delete<void>({ url: `/admin/drama-categories/${id}` })
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
|
import request from '@/axios'
|
||||||
|
import type { EpisodeItem, EpisodeSaveData } from './types'
|
||||||
|
|
||||||
|
export const listEpisodesApi = (dramaId: number, status?: string) => {
|
||||||
|
return request.get<EpisodeItem[]>({ url: '/admin/episodes', params: { dramaId, status } })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createEpisodeApi = (data: EpisodeSaveData) => {
|
||||||
|
return request.post<number>({ url: '/admin/episodes', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateEpisodeApi = (id: number, data: EpisodeSaveData) => {
|
||||||
|
return request.put<void>({ url: `/admin/episodes/${id}`, data })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteEpisodeApi = (id: number) => {
|
||||||
|
return request.delete<void>({ url: `/admin/episodes/${id}` })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sortEpisodesApi = (ids: number[]) => {
|
||||||
|
return request.put<void>({ url: '/admin/episodes/sort', data: { ids } })
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
|
import request from '@/axios'
|
||||||
|
import type { PageResult, CommentItem, ReportItem, SensitiveWordItem } from './types'
|
||||||
|
|
||||||
|
export const pageCommentsApi = (params: Record<string, unknown>) => {
|
||||||
|
return request.get<PageResult<CommentItem>>({ url: '/admin/comments/page', params })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const listCommentAuditRecordsApi = (id: number) => {
|
||||||
|
return request.get<Record<string, unknown>[]>({
|
||||||
|
url: `/admin/comments/${id}/audit-records`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/** result: PASS / REJECT */
|
||||||
|
export const auditCommentApi = (id: number, data: Record<string, unknown>) => {
|
||||||
|
return request.put<void>({ url: `/admin/comments/${id}/audit`, data })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteCommentApi = (id: number) => {
|
||||||
|
return request.delete<void>({ url: `/admin/comments/${id}` })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const pageReportsApi = (params: Record<string, unknown>) => {
|
||||||
|
return request.get<PageResult<ReportItem>>({ url: '/admin/reports/page', params })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** status: handled / rejected;banTarget=true 时联动封禁被举报用户 */
|
||||||
|
export const handleReportApi = (id: number, data: Record<string, unknown>) => {
|
||||||
|
return request.put<void>({ url: `/admin/reports/${id}/handle`, data })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const pageSensitiveWordsApi = (params: Record<string, unknown>) => {
|
||||||
|
return request.get<PageResult<SensitiveWordItem>>({
|
||||||
|
url: '/admin/sensitive-words/page',
|
||||||
|
params
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createSensitiveWordApi = (data: Record<string, unknown>) => {
|
||||||
|
return request.post<number>({ url: '/admin/sensitive-words', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateSensitiveWordApi = (id: number, data: Record<string, unknown>) => {
|
||||||
|
return request.put<void>({ url: `/admin/sensitive-words/${id}`, data })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteSensitiveWordApi = (id: number) => {
|
||||||
|
return request.delete<void>({ url: `/admin/sensitive-words/${id}` })
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
|
import request from '@/axios'
|
||||||
|
import type { OperationLogItem, PageResult } from './types'
|
||||||
|
|
||||||
|
export interface OperationLogPageParams {
|
||||||
|
pageIndex?: number
|
||||||
|
pageSize?: number
|
||||||
|
username?: string
|
||||||
|
module?: string
|
||||||
|
action?: string
|
||||||
|
status?: string
|
||||||
|
startTime?: string
|
||||||
|
endTime?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const pageOperationLogsApi = (params: OperationLogPageParams) => {
|
||||||
|
return request.get<PageResult<OperationLogItem>>({ url: '/admin/operation-logs/page', params })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getOperationLogApi = (id: number) => {
|
||||||
|
return request.get<OperationLogItem>({ url: `/admin/operation-logs/${id}` })
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
|
import request from '@/axios'
|
||||||
|
import type { MediaAsset } from './types'
|
||||||
|
|
||||||
|
export interface UploadInitData {
|
||||||
|
fileName: string
|
||||||
|
fileSize: number
|
||||||
|
/** DRAMA_COVER / EPISODE / AVATAR */
|
||||||
|
bizType?: string
|
||||||
|
chunkSize?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UploadInitResult {
|
||||||
|
uploadId: string
|
||||||
|
chunkSize: number
|
||||||
|
totalChunks: number
|
||||||
|
objectKey: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const initUploadApi = (data: UploadInitData) => {
|
||||||
|
return request.post<UploadInitResult>({ url: '/admin/media/upload/init', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 不设置 Content-Type,交给浏览器带 boundary */
|
||||||
|
export const uploadChunkApi = (uploadId: string, index: number, chunk: Blob, fileName: string) => {
|
||||||
|
const form = new FormData()
|
||||||
|
form.append('uploadId', uploadId)
|
||||||
|
form.append('index', String(index))
|
||||||
|
form.append('file', chunk, fileName)
|
||||||
|
return request.post<number>({ url: '/admin/media/upload/chunk', data: form })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const completeUploadApi = (uploadId: string, durationSec?: number) => {
|
||||||
|
return request.post<MediaAsset>({
|
||||||
|
url: '/admin/media/upload/complete',
|
||||||
|
data: { uploadId, durationSec }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UploadProgress {
|
||||||
|
uploadId: string
|
||||||
|
fileName: string
|
||||||
|
bizType: string
|
||||||
|
fileSize: number
|
||||||
|
chunkSize: number
|
||||||
|
totalChunks: number
|
||||||
|
receivedChunks: number
|
||||||
|
percent: number
|
||||||
|
/** uploading / completed */
|
||||||
|
status: string
|
||||||
|
assetId: number
|
||||||
|
url: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 断点续传回显:传到第几片、还剩多少(服务端说了算,客户端不自己记账) */
|
||||||
|
export const getUploadProgressApi = (uploadId: string) => {
|
||||||
|
return request.get<UploadProgress>({ url: `/admin/media/upload/${uploadId}` })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getMediaApi = (id: number) => {
|
||||||
|
return request.get<MediaAsset>({ url: `/admin/media/${id}` })
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
|
import request from '@/axios'
|
||||||
|
import type { PageResult } from './types'
|
||||||
|
|
||||||
|
/** 系统公告:字段与 /admin/notices 的响应一一对应 */
|
||||||
|
export interface NoticeItem {
|
||||||
|
/** 雪花 id,后端以字符串下发(超过 JS 安全整数范围),原样回传即可 */
|
||||||
|
id: string
|
||||||
|
title: string
|
||||||
|
content: string
|
||||||
|
/** ALL / USER / CREATOR */
|
||||||
|
target: string
|
||||||
|
targetText: string
|
||||||
|
/** SYSTEM / AUDIT / PAYMENT / ACTIVITY */
|
||||||
|
type: string
|
||||||
|
typeText: string
|
||||||
|
/** 1 已发布 / 0 已下架 */
|
||||||
|
status: number
|
||||||
|
statusText: string
|
||||||
|
publishAt?: string
|
||||||
|
expireAt?: string
|
||||||
|
createdAt?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NoticePageParams {
|
||||||
|
pageIndex?: number
|
||||||
|
pageSize?: number
|
||||||
|
status?: number
|
||||||
|
target?: string
|
||||||
|
type?: string
|
||||||
|
keyword?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NoticeSaveData {
|
||||||
|
title: string
|
||||||
|
content: string
|
||||||
|
target: string
|
||||||
|
type: string
|
||||||
|
publishAt?: string | null
|
||||||
|
expireAt?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export const pageNoticesApi = (params: NoticePageParams) => {
|
||||||
|
return request.get<PageResult<NoticeItem>>({ url: '/admin/notices/page', params })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getNoticeApi = (id: string | number) => {
|
||||||
|
return request.get<NoticeItem>({ url: `/admin/notices/${id}` })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createNoticeApi = (data: NoticeSaveData) => {
|
||||||
|
return request.post<string>({ url: '/admin/notices', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateNoticeApi = (id: string | number, data: NoticeSaveData) => {
|
||||||
|
return request.put<void>({ url: `/admin/notices/${id}`, data })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateNoticeStatusApi = (id: string | number, status: number) => {
|
||||||
|
return request.put<void>({ url: `/admin/notices/${id}/status`, params: { status } })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteNoticeApi = (id: string | number) => {
|
||||||
|
return request.delete<void>({ url: `/admin/notices/${id}` })
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
|
import request from '@/axios'
|
||||||
|
import type { PageResult } from './types'
|
||||||
|
|
||||||
|
/** 商品(金币包):字段与 /admin/products 的响应一一对应 */
|
||||||
|
export interface ProductItem {
|
||||||
|
/** 雪花 id,后端以字符串下发(超过 JS 安全整数范围),原样回传即可 */
|
||||||
|
id: string
|
||||||
|
code: string
|
||||||
|
name: string
|
||||||
|
/** COIN_PACK / VIP */
|
||||||
|
type: string
|
||||||
|
typeText: string
|
||||||
|
/** ALL / GOOGLE / APPLE */
|
||||||
|
platform: string
|
||||||
|
storeProductId: string
|
||||||
|
coinAmount: number
|
||||||
|
bonusCoin: number
|
||||||
|
/** coinAmount + bonusCoin,服务端算好,前端不再自己加 */
|
||||||
|
totalCoin: number
|
||||||
|
priceAmount: number
|
||||||
|
currency: string
|
||||||
|
displayPrice: string
|
||||||
|
regionCodes: string
|
||||||
|
vipDays: number
|
||||||
|
sort: number
|
||||||
|
/** 1 已上架 / 0 已下架 */
|
||||||
|
status: number
|
||||||
|
statusText: string
|
||||||
|
effectiveAt?: string
|
||||||
|
expireAt?: string
|
||||||
|
createdAt?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProductPageParams {
|
||||||
|
pageIndex?: number
|
||||||
|
pageSize?: number
|
||||||
|
type?: string
|
||||||
|
status?: number
|
||||||
|
platform?: string
|
||||||
|
keyword?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProductSaveData {
|
||||||
|
code: string
|
||||||
|
name: string
|
||||||
|
type: string
|
||||||
|
platform: string
|
||||||
|
storeProductId: string
|
||||||
|
coinAmount: number
|
||||||
|
bonusCoin: number
|
||||||
|
priceAmount: number
|
||||||
|
currency: string
|
||||||
|
displayPrice: string
|
||||||
|
regionCodes: string
|
||||||
|
vipDays: number
|
||||||
|
sort: number
|
||||||
|
effectiveAt?: string | null
|
||||||
|
expireAt?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export const pageProductsApi = (params: ProductPageParams) => {
|
||||||
|
return request.get<PageResult<ProductItem>>({ url: '/admin/products/page', params })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getProductApi = (id: string | number) => {
|
||||||
|
return request.get<ProductItem>({ url: `/admin/products/${id}` })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createProductApi = (data: ProductSaveData) => {
|
||||||
|
return request.post<string>({ url: '/admin/products', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateProductApi = (id: string | number, data: ProductSaveData) => {
|
||||||
|
return request.put<void>({ url: `/admin/products/${id}`, data })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateProductStatusApi = (id: string | number, status: number) => {
|
||||||
|
return request.put<void>({ url: `/admin/products/${id}/status`, params: { status } })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteProductApi = (id: string | number) => {
|
||||||
|
return request.delete<void>({ url: `/admin/products/${id}` })
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
|
import request from '@/axios'
|
||||||
|
import type {
|
||||||
|
MenuNode,
|
||||||
|
MenuSaveData,
|
||||||
|
PageResult,
|
||||||
|
PermissionGroup,
|
||||||
|
RoleCategoryItem,
|
||||||
|
RoleItem,
|
||||||
|
RoleSaveData
|
||||||
|
} from './types'
|
||||||
|
|
||||||
|
export interface RolePageParams {
|
||||||
|
keyword?: string
|
||||||
|
pageIndex?: number
|
||||||
|
pageSize?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const pageRolesApi = (params: RolePageParams) => {
|
||||||
|
return request.get<PageResult<RoleItem>>({ url: '/admin/roles/page', params })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const listRolesApi = () => {
|
||||||
|
return request.get<RoleItem[]>({ url: '/admin/roles/list' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createRoleApi = (data: RoleSaveData) => {
|
||||||
|
return request.post<number>({ url: '/admin/roles', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateRoleApi = (id: number, data: RoleSaveData) => {
|
||||||
|
return request.put<void>({ url: `/admin/roles/${id}`, data })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteRoleApi = (id: number) => {
|
||||||
|
return request.delete<void>({ url: `/admin/roles/${id}` })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getRolePermissionsApi = (id: number) => {
|
||||||
|
return request.get<string[]>({ url: `/admin/roles/${id}/permissions` })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateRolePermissionsApi = (id: number, permissions: string[]) => {
|
||||||
|
return request.put<void>({ url: `/admin/roles/${id}/permissions`, data: { permissions } })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const listPermissionGroupsApi = () => {
|
||||||
|
return request.get<PermissionGroup[]>({ url: '/admin/permissions' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RoleCategorySaveData {
|
||||||
|
name: string
|
||||||
|
description?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const listRoleCategoriesApi = () => {
|
||||||
|
return request.get<RoleCategoryItem[]>({ url: '/admin/role-categories' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createRoleCategoryApi = (data: RoleCategorySaveData) => {
|
||||||
|
return request.post<number>({ url: '/admin/role-categories', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateRoleCategoryApi = (id: number, data: RoleCategorySaveData) => {
|
||||||
|
return request.put<void>({ url: `/admin/role-categories/${id}`, data })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteRoleCategoryApi = (id: number) => {
|
||||||
|
return request.delete<void>({ url: `/admin/role-categories/${id}` })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getMenuTreeApi = () => {
|
||||||
|
return request.get<MenuNode[]>({ url: '/admin/menus/tree' })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createMenuApi = (data: MenuSaveData) => {
|
||||||
|
return request.post<number>({ url: '/admin/menus', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateMenuApi = (id: number, data: MenuSaveData) => {
|
||||||
|
return request.put<void>({ url: `/admin/menus/${id}`, data })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteMenuApi = (id: number) => {
|
||||||
|
return request.delete<void>({ url: `/admin/menus/${id}` })
|
||||||
|
}
|
||||||
@@ -0,0 +1,391 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
|
export interface PageResult<T> {
|
||||||
|
total: number
|
||||||
|
list: T[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DramaCategory {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
sort?: number
|
||||||
|
status?: number
|
||||||
|
/** 该类型下的短剧数量,管理列表才有 */
|
||||||
|
dramaCount?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DramaItem {
|
||||||
|
id: number
|
||||||
|
code?: string
|
||||||
|
title: string
|
||||||
|
subtitle?: string
|
||||||
|
categoryId?: number
|
||||||
|
categoryIds?: number[]
|
||||||
|
categoryNames?: string[]
|
||||||
|
category?: string
|
||||||
|
genre?: string
|
||||||
|
description?: string
|
||||||
|
cover?: string
|
||||||
|
language?: string
|
||||||
|
dramaType?: 'ANIME' | 'LIVE'
|
||||||
|
dramaTypes?: Array<'ANIME' | 'LIVE'>
|
||||||
|
episodes?: number
|
||||||
|
freeEpisodes?: number
|
||||||
|
unlockCoin?: number
|
||||||
|
/** 是否热门 */
|
||||||
|
isHot?: boolean
|
||||||
|
status: string
|
||||||
|
statusText: string
|
||||||
|
views?: number
|
||||||
|
favoriteCount?: number
|
||||||
|
likeCount?: number
|
||||||
|
commentCount?: number
|
||||||
|
sort?: number
|
||||||
|
publishAt?: string
|
||||||
|
createdAt?: string
|
||||||
|
updatedAt?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DramaSaveData {
|
||||||
|
title: string
|
||||||
|
subtitle?: string
|
||||||
|
categoryId?: number
|
||||||
|
categoryIds?: number[]
|
||||||
|
genre?: string
|
||||||
|
description?: string
|
||||||
|
cover?: string
|
||||||
|
language?: string
|
||||||
|
dramaType?: 'ANIME' | 'LIVE'
|
||||||
|
dramaTypes?: Array<'ANIME' | 'LIVE'>
|
||||||
|
freeEpisodes?: number
|
||||||
|
unlockCoin?: number
|
||||||
|
favoriteCount?: number
|
||||||
|
likeCount?: number
|
||||||
|
heat?: number
|
||||||
|
isHot?: boolean
|
||||||
|
sort?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DramaReviewAction = 'submit' | 'approve' | 'reject' | 'publish' | 'offline'
|
||||||
|
|
||||||
|
export interface EpisodeItem {
|
||||||
|
id: number
|
||||||
|
dramaId: number
|
||||||
|
number: number
|
||||||
|
title: string
|
||||||
|
duration: string
|
||||||
|
durationSec: number
|
||||||
|
cover?: string
|
||||||
|
videoUrl?: string
|
||||||
|
isFree: boolean
|
||||||
|
unlockCoin: number
|
||||||
|
status: string
|
||||||
|
statusText: string
|
||||||
|
views?: number
|
||||||
|
likeCount?: number
|
||||||
|
commentCount?: number
|
||||||
|
favoriteCount?: number
|
||||||
|
heat?: number
|
||||||
|
sort?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EpisodeSaveData {
|
||||||
|
dramaId: number
|
||||||
|
number: number
|
||||||
|
title?: string
|
||||||
|
durationSec?: number
|
||||||
|
cover?: string
|
||||||
|
videoUrl?: string
|
||||||
|
isFree?: boolean
|
||||||
|
unlockCoin?: number
|
||||||
|
likeCount?: number
|
||||||
|
status?: string
|
||||||
|
sort?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminUserItem {
|
||||||
|
id: number
|
||||||
|
username: string
|
||||||
|
name: string
|
||||||
|
roleId: number | null
|
||||||
|
role: string
|
||||||
|
ipWhitelist: string
|
||||||
|
status: number
|
||||||
|
statusText: string
|
||||||
|
twoFactor: number
|
||||||
|
twoFactorText: string
|
||||||
|
createdAt?: string
|
||||||
|
lastLogin?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminUserSaveData {
|
||||||
|
username?: string
|
||||||
|
password?: string
|
||||||
|
name: string
|
||||||
|
roleId: number
|
||||||
|
ipWhitelist?: string
|
||||||
|
status?: number
|
||||||
|
twoFactor?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RoleItem {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
code: string
|
||||||
|
description: string
|
||||||
|
categoryId: number
|
||||||
|
category: string
|
||||||
|
level: number
|
||||||
|
status: number
|
||||||
|
statusText: string
|
||||||
|
builtin: boolean
|
||||||
|
memberCount: number
|
||||||
|
permissions: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RoleSaveData {
|
||||||
|
name: string
|
||||||
|
code?: string
|
||||||
|
description?: string
|
||||||
|
categoryId?: number
|
||||||
|
level?: number
|
||||||
|
status?: number
|
||||||
|
permissions?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RoleCategoryItem {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
description: string
|
||||||
|
roleCount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PermissionItem {
|
||||||
|
code: string
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PermissionGroup {
|
||||||
|
name: string
|
||||||
|
items: PermissionItem[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MenuNode {
|
||||||
|
id: number
|
||||||
|
parentId?: number
|
||||||
|
path?: string
|
||||||
|
name?: string
|
||||||
|
component?: string
|
||||||
|
redirect?: string
|
||||||
|
icon?: string
|
||||||
|
type?: string
|
||||||
|
permissionCode?: string
|
||||||
|
sort?: number
|
||||||
|
visible?: number
|
||||||
|
status?: number
|
||||||
|
meta?: { title?: string; icon?: string; alwaysShow?: boolean; affix?: boolean }
|
||||||
|
children?: MenuNode[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MenuSaveData {
|
||||||
|
parentId?: number
|
||||||
|
title: string
|
||||||
|
routePath?: string
|
||||||
|
routeName?: string
|
||||||
|
component?: string
|
||||||
|
redirect?: string
|
||||||
|
icon?: string
|
||||||
|
permissionCode?: string
|
||||||
|
type?: string
|
||||||
|
sort?: number
|
||||||
|
visible?: number
|
||||||
|
status?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConfigItem {
|
||||||
|
key: string
|
||||||
|
value: string
|
||||||
|
remark?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MediaAsset {
|
||||||
|
id: number
|
||||||
|
bizType: string
|
||||||
|
fileName: string
|
||||||
|
objectKey: string
|
||||||
|
url: string
|
||||||
|
sizeBytes: number
|
||||||
|
md5: string
|
||||||
|
durationSec?: number
|
||||||
|
/** uploading / uploaded / transcoding / ready / failed */
|
||||||
|
status: string
|
||||||
|
createdAt?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BannerItem {
|
||||||
|
id: number
|
||||||
|
imageUrl: string
|
||||||
|
/** HOME_TOP / DISCOVER */
|
||||||
|
position: string
|
||||||
|
positionText: string
|
||||||
|
/** NONE / DRAMA / URL */
|
||||||
|
linkType: string
|
||||||
|
linkTypeText: string
|
||||||
|
linkValue: string
|
||||||
|
/** 跳转目标的展示文案 */
|
||||||
|
linkText: string
|
||||||
|
sort: number
|
||||||
|
status: number
|
||||||
|
statusText: string
|
||||||
|
updatedAt?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BannerSaveData {
|
||||||
|
imageUrl: string
|
||||||
|
position: string
|
||||||
|
linkType: string
|
||||||
|
linkValue?: string
|
||||||
|
sort?: number
|
||||||
|
status?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface OperationLogItem {
|
||||||
|
id: number
|
||||||
|
userId: number
|
||||||
|
username: string
|
||||||
|
module: string
|
||||||
|
action: string
|
||||||
|
requestMethod: string
|
||||||
|
requestUrl: string
|
||||||
|
requestParams?: string
|
||||||
|
requestBody?: string
|
||||||
|
responseBody?: string
|
||||||
|
/** SUCCESS / FAIL */
|
||||||
|
status: string
|
||||||
|
statusText: string
|
||||||
|
errorMsg?: string
|
||||||
|
durationMs: number
|
||||||
|
ip: string
|
||||||
|
createdAt?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** C 端用户(后台视图,比客户端多一个自增 id,用于后台按 id 寻址) */
|
||||||
|
export interface AppUserItem {
|
||||||
|
id: number
|
||||||
|
uid: string
|
||||||
|
nickname: string
|
||||||
|
avatarUrl: string
|
||||||
|
gender: string
|
||||||
|
countryCode: string
|
||||||
|
regionCode: string
|
||||||
|
language: string
|
||||||
|
timezone: string
|
||||||
|
currency: string
|
||||||
|
/** normal / limited / banned / deactivated */
|
||||||
|
status: string
|
||||||
|
/** 脱敏后的手机号或邮箱 */
|
||||||
|
maskedTarget: string
|
||||||
|
createdAt?: string
|
||||||
|
lastLoginAt?: string
|
||||||
|
/** 预留:会员等级与到期(第二阶段) */
|
||||||
|
vipLevel?: number
|
||||||
|
vipExpireAt?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppUserBanRecord {
|
||||||
|
id: number
|
||||||
|
userId: number
|
||||||
|
/** BAN / LIMIT / MUTE */
|
||||||
|
type: string
|
||||||
|
reasonCode: string
|
||||||
|
reason: string
|
||||||
|
refType: string
|
||||||
|
refId: number
|
||||||
|
startAt?: string
|
||||||
|
endAt?: string
|
||||||
|
operatorId: number
|
||||||
|
/** none / pending / approved / rejected */
|
||||||
|
appealStatus: string
|
||||||
|
appealContent: string
|
||||||
|
handleNote: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WatchHistoryItem {
|
||||||
|
id: number
|
||||||
|
userId?: number
|
||||||
|
uid?: string
|
||||||
|
nickname?: string
|
||||||
|
dramaId: number
|
||||||
|
dramaTitle?: string
|
||||||
|
dramaPublished?: boolean
|
||||||
|
episodeId: number
|
||||||
|
episodeNo?: number
|
||||||
|
episodeTitle?: string
|
||||||
|
positionMs: number
|
||||||
|
durationMs: number
|
||||||
|
finished?: boolean
|
||||||
|
source: string
|
||||||
|
deviceId: string
|
||||||
|
watchedAt?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 评论(后台视图,字段与 /admin/comments/page 一一对应) */
|
||||||
|
export interface CommentItem {
|
||||||
|
id: number
|
||||||
|
dramaId: number
|
||||||
|
dramaTitle?: string
|
||||||
|
episodeId: number
|
||||||
|
episodeNo?: number
|
||||||
|
episodeTitle?: string
|
||||||
|
parentId: number
|
||||||
|
rootId: number
|
||||||
|
userId: number
|
||||||
|
uid: string
|
||||||
|
nickname: string
|
||||||
|
content: string
|
||||||
|
/** pending / published / rejected / deleted */
|
||||||
|
status: string
|
||||||
|
auditReason: string
|
||||||
|
likeCount: number
|
||||||
|
replyCount: number
|
||||||
|
reportCount: number
|
||||||
|
createdAt?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReportItem {
|
||||||
|
id: number
|
||||||
|
reporterId: number
|
||||||
|
reporterNickname?: string
|
||||||
|
reporterUid?: string
|
||||||
|
/** COMMENT / USER / DRAMA */
|
||||||
|
targetType: string
|
||||||
|
targetId: number
|
||||||
|
targetSummary?: string
|
||||||
|
dramaId?: number
|
||||||
|
dramaTitle?: string
|
||||||
|
episodeId?: number
|
||||||
|
episodeNo?: number
|
||||||
|
episodeTitle?: string
|
||||||
|
commentContent?: string
|
||||||
|
targetNickname?: string
|
||||||
|
targetUid?: string
|
||||||
|
reasonCode: string
|
||||||
|
description: string
|
||||||
|
/** pending / handled / rejected */
|
||||||
|
status: string
|
||||||
|
handleBy: number
|
||||||
|
handleAt?: string
|
||||||
|
handleNote: string
|
||||||
|
createdAt?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SensitiveWordItem {
|
||||||
|
id: number
|
||||||
|
word: string
|
||||||
|
category: string
|
||||||
|
/** BLOCK / REVIEW / REPLACE */
|
||||||
|
level: string
|
||||||
|
replaceText: string
|
||||||
|
language: string
|
||||||
|
status: number
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
|
import request from '@/axios'
|
||||||
|
import type { AdminUserItem, AdminUserSaveData, PageResult } from './types'
|
||||||
|
|
||||||
|
export interface AdminUserPageParams {
|
||||||
|
pageIndex?: number
|
||||||
|
pageSize?: number
|
||||||
|
username?: string
|
||||||
|
roleId?: number
|
||||||
|
status?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const pageAdminUsersApi = (params: AdminUserPageParams) => {
|
||||||
|
return request.get<PageResult<AdminUserItem>>({ url: '/admin/admin-users/page', params })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createAdminUserApi = (data: AdminUserSaveData) => {
|
||||||
|
return request.post<number>({ url: '/admin/admin-users', data })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateAdminUserApi = (id: number, data: AdminUserSaveData) => {
|
||||||
|
return request.put<void>({ url: `/admin/admin-users/${id}`, data })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteAdminUserApi = (id: number) => {
|
||||||
|
return request.delete<void>({ url: `/admin/admin-users/${id}` })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateAdminUserStatusApi = (id: number, status: number) => {
|
||||||
|
return request.put<void>({ url: `/admin/admin-users/${id}/status`, params: { status } })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const resetAdminUserPasswordApi = (id: number, password: string) => {
|
||||||
|
return request.put<void>({ url: `/admin/admin-users/${id}/password`, data: { password } })
|
||||||
|
}
|
||||||
+13
-5
@@ -1,3 +1,5 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
import request from '@/axios'
|
import request from '@/axios'
|
||||||
import type { UserType } from './types'
|
import type { UserType } from './types'
|
||||||
|
|
||||||
@@ -6,19 +8,25 @@ interface RoleParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const loginApi = (data: UserType): Promise<IResponse<UserType>> => {
|
export const loginApi = (data: UserType): Promise<IResponse<UserType>> => {
|
||||||
return request.post({ url: '/mock/user/login', data })
|
return request.post({ url: '/admin/auth/login', data })
|
||||||
}
|
}
|
||||||
|
|
||||||
export const loginOutApi = (): Promise<IResponse> => {
|
export const loginOutApi = (): Promise<IResponse> => {
|
||||||
return request.get({ url: '/mock/user/loginOut' })
|
return request.post({ url: '/admin/auth/logout' })
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 当前登录人信息(含实时权限) */
|
||||||
|
export const getProfileApi = (): Promise<IResponse<UserType>> => {
|
||||||
|
return request.get({ url: '/admin/auth/profile' })
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getAdminRoleApi = (
|
export const getAdminRoleApi = (
|
||||||
params: RoleParams
|
params: RoleParams
|
||||||
): Promise<IResponse<AppCustomRouteRecordRaw[]>> => {
|
): Promise<IResponse<AppCustomRouteRecordRaw[]>> => {
|
||||||
return request.get({ url: '/mock/role/list', params })
|
return request.get({ url: '/admin/auth/menus', params })
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getTestRoleApi = (params: RoleParams): Promise<IResponse<string[]>> => {
|
/** 自助改密(首登强制改密也走这个接口) */
|
||||||
return request.get({ url: '/mock/role/list2', params })
|
export const changePasswordApi = (data: { oldPassword: string; newPassword: string }) => {
|
||||||
|
return request.post<void>({ url: '/admin/auth/password', data })
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
export type UserLoginType = {
|
export type UserLoginType = {
|
||||||
username: string
|
username: string
|
||||||
password: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type UserType = {
|
export type UserType = {
|
||||||
@@ -9,4 +10,10 @@ export type UserType = {
|
|||||||
role: string
|
role: string
|
||||||
roleId: string
|
roleId: string
|
||||||
permissions: string | string[]
|
permissions: string | string[]
|
||||||
|
/** 登录成功后由后端下发 */
|
||||||
|
token?: string
|
||||||
|
userId?: number
|
||||||
|
name?: string
|
||||||
|
avatar?: string
|
||||||
|
roleName?: string
|
||||||
}
|
}
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 6.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 5.9 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 9.0 KiB |
@@ -1,3 +1,5 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
import { AxiosResponse, InternalAxiosRequestConfig } from './types'
|
import { AxiosResponse, InternalAxiosRequestConfig } from './types'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import qs from 'qs'
|
import qs from 'qs'
|
||||||
@@ -41,6 +43,10 @@ const defaultResponseInterceptors = (response: AxiosResponse) => {
|
|||||||
} else if (response.data.code === SUCCESS_CODE) {
|
} else if (response.data.code === SUCCESS_CODE) {
|
||||||
return response.data
|
return response.data
|
||||||
} else {
|
} else {
|
||||||
|
// 静默请求:调用方要自己看业务码(例如创作者中心据此渲染引导页),不弹全局提示
|
||||||
|
if ((response.config as { silent?: boolean })?.silent) {
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
ElMessage.error(response?.data?.message)
|
ElMessage.error(response?.data?.message)
|
||||||
if (response?.data?.code === 401) {
|
if (response?.data?.code === 401) {
|
||||||
const userStore = useUserStoreWithOut()
|
const userStore = useUserStoreWithOut()
|
||||||
|
|||||||
+16
-3
@@ -1,19 +1,32 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
import service from './service'
|
import service from './service'
|
||||||
import { CONTENT_TYPE } from '@/constants'
|
import { CONTENT_TYPE } from '@/constants'
|
||||||
import { useUserStoreWithOut } from '@/store/modules/user'
|
import { useUserStoreWithOut } from '@/store/modules/user'
|
||||||
|
import { creatorCenterHeaders } from '@/utils/creatorImpersonation'
|
||||||
|
|
||||||
const request = (option: AxiosConfig) => {
|
const request = (option: AxiosConfig) => {
|
||||||
const { url, method, params, data, headers, responseType } = option
|
const { url, method, params, data, headers, responseType, silent } = option
|
||||||
const userStore = useUserStoreWithOut()
|
const userStore = useUserStoreWithOut()
|
||||||
|
// FormData 不能带 JSON 的 Content-Type,否则 axios 会把表单转成 JSON,浏览器也无法补上 boundary
|
||||||
|
const isFormData = typeof FormData !== 'undefined' && data instanceof FormData
|
||||||
|
// 创作者中心的请求统一带上代看目标头(超管选了代看创作者时才有值;
|
||||||
|
// 后端只对具备 impersonate 权限的账号认这个头,创作者带了也不生效)
|
||||||
|
const impersonationHeaders =
|
||||||
|
typeof url === 'string' && url.startsWith('/admin/creator-center') ? creatorCenterHeaders() : {}
|
||||||
return service.request({
|
return service.request({
|
||||||
url: url,
|
url: url,
|
||||||
method,
|
method,
|
||||||
params,
|
params,
|
||||||
data,
|
data,
|
||||||
responseType: responseType,
|
responseType: responseType,
|
||||||
|
silent,
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': CONTENT_TYPE,
|
...(isFormData ? {} : { 'Content-Type': CONTENT_TYPE }),
|
||||||
[userStore.getTokenKey ?? 'Authorization']: userStore.getToken ?? '',
|
[userStore.getTokenKey ?? 'Authorization']: userStore.getToken
|
||||||
|
? `Bearer ${userStore.getToken}`
|
||||||
|
: '',
|
||||||
|
...impersonationHeaders,
|
||||||
...headers
|
...headers
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
import axios, { AxiosError } from 'axios'
|
import axios, { AxiosError } from 'axios'
|
||||||
import { defaultRequestInterceptors, defaultResponseInterceptors } from './config'
|
import { defaultRequestInterceptors, defaultResponseInterceptors } from './config'
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
InternalAxiosRequestConfig,
|
InternalAxiosRequestConfig,
|
||||||
AxiosResponse,
|
AxiosResponse,
|
||||||
@@ -18,6 +20,8 @@ interface RequestInterceptors<T> {
|
|||||||
|
|
||||||
interface RequestConfig<T = AxiosResponse> extends AxiosRequestConfig {
|
interface RequestConfig<T = AxiosResponse> extends AxiosRequestConfig {
|
||||||
interceptors?: RequestInterceptors<T>
|
interceptors?: RequestInterceptors<T>
|
||||||
|
/** 静默请求:业务码非 0 时由调用方处理,不弹全局提示(见 axios/config.ts) */
|
||||||
|
silent?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
|
import AppIcon from './src/AppIcon.vue'
|
||||||
|
|
||||||
|
export { AppIcon }
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<!-- Author: fruit.com.cn -->
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { Icon } from '@/components/Icon'
|
||||||
|
import { normalizeAppIcon } from '@/constants/icons'
|
||||||
|
import { propTypes } from '@/utils/propTypes'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 后台统一图标入口:默认使用 Material Symbols Rounded。
|
||||||
|
*/
|
||||||
|
const props = defineProps({
|
||||||
|
icon: propTypes.string,
|
||||||
|
color: propTypes.string,
|
||||||
|
size: propTypes.number.def(18),
|
||||||
|
hoverColor: propTypes.string
|
||||||
|
})
|
||||||
|
|
||||||
|
const resolvedIcon = computed(() => normalizeAppIcon(props.icon))
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Icon
|
||||||
|
:icon="resolvedIcon"
|
||||||
|
:color="color"
|
||||||
|
:size="size"
|
||||||
|
:hover-color="hoverColor"
|
||||||
|
class="app-icon"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.app-icon {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
line-height: 1;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
|
import AppImage from './src/AppImage.vue'
|
||||||
|
|
||||||
|
export { AppImage }
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
<!-- Author: fruit.com.cn -->
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import type { PropType } from 'vue'
|
||||||
|
import { ElImage } from 'element-plus'
|
||||||
|
import { AppIcon } from '@/components/AppIcon'
|
||||||
|
import { AppIcons } from '@/constants/icons'
|
||||||
|
|
||||||
|
defineOptions({ inheritAttrs: false })
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
src: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
previewList: {
|
||||||
|
type: Array as () => string[],
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
initialIndex: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
fit: {
|
||||||
|
type: String as PropType<'fill' | 'contain' | 'cover' | 'none' | 'scale-down'>,
|
||||||
|
default: 'cover'
|
||||||
|
},
|
||||||
|
class: {
|
||||||
|
type: [String, Array, Object],
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
type: [String, Array, Object],
|
||||||
|
default: undefined
|
||||||
|
},
|
||||||
|
alt: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const resolvedPreviewList = computed(() => {
|
||||||
|
const images = (props.previewList || []).filter(Boolean)
|
||||||
|
return images.length ? images : [props.src]
|
||||||
|
})
|
||||||
|
|
||||||
|
const resolvedInitialIndex = computed(() =>
|
||||||
|
Math.max(0, Math.min(props.initialIndex, resolvedPreviewList.value.length - 1))
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<ElImage
|
||||||
|
v-bind="$attrs"
|
||||||
|
:src="src"
|
||||||
|
:alt="alt"
|
||||||
|
:fit="fit"
|
||||||
|
:preview-src-list="resolvedPreviewList"
|
||||||
|
:initial-index="resolvedInitialIndex"
|
||||||
|
:preview-teleported="true"
|
||||||
|
:hide-on-click-modal="true"
|
||||||
|
:class="['app-image', props.class]"
|
||||||
|
:style="style"
|
||||||
|
>
|
||||||
|
<template #error>
|
||||||
|
<div class="app-image-error">
|
||||||
|
<AppIcon :icon="AppIcons.error" :size="20" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ElImage>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.app-image {
|
||||||
|
cursor: zoom-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-image :deep(.el-image__inner) {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-image-error {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
color: var(--el-text-color-placeholder);
|
||||||
|
background: var(--el-fill-color-lighter);
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
import Backtop from './src/Backtop.vue'
|
import Backtop from './src/Backtop.vue'
|
||||||
|
|
||||||
export { Backtop }
|
export { Backtop }
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
<!-- Author: fruit.com.cn -->
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ElBacktop } from 'element-plus'
|
import { ElBacktop } from 'element-plus'
|
||||||
import { useDesign } from '@/hooks/web/useDesign'
|
import { useDesign } from '@/hooks/web/useDesign'
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
import Breadcrumb from './src/Breadcrumb.vue'
|
import Breadcrumb from './src/Breadcrumb.vue'
|
||||||
|
|
||||||
export { Breadcrumb }
|
export { Breadcrumb }
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
<!-- Author: fruit.com.cn -->
|
||||||
|
|
||||||
<script lang="tsx">
|
<script lang="tsx">
|
||||||
import { ElBreadcrumb, ElBreadcrumbItem } from 'element-plus'
|
import { ElBreadcrumb, ElBreadcrumbItem } from 'element-plus'
|
||||||
import { ref, watch, computed, unref, defineComponent, TransitionGroup } from 'vue'
|
import { ref, watch, computed, unref, defineComponent, TransitionGroup } from 'vue'
|
||||||
@@ -7,7 +9,7 @@ import { filterBreadcrumb } from './helper'
|
|||||||
import { filter, treeToList } from '@/utils/tree'
|
import { filter, treeToList } from '@/utils/tree'
|
||||||
import type { RouteLocationNormalizedLoaded, RouteMeta } from 'vue-router'
|
import type { RouteLocationNormalizedLoaded, RouteMeta } from 'vue-router'
|
||||||
import { useI18n } from '@/hooks/web/useI18n'
|
import { useI18n } from '@/hooks/web/useI18n'
|
||||||
import { Icon } from '@/components/Icon'
|
import { AppIcon } from '@/components/AppIcon'
|
||||||
import { useAppStore } from '@/store/modules/app'
|
import { useAppStore } from '@/store/modules/app'
|
||||||
import { useDesign } from '@/hooks/web/useDesign'
|
import { useDesign } from '@/hooks/web/useDesign'
|
||||||
|
|
||||||
@@ -52,7 +54,7 @@ export default defineComponent({
|
|||||||
<ElBreadcrumbItem to={{ path: disabled ? '' : v.path }} key={v.name}>
|
<ElBreadcrumbItem to={{ path: disabled ? '' : v.path }} key={v.name}>
|
||||||
{meta?.icon && breadcrumbIcon.value ? (
|
{meta?.icon && breadcrumbIcon.value ? (
|
||||||
<>
|
<>
|
||||||
<Icon icon={meta.icon} class="mr-[5px]"></Icon> {t(v?.meta?.title)}
|
<AppIcon icon={meta.icon} class="mr-[5px]"></AppIcon> {t(v?.meta?.title)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
t(v?.meta?.title)
|
t(v?.meta?.title)
|
||||||
@@ -115,10 +117,10 @@ export default defineComponent({
|
|||||||
|
|
||||||
:deep(&__item):last-child {
|
:deep(&__item):last-child {
|
||||||
.@{prefix-cls}__inner {
|
.@{prefix-cls}__inner {
|
||||||
color: var(--el-text-color-placeholder);
|
color: var(--el-text-color-regular);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: var(--el-text-color-placeholder);
|
color: var(--el-color-primary);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
import { pathResolve } from '@/utils/routerHelper'
|
import { pathResolve } from '@/utils/routerHelper'
|
||||||
import type { RouteMeta } from 'vue-router'
|
import type { RouteMeta } from 'vue-router'
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
import BaseButton from './src/Button.vue'
|
import BaseButton from './src/Button.vue'
|
||||||
|
|
||||||
export { BaseButton }
|
export { BaseButton }
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
<!-- Author: fruit.com.cn -->
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useDesign } from '@/hooks/web/useDesign'
|
import { useDesign } from '@/hooks/web/useDesign'
|
||||||
import { ElButton, ComponentSize, ButtonType } from 'element-plus'
|
import { ElButton, ComponentSize, ButtonType } from 'element-plus'
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
import Collapse from './src/Collapse.vue'
|
import Collapse from './src/Collapse.vue'
|
||||||
|
|
||||||
export { Collapse }
|
export { Collapse }
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
|
<!-- Author: fruit.com.cn -->
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, unref } from 'vue'
|
import { computed, unref } from 'vue'
|
||||||
import { useAppStore } from '@/store/modules/app'
|
import { useAppStore } from '@/store/modules/app'
|
||||||
import { propTypes } from '@/utils/propTypes'
|
import { propTypes } from '@/utils/propTypes'
|
||||||
import { useDesign } from '@/hooks/web/useDesign'
|
import { useDesign } from '@/hooks/web/useDesign'
|
||||||
|
import { AppIcons } from '@/constants/icons'
|
||||||
|
|
||||||
const { getPrefixCls } = useDesign()
|
const { getPrefixCls } = useDesign()
|
||||||
|
|
||||||
@@ -24,9 +27,9 @@ const toggleCollapse = () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="prefixCls" @click="toggleCollapse">
|
<div :class="prefixCls" @click="toggleCollapse">
|
||||||
<Icon
|
<AppIcon
|
||||||
:size="18"
|
:size="18"
|
||||||
:icon="collapse ? 'ant-design:menu-unfold-outlined' : 'ant-design:menu-fold-outlined'"
|
:icon="collapse ? AppIcons.menuOpen : AppIcons.menu"
|
||||||
:color="color"
|
:color="color"
|
||||||
class="cursor-pointer"
|
class="cursor-pointer"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
import ConfigGlobal from './src/ConfigGlobal.vue'
|
import ConfigGlobal from './src/ConfigGlobal.vue'
|
||||||
|
|
||||||
export type { ConfigGlobalTypes } from './src/types'
|
export type { ConfigGlobalTypes } from './src/types'
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
<!-- Author: fruit.com.cn -->
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { provide, computed, watch, onMounted } from 'vue'
|
import { provide, computed, watch, onMounted } from 'vue'
|
||||||
import { propTypes } from '@/utils/propTypes'
|
import { propTypes } from '@/utils/propTypes'
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
import { ComponentSize } from 'element-plus'
|
import { ComponentSize } from 'element-plus'
|
||||||
|
|
||||||
export interface ConfigGlobalTypes {
|
export interface ConfigGlobalTypes {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
import ContentDetailWrap from './src/ContentDetailWrap.vue'
|
import ContentDetailWrap from './src/ContentDetailWrap.vue'
|
||||||
|
|
||||||
export { ContentDetailWrap }
|
export { ContentDetailWrap }
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
<!-- Author: fruit.com.cn -->
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ElCard } from 'element-plus'
|
import { ElCard } from 'element-plus'
|
||||||
import { propTypes } from '@/utils/propTypes'
|
import { propTypes } from '@/utils/propTypes'
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
import ContentWrap from './src/ContentWrap.vue'
|
import ContentWrap from './src/ContentWrap.vue'
|
||||||
|
|
||||||
export { ContentWrap }
|
export { ContentWrap }
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
|
<!-- Author: fruit.com.cn -->
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ElCard, ElTooltip } from 'element-plus'
|
import { ElCard, ElTooltip } from 'element-plus'
|
||||||
import { propTypes } from '@/utils/propTypes'
|
import { propTypes } from '@/utils/propTypes'
|
||||||
import { useDesign } from '@/hooks/web/useDesign'
|
import { useDesign } from '@/hooks/web/useDesign'
|
||||||
|
import { AppIcons } from '@/constants/icons'
|
||||||
|
|
||||||
const { getPrefixCls } = useDesign()
|
const { getPrefixCls } = useDesign()
|
||||||
|
|
||||||
@@ -22,7 +25,7 @@ defineProps({
|
|||||||
<template #content>
|
<template #content>
|
||||||
<div class="max-w-200px">{{ message }}</div>
|
<div class="max-w-200px">{{ message }}</div>
|
||||||
</template>
|
</template>
|
||||||
<Icon class="ml-5px" icon="bi:question-circle-fill" :size="14" />
|
<AppIcon class="ml-5px" :icon="AppIcons.help" :size="14" />
|
||||||
</ElTooltip>
|
</ElTooltip>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
import ContextMenu from './src/ContextMenu.vue'
|
import ContextMenu from './src/ContextMenu.vue'
|
||||||
import { ElDropdown } from 'element-plus'
|
import { ElDropdown } from 'element-plus'
|
||||||
import type { RouteLocationNormalizedLoaded } from 'vue-router'
|
import type { RouteLocationNormalizedLoaded } from 'vue-router'
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
<!-- Author: fruit.com.cn -->
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ElDropdown, ElDropdownMenu, ElDropdownItem } from 'element-plus'
|
import { ElDropdown, ElDropdownMenu, ElDropdownItem } from 'element-plus'
|
||||||
import { PropType, ref } from 'vue'
|
import { PropType, ref } from 'vue'
|
||||||
@@ -64,7 +66,7 @@ defineExpose({
|
|||||||
:disabled="item.disabled"
|
:disabled="item.disabled"
|
||||||
:command="item"
|
:command="item"
|
||||||
>
|
>
|
||||||
<Icon :icon="item.icon" /> {{ t(item.label) }}
|
<AppIcon :icon="item.icon" /> {{ t(item.label) }}
|
||||||
</ElDropdownItem>
|
</ElDropdownItem>
|
||||||
</ElDropdownMenu>
|
</ElDropdownMenu>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
export interface ContextMenuSchema {
|
export interface ContextMenuSchema {
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
divided?: boolean
|
divided?: boolean
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
import Descriptions from './src/Descriptions.vue'
|
import Descriptions from './src/Descriptions.vue'
|
||||||
|
|
||||||
export type { DescriptionsSchema } from './src/types'
|
export type { DescriptionsSchema } from './src/types'
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
<!-- Author: fruit.com.cn -->
|
||||||
|
|
||||||
<script lang="tsx">
|
<script lang="tsx">
|
||||||
import { ElCollapseTransition, ElDescriptions, ElDescriptionsItem, ElTooltip } from 'element-plus'
|
import { ElCollapseTransition, ElDescriptions, ElDescriptionsItem, ElTooltip } from 'element-plus'
|
||||||
import { useDesign } from '@/hooks/web/useDesign'
|
import { useDesign } from '@/hooks/web/useDesign'
|
||||||
@@ -5,8 +7,9 @@ import { propTypes } from '@/utils/propTypes'
|
|||||||
import { ref, unref, PropType, computed, defineComponent } from 'vue'
|
import { ref, unref, PropType, computed, defineComponent } from 'vue'
|
||||||
import { useAppStore } from '@/store/modules/app'
|
import { useAppStore } from '@/store/modules/app'
|
||||||
import { DescriptionsSchema } from './types'
|
import { DescriptionsSchema } from './types'
|
||||||
import { Icon } from '@/components/Icon'
|
import { AppIcon } from '@/components/AppIcon'
|
||||||
import { get } from 'lodash-es'
|
import { get } from 'lodash-es'
|
||||||
|
import { AppIcons } from '@/constants/icons'
|
||||||
|
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
|
|
||||||
@@ -92,12 +95,14 @@ export default defineComponent({
|
|||||||
{props.title}
|
{props.title}
|
||||||
{props.message ? (
|
{props.message ? (
|
||||||
<ElTooltip content={props.message} placement="right">
|
<ElTooltip content={props.message} placement="right">
|
||||||
<Icon icon="bi:question-circle-fill" class="ml-5px" size={14} />
|
<AppIcon icon={AppIcons.help} class="ml-5px" size={14} />
|
||||||
</ElTooltip>
|
</ElTooltip>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{props.collapse ? <Icon icon={show.value ? 'ep:arrow-down' : 'ep:arrow-up'} /> : null}
|
{props.collapse ? (
|
||||||
|
<AppIcon icon={show.value ? AppIcons.chevronDown : AppIcons.chevronUp} />
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
export interface DescriptionsSchema {
|
export interface DescriptionsSchema {
|
||||||
span?: number // 占多少分
|
span?: number // 占多少分
|
||||||
field: string // 字段名
|
field: string // 字段名
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
import Dialog from './src/Dialog.vue'
|
import Dialog from './src/Dialog.vue'
|
||||||
|
|
||||||
export { Dialog }
|
export { Dialog }
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
|
<!-- Author: fruit.com.cn -->
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ElDialog, ElScrollbar } from 'element-plus'
|
import { ElDialog, ElScrollbar } from 'element-plus'
|
||||||
import { propTypes } from '@/utils/propTypes'
|
import { propTypes } from '@/utils/propTypes'
|
||||||
import { computed, useAttrs, ref, unref, useSlots, watch, nextTick } from 'vue'
|
import { computed, useAttrs, ref, unref, useSlots, watch, nextTick } from 'vue'
|
||||||
import { isNumber } from '@/utils/is'
|
import { isNumber } from '@/utils/is'
|
||||||
|
import { AppIcons } from '@/constants/icons'
|
||||||
|
|
||||||
const slots = useSlots()
|
const slots = useSlots()
|
||||||
|
|
||||||
@@ -75,17 +78,17 @@ const dialogStyle = computed(() => {
|
|||||||
<div
|
<div
|
||||||
class="h-54px flex justify-between items-center absolute top-[50%] right-15px translate-y-[-50%]"
|
class="h-54px flex justify-between items-center absolute top-[50%] right-15px translate-y-[-50%]"
|
||||||
>
|
>
|
||||||
<Icon
|
<AppIcon
|
||||||
v-if="fullscreen"
|
v-if="fullscreen"
|
||||||
class="cursor-pointer is-hover !h-54px mr-10px"
|
class="cursor-pointer is-hover !h-54px mr-10px"
|
||||||
:icon="isFullscreen ? 'radix-icons:exit-full-screen' : 'radix-icons:enter-full-screen'"
|
:icon="isFullscreen ? AppIcons.fullscreenExit : AppIcons.fullscreen"
|
||||||
color="var(--el-color-info)"
|
color="var(--el-color-info)"
|
||||||
hover-color="var(--el-color-primary)"
|
hover-color="var(--el-color-primary)"
|
||||||
@click="toggleFull"
|
@click="toggleFull"
|
||||||
/>
|
/>
|
||||||
<Icon
|
<AppIcon
|
||||||
class="cursor-pointer is-hover !h-54px"
|
class="cursor-pointer is-hover !h-54px"
|
||||||
icon="ep:close"
|
:icon="AppIcons.close"
|
||||||
hover-color="var(--el-color-primary)"
|
hover-color="var(--el-color-primary)"
|
||||||
color="var(--el-color-info)"
|
color="var(--el-color-info)"
|
||||||
@click="close"
|
@click="close"
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
import Editor from './src/Editor.vue'
|
import Editor from './src/Editor.vue'
|
||||||
import { IDomEditor } from '@wangeditor/editor'
|
import { IDomEditor } from '@wangeditor/editor'
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
<!-- Author: fruit.com.cn -->
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onBeforeUnmount, computed, PropType, unref, nextTick, ref, watch, shallowRef } from 'vue'
|
import { onBeforeUnmount, computed, PropType, unref, nextTick, ref, watch, shallowRef } from 'vue'
|
||||||
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
|
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
import Error from './src/Error.vue'
|
import Error from './src/Error.vue'
|
||||||
|
|
||||||
export { Error }
|
export { Error }
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
<!-- Author: fruit.com.cn -->
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import pageError from '@/assets/svgs/404.svg'
|
import pageError from '@/assets/svgs/404.svg'
|
||||||
import networkError from '@/assets/svgs/500.svg'
|
import networkError from '@/assets/svgs/500.svg'
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
import Form from './src/Form.vue'
|
import Form from './src/Form.vue'
|
||||||
import type { FormSchema, FormSetProps } from './src/types'
|
import type { FormSchema, FormSetProps } from './src/types'
|
||||||
export type {
|
export type {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
<!-- Author: fruit.com.cn -->
|
||||||
|
|
||||||
<script lang="tsx">
|
<script lang="tsx">
|
||||||
import { PropType, defineComponent, ref, computed, unref, watch, onMounted } from 'vue'
|
import { PropType, defineComponent, ref, computed, unref, watch, onMounted } from 'vue'
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
|
import { defineAsyncComponent } from 'vue'
|
||||||
import type { Component } from 'vue'
|
import type { Component } from 'vue'
|
||||||
import {
|
import {
|
||||||
ElCascader,
|
ElCascader,
|
||||||
@@ -21,9 +24,12 @@ import {
|
|||||||
ElUpload
|
ElUpload
|
||||||
} from 'element-plus'
|
} from 'element-plus'
|
||||||
import { InputPassword } from '@/components/InputPassword'
|
import { InputPassword } from '@/components/InputPassword'
|
||||||
import { Editor } from '@/components/Editor'
|
|
||||||
import { ComponentName } from '../types'
|
import { ComponentName } from '../types'
|
||||||
|
|
||||||
|
// wang-editor 打包后约 1.1MB:改成异步组件,只有真正渲染编辑器字段时才加载,
|
||||||
|
// 避免登录页这类只是用了 Form 的页面把它一并拉进首屏。
|
||||||
|
const Editor = defineAsyncComponent(() => import('@/components/Editor'))
|
||||||
|
|
||||||
const componentMap: Recordable<Component, ComponentName> = {
|
const componentMap: Recordable<Component, ComponentName> = {
|
||||||
RadioGroup: ElRadioGroup,
|
RadioGroup: ElRadioGroup,
|
||||||
RadioButton: ElRadioGroup,
|
RadioButton: ElRadioGroup,
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
import { useI18n } from '@/hooks/web/useI18n'
|
import { useI18n } from '@/hooks/web/useI18n'
|
||||||
import { PlaceholderModel, FormSchema, ComponentNameEnum, ColProps } from '../types'
|
import { PlaceholderModel, FormSchema, ComponentNameEnum, ColProps } from '../types'
|
||||||
import { isFunction } from '@/utils/is'
|
import { isFunction } from '@/utils/is'
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AutocompleteProps,
|
AutocompleteProps,
|
||||||
InputNumberProps,
|
InputNumberProps,
|
||||||
@@ -20,7 +22,8 @@ import {
|
|||||||
ISelectProps,
|
ISelectProps,
|
||||||
UploadProps
|
UploadProps
|
||||||
} from 'element-plus'
|
} from 'element-plus'
|
||||||
import { IEditorConfig } from '@wangeditor/editor'
|
// 仅类型引用:必须用 import type,否则会把整个 wang-editor 打进登录页的依赖链
|
||||||
|
import type { IEditorConfig } from '@wangeditor/editor'
|
||||||
import { CSSProperties } from 'vue'
|
import { CSSProperties } from 'vue'
|
||||||
|
|
||||||
export interface PlaceholderModel {
|
export interface PlaceholderModel {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
import Icon from './src/Icon.vue'
|
import Icon from './src/Icon.vue'
|
||||||
|
|
||||||
export type { IconTypes } from './src/types'
|
export type { IconTypes } from './src/types'
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
|
<!-- Author: fruit.com.cn -->
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, unref } from 'vue'
|
import { computed, unref } from 'vue'
|
||||||
import { ElIcon } from 'element-plus'
|
import { ElIcon } from 'element-plus'
|
||||||
import { propTypes } from '@/utils/propTypes'
|
import { propTypes } from '@/utils/propTypes'
|
||||||
import { useDesign } from '@/hooks/web/useDesign'
|
import { useDesign } from '@/hooks/web/useDesign'
|
||||||
import { Icon } from '@iconify/vue'
|
import { Icon } from '@iconify/vue'
|
||||||
|
import { normalizeAppIcon } from '@/constants/icons'
|
||||||
|
|
||||||
const { getPrefixCls } = useDesign()
|
const { getPrefixCls } = useDesign()
|
||||||
|
|
||||||
@@ -20,9 +23,10 @@ const props = defineProps({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const isLocal = computed(() => props.icon.startsWith('svg-icon:'))
|
const isLocal = computed(() => props.icon.startsWith('svg-icon:'))
|
||||||
|
const normalizedIcon = computed(() => normalizeAppIcon(props.icon))
|
||||||
|
|
||||||
const symbolId = computed(() => {
|
const symbolId = computed(() => {
|
||||||
return unref(isLocal) ? `#icon-${props.icon.split('svg-icon:')[1]}` : props.icon
|
return unref(isLocal) ? `#icon-${props.icon.split('svg-icon:')[1]}` : unref(normalizedIcon)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 是否使用在线图标
|
// 是否使用在线图标
|
||||||
@@ -46,8 +50,8 @@ const getIconifyStyle = computed(() => {
|
|||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<Icon v-if="isUseOnline" :icon="icon" :style="getIconifyStyle" />
|
<Icon v-if="isUseOnline" :icon="normalizedIcon" :style="getIconifyStyle" />
|
||||||
<div v-else :class="`${icon} iconify`" :style="getIconifyStyle"></div>
|
<div v-else :class="`${normalizedIcon} iconify`" :style="getIconifyStyle"></div>
|
||||||
</template>
|
</template>
|
||||||
</ElIcon>
|
</ElIcon>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
export interface IconTypes {
|
export interface IconTypes {
|
||||||
size?: number
|
size?: number
|
||||||
color?: string
|
color?: string
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
import InputPassword from './src/InputPassword.vue'
|
import InputPassword from './src/InputPassword.vue'
|
||||||
|
|
||||||
export { InputPassword }
|
export { InputPassword }
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
<!-- Author: fruit.com.cn -->
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, unref, computed, watch } from 'vue'
|
import { ref, unref, computed, watch } from 'vue'
|
||||||
import { ElInput } from 'element-plus'
|
import { ElInput } from 'element-plus'
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
import LocaleDropdown from './src/LocaleDropdown.vue'
|
import LocaleDropdown from './src/LocaleDropdown.vue'
|
||||||
|
|
||||||
export type { Language, LocaleDropdownType } from './src/types'
|
export type { Language, LocaleDropdownType } from './src/types'
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
<!-- Author: fruit.com.cn -->
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, unref } from 'vue'
|
import { computed, unref } from 'vue'
|
||||||
import { ElDropdown, ElDropdownMenu, ElDropdownItem } from 'element-plus'
|
import { ElDropdown, ElDropdownMenu, ElDropdownItem } from 'element-plus'
|
||||||
@@ -5,6 +7,7 @@ import { useLocaleStore } from '@/store/modules/locale'
|
|||||||
import { useLocale } from '@/hooks/web/useLocale'
|
import { useLocale } from '@/hooks/web/useLocale'
|
||||||
import { propTypes } from '@/utils/propTypes'
|
import { propTypes } from '@/utils/propTypes'
|
||||||
import { useDesign } from '@/hooks/web/useDesign'
|
import { useDesign } from '@/hooks/web/useDesign'
|
||||||
|
import { AppIcons } from '@/constants/icons'
|
||||||
|
|
||||||
const { getPrefixCls } = useDesign()
|
const { getPrefixCls } = useDesign()
|
||||||
|
|
||||||
@@ -34,9 +37,9 @@ const setLang = (lang: LocaleType) => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<ElDropdown :class="prefixCls" trigger="click" @command="setLang">
|
<ElDropdown :class="prefixCls" trigger="click" @command="setLang">
|
||||||
<Icon
|
<AppIcon
|
||||||
:size="18"
|
:size="18"
|
||||||
icon="ion:language-sharp"
|
:icon="AppIcons.language"
|
||||||
class="cursor-pointer !p-0"
|
class="cursor-pointer !p-0"
|
||||||
:class="$attrs.class"
|
:class="$attrs.class"
|
||||||
:color="color"
|
:color="color"
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
export interface Language {
|
export interface Language {
|
||||||
el: Recordable
|
el: Recordable
|
||||||
name: string
|
name: string
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
import Logo from './src/Logo.vue'
|
import Logo from './src/Logo.vue'
|
||||||
|
|
||||||
export { Logo }
|
export { Logo }
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
<!-- Author: fruit.com.cn -->
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch, computed, onMounted, unref } from 'vue'
|
import { ref, watch, computed, onMounted, unref } from 'vue'
|
||||||
import { useAppStore } from '@/store/modules/app'
|
import { useAppStore } from '@/store/modules/app'
|
||||||
@@ -65,8 +67,13 @@ watch(
|
|||||||
to="/"
|
to="/"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src="@/assets/imgs/logo.png"
|
src="@/assets/imgs/brand-mark.png"
|
||||||
class="w-[calc(var(--logo-height)-10px)] h-[calc(var(--logo-height)-10px)]"
|
class="object-cover"
|
||||||
|
style="
|
||||||
|
width: var(--brand-mark-size);
|
||||||
|
height: var(--brand-mark-size);
|
||||||
|
border-radius: var(--brand-mark-radius);
|
||||||
|
"
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
v-if="show"
|
v-if="show"
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
import Menu from './src/Menu.vue'
|
import Menu from './src/Menu.vue'
|
||||||
|
|
||||||
export { Menu }
|
export { Menu }
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
<!-- Author: fruit.com.cn -->
|
||||||
|
|
||||||
<script lang="tsx">
|
<script lang="tsx">
|
||||||
import { computed, defineComponent, unref, PropType } from 'vue'
|
import { computed, defineComponent, unref, PropType } from 'vue'
|
||||||
import { ElMenu, ElScrollbar } from 'element-plus'
|
import { ElMenu, ElScrollbar } from 'element-plus'
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ const { renderMenuTitle } = useRenderMenuTitle()
|
|||||||
export const useRenderMenuItem = () =>
|
export const useRenderMenuItem = () =>
|
||||||
// allRouters: AppRouteRecordRaw[] = [],
|
// allRouters: AppRouteRecordRaw[] = [],
|
||||||
{
|
{
|
||||||
const renderMenuItem = (routers: AppRouteRecordRaw[], parentPath = '/') => {
|
const renderMenuItem = (routers: AppRouteRecordRaw[], parentPath = '/', nested = false) => {
|
||||||
return routers
|
return routers
|
||||||
.filter((v) => !v.meta?.hidden)
|
.filter((v) => !v.meta?.hidden)
|
||||||
.map((v) => {
|
.map((v) => {
|
||||||
@@ -22,21 +22,25 @@ export const useRenderMenuItem = () =>
|
|||||||
(!onlyOneChild?.children || onlyOneChild?.noShowingChildren) &&
|
(!onlyOneChild?.children || onlyOneChild?.noShowingChildren) &&
|
||||||
!meta?.alwaysShow
|
!meta?.alwaysShow
|
||||||
) {
|
) {
|
||||||
|
const leafMeta = onlyOneChild?.meta
|
||||||
|
? {
|
||||||
|
...onlyOneChild.meta,
|
||||||
|
icon: onlyOneChild.meta.icon || meta.icon
|
||||||
|
}
|
||||||
|
: meta
|
||||||
return (
|
return (
|
||||||
<ElMenuItem
|
<ElMenuItem
|
||||||
index={onlyOneChild ? pathResolve(fullPath, onlyOneChild.path) : fullPath}
|
index={onlyOneChild ? pathResolve(fullPath, onlyOneChild.path) : fullPath}
|
||||||
>
|
>
|
||||||
{{
|
{{ default: () => renderMenuTitle(leafMeta, !nested) }}
|
||||||
default: () => renderMenuTitle(onlyOneChild ? onlyOneChild?.meta : meta)
|
|
||||||
}}
|
|
||||||
</ElMenuItem>
|
</ElMenuItem>
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<ElSubMenu index={fullPath}>
|
<ElSubMenu index={fullPath}>
|
||||||
{{
|
{{
|
||||||
title: () => renderMenuTitle(meta),
|
title: () => renderMenuTitle(meta, !nested),
|
||||||
default: () => renderMenuItem(v.children!, fullPath)
|
default: () => renderMenuItem(v.children!, fullPath, true)
|
||||||
}}
|
}}
|
||||||
</ElSubMenu>
|
</ElSubMenu>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
import type { RouteMeta } from 'vue-router'
|
import type { RouteMeta } from 'vue-router'
|
||||||
import { Icon } from '@/components/Icon'
|
import { AppIcon } from '@/components/AppIcon'
|
||||||
import { useI18n } from '@/hooks/web/useI18n'
|
import { useI18n } from '@/hooks/web/useI18n'
|
||||||
|
|
||||||
export const useRenderMenuTitle = () => {
|
export const useRenderMenuTitle = () => {
|
||||||
const renderMenuTitle = (meta: RouteMeta) => {
|
const renderMenuTitle = (meta: RouteMeta, showIcon = true) => {
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const { title = 'Please set title', icon } = meta
|
const { title = 'Please set title', icon } = meta
|
||||||
|
|
||||||
return icon ? (
|
return icon && showIcon ? (
|
||||||
<>
|
<>
|
||||||
<Icon icon={meta.icon}></Icon>
|
<AppIcon icon={meta.icon}></AppIcon>
|
||||||
<span class="v-menu__title overflow-hidden overflow-ellipsis whitespace-nowrap">
|
<span class="v-menu__title overflow-hidden overflow-ellipsis whitespace-nowrap">
|
||||||
{t(title as string)}
|
{t(title as string)}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
import { ref, unref } from 'vue'
|
import { ref, unref } from 'vue'
|
||||||
import { findPath } from '@/utils/tree'
|
import { findPath } from '@/utils/tree'
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
import Screenfull from './src/Screenfull.vue'
|
import Screenfull from './src/Screenfull.vue'
|
||||||
|
|
||||||
export { Screenfull }
|
export { Screenfull }
|
||||||
Loaded 100 of 279 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user