Compare commits
72
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a28eaa4514 | ||
|
|
189a7f689a | ||
|
|
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 |
No files matched your search
@@ -17,4 +17,4 @@ VITE_USE_ALL_ELEMENT_PLUS_STYLE=true
|
||||
VITE_USE_MOCK=false
|
||||
|
||||
# 是否使用在线图标
|
||||
VITE_USE_ONLINE_ICON=true
|
||||
VITE_USE_ONLINE_ICON=false
|
||||
@@ -29,10 +29,10 @@ VITE_USE_BUNDLE_ANALYZER=false
|
||||
VITE_USE_ALL_ELEMENT_PLUS_STYLE=false
|
||||
|
||||
# 是否开启mock
|
||||
VITE_USE_MOCK=true
|
||||
VITE_USE_MOCK=false
|
||||
|
||||
# 是否切割css
|
||||
VITE_USE_CSS_SPLIT=true
|
||||
|
||||
# 是否使用在线图标
|
||||
VITE_USE_ONLINE_ICON=true
|
||||
VITE_USE_ONLINE_ICON=false
|
||||
@@ -29,10 +29,10 @@ VITE_USE_BUNDLE_ANALYZER=true
|
||||
VITE_USE_ALL_ELEMENT_PLUS_STYLE=false
|
||||
|
||||
# 是否开启mock
|
||||
VITE_USE_MOCK=true
|
||||
VITE_USE_MOCK=false
|
||||
|
||||
# 是否切割css
|
||||
VITE_USE_CSS_SPLIT=true
|
||||
|
||||
# 是否使用在线图标
|
||||
VITE_USE_ONLINE_ICON=true
|
||||
VITE_USE_ONLINE_ICON=false
|
||||
@@ -29,10 +29,10 @@ VITE_USE_BUNDLE_ANALYZER=false
|
||||
VITE_USE_ALL_ELEMENT_PLUS_STYLE=false
|
||||
|
||||
# 是否开启mock
|
||||
VITE_USE_MOCK=true
|
||||
VITE_USE_MOCK=false
|
||||
|
||||
# 是否切割css
|
||||
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
|
||||
const { defineConfig } = require('eslint-define-config')
|
||||
module.exports = defineConfig({
|
||||
|
||||
@@ -7,3 +7,9 @@ dist-ssr
|
||||
*-lock.*
|
||||
pnpm-debug
|
||||
stats.html
|
||||
*.apk
|
||||
|
||||
# 本机运行时产物(start-dev.bat / vite 日志)
|
||||
start-dev.bat
|
||||
dev.out.log
|
||||
dev.err.log
|
||||
@@ -1,4 +1,9 @@
|
||||
#!/bin/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"
|
||||
@@ -1,3 +1,5 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
module.exports = {
|
||||
'*.{js,jsx,ts,tsx}': ['eslint --fix', 'prettier --write'],
|
||||
'{!(package)*.json,*.code-snippets,.!(browserslist)*rc}': ['prettier --write--parser json'],
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
module.exports = {
|
||||
extends: ['@commitlint/config-conventional'],
|
||||
rules: {
|
||||
|
||||
+8
-25
@@ -2,7 +2,7 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<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" />
|
||||
<title><%= title %></title>
|
||||
</head>
|
||||
@@ -16,7 +16,8 @@
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
background: #f0f2f5;
|
||||
color: #f8f1ea;
|
||||
background: #100c0a;
|
||||
}
|
||||
|
||||
.app-loading .app-loading-wrap {
|
||||
@@ -38,8 +39,9 @@
|
||||
}
|
||||
|
||||
.app-loading .app-loading-logo {
|
||||
width: 100px;
|
||||
width: 72px;
|
||||
margin: 0 auto 15px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.app-loading .app-loading-item {
|
||||
@@ -55,7 +57,7 @@
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 4px solid #2d8cf0;
|
||||
border: 4px solid #c4322a;
|
||||
border-bottom: 0;
|
||||
border-left-color: transparent;
|
||||
border-radius: 50%;
|
||||
@@ -68,7 +70,7 @@
|
||||
left: calc(50% - 20px);
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid #87bdff;
|
||||
border: 4px solid #ff8a72;
|
||||
border-right: 0;
|
||||
border-top-color: transparent;
|
||||
border-radius: 50%;
|
||||
@@ -85,16 +87,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes loader-outter {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes loader-inner {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
@@ -105,20 +97,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes loader-inner {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(-360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<div class="app-loading">
|
||||
<div class="app-loading-wrap">
|
||||
<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>
|
||||
<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
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
+1
-4
@@ -6,7 +6,7 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"i": "pnpm install",
|
||||
"dev": "VITE_CJS_IGNORE_WARNING=1 vite --mode base",
|
||||
"dev": "vite --mode base",
|
||||
"ts:check": "pnpm vue-tsc --noEmit --skipLibCheck",
|
||||
"build:pro": "VITE_CJS_IGNORE_WARNING=1 vite build --mode pro",
|
||||
"build:dev": "VITE_CJS_IGNORE_WARNING=1 vite build --mode dev",
|
||||
@@ -24,8 +24,6 @@
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.2",
|
||||
"@faker-js/faker": "^8.4.0",
|
||||
"@iconify/iconify": "^3.1.1",
|
||||
"@iconify/vue": "^4.1.1",
|
||||
"@vueuse/core": "^10.7.2",
|
||||
@@ -103,7 +101,6 @@
|
||||
"vite": "5.0.12",
|
||||
"vite-plugin-ejs": "^1.7.0",
|
||||
"vite-plugin-eslint": "^1.8.1",
|
||||
"vite-plugin-mock": "2.9.6",
|
||||
"vite-plugin-progress": "^0.0.7",
|
||||
"vite-plugin-purge-icons": "^0.10.0",
|
||||
"vite-plugin-style-import": "2.0.0",
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
module.exports = {
|
||||
plugins: {
|
||||
autoprefixer: {}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
module.exports = {
|
||||
printWidth: 100,
|
||||
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">
|
||||
import { computed } from 'vue'
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import request from '@/axios'
|
||||
import type { AppUserBanRecord, AppUserItem, PageResult, WatchHistoryItem } from './types'
|
||||
|
||||
@@ -11,6 +13,20 @@ export interface AppUserPageParams {
|
||||
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 })
|
||||
}
|
||||
@@ -30,6 +46,13 @@ export const listAppUserWatchHistoryApi = (id: number, limit = 50) => {
|
||||
})
|
||||
}
|
||||
|
||||
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>({
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import request from '@/axios'
|
||||
import type { BannerItem, BannerSaveData, PageResult } from './types'
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import request from '@/axios'
|
||||
import type { PageResult } from './types'
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import request from '@/axios'
|
||||
import type { ConfigItem } from './types'
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import request from '@/axios'
|
||||
import type { PageResult } from './types'
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import request from '@/axios'
|
||||
import type { PageResult } from './types'
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import request from '@/axios'
|
||||
|
||||
/** 指标口径见后端 DashboardService 注释与 docs/api-phase1.md「2.8 数据看板」 */
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import request from '@/axios'
|
||||
import type {
|
||||
DramaCategory,
|
||||
@@ -13,7 +15,9 @@ export interface DramaPageParams {
|
||||
keyword?: string
|
||||
status?: string
|
||||
categoryId?: number
|
||||
categoryIds?: number[]
|
||||
isHot?: boolean
|
||||
dramaTypes?: Array<'ANIME' | 'LIVE'>
|
||||
}
|
||||
|
||||
export const pageDramasApi = (params: DramaPageParams) => {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import request from '@/axios'
|
||||
import type { EpisodeItem, EpisodeSaveData } from './types'
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import request from '@/axios'
|
||||
import type { PageResult, CommentItem, ReportItem, SensitiveWordItem } from './types'
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import request from '@/axios'
|
||||
import type { OperationLogItem, PageResult } from './types'
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import request from '@/axios'
|
||||
import type { MediaAsset } from './types'
|
||||
|
||||
|
||||
@@ -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}` })
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import request from '@/axios'
|
||||
import type {
|
||||
MenuNode,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
export interface PageResult<T> {
|
||||
total: number
|
||||
list: T[]
|
||||
@@ -18,11 +20,15 @@ export interface DramaItem {
|
||||
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
|
||||
@@ -31,6 +37,9 @@ export interface DramaItem {
|
||||
status: string
|
||||
statusText: string
|
||||
views?: number
|
||||
favoriteCount?: number
|
||||
likeCount?: number
|
||||
commentCount?: number
|
||||
sort?: number
|
||||
publishAt?: string
|
||||
createdAt?: string
|
||||
@@ -41,12 +50,18 @@ 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
|
||||
}
|
||||
@@ -67,6 +82,10 @@ export interface EpisodeItem {
|
||||
status: string
|
||||
statusText: string
|
||||
views?: number
|
||||
likeCount?: number
|
||||
commentCount?: number
|
||||
favoriteCount?: number
|
||||
heat?: number
|
||||
sort?: number
|
||||
}
|
||||
|
||||
@@ -79,6 +98,7 @@ export interface EpisodeSaveData {
|
||||
videoUrl?: string
|
||||
isFree?: boolean
|
||||
unlockCoin?: number
|
||||
likeCount?: number
|
||||
status?: string
|
||||
sort?: number
|
||||
}
|
||||
@@ -292,11 +312,15 @@ export interface AppUserBanRecord {
|
||||
|
||||
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
|
||||
@@ -309,7 +333,10 @@ export interface WatchHistoryItem {
|
||||
export interface CommentItem {
|
||||
id: number
|
||||
dramaId: number
|
||||
dramaTitle?: string
|
||||
episodeId: number
|
||||
episodeNo?: number
|
||||
episodeTitle?: string
|
||||
parentId: number
|
||||
rootId: number
|
||||
userId: number
|
||||
@@ -328,9 +355,20 @@ export interface CommentItem {
|
||||
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 */
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import request from '@/axios'
|
||||
import type { AdminUserItem, AdminUserSaveData, PageResult } from './types'
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import request from '@/axios'
|
||||
import type { UserType } from './types'
|
||||
|
||||
@@ -24,10 +26,6 @@ export const getAdminRoleApi = (
|
||||
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 = {
|
||||
username: string
|
||||
password: string
|
||||
}
|
||||
|
||||
export type UserType = {
|
||||
|
||||
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 { ElMessage } from 'element-plus'
|
||||
import qs from 'qs'
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import service from './service'
|
||||
import { CONTENT_TYPE } from '@/constants'
|
||||
import { useUserStoreWithOut } from '@/store/modules/user'
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import axios, { AxiosError } from 'axios'
|
||||
import { defaultRequestInterceptors, defaultResponseInterceptors } from './config'
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import type {
|
||||
InternalAxiosRequestConfig,
|
||||
AxiosResponse,
|
||||
|
||||
@@ -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'
|
||||
|
||||
export { Backtop }
|
||||
@@ -1,3 +1,5 @@
|
||||
<!-- Author: fruit.com.cn -->
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElBacktop } from 'element-plus'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import Breadcrumb from './src/Breadcrumb.vue'
|
||||
|
||||
export { Breadcrumb }
|
||||
@@ -1,3 +1,5 @@
|
||||
<!-- Author: fruit.com.cn -->
|
||||
|
||||
<script lang="tsx">
|
||||
import { ElBreadcrumb, ElBreadcrumbItem } from 'element-plus'
|
||||
import { ref, watch, computed, unref, defineComponent, TransitionGroup } from 'vue'
|
||||
@@ -7,7 +9,7 @@ import { filterBreadcrumb } from './helper'
|
||||
import { filter, treeToList } from '@/utils/tree'
|
||||
import type { RouteLocationNormalizedLoaded, RouteMeta } from 'vue-router'
|
||||
import { useI18n } from '@/hooks/web/useI18n'
|
||||
import { Icon } from '@/components/Icon'
|
||||
import { AppIcon } from '@/components/AppIcon'
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
|
||||
@@ -52,7 +54,7 @@ export default defineComponent({
|
||||
<ElBreadcrumbItem to={{ path: disabled ? '' : v.path }} key={v.name}>
|
||||
{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)
|
||||
@@ -115,10 +117,10 @@ export default defineComponent({
|
||||
|
||||
:deep(&__item):last-child {
|
||||
.@{prefix-cls}__inner {
|
||||
color: var(--el-text-color-placeholder);
|
||||
color: var(--el-text-color-regular);
|
||||
|
||||
&: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 type { RouteMeta } from 'vue-router'
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import BaseButton from './src/Button.vue'
|
||||
|
||||
export { BaseButton }
|
||||
@@ -1,3 +1,5 @@
|
||||
<!-- Author: fruit.com.cn -->
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
import { ElButton, ComponentSize, ButtonType } from 'element-plus'
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import Collapse from './src/Collapse.vue'
|
||||
|
||||
export { Collapse }
|
||||
@@ -1,8 +1,11 @@
|
||||
<!-- Author: fruit.com.cn -->
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, unref } from 'vue'
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
import { AppIcons } from '@/constants/icons'
|
||||
|
||||
const { getPrefixCls } = useDesign()
|
||||
|
||||
@@ -24,9 +27,9 @@ const toggleCollapse = () => {
|
||||
|
||||
<template>
|
||||
<div :class="prefixCls" @click="toggleCollapse">
|
||||
<Icon
|
||||
<AppIcon
|
||||
:size="18"
|
||||
:icon="collapse ? 'ant-design:menu-unfold-outlined' : 'ant-design:menu-fold-outlined'"
|
||||
:icon="collapse ? AppIcons.menuOpen : AppIcons.menu"
|
||||
:color="color"
|
||||
class="cursor-pointer"
|
||||
/>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import ConfigGlobal from './src/ConfigGlobal.vue'
|
||||
|
||||
export type { ConfigGlobalTypes } from './src/types'
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
<!-- Author: fruit.com.cn -->
|
||||
|
||||
<script setup lang="ts">
|
||||
import { provide, computed, watch, onMounted } from 'vue'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import { ComponentSize } from 'element-plus'
|
||||
|
||||
export interface ConfigGlobalTypes {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import ContentDetailWrap from './src/ContentDetailWrap.vue'
|
||||
|
||||
export { ContentDetailWrap }
|
||||
@@ -1,3 +1,5 @@
|
||||
<!-- Author: fruit.com.cn -->
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElCard } from 'element-plus'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import ContentWrap from './src/ContentWrap.vue'
|
||||
|
||||
export { ContentWrap }
|
||||
@@ -1,7 +1,10 @@
|
||||
<!-- Author: fruit.com.cn -->
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElCard, ElTooltip } from 'element-plus'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
import { AppIcons } from '@/constants/icons'
|
||||
|
||||
const { getPrefixCls } = useDesign()
|
||||
|
||||
@@ -22,7 +25,7 @@ defineProps({
|
||||
<template #content>
|
||||
<div class="max-w-200px">{{ message }}</div>
|
||||
</template>
|
||||
<Icon class="ml-5px" icon="bi:question-circle-fill" :size="14" />
|
||||
<AppIcon class="ml-5px" :icon="AppIcons.help" :size="14" />
|
||||
</ElTooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import ContextMenu from './src/ContextMenu.vue'
|
||||
import { ElDropdown } from 'element-plus'
|
||||
import type { RouteLocationNormalizedLoaded } from 'vue-router'
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
<!-- Author: fruit.com.cn -->
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElDropdown, ElDropdownMenu, ElDropdownItem } from 'element-plus'
|
||||
import { PropType, ref } from 'vue'
|
||||
@@ -64,7 +66,7 @@ defineExpose({
|
||||
:disabled="item.disabled"
|
||||
:command="item"
|
||||
>
|
||||
<Icon :icon="item.icon" /> {{ t(item.label) }}
|
||||
<AppIcon :icon="item.icon" /> {{ t(item.label) }}
|
||||
</ElDropdownItem>
|
||||
</ElDropdownMenu>
|
||||
</template>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
export interface ContextMenuSchema {
|
||||
disabled?: boolean
|
||||
divided?: boolean
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import Descriptions from './src/Descriptions.vue'
|
||||
|
||||
export type { DescriptionsSchema } from './src/types'
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
<!-- Author: fruit.com.cn -->
|
||||
|
||||
<script lang="tsx">
|
||||
import { ElCollapseTransition, ElDescriptions, ElDescriptionsItem, ElTooltip } from 'element-plus'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
@@ -5,8 +7,9 @@ import { propTypes } from '@/utils/propTypes'
|
||||
import { ref, unref, PropType, computed, defineComponent } from 'vue'
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
import { DescriptionsSchema } from './types'
|
||||
import { Icon } from '@/components/Icon'
|
||||
import { AppIcon } from '@/components/AppIcon'
|
||||
import { get } from 'lodash-es'
|
||||
import { AppIcons } from '@/constants/icons'
|
||||
|
||||
const appStore = useAppStore()
|
||||
|
||||
@@ -92,12 +95,14 @@ export default defineComponent({
|
||||
{props.title}
|
||||
{props.message ? (
|
||||
<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>
|
||||
) : null}
|
||||
</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>
|
||||
) : null}
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
export interface DescriptionsSchema {
|
||||
span?: number // 占多少分
|
||||
field: string // 字段名
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import Dialog from './src/Dialog.vue'
|
||||
|
||||
export { Dialog }
|
||||
@@ -1,8 +1,11 @@
|
||||
<!-- Author: fruit.com.cn -->
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElDialog, ElScrollbar } from 'element-plus'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { computed, useAttrs, ref, unref, useSlots, watch, nextTick } from 'vue'
|
||||
import { isNumber } from '@/utils/is'
|
||||
import { AppIcons } from '@/constants/icons'
|
||||
|
||||
const slots = useSlots()
|
||||
|
||||
@@ -75,17 +78,17 @@ const dialogStyle = computed(() => {
|
||||
<div
|
||||
class="h-54px flex justify-between items-center absolute top-[50%] right-15px translate-y-[-50%]"
|
||||
>
|
||||
<Icon
|
||||
<AppIcon
|
||||
v-if="fullscreen"
|
||||
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)"
|
||||
hover-color="var(--el-color-primary)"
|
||||
@click="toggleFull"
|
||||
/>
|
||||
<Icon
|
||||
<AppIcon
|
||||
class="cursor-pointer is-hover !h-54px"
|
||||
icon="ep:close"
|
||||
:icon="AppIcons.close"
|
||||
hover-color="var(--el-color-primary)"
|
||||
color="var(--el-color-info)"
|
||||
@click="close"
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import Editor from './src/Editor.vue'
|
||||
import { IDomEditor } from '@wangeditor/editor'
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
<!-- Author: fruit.com.cn -->
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, computed, PropType, unref, nextTick, ref, watch, shallowRef } from 'vue'
|
||||
import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import Error from './src/Error.vue'
|
||||
|
||||
export { Error }
|
||||
@@ -1,3 +1,5 @@
|
||||
<!-- Author: fruit.com.cn -->
|
||||
|
||||
<script setup lang="ts">
|
||||
import pageError from '@/assets/svgs/404.svg'
|
||||
import networkError from '@/assets/svgs/500.svg'
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import Form from './src/Form.vue'
|
||||
import type { FormSchema, FormSetProps } from './src/types'
|
||||
export type {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
<!-- Author: fruit.com.cn -->
|
||||
|
||||
<script lang="tsx">
|
||||
import { PropType, defineComponent, ref, computed, unref, watch, onMounted } from 'vue'
|
||||
import {
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
import type { Component } from 'vue'
|
||||
import {
|
||||
ElCascader,
|
||||
@@ -21,9 +24,12 @@ import {
|
||||
ElUpload
|
||||
} from 'element-plus'
|
||||
import { InputPassword } from '@/components/InputPassword'
|
||||
import { Editor } from '@/components/Editor'
|
||||
import { ComponentName } from '../types'
|
||||
|
||||
// wang-editor 打包后约 1.1MB:改成异步组件,只有真正渲染编辑器字段时才加载,
|
||||
// 避免登录页这类只是用了 Form 的页面把它一并拉进首屏。
|
||||
const Editor = defineAsyncComponent(() => import('@/components/Editor'))
|
||||
|
||||
const componentMap: Recordable<Component, ComponentName> = {
|
||||
RadioGroup: ElRadioGroup,
|
||||
RadioButton: ElRadioGroup,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import { useI18n } from '@/hooks/web/useI18n'
|
||||
import { PlaceholderModel, FormSchema, ComponentNameEnum, ColProps } from '../types'
|
||||
import { isFunction } from '@/utils/is'
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import {
|
||||
AutocompleteProps,
|
||||
InputNumberProps,
|
||||
@@ -20,7 +22,8 @@ import {
|
||||
ISelectProps,
|
||||
UploadProps
|
||||
} from 'element-plus'
|
||||
import { IEditorConfig } from '@wangeditor/editor'
|
||||
// 仅类型引用:必须用 import type,否则会把整个 wang-editor 打进登录页的依赖链
|
||||
import type { IEditorConfig } from '@wangeditor/editor'
|
||||
import { CSSProperties } from 'vue'
|
||||
|
||||
export interface PlaceholderModel {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import Icon from './src/Icon.vue'
|
||||
|
||||
export type { IconTypes } from './src/types'
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
<!-- Author: fruit.com.cn -->
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, unref } from 'vue'
|
||||
import { ElIcon } from 'element-plus'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { normalizeAppIcon } from '@/constants/icons'
|
||||
|
||||
const { getPrefixCls } = useDesign()
|
||||
|
||||
@@ -20,9 +23,10 @@ const props = defineProps({
|
||||
})
|
||||
|
||||
const isLocal = computed(() => props.icon.startsWith('svg-icon:'))
|
||||
const normalizedIcon = computed(() => normalizeAppIcon(props.icon))
|
||||
|
||||
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>
|
||||
|
||||
<template v-else>
|
||||
<Icon v-if="isUseOnline" :icon="icon" :style="getIconifyStyle" />
|
||||
<div v-else :class="`${icon} iconify`" :style="getIconifyStyle"></div>
|
||||
<Icon v-if="isUseOnline" :icon="normalizedIcon" :style="getIconifyStyle" />
|
||||
<div v-else :class="`${normalizedIcon} iconify`" :style="getIconifyStyle"></div>
|
||||
</template>
|
||||
</ElIcon>
|
||||
</template>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
export interface IconTypes {
|
||||
size?: number
|
||||
color?: string
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import InputPassword from './src/InputPassword.vue'
|
||||
|
||||
export { InputPassword }
|
||||
@@ -1,3 +1,5 @@
|
||||
<!-- Author: fruit.com.cn -->
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, unref, computed, watch } from 'vue'
|
||||
import { ElInput } from 'element-plus'
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import LocaleDropdown from './src/LocaleDropdown.vue'
|
||||
|
||||
export type { Language, LocaleDropdownType } from './src/types'
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
<!-- Author: fruit.com.cn -->
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, unref } from 'vue'
|
||||
import { ElDropdown, ElDropdownMenu, ElDropdownItem } from 'element-plus'
|
||||
@@ -5,6 +7,7 @@ import { useLocaleStore } from '@/store/modules/locale'
|
||||
import { useLocale } from '@/hooks/web/useLocale'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
import { AppIcons } from '@/constants/icons'
|
||||
|
||||
const { getPrefixCls } = useDesign()
|
||||
|
||||
@@ -34,9 +37,9 @@ const setLang = (lang: LocaleType) => {
|
||||
|
||||
<template>
|
||||
<ElDropdown :class="prefixCls" trigger="click" @command="setLang">
|
||||
<Icon
|
||||
<AppIcon
|
||||
:size="18"
|
||||
icon="ion:language-sharp"
|
||||
:icon="AppIcons.language"
|
||||
class="cursor-pointer !p-0"
|
||||
:class="$attrs.class"
|
||||
:color="color"
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
export interface Language {
|
||||
el: Recordable
|
||||
name: string
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import Logo from './src/Logo.vue'
|
||||
|
||||
export { Logo }
|
||||
@@ -1,3 +1,5 @@
|
||||
<!-- Author: fruit.com.cn -->
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, computed, onMounted, unref } from 'vue'
|
||||
import { useAppStore } from '@/store/modules/app'
|
||||
@@ -65,8 +67,13 @@ watch(
|
||||
to="/"
|
||||
>
|
||||
<img
|
||||
src="@/assets/imgs/logo.png"
|
||||
class="w-[calc(var(--logo-height)-10px)] h-[calc(var(--logo-height)-10px)]"
|
||||
src="@/assets/imgs/brand-mark.png"
|
||||
class="object-cover"
|
||||
style="
|
||||
width: var(--brand-mark-size);
|
||||
height: var(--brand-mark-size);
|
||||
border-radius: var(--brand-mark-radius);
|
||||
"
|
||||
/>
|
||||
<div
|
||||
v-if="show"
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import Menu from './src/Menu.vue'
|
||||
|
||||
export { Menu }
|
||||
@@ -1,3 +1,5 @@
|
||||
<!-- Author: fruit.com.cn -->
|
||||
|
||||
<script lang="tsx">
|
||||
import { computed, defineComponent, unref, PropType } from 'vue'
|
||||
import { ElMenu, ElScrollbar } from 'element-plus'
|
||||
|
||||
@@ -9,7 +9,7 @@ const { renderMenuTitle } = useRenderMenuTitle()
|
||||
export const useRenderMenuItem = () =>
|
||||
// allRouters: AppRouteRecordRaw[] = [],
|
||||
{
|
||||
const renderMenuItem = (routers: AppRouteRecordRaw[], parentPath = '/') => {
|
||||
const renderMenuItem = (routers: AppRouteRecordRaw[], parentPath = '/', nested = false) => {
|
||||
return routers
|
||||
.filter((v) => !v.meta?.hidden)
|
||||
.map((v) => {
|
||||
@@ -22,21 +22,25 @@ export const useRenderMenuItem = () =>
|
||||
(!onlyOneChild?.children || onlyOneChild?.noShowingChildren) &&
|
||||
!meta?.alwaysShow
|
||||
) {
|
||||
const leafMeta = onlyOneChild?.meta
|
||||
? {
|
||||
...onlyOneChild.meta,
|
||||
icon: onlyOneChild.meta.icon || meta.icon
|
||||
}
|
||||
: meta
|
||||
return (
|
||||
<ElMenuItem
|
||||
index={onlyOneChild ? pathResolve(fullPath, onlyOneChild.path) : fullPath}
|
||||
>
|
||||
{{
|
||||
default: () => renderMenuTitle(onlyOneChild ? onlyOneChild?.meta : meta)
|
||||
}}
|
||||
{{ default: () => renderMenuTitle(leafMeta, !nested) }}
|
||||
</ElMenuItem>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<ElSubMenu index={fullPath}>
|
||||
{{
|
||||
title: () => renderMenuTitle(meta),
|
||||
default: () => renderMenuItem(v.children!, fullPath)
|
||||
title: () => renderMenuTitle(meta, !nested),
|
||||
default: () => renderMenuItem(v.children!, fullPath, true)
|
||||
}}
|
||||
</ElSubMenu>
|
||||
)
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import type { RouteMeta } from 'vue-router'
|
||||
import { Icon } from '@/components/Icon'
|
||||
import { AppIcon } from '@/components/AppIcon'
|
||||
import { useI18n } from '@/hooks/web/useI18n'
|
||||
|
||||
export const useRenderMenuTitle = () => {
|
||||
const renderMenuTitle = (meta: RouteMeta) => {
|
||||
const renderMenuTitle = (meta: RouteMeta, showIcon = true) => {
|
||||
const { t } = useI18n()
|
||||
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">
|
||||
{t(title as string)}
|
||||
</span>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import { ref, unref } from 'vue'
|
||||
import { findPath } from '@/utils/tree'
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import Screenfull from './src/Screenfull.vue'
|
||||
|
||||
export { Screenfull }
|
||||
@@ -1,8 +1,11 @@
|
||||
<!-- Author: fruit.com.cn -->
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Icon } from '@/components/Icon'
|
||||
import { AppIcon } from '@/components/AppIcon'
|
||||
import { useFullscreen } from '@vueuse/core'
|
||||
import { propTypes } from '@/utils/propTypes'
|
||||
import { useDesign } from '@/hooks/web/useDesign'
|
||||
import { AppIcons } from '@/constants/icons'
|
||||
|
||||
const { getPrefixCls } = useDesign()
|
||||
|
||||
@@ -21,9 +24,9 @@ const toggleFullscreen = () => {
|
||||
|
||||
<template>
|
||||
<div :class="prefixCls" @click="toggleFullscreen">
|
||||
<Icon
|
||||
<AppIcon
|
||||
:size="18"
|
||||
:icon="isFullscreen ? 'zmdi:fullscreen-exit' : 'zmdi:fullscreen'"
|
||||
:icon="isFullscreen ? AppIcons.fullscreenExit : AppIcons.fullscreen"
|
||||
:color="color"
|
||||
/>
|
||||
</div>
|
||||
|
||||
Loaded 100 of 270 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user