Author SHA1 Message Date
Zhou WeidongandCursor cff864d582 refactor(admin): 拆分剧集管理页超长组件
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-21 22:07:04 +08:00
Zhou WeidongandCursor c71a12f6f4 fix(admin): 统一剧集上传格式与提示文案
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-21 22:04:36 +08:00
Zhou WeidongandCursor 7d39e3c684 chore(admin): 合并剧集多选分片上传与断点续传
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-21 21:58:40 +08:00
Zhou WeidongandCursor 6b3620c3c4 feat(admin): 剧集多选分片上传与断点续传
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-21 21:58:28 +08:00
Zhou Weidong 8e1a02ace0 chore(web): 忽略本机启动脚本与运行日志 2026-10-21 18:55:24 +08:00
Zhou Weidong 982346045d Merge branch 'develop' of http://git.reelshort2.com/jianghaifeng/ReelShortWeb into develop 2026-10-21 18:52:34 +08:00
Zhou Weidong 48b09ee8cf feat(admin): 短剧编辑支持排序权重 2026-10-21 18:52:15 +08:00
Zhou Weidong ceb7905697 Merge branch 'develop' of http://git.reelshort2.com/jianghaifeng/ReelShortWeb into develop 2026-10-21 18:10:21 +08:00
Zhou Weidong 979a502373 merge: 合并 K93.1 多选完整展示 2026-10-21 17:16:54 +08:00
Zhou Weidong 3e779e4efd fix(admin): 多选类型与内容类型完整展示 2026-10-21 17:16:40 +08:00
Zhou Weidong 481c231dd9 merge: 合并 K93 短剧多类型管理 2026-10-21 17:01:37 +08:00
Zhou Weidong 9e427d485f feat(admin): 短剧类型与内容类型多选 2026-10-21 17:01:22 +08:00
Zhou Weidong 58380170b0 merge: 合并 K92 举报列表短剧与分集展示 2026-10-21 16:45:08 +08:00
Zhou Weidong e583bfed5f feat(admin): 举报列表展示短剧与分集 2026-10-21 16:44:55 +08:00
Zhou Weidong 923a350cde merge: 合并 K91 观看记录页面与治理筛选 2026-10-21 16:36:09 +08:00
Zhou Weidong 0710ea55f3 feat(admin): 观看记录独立菜单与治理状态全部筛选 2026-10-21 16:35:41 +08:00
Zhou Weidong 50f2b177b1 merge: 合并 K89 分集互动数据管理 2026-10-21 15:59:13 +08:00
Zhou Weidong 0e814840b1 feat(admin): 剧集列表展示分集互动数据 2026-10-21 15:59:08 +08:00
Zhou Weidong c1e03f94be merge: 合并 K88 短剧内容类型与互动数据管理 2026-10-21 15:44:12 +08:00
Zhou Weidong c8fe571be1 feat(admin): 短剧列表展示并编辑互动数据 2026-10-21 15:43:51 +08:00
Zhou Weidong 45507dee8d perf(admin): 降低首屏体积并修复 Windows 访问白屏
1) Editor 改 defineAsyncComponent,IEditorConfig 改 import type
2) dev server 绑定 :: 双栈,避免 Windows 下 localhost 走 ::1 超时
3) dev 脚本去掉 POSIX 环境变量前缀,Windows 可直接 pnpm dev
2026-10-21 13:35:55 +08:00
Zhou Weidong 1737f37d82 fix(login): 忘记密码链接加提示,避免点击无响应 2026-10-20 09:15:19 +08:00
Zhou Weidong d283d223c0 chore: 忽略构建产物 apk 2026-10-20 09:12:11 +08:00
Zhou Weidong f78960fcce chore(web): 合并 release 分片上传修复 2026-10-19 18:46:27 +08:00
Zhou Weidong 8e9435ee7d fix(web): 修复分片上传 chunkSize 字符串拼接导致大文件超限 2026-10-19 18:38:06 +08:00
Zhou Weidong 83e535d12a chore(web): 合并剧集编辑弹窗 2026-10-19 13:55:29 +08:00
Zhou Weidong 2b5dda7c71 feat(web): 重做剧集编辑弹窗 2026-10-19 13:53:57 +08:00
Zhou Weidong 685d4fd1bd chore(web): 合并轮播图编辑弹窗 2026-10-19 13:49:09 +08:00
Zhou Weidong f281dcf1b3 feat(web): 重做轮播图编辑弹窗 2026-10-19 13:47:42 +08:00
Zhou Weidong 5db565b15d chore(web): 合并列表图片预览 2026-10-19 13:42:57 +08:00
Zhou Weidong 8a84f24baf feat(web): 支持后台列表图片大图预览 2026-10-19 13:42:28 +08:00
Zhou Weidong 046400ae4a chore(web): 合并剧集管理布局修复 2026-10-19 13:32:02 +08:00
Zhou Weidong 122aa21323 fix(web): 重做剧集管理顶部布局 2026-10-19 13:30:29 +08:00
Zhou Weidong 9f92d113f2 chore(web): 合并二级菜单背景修复 2026-10-19 13:28:07 +08:00
Zhou Weidong f6f4d264ee fix(web): 调整二级菜单背景样式 2026-10-19 13:27:44 +08:00
Zhou Weidong f023c7d416 chore(web): 合并工作台菜单图标修复 2026-10-19 13:22:44 +08:00
Zhou Weidong bb03a5ad84 fix(web): 修复工作台菜单图标继承 2026-10-19 13:21:09 +08:00
Zhou Weidong 73da0bbd85 chore(web): 合并 K23.1 布局修复 2026-10-19 12:58:02 +08:00
Zhou Weidong c8bf4a8fd4 fix(web): 修复禁用按钮与弹窗布局 2026-10-19 12:56:07 +08:00
Zhou Weidong 6f522303a0 chore(web): 合并 K23.1 后台修正 2026-10-19 12:34:21 +08:00
Zhou Weidong f5b399a52f fix(web): 统一危险操作按钮样式 2026-10-19 12:32:23 +08:00
Zhou Weidong e77db70278 fix(web): 修正浅色品牌文字颜色 2026-10-19 12:32:01 +08:00
Zhou Weidong fe1ae954e3 chore(web): 合并菜单对齐修复 2026-10-19 12:04:22 +08:00
Zhou Weidong 40fbd6ec7b fix(web): 对齐父子菜单文字基线 2026-10-19 12:04:18 +08:00
Zhou Weidong ee3bb1c286 chore(web): 合并 K23 后台视觉深化 2026-10-19 12:00:41 +08:00
Zhou Weidong 898e439b84 fix(web): 优化锁屏弹窗布局与交互 2026-10-19 12:00:30 +08:00
Zhou Weidong 805d7ddc52 feat(web): 重做短剧编辑弹窗 2026-10-19 12:00:26 +08:00
Zhou Weidong ba90bf9bfb style(web): 统一辣椒红主色与菜单层级 2026-10-19 12:00:21 +08:00
Zhou Weidong 3e8312ea58 chore(web): 合并首屏 loading 文字修复 2026-10-19 11:34:19 +08:00
Zhou Weidong a97b8cc026 fix(web): 修正首屏 loading 文字颜色 2026-10-19 11:34:15 +08:00
Zhou Weidong e5c5101450 chore(web): 合并 K22.1 后台收尾 2026-10-19 11:32:09 +08:00
Zhou Weidong 87970487af chore(web): 清理 mock 与旧品牌资源 2026-10-19 11:32:00 +08:00
Zhou Weidong ada741f53d style(web): 精简登录品牌与注册入口 2026-10-19 11:31:55 +08:00
Zhou Weidong 5494190330 chore(web): 合并 K22 路由图标收尾 2026-10-19 11:18:15 +08:00
Zhou Weidong 29465ab4ed fix(web): 统一静态路由图标 2026-10-19 11:18:11 +08:00
Zhou Weidong 8f3447e528 chore(web): 合并 K22 后台视觉统一 2026-10-19 11:12:52 +08:00
Zhou Weidong 97fbeda508 style(web): 统一后台图标与全局视觉 2026-10-19 11:12:38 +08:00
Zhou Weidong 5c087f4db9 chore(admin): 合并注册限流配置项 2026-10-17 23:29:04 +08:00
Zhou Weidong 54db082ffb feat(admin): 增加注册限流配置项 2026-10-17 23:28:40 +08:00
Vicky 3ec9ea6717 fix(web): 适配后端 ID 字符串化(雪花 id 不能过 Number)
后端 J1 起所有 Long id 以字符串下发,审计出 4 处把 id 当数字用的地方:

- utils/creatorImpersonation.ts:代看创作者 id 曾 Number(raw) 存 localStorage,
  19 位 id 会被四舍五入导致 X-Creator-Id 头带错 id;改为全程字符串
- contentManagement/EpisodeManagementView.vue:Number(route.query.drama) 与 item.id 比较,
  从短剧管理带 ?drama= 跳过来会选不中;改为字符串比较
- creator-center/composables/useDramaEdit.js:Number(route.params.id) 同上,作品编辑打不开
- permission/composables/useMenuManagement.js:顶级菜单哨兵 0 与接口的 "0" 类型不一致,
  下拉回显会变裸 0;统一成字符串 '0'

核过不需要改:分页 total 后端保持数字;role.id === selectedRoleId 等两侧都来自接口;
formatCount 内部 Number() 吃字符串;全局无 id 算术/下标/a.id-b.id。
2026-10-16 16:44:19 +08:00
Vicky db313f4aa6 feat(admin): 新增商品配置与系统公告两个后台页面
- src/api/admin/product.ts、src/api/admin/notice.ts 对齐后端新接口
- ProductManagementView:列表/筛选/新建编辑/上下架/删除,新建默认下架有提示
- NoticeManagementView:列表/筛选/新建编辑/发布下架/删除
- 状态列一律用接口返回的 statusText,不在前端写死中文
- id 按字符串处理(雪花 id 超过 JS 安全整数范围,数字会被四舍五入)
2026-10-16 16:33:22 +08:00
Vicky 1148458ca1 chore(merge): 合入远端 main(全量作者标识)到 develop
为什么:远端 main 领先一个提交(1fd0b8b 全量补充作者标识),本地 main 领先一个(4564aa8
el-radio 修复);按条目要求先把远端合进来,再在 develop 上做 I1,main 本身不动(合并走 PR)。
2026-10-16 16:14:15 +08:00
Vicky 4564aa894d chore(web): 修掉 eslint 全量检查的 2 个 error,门禁口径改跑 eslint + pnpm build:pro
为什么:红线第 8 条要求 Web 门禁是 `pnpm build` + eslint 零错误,而此前只跑过 ts:check,
全量 eslint 存量有 2 个 error。Web 已冻结,本次只做"去噪"式修复,不改任何行为。

- BannerManagementView: 表格 slot 解构里去掉未被使用的 row($index 仍在用)
- CreatorCenterPage: 自闭合 <slot /> 改为 <slot></slot>(等价写法)
- 实测:eslint 0 error / 0 warning;pnpm build:pro = Build successful
2026-10-16 09:41:19 +08:00
Zhou Weidong 1fd0b8b100 style: 全量补充作者标识 // Author: fruit.com.cn 2026-10-16 00:02:03 +08:00
269 changed files with 6285 additions and 1837 deletions

No files matched your search

+1 -1
View File
@@ -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
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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
+2
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
// @ts-check
const { defineConfig } = require('eslint-define-config')
module.exports = defineConfig({
+6
View File
@@ -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
+2
View File
@@ -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'],
+2
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
+8 -25
View File
@@ -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">
-18
View File
@@ -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)
}
-129
View File
@@ -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[]
-90
View File
@@ -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
View File
@@ -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",
+2
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
module.exports = {
plugins: {
autoprefixer: {}
+2
View File
@@ -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

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

+2
View File
@@ -1,3 +1,5 @@
<!-- Author: fruit.com.cn -->
<script setup lang="ts">
import { computed } from 'vue'
import { useAppStore } from '@/store/modules/app'
+23
View File
@@ -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>({
+2
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
import request from '@/axios'
import type { BannerItem, BannerSaveData, PageResult } from './types'
+2
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
import request from '@/axios'
import type { PageResult } from './types'
+2
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
import request from '@/axios'
import type { ConfigItem } from './types'
+2
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
import request from '@/axios'
import type { PageResult } from './types'
+2
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
import request from '@/axios'
import type { PageResult } from './types'
+2
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
import request from '@/axios'
/** 指标口径见后端 DashboardService 注释与 docs/api-phase1.md「2.8 数据看板」 */
+4
View File
@@ -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) => {
+2
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
import request from '@/axios'
import type { EpisodeItem, EpisodeSaveData } from './types'
+2
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
import request from '@/axios'
import type { PageResult, CommentItem, ReportItem, SensitiveWordItem } from './types'
+2
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
import request from '@/axios'
import type { OperationLogItem, PageResult } from './types'
+2
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
import request from '@/axios'
import type { MediaAsset } from './types'
+66
View File
@@ -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}` })
}
+85
View File
@@ -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}` })
}
+2
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
import request from '@/axios'
import type {
MenuNode,
+38
View File
@@ -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 */
+2
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
import request from '@/axios'
import type { AdminUserItem, AdminUserSaveData, PageResult } from './types'
+2 -4
View File
@@ -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 })
+2 -1
View File
@@ -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

+2
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
import { AxiosResponse, InternalAxiosRequestConfig } from './types'
import { ElMessage } from 'element-plus'
import qs from 'qs'
+2
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
import service from './service'
import { CONTENT_TYPE } from '@/constants'
import { useUserStoreWithOut } from '@/store/modules/user'
+2
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
import axios, { AxiosError } from 'axios'
import { defaultRequestInterceptors, defaultResponseInterceptors } from './config'
+2
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
import type {
InternalAxiosRequestConfig,
AxiosResponse,
+5
View File
@@ -0,0 +1,5 @@
// Author: fruit.com.cn
import AppIcon from './src/AppIcon.vue'
export { AppIcon }
+40
View File
@@ -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>
+5
View File
@@ -0,0 +1,5 @@
// Author: fruit.com.cn
import AppImage from './src/AppImage.vue'
export { AppImage }
+94
View File
@@ -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>
+2
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
import Backtop from './src/Backtop.vue'
export { Backtop }
+2
View File
@@ -1,3 +1,5 @@
<!-- Author: fruit.com.cn -->
<script setup lang="ts">
import { ElBacktop } from 'element-plus'
import { useDesign } from '@/hooks/web/useDesign'
+2
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
import Breadcrumb from './src/Breadcrumb.vue'
export { Breadcrumb }
+6 -4
View File
@@ -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);
}
}
}
+2
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
import { pathResolve } from '@/utils/routerHelper'
import type { RouteMeta } from 'vue-router'
+2
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
import BaseButton from './src/Button.vue'
export { BaseButton }
+2
View File
@@ -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'
+2
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
import Collapse from './src/Collapse.vue'
export { Collapse }
+5 -2
View File
@@ -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"
/>
+2
View File
@@ -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'
+2
View File
@@ -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>
+2
View File
@@ -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
+2
View File
@@ -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 // 字段名
+2
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
import Dialog from './src/Dialog.vue'
export { Dialog }
+7 -4
View File
@@ -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"
+2
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
import Editor from './src/Editor.vue'
import { IDomEditor } from '@wangeditor/editor'
+2
View File
@@ -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'
+2
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
import Error from './src/Error.vue'
export { Error }
+2
View File
@@ -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'
+2
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
import Form from './src/Form.vue'
import type { FormSchema, FormSetProps } from './src/types'
export type {
+2
View File
@@ -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,
+2
View File
@@ -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'
+4 -1
View File
@@ -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 {
+2
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
import Icon from './src/Icon.vue'
export type { IconTypes } from './src/types'
+7 -3
View File
@@ -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>
+2
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
export interface IconTypes {
size?: number
color?: string
+2
View File
@@ -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'
+2
View File
@@ -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
+2
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
import Logo from './src/Logo.vue'
export { Logo }
+9 -2
View File
@@ -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"
+2
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
import Menu from './src/Menu.vue'
export { Menu }
+2
View File
@@ -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>
+2
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
import { ref, unref } from 'vue'
import { findPath } from '@/utils/tree'
+2
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
import Screenfull from './src/Screenfull.vue'
export { Screenfull }
+6 -3
View File
@@ -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>
+2
View File
@@ -1,3 +1,5 @@
// Author: fruit.com.cn
import { FormSchema, FormSetProps } from '../Form'
import Search from './src/Search.vue'
Loaded 100 of 269 files, more files were not shown because too many files have changed in this diff. Show more