初始化admin pc端

This commit is contained in:
Mrtangl
2022-04-08 10:42:44 +08:00
parent d9c9f27530
commit 19665b64fb
137 changed files with 11991 additions and 0 deletions

4
admin/.env.development Normal file
View File

@@ -0,0 +1,4 @@
NODE_ENV = 'development'
# Base API
VITE_APP_BASE_URL='https://likeadmin.yixiangonline.com'

3
admin/.env.production Normal file
View File

@@ -0,0 +1,3 @@
NODE_ENV = 'production'
# Base API
VITE_APP_BASE_URL=''

156
admin/.eslintrc.js Normal file
View File

@@ -0,0 +1,156 @@
module.exports = {
root: true,
env: {
browser: true,
node: true,
es2021: true
},
parser: 'vue-eslint-parser',
extends: [
'eslint:recommended',
'plugin:vue/vue3-recommended',
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
// eslint-config-prettier 的缩写
'prettier'
],
parserOptions: {
ecmaVersion: 12,
parser: '@typescript-eslint/parser',
sourceType: 'module',
ecmaFeatures: {
jsx: true
}
},
// eslint-plugin-vue @typescript-eslint/eslint-plugin eslint-plugin-prettier的缩写
plugins: ['vue', '@typescript-eslint', 'prettier'],
rules: {
'@typescript-eslint/ban-ts-ignore': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-var-requires': 'off',
'@typescript-eslint/no-empty-function': 'off',
'@typescript-eslint/no-use-before-define': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/ban-types': 'off',
'@typescript-eslint/no-non-null-assertion': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'no-var': 'error',
'prettier/prettier': 'error',
// 禁止出现console
'no-console': 'warn',
// 禁用debugger
'no-debugger': 'warn',
// 禁止出现重复的 case 标签
'no-duplicate-case': 'warn',
// 禁止出现空语句块
'no-empty': 'warn',
// 禁止不必要的括号
'no-extra-parens': 'off',
// 禁止对 function 声明重新赋值
'no-func-assign': 'warn',
// 禁止在 return、throw、continue 和 break 语句之后出现不可达代码
'no-unreachable': 'warn',
// 强制所有控制语句使用一致的括号风格
curly: 'warn',
// 要求 switch 语句中有 default 分支
'default-case': 'warn',
// 强制尽可能地使用点号
'dot-notation': 'warn',
// 要求使用 === 和 !==
eqeqeq: 'warn',
// 禁止 if 语句中 return 语句之后有 else 块
'no-else-return': 'warn',
// 禁止出现空函数
'no-empty-function': 'warn',
// 禁用不必要的嵌套块
'no-lone-blocks': 'warn',
// 禁止使用多个空格
'no-multi-spaces': 'warn',
// 禁止多次声明同一变量
'no-redeclare': 'warn',
// 禁止在 return 语句中使用赋值语句
'no-return-assign': 'warn',
// 禁用不必要的 return await
'no-return-await': 'warn',
// 禁止自我赋值
'no-self-assign': 'warn',
// 禁止自身比较
'no-self-compare': 'warn',
// 禁止不必要的 catch 子句
'no-useless-catch': 'warn',
// 禁止多余的 return 语句
'no-useless-return': 'warn',
// 禁止变量声明与外层作用域的变量同名
'no-shadow': 'off',
// 允许delete变量
'no-delete-var': 'off',
// 强制数组方括号中使用一致的空格
'array-bracket-spacing': 'warn',
// 强制在代码块中使用一致的大括号风格
'brace-style': 'warn',
// 强制使用骆驼拼写法命名约定
camelcase: 'warn',
// 强制使用一致的缩进
indent: 'off',
// 强制在 JSX 属性中一致地使用双引号或单引号
// 'jsx-quotes': 'warn',
// 强制可嵌套的块的最大深度4
'max-depth': 'warn',
// 强制最大行数 300
// "max-lines": ["warn", { "max": 1200 }],
// 强制函数最大代码行数 50
// 'max-lines-per-function': ['warn', { max: 70 }],
// 强制函数块最多允许的的语句数量20
'max-statements': ['warn', 100],
// 强制回调函数最大嵌套深度
'max-nested-callbacks': ['warn', 3],
// 强制函数定义中最多允许的参数数量
'max-params': ['warn', 3],
// 强制每一行中所允许的最大语句数量
'max-statements-per-line': ['warn', { max: 1 }],
// 要求方法链中每个调用都有一个换行符
'newline-per-chained-call': ['warn', { ignoreChainWithDepth: 3 }],
// 禁止 if 作为唯一的语句出现在 else 语句中
'no-lonely-if': 'warn',
// 禁止空格和 tab 的混合缩进
'no-mixed-spaces-and-tabs': 'warn',
// 禁止出现多行空行
'no-multiple-empty-lines': 'warn',
// 禁止出现;
semi: ['warn', 'never'],
// 强制在块之前使用一致的空格
'space-before-blocks': 'warn',
// 强制在 function的左括号之前使用一致的空格
// 'space-before-function-paren': ['warn', 'never'],
// 强制在圆括号内使用一致的空格
'space-in-parens': 'warn',
// 要求操作符周围有空格
'space-infix-ops': 'warn',
// 强制在一元操作符前后使用一致的空格
'space-unary-ops': 'warn',
// 强制在注释中 // 或 /* 使用一致的空格
// "spaced-comment": "warn",
// 强制在 switch 的冒号左右有空格
'switch-colon-spacing': 'warn',
// 强制箭头函数的箭头前后使用一致的空格
'arrow-spacing': 'warn',
'no-var': 'warn',
'prefer-const': 'warn',
'prefer-rest-params': 'warn',
'no-useless-escape': 'warn',
'no-irregular-whitespace': 'warn',
'no-prototype-builtins': 'warn',
'no-fallthrough': 'warn',
'no-extra-boolean-cast': 'warn',
'no-case-declarations': 'warn',
'no-async-promise-executor': 'warn'
},
globals: {
defineProps: 'readonly',
defineEmits: 'readonly',
defineExpose: 'readonly',
withDefaults: 'readonly'
}
}

25
admin/.gitignore vendored Normal file
View File

@@ -0,0 +1,25 @@
node_modules
.DS_Store
dist
dist-ssr
*.local
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

9
admin/.prettierrc Normal file
View File

@@ -0,0 +1,9 @@
{
"tabWidth": 4,
"semi": false,
"singleQuote": true,
"printWidth": 100,
"trailingComma": "none",
"arrowParens": "avoid",
"bracketSpacing": true
}

11
admin/README.md Normal file
View File

@@ -0,0 +1,11 @@
# Vue 3 + Typescript + Vite
This template should help get you started developing with Vue 3 and Typescript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
## Recommended IDE Setup
- [VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=johnsoncodehk.volar)
## Type Support For `.vue` Imports in TS
Since TypeScript cannot handle type information for `.vue` imports, they are shimmed to be a generic Vue component type by default. In most cases this is fine if you don't really care about component prop types outside of templates. However, if you wish to get actual prop types in `.vue` imports (for example to get props validation when using manual `h(...)` calls), you can enable Volar's `.vue` type support plugin by running `Volar: Switch TS Plugin on/off` from VSCode command palette.

66
admin/index.html Normal file
View File

@@ -0,0 +1,66 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>admin</title>
<style>
* {
margin: 0;
padding: 0;
}
.preload {
display: flex;
align-items: center;
justify-content: center;
height: 100vh;
width: 100vw;
}
.circular {
height: 42px;
width: 42px;
animation: loading-rotate 2s linear infinite;
}
.circular .path {
animation: loading-dash 1.5s ease-in-out infinite;
stroke-dasharray: 90, 150;
stroke-dashoffset: 0;
stroke-width: 2;
stroke: #4073fa;
stroke-linecap: round;
}
@keyframes loading-rotate {
100% {
transform: rotate(1turn);
}
}
@keyframes loading-dash {
0% {
stroke-dasharray: 90, 150;
stroke-dashoffset: -40px;
}
100% {
stroke-dasharray: 90, 150;
stroke-dashoffset: -120px;
}
}
</style>
</head>
<body>
<div id="app">
<div class="preload">
<svg viewBox="25 25 50 50" class="circular">
<circle cx="50" cy="50" r="20" fill="none" class="path"></circle>
</svg>
</div>
</div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

40
admin/package.json Normal file
View File

@@ -0,0 +1,40 @@
{
"name": "admin",
"version": "0.0.0",
"scripts": {
"dev": "vite",
"build": "vite build && ./release.sh",
"preview": "vite preview",
"lint": "eslint --fix --ext .ts,.tsx,.vue,.js,.jsx src/",
"prettier": "prettier --write src"
},
"dependencies": {
"@element-plus/icons-vue": "^0.2.4",
"@tinymce/tinymce-vue": "^4.0.5",
"clipboard": "^2.0.8",
"echarts": "^5.2.2",
"element-plus": "^1.2.0-beta.6",
"nprogress": "^0.2.0",
"vue": "^3.2.25",
"vue-echarts": "^6.0.0",
"vue-router": "^4.0.0-0",
"vuedraggable": "^4.1.0",
"vuex": "^4.0.0-0"
},
"devDependencies": {
"@types/node": "^17.0.15",
"@typescript-eslint/eslint-plugin": "^5.14.0",
"@typescript-eslint/parser": "^5.14.0",
"@vitejs/plugin-vue": "^2.0.0",
"axios": "^0.24.0",
"eslint": "^8.10.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-vue": "^8.5.0",
"prettier": "^2.5.1",
"sass": "^1.49.7",
"typescript": "^4.4.4",
"vite": "^2.7.2",
"vue-tsc": "^0.29.8"
}
}

BIN
admin/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

17
admin/release.sh Normal file
View File

@@ -0,0 +1,17 @@
#!/bin/bash
# 文件原路径
srcPath="./dist/"
# 发布路径文件夹
releasePath="../server/public/admin"
#删除发布目录下的mobile文件
rm -r $releasePath
echo "已删除 ==> $releasePath 下的目录文件"
mkdir $releasePath
echo "已新建 ==> $releasePath 目录"
# 复制打包目录内的文件到发布目录
cp -r $srcPath/* $releasePath
echo "已复制 $srcPath/* ==> $releasePath"
cp $releasePath/../favicon.ico $releasePath

49
admin/src/App.vue Normal file
View File

@@ -0,0 +1,49 @@
<template>
<keep-alive>
<router-view v-if="keepAlive && routerAlive" />
</keep-alive>
<router-view v-if="!keepAlive && routerAlive" />
</template>
<script lang="ts">
import { computed, defineComponent, ref, nextTick, provide, onMounted } from 'vue'
import { useAdmin } from './core/hooks/app'
export default defineComponent({
setup() {
const { store, route } = useAdmin()
const routerAlive = ref(true)
const keepAlive = computed(() => route.meta.keepAlive)
const reload = () => {
routerAlive.value = false
nextTick(() => {
routerAlive.value = true
})
}
provide('reload', reload)
onMounted(async () => {
// 获取配置
const data = await store.dispatch('app/getConfig')
// 设置网站logo
let favicon: HTMLLinkElement = document.querySelector('link[rel="icon"]')!
if (favicon) {
favicon.href = data.web_favicon
return
}
favicon = document.createElement('link')
favicon.rel = 'icon'
favicon.href = data.web_favicon
document.head.appendChild(favicon)
})
return {
routerAlive,
keepAlive
}
}
})
</script>
<style lang="scss">
@import './assets/font/iconfont.css';
@import './styles/index.scss';
</style>

44
admin/src/api/app.ts Normal file
View File

@@ -0,0 +1,44 @@
import request from '@/utils/request'
export function apiFileCateAdd(params: any) {
return request.post('/file/addCate', params)
}
export function apiFileCateEdit(params: { id: number; name: string }) {
return request.post('/file/editCate', params)
}
// 文件分类删除
export function apiFileCateDelete(params: { id: number }) {
return request.post('/file/delCate', params)
}
// 文件分类列表
export function apiFileCateLists(params: any) {
return request.get('/file/listCate', { params })
}
// 文件列表
export function apiFileList(params: any) {
return request.get('/file/lists', { params })
}
// 文件删除
export function apiFileDelete(params: { ids: any[] }) {
return request.post('/file/delete', params)
}
// 文件移动
export function apiFileMove(params: { ids: any[]; cid: number }) {
return request.post('/file/move', params)
}
// 文件重命名
export function apiFileRename(params: { id: number; name: string }) {
return request.post('/file/rename', params)
}
// 配置
export function apiConfig() {
return request.get('/config/getConfig')
}

View File

@@ -0,0 +1,31 @@
import request from '@/utils/request'
// 短信通知列表
export function apiNoticeLists(params: any) {
return request.get('/notice.notice/settingLists', { params })
}
// 短信通知详情
export function apiNoticeDetail(params: any) {
return request.get('/notice.notice/detail', { params })
}
// 设置短信通知
export function apiNoticeEdit(params: any) {
return request.post('/notice.notice/set', params)
}
// 短信设置列表
export function apiSmsLists() {
return request.get('/notice.sms_config/getConfig')
}
// 短信设置详情
export function apiSmsDetail(params: any) {
return request.get('/notice.sms_config/detail', { params })
}
// 设置短信通知
export function apiSmsEdit(params: any) {
return request.post('/notice.sms_config/setConfig', params)
}

55
admin/src/api/auth.ts Normal file
View File

@@ -0,0 +1,55 @@
import request from '@/utils/request'
import { terminal } from '@/config/app'
export function adminLists(params: any) {
return request.get('/auth.admin/lists', { params })
}
// 管理员添加
export function apiAdminAdd(params: any) {
return request.post('/auth.admin/add', params)
}
export function apiAdminEdit(params: any) {
return request.post('/auth.admin/edit', params)
}
// 管理员删除
export function apiAdminDelete(params: { id: number }) {
return request.post('/auth.admin/delete', params)
}
// 管理员详情
export function apiAdminDetail(params: any) {
return request.get('/auth.admin/detail', { params })
}
// 角色列表
export function apiRoleLists(params: any) {
return request.get('/auth.role/lists', { params })
}
// 添加角色
export function apiRoleAdd(params: any) {
return request.post('/auth.role/add', { ...params })
}
// 编辑角色
export function apiRoleEdit(params: any) {
return request.post('/auth.role/edit', { ...params })
}
// 删除角色
export function apiRoleDel(params: any) {
return request.post('/auth.role/delete', { ...params })
}
// 角色详情
export function apiRoleDetail(params: any) {
return request.get('/auth.role/detail', { params })
}
// 角色权限菜单
export function apiConfigGetMenu() {
return request.get('/config/getMenu')
}
// 角色权限
export function apiConfigGetAuth() {
return request.get('/config/getAuth')
}

17
admin/src/api/channel/app_store.d.ts vendored Normal file
View File

@@ -0,0 +1,17 @@
/** S APP设置 **/
export interface AppSettings_Res {
ios_download_url: string, // 苹果APP下载链接
android_download_url: string, // 安卓APP下载链接
download_title: string, // APP下载引导文案
app_id: string, // 开放平台appid
app_secret: string // 开放平台appSecrets
}
export interface AppSettings_Req {
ios_download_url: string, // 苹果APP下载链接
android_download_url: string, // 安卓APP下载链接
download_title: string, // APP下载引导文案
app_id: string, // 开放平台appid
app_secret: string // 开放平台appSecrets
}
/** E APP设置 **/

View File

@@ -0,0 +1,11 @@
import request from "@/utils/request";
import * as Interface from './channel/app_store.d.ts'
/** S APP设置 **/
// 获取APP设置
export const apiAppSettings = (): Promise<Interface.AppSettings_Res> =>
request.get('/channel.app_setting/getConfig')
// APP设置
export const apiAppSettingsSet = (data: Interface.AppSettings_Req): Promise<any> =>
request.post('/channel.app_setting/setConfig', data)
/** E APP设置 **/

View File

@@ -0,0 +1,10 @@
import request from "@/utils/request";
/** S H5设置 **/
// 获取H5设置
export const apiH5Settings = (): Promise<any> =>
request.get('/channel.h5_setting/getConfig')
// H5设置
export const apiH5SettingsSet = (data: any): Promise<any> =>
request.post('/channel.h5_setting/setConfig', data)
/** E H5设置 **/

View File

@@ -0,0 +1,10 @@
import request from "@/utils/request";
/** S 字节小程序设置 **/
// 获取字节小程序设置
export const apiToutiaoSetting = () =>
request.get('/toutiao.toutiao_setting/getConfig')
// 字节小程序设置
export const apiToutiaoSettingSet = (data: any) =>
request.post('/toutiao.toutiao_setting/setConfig', data)
/** E 字节小程序设置 **/

49
admin/src/api/channel/mp_wechat.d.ts vendored Normal file
View File

@@ -0,0 +1,49 @@
import * as Common from '../common.d.ts'
import {apiMpWeChatMenuSave} from "@/api/channel/mp_wechat";
/** S 渠道信息 **/
export interface MPWeChatConfigInfo_Res extends Common.Indexes {
name: string, // 公众号名称
original_id: string, // 原始id
qr_code: string, // 二维码
app_id: string, // APP ID
app_secret: string, // App Secret
url: string, // URL
token: string, // Token
encoding_aes_key: string, // Encoding AES Key
encryption_type: string, // 消息加密方式: 1-明文模式 2-兼容模式 3-安全模式
business_domain: string, // 业务域名
js_secure_domain: string, // JS接口安全域名
web_auth_domain: string, // 网页授权域名
}
export interface MPWeChatConfigEdit_Req {
name?: string, // 公众号名称
original_id?: string, // 原始id
qr_code?: string, // 二维码
app_id: string, // APP ID
app_secret: string, // App Secret
token?: string, // Token
encoding_aes_key?: string, // Encoding AES Key
encryption_type: string, // 消息加密方式: 1-明文模式 2-兼容模式 3-安全模式
}
/** E 渠道信息 **/
/** S 菜单配置 **/
export interface MPWeChatMenu {
name: string, // 菜单名称
type: string, // 菜单类型click-关键字view-网页miniprogram-小程序
key?: string, // 关键字
url?: string, // 网页URL
appid?: string, // 小程序AppID
pagepath?: string, // 小程序路径
sub_button?: Array<MPWeChatMenu>, // 二级菜单
}
export interface MPWeChatMenuSave_Req {
menu: Array<MPWeChatMenu>
}
/** E 菜单配置 **/

View File

@@ -0,0 +1,60 @@
import request from "@/utils/request";
import * as Interface from './mp_wechat.d.ts'
/** S 渠道设置 **/
// 获取渠道信息
export const apiMPWeChatConfigInfo = (): Promise<any> =>
request.get('/channel.official_account_setting/getConfig')
// 编辑渠道信息
export const apiMpWeChatConfigEdit = (params: Interface.MPWeChatConfigEdit_Req) =>
request.post('/channel.official_account_setting/setConfig', params)
/** E 渠道设置 **/
/** S 菜单设置 **/
// 获取菜单详情
export const apiMpWeChatMenuDetail = (): Promise<any> =>
request.get('/channel.official_account_menu/detail')
// 保存菜单配置
export const apiMpWeChatMenuSave = (params: any) =>
request.post('/channel.official_account_menu/save', params)
// 发布菜单配置
export const apiMpWeChatMenuPublish = (params: any) =>
request.post('/channel.official_account_menu/saveAndPublish', params)
/** E 菜单设置 **/
/** S 回复管理 **/
// 新增回复(关注/关词词/默认)
export const apiMpWeChatReplyAdd = (params: any): Promise<any> =>
request.post('/channel.official_account_reply/add', params)
// 编辑回复(关注/关键词/默认)
export const apiMpWeChatReplyEdit = (params: any): Promise<any> =>
request.post('/channel.official_account_reply/edit', params)
// 获取回复详情
export const apiMpWeChatReplyDetail = (params: any): Promise<any> =>
request.get('/channel.official_account_reply/detail', {params})
// 删除回复
export const apiMpWeChatReplyDelete = (params: any): Promise<any> =>
request.post('/channel.official_account_reply/delete', params)
// 更新排序
export const apiMpWeChatReplySort = (params: any): Promise<any> =>
request.post('/channel.official_account_reply/sort', params)
// 回复列表
export const apiMpWeChatReplyLists = (params: any): Promise<any> =>
request.get('/channel.official_account_reply/lists', {params})
// 回复列表
export const apiMpWeChatReplyStatus = (params: any): Promise<any> =>
request.post('/channel.official_account_reply/status', params)
/** E 回复管理 **/

32
admin/src/api/channel/wechat_app.d.ts vendored Normal file
View File

@@ -0,0 +1,32 @@
/** S 微信小程序设置 **/
export interface WechatMiniSetting_Res {
name: string, // 小程序名称
original_id: string, // 原始id
qr_code: string, // 二维码
app_id: string,
app_secret: string,
request_domain: string, // request合法域名
socket_domain: string, // socket合法域名
upload_file_domain: string, // uploadFile合法域名
download_file_domain: string, // downloadFile合法域名
udp_domain: string, // udp合法域名
business_domain: string, // 业务域名
url: string,
token: string,
encoding_aes_key: string,
encryption_type: 1 | 2 | 3 , // 消息加密方式 1-明文模式 2-兼容模式 3-安全模式
data_format: 1 | 2 // 数据格式 1-JSON 2-XML
}
export interface WechatMiniSetting_Req {
name: string, // 小程序名称
original_id: string, // 原始id
qr_code: string, // 二维码
app_id: string,
app_secret: string,
token: string,
encoding_aes_key: string,
encryption_type: 1 | 2 | 3 , // 消息加密方式 1-明文模式 2-兼容模式 3-安全模式
data_format: 1 | 2 // 数据格式 1-JSON 2-XML
}
/** E 微信小程序设置 **/

View File

@@ -0,0 +1,11 @@
import request from "@/utils/request";
import * as Interface from './wechat_app.d.ts'
/** S 微信小程序设置 **/
// 获取微信小程序设置
export const apiWechatMiniSetting = (): Promise<Interface.WechatMiniSetting_Res> =>
request.get('/channel.mnp_settings/getConfig')
// 微信小程序设置
export const apiWechatMiniSettingSet = (data: Interface.WechatMiniSetting_Req): Promise<any> =>
request.post('/channel.mnp_settings/setConfig', data)
/** E 微信小程序设置 **/

View File

@@ -0,0 +1,11 @@
import request from "@/utils/request";
/** S 微信公众平台设置 **/
// 获取pc设置
export const apiWechatPlatformGet = (): Promise<any> =>
request.get('/channel.open_setting/getConfig')
// pc设置
export const apiWechatPlatformSet = (data: any): Promise<any> =>
request.post('/channel.open_setting/setConfig', data)
/** E pc设置 **/

101
admin/src/api/decoration.ts Normal file
View File

@@ -0,0 +1,101 @@
/* 装修管理 */
import request from '@/utils/request'
/* 首页装修 Start */
// 列表
export function apiHomeMenuLists() {
return request.get('/decorate.menu/lists')
}
// 商城页面列表
export function apiShowPage() {
return request.get('/decorate.menu/shopPage')
}
// 商品分类一级页面
export function apiGoodsCategoryPage() {
return request.get('/decorate.menu/goodsCategoryPage')
}
// 详情
export function apiHomeMenuDetail(params: any) {
return request.get('/decorate.menu/detail', { params })
}
// 添加
export function apiHomeMenuAdd(params: any) {
return request.post('/decorate.menu/add', params)
}
// 编辑
export function apiHomeMenuEdit(params: any) {
return request.post('/decorate.menu/edit', params)
}
// 状态
export function apiHomeMenuStatusEdit(params: any) {
return request.post('/decorate.menu/status', params)
}
// 删除
export function apiHomeMenuDel(params: any) {
return request.post('/decorate.menu/del', params)
}
/* 首页装修 End */
/* 底部导航 Start */
// 列表
export function apiTabBarLists() {
return request.get('/decorate.navigation/lists')
}
// 详情
export function apiTabBarDetail(params: any) {
return request.get('/decorate.navigation/detail', { params })
}
// 编辑
export function apiTabBarEdit(params: any) {
return request.post('/decorate.navigation/edit', params)
}
/* 底部导航 End */
/* 广告管理 Start */
// 列表
export function apiAdLists(params: any) {
return request.get('/ad.ad/lists', { params })
}
// 广告位列表
export function apiAdPositionLists() {
return request.get('/ad.ad_position/lists')
}
// 详情
export function apiAdDetail(params: any) {
return request.get('/ad.ad/detail', { params })
}
// 添加
export function apiAdAdd(params: any) {
return request.post('/ad.ad/add', params)
}
// 删除
export function apiAdDel(params: any) {
return request.post('/ad.ad/del', params)
}
// 状态修改
export function apiAdEditStatus(params: any) {
return request.post('/ad.ad/status', params)
}
// 编辑
export function apiAdEdit(params: any) {
return request.post('/ad.ad/edit', params)
}
/* 广告管理 End */

View File

@@ -0,0 +1,70 @@
import request from '@/utils/request'
/** 资讯分类 Start **/
// 列表
export function apiArticleCategoryList(params: any) {
return request.get('/article.articleCategory/lists', { params })
}
// 添加
export function apiArticleCategoryAdd(params: any) {
return request.post('/article.articleCategory/add', params)
}
// 编辑
export function apiArticleCategoryEdit(params: any) {
return request.post('/article.articleCategory/edit', params)
}
// 详情
export function apiArticleCategoryDetail(params: any) {
return request.get('/article.articleCategory/detail', { params })
}
// 删除
export function apiArticleCategoryDelete(params: any) {
return request.post('/article.articleCategory/delete', params)
}
// 状态
export function apiArticleCategoryStatus(params: any) {
return request.post('/article.articleCategory/updateStatus', params)
}
/** 资讯分类 End **/
/** 资讯列表 Start **/
// 列表
export function apiArticleList(params: any) {
return request.get('/article.article/lists', { params })
}
// 添加
export function apiArticleAdd(params: any) {
return request.post('/article.article/add', params)
}
// 编辑
export function apiArticleEdit(params: any) {
return request.post('/article.article/edit', params)
}
// 详情
export function apiArticleDetail(params: any) {
return request.get('/article.article/detail', { params })
}
// 删除
export function apiArticleDelete(params: any) {
return request.post('/article.article/delete', params)
}
// 状态
export function apiArticleStatus(params: any) {
return request.post('/article.article/updateStatus', params)
}
// 所有资讯分类
export function apiAllArticleCategory() {
return request.get('/article.articleCategory/selectArticleCategory')
}
/** 资讯列表 End **/

65
admin/src/api/setting.ts Normal file
View File

@@ -0,0 +1,65 @@
import request from '@/utils/request'
// 获取备案信息
export function apiGetCopyright() {
return request.get('/setting.web.web_setting/getCopyright')
}
// 设置备案信息
export function apiSetCopyright(params: any) {
return request.post('/setting.web.web_setting/setCopyright', { ...params })
}
// 获取网站信息
export function apiGetWebsite() {
return request.get('/setting.web.web_setting/getWebsite')
}
// 设置网站信息
export function apiSetWebsite(params: any) {
return request.post('/setting.web.web_setting/setWebsite', { ...params })
}
// 获取政策协议
export function apiGetProtocol() {
return request.get('/setting.web.web_setting/getAgreement')
}
// 设置政策协议
export function apiSetProtocol(params: any) {
return request.post('/setting.web.web_setting/setAgreement', params)
}
// 获取系统环境
export function apiSystemInfo() {
return request.get('/setting.system.system/info')
}
/** S 在线客服 **/
// 获取客服设置
export const apiCustomerServiceGetConfig = (): Promise<any> =>
request.get('/setting.customer_service/getConfig')
// 设置客服设置
export const apiCustomerServiceSetConfig = (params: any): Promise<any> =>
request.post('/setting.customer_service/setConfig', params)
/** E 在线客服 **/
/** S 用户设置 **/
// 获取用户设置
export function apiUserConfigGet() {
return request.get('/setting.user.user/getConfig')
}
// 用户设置
export function apiUserConfigSet(params: any) {
return request.post('/setting.user.user/setConfig', params)
}
// 获取登录注册设置
export function apiLoginConfigGet() {
return request.get('/setting.user.user/getRegisterConfig')
}
// 登录注册设置
export function apiLoginConfigSet(params: any) {
return request.post('/setting.user.user/setRegisterConfig', params)
}
/** E 用户设置 **/

17
admin/src/api/user.ts Normal file
View File

@@ -0,0 +1,17 @@
import request from '@/utils/request'
import { terminal } from '@/config/app'
// 登录
export function apiLogin(params: { account: string; password: string }) {
return request.post('/login/account', { ...params, terminal })
}
// 退出登录
export function apiLogout() {
return request.post('/login/logout')
}
// 用户信息
export function apiUserInfo() {
return request.get('/auth.admin/mySelf')
}

View File

@@ -0,0 +1,6 @@
import request from '@/utils/request'
// 工作台主页
export function apiWorkbench() {
return request.get('/Workbench/index')
}

View File

@@ -0,0 +1,539 @@
/* Logo 字体 */
@font-face {
font-family: 'iconfont logo';
src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834');
src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834#iefix')
format('embedded-opentype'),
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.woff?t=1545807318834') format('woff'),
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.ttf?t=1545807318834')
format('truetype'),
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.svg?t=1545807318834#iconfont')
format('svg');
}
.logo {
font-family: 'iconfont logo';
font-size: 160px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* tabs */
.nav-tabs {
position: relative;
}
.nav-tabs .nav-more {
position: absolute;
right: 0;
bottom: 0;
height: 42px;
line-height: 42px;
color: #666;
}
#tabs {
border-bottom: 1px solid #eee;
}
#tabs li {
cursor: pointer;
width: 100px;
height: 40px;
line-height: 40px;
text-align: center;
font-size: 16px;
border-bottom: 2px solid transparent;
position: relative;
z-index: 1;
margin-bottom: -1px;
color: #666;
}
#tabs .active {
border-bottom-color: #f00;
color: #222;
}
.tab-container .content {
display: none;
}
/* 页面布局 */
.main {
padding: 30px 100px;
width: 960px;
margin: 0 auto;
}
.main .logo {
color: #333;
text-align: left;
margin-bottom: 30px;
line-height: 1;
height: 110px;
margin-top: -50px;
overflow: hidden;
*zoom: 1;
}
.main .logo a {
font-size: 160px;
color: #333;
}
.helps {
margin-top: 40px;
}
.helps pre {
padding: 20px;
margin: 10px 0;
border: solid 1px #e7e1cd;
background-color: #fffdef;
overflow: auto;
}
.icon_lists {
width: 100% !important;
overflow: hidden;
*zoom: 1;
}
.icon_lists li {
width: 100px;
margin-bottom: 10px;
margin-right: 20px;
text-align: center;
list-style: none !important;
cursor: default;
}
.icon_lists li .code-name {
line-height: 1.2;
}
.icon_lists .icon {
display: block;
height: 100px;
line-height: 100px;
font-size: 42px;
margin: 10px auto;
color: #333;
-webkit-transition: font-size 0.25s linear, width 0.25s linear;
-moz-transition: font-size 0.25s linear, width 0.25s linear;
transition: font-size 0.25s linear, width 0.25s linear;
}
.icon_lists .icon:hover {
font-size: 100px;
}
.icon_lists .svg-icon {
/* 通过设置 font-size 来改变图标大小 */
width: 1em;
/* 图标和文字相邻时,垂直对齐 */
vertical-align: -0.15em;
/* 通过设置 color 来改变 SVG 的颜色/fill */
fill: currentColor;
/* path 和 stroke 溢出 viewBox 部分在 IE 下会显示
normalize.css 中也包含这行 */
overflow: hidden;
}
.icon_lists li .name,
.icon_lists li .code-name {
color: #666;
}
/* markdown 样式 */
.markdown {
color: #666;
font-size: 14px;
line-height: 1.8;
}
.highlight {
line-height: 1.5;
}
.markdown img {
vertical-align: middle;
max-width: 100%;
}
.markdown h1 {
color: #404040;
font-weight: 500;
line-height: 40px;
margin-bottom: 24px;
}
.markdown h2,
.markdown h3,
.markdown h4,
.markdown h5,
.markdown h6 {
color: #404040;
margin: 1.6em 0 0.6em 0;
font-weight: 500;
clear: both;
}
.markdown h1 {
font-size: 28px;
}
.markdown h2 {
font-size: 22px;
}
.markdown h3 {
font-size: 16px;
}
.markdown h4 {
font-size: 14px;
}
.markdown h5 {
font-size: 12px;
}
.markdown h6 {
font-size: 12px;
}
.markdown hr {
height: 1px;
border: 0;
background: #e9e9e9;
margin: 16px 0;
clear: both;
}
.markdown p {
margin: 1em 0;
}
.markdown > p,
.markdown > blockquote,
.markdown > .highlight,
.markdown > ol,
.markdown > ul {
width: 80%;
}
.markdown ul > li {
list-style: circle;
}
.markdown > ul li,
.markdown blockquote ul > li {
margin-left: 20px;
padding-left: 4px;
}
.markdown > ul li p,
.markdown > ol li p {
margin: 0.6em 0;
}
.markdown ol > li {
list-style: decimal;
}
.markdown > ol li,
.markdown blockquote ol > li {
margin-left: 20px;
padding-left: 4px;
}
.markdown code {
margin: 0 3px;
padding: 0 5px;
background: #eee;
border-radius: 3px;
}
.markdown strong,
.markdown b {
font-weight: 600;
}
.markdown > table {
border-collapse: collapse;
border-spacing: 0px;
empty-cells: show;
border: 1px solid #e9e9e9;
width: 95%;
margin-bottom: 24px;
}
.markdown > table th {
white-space: nowrap;
color: #333;
font-weight: 600;
}
.markdown > table th,
.markdown > table td {
border: 1px solid #e9e9e9;
padding: 8px 16px;
text-align: left;
}
.markdown > table th {
background: #f7f7f7;
}
.markdown blockquote {
font-size: 90%;
color: #999;
border-left: 4px solid #e9e9e9;
padding-left: 0.8em;
margin: 1em 0;
}
.markdown blockquote p {
margin: 0;
}
.markdown .anchor {
opacity: 0;
transition: opacity 0.3s ease;
margin-left: 8px;
}
.markdown .waiting {
color: #ccc;
}
.markdown h1:hover .anchor,
.markdown h2:hover .anchor,
.markdown h3:hover .anchor,
.markdown h4:hover .anchor,
.markdown h5:hover .anchor,
.markdown h6:hover .anchor {
opacity: 1;
display: inline-block;
}
.markdown > br,
.markdown > p > br {
clear: both;
}
.hljs {
display: block;
background: white;
padding: 0.5em;
color: #333333;
overflow-x: auto;
}
.hljs-comment,
.hljs-meta {
color: #969896;
}
.hljs-string,
.hljs-variable,
.hljs-template-variable,
.hljs-strong,
.hljs-emphasis,
.hljs-quote {
color: #df5000;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-type {
color: #a71d5d;
}
.hljs-literal,
.hljs-symbol,
.hljs-bullet,
.hljs-attribute {
color: #0086b3;
}
.hljs-section,
.hljs-name {
color: #63a35c;
}
.hljs-tag {
color: #333333;
}
.hljs-title,
.hljs-attr,
.hljs-selector-id,
.hljs-selector-class,
.hljs-selector-attr,
.hljs-selector-pseudo {
color: #795da3;
}
.hljs-addition {
color: #55a532;
background-color: #eaffea;
}
.hljs-deletion {
color: #bd2c00;
background-color: #ffecec;
}
.hljs-link {
text-decoration: underline;
}
/* 代码高亮 */
/* PrismJS 1.15.0
https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript */
/**
* prism.js default theme for JavaScript, CSS and HTML
* Based on dabblet (http://dabblet.com)
* @author Lea Verou
*/
code[class*='language-'],
pre[class*='language-'] {
color: black;
background: none;
text-shadow: 0 1px white;
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
pre[class*='language-']::-moz-selection,
pre[class*='language-'] ::-moz-selection,
code[class*='language-']::-moz-selection,
code[class*='language-'] ::-moz-selection {
text-shadow: none;
background: #b3d4fc;
}
pre[class*='language-']::selection,
pre[class*='language-'] ::selection,
code[class*='language-']::selection,
code[class*='language-'] ::selection {
text-shadow: none;
background: #b3d4fc;
}
@media print {
code[class*='language-'],
pre[class*='language-'] {
text-shadow: none;
}
}
/* Code blocks */
pre[class*='language-'] {
padding: 1em;
margin: 0.5em 0;
overflow: auto;
}
:not(pre) > code[class*='language-'],
pre[class*='language-'] {
background: #f5f2f0;
}
/* Inline code */
:not(pre) > code[class*='language-'] {
padding: 0.1em;
border-radius: 0.3em;
white-space: normal;
}
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: slategray;
}
.token.punctuation {
color: #999;
}
.namespace {
opacity: 0.7;
}
.token.property,
.token.tag,
.token.boolean,
.token.number,
.token.constant,
.token.symbol,
.token.deleted {
color: #905;
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin,
.token.inserted {
color: #690;
}
.token.operator,
.token.entity,
.token.url,
.language-css .token.string,
.style .token.string {
color: #9a6e3a;
background: hsla(0, 0%, 100%, 0.5);
}
.token.atrule,
.token.attr-value,
.token.keyword {
color: #07a;
}
.token.function,
.token.class-name {
color: #dd4a68;
}
.token.regex,
.token.important,
.token.variable {
color: #e90;
}
.token.important,
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}

View File

@@ -0,0 +1,282 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>iconfont Demo</title>
<link
rel="shortcut icon"
href="//img.alicdn.com/imgextra/i2/O1CN01ZyAlrn1MwaMhqz36G_!!6000000001499-73-tps-64-64.ico"
type="image/x-icon"
/>
<link
rel="icon"
type="image/svg+xml"
href="//img.alicdn.com/imgextra/i4/O1CN01EYTRnJ297D6vehehJ_!!6000000008020-55-tps-64-64.svg"
/>
<link rel="stylesheet" href="https://g.alicdn.com/thx/cube/1.3.2/cube.min.css" />
<link rel="stylesheet" href="demo.css" />
<link rel="stylesheet" href="iconfont.css" />
<script src="iconfont.js"></script>
<!-- jQuery -->
<script src="https://a1.alicdn.com/oss/uploads/2018/12/26/7bfddb60-08e8-11e9-9b04-53e73bb6408b.js"></script>
<!-- 代码高亮 -->
<script src="https://a1.alicdn.com/oss/uploads/2018/12/26/a3f714d0-08e6-11e9-8a15-ebf944d7534c.js"></script>
<style>
.main .logo {
margin-top: 0;
height: auto;
}
.main .logo a {
display: flex;
align-items: center;
}
.main .logo .sub-title {
margin-left: 0.5em;
font-size: 22px;
color: #fff;
background: linear-gradient(-45deg, #3967ff, #b500fe);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
</style>
</head>
<body>
<div class="main">
<h1 class="logo">
<a href="https://www.iconfont.cn/" title="iconfont 首页" target="_blank">
<img
width="200"
src="https://img.alicdn.com/imgextra/i3/O1CN01Mn65HV1FfSEzR6DKv_!!6000000000514-55-tps-228-59.svg"
/>
</a>
</h1>
<div class="nav-tabs">
<ul id="tabs" class="dib-box">
<li class="dib active"><span>Unicode</span></li>
<li class="dib"><span>Font class</span></li>
<li class="dib"><span>Symbol</span></li>
</ul>
<a
href="https://www.iconfont.cn/manage/index?manage_type=myprojects&projectId=3112541"
target="_blank"
class="nav-more"
>查看项目</a
>
</div>
<div class="tab-container">
<div class="content unicode" style="display: block">
<ul class="icon_lists dib-box">
<li class="dib">
<span class="icon iconfont">&#xe658;</span>
<div class="name">quanxian</div>
<div class="code-name">&amp;#xe658;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe659;</span>
<div class="name">setting</div>
<div class="code-name">&amp;#xe659;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe65a;</span>
<div class="name">home</div>
<div class="code-name">&amp;#xe65a;</div>
</li>
</ul>
<div class="article markdown">
<h2 id="unicode-">Unicode 引用</h2>
<hr />
<p>Unicode 是字体在网页端最原始的应用方式,特点是:</p>
<ul>
<li>支持按字体的方式去动态调整图标大小,颜色等等。</li>
<li>默认情况下不支持多色,直接添加多色图标会自动去色。</li>
</ul>
<blockquote>
<p>
注意:新版 iconfont 支持两种方式引用多色图标SVG symbol
引用方式和彩色字体图标模式。(使用彩色字体图标需要在「编辑项目」中开启「彩色」选项后并重新生成。)
</p>
</blockquote>
<p>Unicode 使用步骤如下:</p>
<h3 id="-font-face">第一步:拷贝项目下面生成的 <code>@font-face</code></h3>
<pre><code class="language-css"
>@font-face {
font-family: 'iconfont';
src: url('iconfont.woff2?t=1640939015921') format('woff2'),
url('iconfont.woff?t=1640939015921') format('woff'),
url('iconfont.ttf?t=1640939015921') format('truetype');
}
</code></pre>
<h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
<pre><code class="language-css"
>.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
</code></pre>
<h3 id="-">第三步:挑选相应图标并获取字体编码,应用于页面</h3>
<pre>
<code class="language-html"
>&lt;span class="iconfont"&gt;&amp;#x33;&lt;/span&gt;
</code></pre>
<blockquote>
<p>
"iconfont" 是你项目下的 font-family。可以通过编辑项目查看默认是
"iconfont"。
</p>
</blockquote>
</div>
</div>
<div class="content font-class">
<ul class="icon_lists dib-box">
<li class="dib">
<span class="icon iconfont icon-quanxian"></span>
<div class="name">quanxian</div>
<div class="code-name">.icon-quanxian</div>
</li>
<li class="dib">
<span class="icon iconfont icon-setting"></span>
<div class="name">setting</div>
<div class="code-name">.icon-setting</div>
</li>
<li class="dib">
<span class="icon iconfont icon-home"></span>
<div class="name">home</div>
<div class="code-name">.icon-home</div>
</li>
</ul>
<div class="article markdown">
<h2 id="font-class-">font-class 引用</h2>
<hr />
<p>
font-class 是 Unicode 使用方式的一种变种,主要是解决 Unicode
书写不直观,语意不明确的问题。
</p>
<p>与 Unicode 使用方式相比,具有如下特点:</p>
<ul>
<li>
相比于 Unicode 语意明确,书写更直观。可以很容易分辨这个 icon
是什么。
</li>
<li>
因为使用 class 来定义图标,所以当要替换图标时,只需要修改 class
里面的 Unicode 引用。
</li>
</ul>
<p>使用步骤如下:</p>
<h3 id="-fontclass-">第一步:引入项目下面生成的 fontclass 代码:</h3>
<pre><code class="language-html">&lt;link rel="stylesheet" href="./iconfont.css"&gt;
</code></pre>
<h3 id="-">第二步:挑选相应图标并获取类名,应用于页面:</h3>
<pre><code class="language-html">&lt;span class="iconfont icon-xxx"&gt;&lt;/span&gt;
</code></pre>
<blockquote>
<p>
" iconfont" 是你项目下的 font-family。可以通过编辑项目查看默认是
"iconfont"。
</p>
</blockquote>
</div>
</div>
<div class="content symbol">
<ul class="icon_lists dib-box">
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-quanxian"></use>
</svg>
<div class="name">quanxian</div>
<div class="code-name">#icon-quanxian</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-setting"></use>
</svg>
<div class="name">setting</div>
<div class="code-name">#icon-setting</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-home"></use>
</svg>
<div class="name">home</div>
<div class="code-name">#icon-home</div>
</li>
</ul>
<div class="article markdown">
<h2 id="symbol-">Symbol 引用</h2>
<hr />
<p>
这是一种全新的使用方式,应该说这才是未来的主流,也是平台目前推荐的用法。相关介绍可以参考这篇<a
href=""
>文章</a
>
这种用法其实是做了一个 SVG 的集合,与另外两种相比具有如下特点:
</p>
<ul>
<li>支持多色图标了,不再受单色限制。</li>
<li>
通过一些技巧,支持像字体那样,通过 <code>font-size</code>,
<code>color</code> 来调整样式。
</li>
<li>兼容性较差,支持 IE9+,及现代浏览器。</li>
<li>浏览器渲染 SVG 的性能一般,还不如 png。</li>
</ul>
<p>使用步骤如下:</p>
<h3 id="-symbol-">第一步:引入项目下面生成的 symbol 代码:</h3>
<pre><code class="language-html">&lt;script src="./iconfont.js"&gt;&lt;/script&gt;
</code></pre>
<h3 id="-css-">第二步:加入通用 CSS 代码(引入一次就行):</h3>
<pre><code class="language-html">&lt;style&gt;
.icon {
width: 1em;
height: 1em;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}
&lt;/style&gt;
</code></pre>
<h3 id="-">第三步:挑选相应图标并获取类名,应用于页面:</h3>
<pre><code class="language-html">&lt;svg class="icon" aria-hidden="true"&gt;
&lt;use xlink:href="#icon-xxx"&gt;&lt;/use&gt;
&lt;/svg&gt;
</code></pre>
</div>
</div>
</div>
</div>
<script>
$(document).ready(function () {
$('.tab-container .content:first').show()
$('#tabs li').click(function (e) {
var tabContent = $('.tab-container .content')
var index = $(this).index()
if ($(this).hasClass('active')) {
return
} else {
$('#tabs li').removeClass('active')
$(this).addClass('active')
tabContent.hide().eq(index).fadeIn()
}
})
})
</script>
</body>
</html>

View File

@@ -0,0 +1,26 @@
@font-face {
font-family: 'iconfont'; /* Project id 3112541 */
src: url('iconfont.woff2?t=1640939015921') format('woff2'),
url('iconfont.woff?t=1640939015921') format('woff'),
url('iconfont.ttf?t=1640939015921') format('truetype');
}
.iconfont {
font-family: 'iconfont' !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-quanxian:before {
content: '\e658';
}
.icon-setting:before {
content: '\e659';
}
.icon-home:before {
content: '\e65a';
}

View File

@@ -0,0 +1,30 @@
{
"id": "3112541",
"name": "likesadmin",
"font_family": "iconfont",
"css_prefix_text": "icon-",
"description": "",
"glyphs": [
{
"icon_id": "26923968",
"name": "quanxian",
"font_class": "quanxian",
"unicode": "e658",
"unicode_decimal": 58968
},
{
"icon_id": "26923969",
"name": "setting",
"font_class": "setting",
"unicode": "e659",
"unicode_decimal": 58969
},
{
"icon_id": "26923970",
"name": "home",
"font_class": "home",
"unicode": "e65a",
"unicode_decimal": 58970
}
]
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 605 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -0,0 +1,53 @@
<template>
<div class="del-wrap">
<slot></slot>
<div v-if="showClose" class="icon-close" @click.stop="handleClose">
<el-icon :size="12"><close-bold /></el-icon>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
components: {},
props: {
showClose: {
type: Boolean,
default: true
}
},
emits: ['close'],
setup(props, { emit }) {
const handleClose = () => {
emit('close')
}
return {
handleClose
}
}
})
</script>
<style scoped lang="scss">
.del-wrap {
position: relative;
&:hover > .icon-close {
display: flex;
}
.icon-close {
display: none;
position: absolute;
top: -8px;
right: -8px;
width: 16px;
height: 16px;
background-color: rgba(0, 0, 0, 0.3);
justify-content: center;
align-items: center;
border-radius: 50%;
color: #fff;
cursor: pointer;
}
}
</style>

View File

@@ -0,0 +1,237 @@
<template>
<div class="cl-editor-quill">
<div ref="editorRef" class="editor" :style="style"></div>
<material-select ref="materialRef" :hidden="true" :limit="-1" @change="filesChange" />
</div>
</template>
<script setup lang="ts">
import { computed, defineComponent, onMounted, onUnmounted, ref, toRefs, watch } from 'vue'
import Quill from 'quill'
import 'quill/dist/quill.snow.css'
import MaterialSelect from '@/components/material-select/index.vue'
interface Props {
// 绑定的值
modelValue: string
// 编辑器的参数
options?: Record<any, any>
// 编辑器的宽度
width?: number | string
// 编辑器的高
height?: number | string
minHeight?: number | string
}
const props = withDefaults(defineProps<Props>(), {
options: () => ({}),
width: '100%',
height: '100%',
})
const emit = defineEmits<{
(event: 'update:modelValue', val: string): void
(event: 'load', quill: any): void
}>()
let quill: any = null
const editorRef = ref()
const materialRef = ref()
// 文本内容
const content = ref('')
// 光标位置
const cursorIndex = ref(0)
// 图片上传处理
const uploadFileHandler = () => {
const selection = quill.getSelection()
if (selection) {
cursorIndex.value = selection.index
}
materialRef.value.showPopup()
}
// 图片插入
const filesChange = (files: any[]) => {
if (files.length > 0) {
// 批量插入图片
files.forEach((file, i) => {
quill.insertEmbed(cursorIndex.value + i, 'image', file, Quill.sources.USER)
})
// 移动光标到图片后一位
quill.setSelection(cursorIndex.value + files.length)
}
}
// 设置内容
const setContent = (val: string) => {
quill.root.innerHTML = val || ''
}
// 编辑框样式
const style = computed<any>(() => {
return {
height: typeof props.height == 'string' ? props.height : `${props.height}px`,
width: typeof props.width == 'string' ? props.width : `${props.width}px`,
}
})
// 监听绑定值
watch(
() => props.modelValue,
(val: string) => {
if (val) {
if (val !== content.value) {
setContent(val)
}
} else {
setContent('')
}
}
)
onMounted(() => {
// 实例化
quill = new Quill(editorRef.value, {
theme: 'snow',
placeholder: '输入内容',
modules: {
toolbar: [
['bold', 'italic', 'underline', 'strike'],
['blockquote', 'code-block'],
[{ header: 1 }, { header: 2 }],
[{ list: 'ordered' }, { list: 'bullet' }],
[{ script: 'sub' }, { script: 'super' }],
[{ indent: '-1' }, { indent: '+1' }],
[{ direction: 'rtl' }],
[{ size: ['small', false, 'large', 'huge'] }],
[{ header: [1, 2, 3, 4, 5, 6, false] }],
[{ color: [] }, { background: [] }],
[{ font: [] }],
[{ align: [] }],
['clean'],
['link', 'image'],
],
},
...props.options,
})
// 添加图片工具
quill.getModule('toolbar').addHandler('image', uploadFileHandler)
// 监听输入
quill.on('text-change', () => {
content.value = quill.root.innerHTML
emit('update:modelValue', content.value)
})
setContent(props.modelValue)
// 加载回调
emit('load', quill)
})
</script>
<style lang="scss">
.cl-editor-quill {
background-color: #fff;
.ql-snow {
line-height: 22px !important;
}
#quill-upload-btn {
display: none;
}
.ql-snow {
border: 1px solid #dcdfe6;
}
.ql-snow .ql-tooltip[data-mode='link']::before {
content: '请输入链接地址:';
}
.ql-snow .ql-tooltip.ql-editing a.ql-action::after {
border-right: 0px;
content: '保存';
padding-right: 0px;
}
.ql-snow .ql-tooltip[data-mode='video']::before {
content: '请输入视频地址:';
}
.ql-snow .ql-picker.ql-size .ql-picker-label::before,
.ql-snow .ql-picker.ql-size .ql-picker-item::before {
content: '14px';
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='small']::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='small']::before {
content: '10px';
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='large']::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='large']::before {
content: '18px';
}
.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='huge']::before,
.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='huge']::before {
content: '32px';
}
.ql-snow .ql-picker.ql-header .ql-picker-label::before,
.ql-snow .ql-picker.ql-header .ql-picker-item::before {
content: '文本';
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='1']::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='1']::before {
content: '标题1';
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='2']::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='2']::before {
content: '标题2';
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='3']::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='3']::before {
content: '标题3';
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='4']::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='4']::before {
content: '标题4';
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='5']::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='5']::before {
content: '标题5';
}
.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='6']::before,
.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='6']::before {
content: '标题6';
}
.ql-snow .ql-picker.ql-font .ql-picker-label::before,
.ql-snow .ql-picker.ql-font .ql-picker-item::before {
content: '标准字体';
}
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value='serif']::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value='serif']::before {
content: '衬线字体';
}
.ql-snow .ql-picker.ql-font .ql-picker-label[data-value='monospace']::before,
.ql-snow .ql-picker.ql-font .ql-picker-item[data-value='monospace']::before {
content: '等宽字体';
}
}
</style>

View File

@@ -0,0 +1,124 @@
<template>
<div
class="tinymce-boxz"
:style="{
'max-width': width + 'px',
'max-height': height + 'px'
}"
>
<!-- tinymce-vue -->
<Editor v-model="content" :api-key="tiny.apiKey" :init="tiny.init" />
<!-- 文件管理器 -->
<material-select
:hiddenUpload="true"
:type="tinymce.type"
ref="materialRef"
@change="handleMaterialFile"
></material-select>
</div>
</template>
<script lang="ts" setup>
import Editor from "@tinymce/tinymce-vue";
import { reactive, ref, withDefaults, computed } from "vue";
import materialSelect from '@/components/material-select/index.vue'
/** Props Start **/
const props = withDefaults(defineProps<{
modelValue?: string
width?: string
height?: string
}>(), {
modelValue: '',
width: '1000',
height: '1000'
})
/** Props End **/
/** Emit Start **/
const emit = defineEmits(['update:modelValue'])
/** Emit End **/
/** Computed Start **/
let content = computed({
get: () => {
return props.modelValue || ''
},
set: (value) => {
emit('update:modelValue', value)
}
})
/** Computed End **/
/** Data Start **/
const materialRef = ref<InstanceType<typeof materialSelect> | null>(null)
const tinymce = ref<any>({
callback: null,//回调函数
type: 'image' //选择文件类型 /image图片/video视频
})
// 富文本基础配置
const tiny = reactive({
apiKey: "mejzqiqf65aswd278mtojz1w7g3zysvdhg3sjen77zf7f6e9",
init: {
language: "zh_CN", //语言类型
placeholder: "在这里输入文字", //textarea中的提示信息
min_width: props.width,
min_height: props.height,
height: props.height, //注引入autoresize插件时此属性失效
resize: "both", //编辑器宽高是否可变false-否,true-高可变,'both'-宽高均可,注意引号
branding: false, //tiny技术支持信息是否显
font_formats:
"微软雅黑=Microsoft YaHei,Helvetica Neue,PingFang SC,sans-serif;苹果苹方=PingFang SC,Microsoft YaHei,sans-serif;宋体=simsun,serif;仿宋体=FangSong,serif;黑体=SimHei,sans-serif;Arial=arial,helvetica,sans-serif;Arial Black=arial black,avant garde;Book Antiqua=book antiqua,palatino;", //字体样式
plugins:
"preview searchreplace autolink directionality visualblocks visualchars fullscreen image link media template code codesample table charmap hr pagebreak nonbreaking anchor insertdatetime advlist lists wordcount textpattern autosave", //插件配置 axupimgs indent2em
toolbar: [
"fullscreen undo redo restoredraft | cut copy paste pastetext | forecolor backcolor bold italic underline strikethrough link anchor | alignleft aligncenter alignright alignjustify outdent indent | bullist numlist | blockquote subscript superscript removeformat ",
"styleselect formatselect fontselect fontsizeselect | table image axupimgs media charmap hr pagebreak insertdatetime selectall visualblocks searchreplace | code print preview | indent2em lineheight formatpainter",
], //工具栏配置设为false则隐藏
paste_data_images: true, //图片是否可粘贴
file_picker_types: "file image media",
// 文件上传处理函数
file_picker_callback: (callback: any, value: any, meta: any) => {
if (meta.filetype == "image") {
tinymce.value.type = 'image';
} else if (meta.filetype == "media") {
tinymce.value.type = 'video';
} else {
tinymce.value.type = 'file';
}
// 打开资源管理器
materialRef.value.showPopup(1)
// 保存回调到全局
tinymce.value.callback = callback
}
}
})
/** Data End **/
/** Methods Start **/
// 确认选择文件时
const handleMaterialFile = (event: Event) => {
tinymce.value.callback(event);
materialRef.value.fileList = []
}
/** Methods End **/
</script>
<style scoped>
.tinymce-boxz > textarea {
display: none;
}
</style>
<style>
/* 隐藏apikey没有绑定当前域名的提示 */
.tox-notifications-container .tox-notification--warning {
display: none !important;
}
.tox.tox-tinymce {
max-width: 100%;
}
</style>

View File

@@ -0,0 +1,29 @@
<template>
<div class="footer-wrap">
<div class="footer-content">
<div class="flex flex-center">
<slot></slot>
</div>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({})
</script>
<style scoped lang="scss">
.footer-wrap {
height: 60px;
.footer-content {
position: fixed;
bottom: 0;
left: 0;
padding-left: $layout-aside-width;
height: 60px;
right: 0;
z-index: 99;
}
}
</style>

View File

@@ -0,0 +1,57 @@
<template>
<div>
<del-wrap @close="$emit('close')">
<div class="file-item" :style="{ height: fileSize, width: fileSize }">
<el-image v-if="type == 'image'" class="image" fit="contain" :src="uri"></el-image>
<video v-else-if="type == 'video'" class="video" :src="uri"></video>
<slot></slot>
</div>
</del-wrap>
</div>
</template>
<script lang="ts">
import DelWrap from '@/components/del-wrap/index.vue'
import { defineComponent, inject } from 'vue'
export default defineComponent({
components: {
DelWrap
},
props: {
// 图片地址
uri: {
type: String
},
// 图片尺寸
fileSize: {
type: String,
default: '100px'
}
},
emits: ['close'],
setup() {
const type = inject('type')
return {
type
}
}
})
</script>
<style scoped lang="scss">
.file-item {
background-color: $border-color-light;
border: 1px solid $border-color-light;
box-sizing: border-box;
position: relative;
border-radius: 4px;
overflow: hidden;
.image,
.video {
display: block;
box-sizing: border-box;
width: 100%;
height: 100%;
}
}
</style>

View File

@@ -0,0 +1,162 @@
import {
apiFileCateAdd,
apiFileCateDelete,
apiFileCateEdit,
apiFileCateLists,
apiFileDelete,
apiFileList,
apiFileMove
} from '@/api/app'
import { usePages } from '@/core/hooks/pages'
import { ElMessage } from 'element-plus'
import { computed, inject, reactive, ref, Ref } from 'vue'
// 左侧分组的钩子函数
export function useCate(typeValue: Ref<any>) {
// 分组列表
const cateLists: Ref<any[]> = ref([])
// 选中的分组id
const cateId = ref('')
// 添加分组
const handleAddCate = (val: string) => {
apiFileCateAdd({
type: typeValue.value,
pid: 0,
name: val
}).then(() => {
getCateLists()
})
}
// 编辑分组
const handleEditCate = (val: string, id: number) => {
apiFileCateEdit({
id,
name: val
}).then(() => {
getCateLists()
})
}
// 删除分组
const handleDeleteCate = (id: number) => {
apiFileCateDelete({
id
}).then(() => {
getCateLists()
})
}
// 获取分组列表
const getCateLists = () => {
return new Promise((resolve, reject) => {
apiFileCateLists({
type: typeValue.value,
page_type: 1
}).then((res: any) => {
const item: any[] = [
{
name: '全部',
id: ''
},
{
name: '未分组',
id: 0
}
]
cateLists.value = res?.lists
cateLists.value.unshift(...item)
resolve(cateLists)
})
})
}
return {
cateId,
cateLists,
handleAddCate,
handleEditCate,
handleDeleteCate,
getCateLists
}
}
// 处理文件的钩子函数
export function useFile(cateId: Ref<string>, type: Ref<any>, limit: Ref<number>) {
const moveId = ref(0)
const select: Ref<any[]> = ref([])
const fileParams = reactive({
name: '',
type: type,
cid: cateId
})
const { pager, requestApi, resetPage } = usePages({
callback: apiFileList,
params: fileParams
})
const selectStatus = computed(
() => (id: number) => select.value.find((item: any) => item.id == id)
)
const getFileList = () => {
requestApi()
}
const refresh = () => {
resetPage()
}
const batchFileDelete = (id?: number[]) => {
const ids = id ? id : select.value.map((item: any) => item.id)
apiFileDelete({
ids
}).then(res => {
getFileList()
clearSelect()
})
}
const batchFileMove = () => {
const ids = select.value.map((item: any) => item.id)
apiFileMove({
ids,
cid: moveId.value
}).then(res => {
moveId.value = 0
getFileList()
clearSelect()
})
}
const selectFile = (item: any) => {
const index = select.value.findIndex((items: any) => items.id == item.id)
if (index != -1) {
select.value.splice(index, 1)
return
}
if (select.value.length == limit.value) {
if (limit.value == 1) {
select.value = []
select.value.push(item)
return
}
ElMessage.warning('已达到选择上限')
return
}
select.value.push(item)
}
const clearSelect = () => {
select.value = []
}
const cancelSelete = (id: number) => {
select.value = select.value.filter(item => item.id != id)
}
return {
moveId,
pager,
fileParams,
select,
getFileList,
refresh,
batchFileDelete,
batchFileMove,
selectFile,
selectStatus,
clearSelect,
cancelSelete
}
}

View File

@@ -0,0 +1,255 @@
<template>
<div class="material-select">
<popup
ref="popupRef"
width="950px"
custom-class="body-padding"
:title="`选择${tipsText}`"
@confirm="handleConfirm"
>
<template #trigger>
<div class="material-select__trigger clearfix" @click.stop>
<draggable v-model="fileList" class="draggable" animation="300" item-key="id">
<template #item="{ element, index }">
<div
class="material-preview"
:class="{
'is-disabled': disabled,
'is-one': limit == 1
}"
@click="showPopup(index)"
>
<file-item
:uri="element"
:file-size="size"
@close="deleteImg(index)"
/>
</div>
</template>
</draggable>
<div
v-show="showUpload"
class="material-upload"
:class="{
'is-disabled': disabled,
'is-one': limit == 1
}"
@click="showPopup(-1)"
>
<slot name="upload">
<div
class="upload-btn flex flex-col flex-center"
:style="{
width: size,
height: size
}"
>
<el-icon :size="25"><plus /></el-icon>
<span>添加</span>
</div>
</slot>
</div>
</div>
</template>
<div class="material-wrap">
<material
ref="materialRefs"
:file-size="fileSize"
:limit="meterialLimit"
@change="selectChange"
/>
</div>
</popup>
</div>
</template>
<script lang="ts">
import {
provide,
reactive,
defineComponent,
computed,
ref,
Ref,
toRef,
toRefs,
watch,
nextTick
} from 'vue'
import Draggable from 'vuedraggable'
import Popup from '@/components/popup/index.vue'
import FileItem from './file-item.vue'
import Material from './material.vue'
export default defineComponent({
components: {
Popup,
Draggable,
FileItem,
Material
},
props: {
modelValue: {
type: [String, Array],
default: () => []
},
// 文件类型
type: {
type: String,
default: 'image'
},
// 选择器尺寸
size: {
type: String,
default: '100px'
},
// 文件尺寸
fileSize: {
type: String,
default: '100px'
},
// 选择数量限制
limit: {
type: Number,
default: 1
},
// 禁用选择
disabled: {
type: Boolean,
default: false
}
},
emits: ['change', 'update:modelValue'],
setup(props, { emit }) {
const popupRef: Ref<typeof Popup | null> = ref(null)
const materialRefs: Ref<typeof Material | null> = ref(null)
const fileList: Ref<any[]> = ref([])
const select: Ref<any[]> = ref([])
const isAdd = ref(true)
const currentIndex = ref(-1)
const { disabled, limit, modelValue } = toRefs(props)
const tipsText = computed(() => {
switch (props.type) {
case 'image':
return '图片'
case 'video':
return '视频'
}
})
const typeValue = computed(() => {
switch (props.type) {
case 'image':
return 10
case 'video':
return 20
case 'file':
return 30
}
})
const showUpload = computed(() => {
return props.limit - fileList.value.length > 0
})
const meterialLimit = computed(() => {
if (!isAdd.value) {
return 1
}
if (!limit.value) {
return null
}
return limit.value - fileList.value.length
})
const handleConfirm = () => {
const selectUri = select.value.map(item => item.uri)
if (!isAdd.value) {
fileList.value.splice(currentIndex.value, 1, selectUri.shift())
} else {
fileList.value = [...fileList.value, ...selectUri]
}
handleChange()
}
const showPopup = (index: number) => {
if (disabled.value) {
return
}
if (index >= 0) {
isAdd.value = false
currentIndex.value = index
} else {
isAdd.value = true
}
popupRef.value?.open()
}
const selectChange = (val: any[]) => {
select.value = val
}
const handleChange = () => {
const valueImg = limit.value != 1 ? fileList.value : fileList.value[0] || ''
emit('update:modelValue', valueImg)
emit('change', valueImg)
nextTick(() => {
materialRefs.value?.clearSelect()
})
}
const deleteImg = (index: number) => {
fileList.value.splice(index, 1)
handleChange()
}
watch(modelValue, (val: any[] | string) => {
console.log(val)
fileList.value = Array.isArray(val) ? val : val == '' ? [] : [val]
})
provide('type', props.type)
provide('fileSize', props.fileSize)
provide('limit', props.limit)
provide('typeValue', typeValue)
return {
popupRef,
materialRefs,
fileList,
tipsText,
handleConfirm,
meterialLimit,
showUpload,
showPopup,
selectChange,
deleteImg
}
}
})
</script>
<style scoped lang="scss">
.material-select {
.material-upload,
.material-preview {
border-radius: 4px;
cursor: pointer;
color: $color-text-secondary;
margin-right: 8px;
margin-bottom: 8px;
box-sizing: border-box;
float: left;
&.is-disabled {
cursor: not-allowed;
}
&.is-one {
margin-bottom: 0;
}
}
.material-upload {
.upload-btn {
box-sizing: border-box;
border-radius: 4px;
border: 1px dashed #d7d7d7;
}
}
}
.material-wrap {
height: 540px;
border-top: 1px solid $border-color-base;
border-bottom: 1px solid $border-color-base;
}
</style>

View File

@@ -0,0 +1,347 @@
<template>
<div v-loading="pager.loading" class="material flex col-stretch">
<div class="material__left">
<el-scrollbar class="ls-scrollbar" style="height: calc(100% - 40px)">
<div class="material-left__content p-t-16 p-b-16">
<el-tree
ref="treeRefs"
node-key="id"
:data="cateLists"
empty-text
:highlight-current="true"
:expand-on-click-node="false"
icon-class="el-icon-arrow-right"
:current-node-key="cateId"
@node-click="currentChange"
>
<template #default="{ data }">
<div class="flex flex-1 flex-center" style="min-width: 0">
<img
style="width: 20px; height: 16px"
src="@/assets/images/icon_folder.png"
alt
class="m-r-10"
/>
<span class="flex-1 line-1 m-r-10">
{{ data.name }}
</span>
<el-dropdown v-if="data.id > 0" :hide-on-click="false">
<span class="muted m-r-10">···</span>
<template #dropdown>
<el-dropdown-menu>
<div>
<popover-input
type="text"
tips="分类名称"
@confirm="handleEditCate($event, data.id)"
>
<el-dropdown-item>命名分组</el-dropdown-item>
</popover-input>
</div>
<div @click="handleDeleteCate(data.id)">
<el-dropdown-item>删除分组</el-dropdown-item>
</div>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</template>
</el-tree>
</div>
</el-scrollbar>
<div class="flex flex-center">
<popover-input tips="分类名称" type="text" @confirm="handleAddCate">
<el-button size="small">添加分组</el-button>
</popover-input>
</div>
</div>
<div class="material__center flex flex-col">
<div class="operate-btn flex">
<div class="flex-1 flex">
<upload
class="m-r-10"
:data="{ cid: cateId }"
:type="type"
:show-progress="true"
@change="refresh"
>
<el-button size="small" type="primary">本地上传</el-button>
</upload>
<popup
class="m-r-10 inline"
content="确定删除选中的文件?"
:disabled="!select.length"
@confirm="batchFileDelete()"
>
<template #trigger>
<el-button size="small" :disabled="!select.length">删除</el-button>
</template>
</popup>
<popup
class="m-r-10 inline"
:disabled="!select.length"
title="移动文件"
@confirm="batchFileMove"
>
<template #trigger>
<el-button size="small" :disabled="!select.length">移动</el-button>
</template>
<div>
<span class="m-r-20">移动文件至</span>
<el-select v-model="moveId" placeholder="请选择">
<template v-for="item in cateLists" :key="item.id">
<el-option
v-if="item.id !== ''"
:label="item.name"
:value="item.id"
></el-option>
</template>
</el-select>
</div>
</popup>
</div>
<el-input
v-model="fileParams.name"
size="small"
placeholder="请输入名字"
style="width: 280px"
@keyup.enter="refresh"
>
<template #append>
<el-button :icon="Search" @click="refresh"></el-button>
</template>
</el-input>
</div>
<div class="material-center__content flex flex-col flex-1">
<ul class="file-list flex flex-wrap m-t-14">
<li
v-for="item in pager.lists"
:key="item.id"
class="file-item-wrap"
:style="{ width: fileSize }"
@click="selectFile(item)"
>
<file-item
:uri="item.uri"
:file-size="fileSize"
@close="batchFileDelete([item.id])"
>
<div v-if="selectStatus(item.id)" class="item-selected">
<el-icon color="#fff" size="24">
<check />
</el-icon>
</div>
</file-item>
<div class="item-name line-1 xs p-t-10">{{ item.name }}</div>
</li>
</ul>
<div
v-if="!pager.loading && !pager.lists.length"
class="flex flex-1 row-center col-center"
>
暂无数据~
</div>
</div>
<div class="material-center__footer flex row-right">
<pagination
v-model="pager"
layout="total, prev, pager, next, jumper"
@change="getFileList"
/>
</div>
</div>
<div class="material__right">
<div class="flex row-between p-l-10 p-r-10">
<div class="sm flex flex-center">
已选择 {{ select.length }}
<span v-if="limit">/{{ limit }}</span>
</div>
<el-button type="text" size="small" @click="clearSelect">清空</el-button>
</div>
<el-scrollbar class="ls-scrollbar" style="height: calc(100% - 32px)">
<ul class="select-lists flex-col p-t-10">
<li v-for="item in select" :key="item.id" class="m-b-16">
<div class="select-item">
<file-item
:uri="item.uri"
file-size="100px"
@close="cancelSelete(item.id)"
></file-item>
</div>
</li>
</ul>
</el-scrollbar>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, inject, Ref, ref, toRefs, watch } from 'vue'
import { useCate, useFile } from './hook'
import PopoverInput from '@/components/popover-input/index.vue'
import Pagination from '@/components/pagination/index.vue'
import Popup from '@/components/popup/index.vue'
import Upload from '@/components/upload/index.vue'
import FileItem from './file-item.vue'
import { Search } from '@element-plus/icons-vue'
import { ElTree } from 'element-plus'
export default defineComponent({
components: {
PopoverInput,
Pagination,
Popup,
Upload,
FileItem
},
props: {
fileSize: {
type: String,
default: '100px'
},
limit: {
type: Number,
default: 1
}
},
emits: ['change'],
setup(props, { emit }) {
const treeRefs: Ref<typeof ElTree | null> = ref(null)
const type = inject('type') as Ref<string>
const { limit } = toRefs(props)
const typeValue = inject('typeValue') as Ref<10 | 20 | 30>
const visible = inject('visible') as Ref<boolean>
const { cateId, cateLists, handleAddCate, handleEditCate, handleDeleteCate, getCateLists } =
useCate(typeValue)
const {
moveId,
pager,
fileParams,
select,
getFileList,
refresh,
batchFileDelete,
batchFileMove,
selectFile,
selectStatus,
clearSelect,
cancelSelete
} = useFile(cateId, typeValue, limit)
const currentChange = (item: any) => {
cateId.value = item.id
}
watch(
visible,
async (val: boolean) => {
if (val) {
await getCateLists()
treeRefs.value?.setCurrentKey(cateId.value)
getFileList()
}
},
{
immediate: true
}
)
watch(cateId, (val: string) => {
fileParams.name = ''
refresh()
})
watch(
select,
(val: any[]) => {
emit('change', val)
},
{
deep: true
}
)
return {
treeRefs,
Search,
type,
limit,
cateId,
cateLists,
handleAddCate,
handleEditCate,
handleDeleteCate,
currentChange,
moveId,
pager,
fileParams,
select,
getFileList,
refresh,
batchFileDelete,
batchFileMove,
selectFile,
selectStatus,
clearSelect,
cancelSelete
}
}
})
</script>
<style scoped lang="scss">
.material {
height: 100%;
flex: 1;
&__left {
width: 170px;
:deep(.el-tree-node__content) {
height: 40px;
}
}
&__center {
flex: 1;
border-left: 1px solid $border-color-base;
padding: 16px;
.file-list {
.file-item-wrap {
margin-right: 16px;
margin-bottom: 16px;
line-height: 1.3;
cursor: pointer;
.item-selected {
display: flex;
align-items: center;
justify-content: center;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border-radius: 4px;
background-color: rgba(0, 0, 0, 0.5);
box-sizing: border-box;
}
.operation-btns {
height: 28px;
visibility: hidden;
}
&:hover .operation-btns {
visibility: visible;
}
}
}
}
&__right {
border-left: 1px solid $border-color-base;
width: 150px;
.select-lists {
padding: 10px;
.select-item {
width: 100px;
height: 100px;
}
}
}
}
</style>

View File

@@ -0,0 +1,64 @@
<template>
<div class="pagination">
<el-pagination
v-model:currentPage="modelValue.page"
v-model:pageSize="modelValue.size"
:page-sizes="pageSizes"
:layout="layout"
:total="modelValue.count"
hide-on-single-page
@size-change="sizeChange"
@current-change="pageChange"
>
</el-pagination>
</div>
</template>
<script lang="ts">
import { defineComponent, onMounted, reactive, toRefs } from 'vue'
export default defineComponent({
components: {},
props: {
// 每一页条数
modelValue: {
type: Object,
default: () => ({})
},
// 允许选择的每一页条数
pageSizes: {
type: Array,
default: () => [10, 20, 30, 40]
},
// 分页的布局参考element的分页组件
layout: {
type: String,
default: 'total, sizes, prev, pager, next, jumper'
}
},
emits: ['change'],
setup(props, { emit }) {
const sizeChange = () => {
props.modelValue.page = 1
emit('change')
}
const pageChange = () => {
emit('change')
}
return {
sizeChange,
pageChange
}
}
})
</script>
<style lang="scss">
.pagination {
height: 100%;
.pagination-footer {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
}
</style>

View File

@@ -0,0 +1,78 @@
<template>
<div class="popover-input">
<el-popover v-model:visible="visible" placement="top" :width="width" trigger="manual">
<div class="flex">
<div class="popover-input__input m-r-10 flex-1">
<el-input
v-model="value"
:type="type"
size="mini"
:placeholder="placeholder"
></el-input>
</div>
<div class="popover-input__btns flex-none">
<el-button type="text" size="mini" @click="close">取消</el-button>
<el-button type="primary" size="mini" @click="handleConfirm">确定</el-button>
</div>
</div>
<template #reference>
<div class="inline" type="text" @click="open">
<slot></slot>
</div>
</template>
</el-popover>
</div>
</template>
<script lang="ts">
import { defineComponent, ref } from 'vue'
export default defineComponent({
props: {
type: {
type: String,
default: 'number'
},
width: {
type: Number,
default: 250
},
placeholder: {
type: String,
default: ''
},
disabled: {
type: Boolean,
default: false
}
},
emits: ['confirm'],
setup(props, { emit }) {
const visible = ref(false)
const value = ref('')
const open = () => {
if (props.disabled) {
return
}
visible.value = true
}
const close = () => {
visible.value = false
}
const handleConfirm = () => {
if (value.value) {
emit('confirm', value.value)
value.value = ''
}
close()
}
return {
visible,
value,
open,
close,
handleConfirm
}
}
})
</script>

View File

@@ -0,0 +1,136 @@
<template>
<div class="dialog">
<div class="dialog__trigger" @click="open">
<!-- 触发弹窗 -->
<slot name="trigger"></slot>
</div>
<el-dialog
v-model="visible"
:custom-class="customClass"
:append-to-body="true"
:width="width"
:close-on-click-modal="clickModalClose"
>
<!-- 弹窗内容 -->
<template v-if="title" #title>
{{ title }}
</template>
<template v-else #title>
<div class="flex col-center">
<el-icon :size="25" :color="$variables.color_warning"
><warning-filled
/></el-icon>
<span class="m-l-6">温馨提示</span>
</div>
</template>
<!-- 自定义内容 -->
<slot>
{{ content }}
</slot>
<!-- 底部弹窗页脚 -->
<template #footer>
<div class="dialog-footer">
<el-button v-if="cancelButtonText" size="small" @click="handleEvent('cancel')">
{{ cancelButtonText }}
</el-button>
<el-button
v-if="confirmButtonText"
size="small"
type="primary"
@click="handleEvent('confirm')"
>
{{ confirmButtonText }}
</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script lang="ts">
import { defineComponent, provide, ref } from 'vue'
export default defineComponent({
components: {},
props: {
title: {
// 弹窗标题
type: String,
default: ''
},
content: {
// 弹窗内容
type: String,
default: '确认要删除?'
},
confirmButtonText: {
// 确认按钮内容
type: [String, Boolean],
default: '确认'
},
cancelButtonText: {
// 取消按钮内容
type: [String, Boolean],
default: '取消'
},
width: {
// 弹窗的宽度
type: String,
default: '400px'
},
disabled: {
// 是否禁用
type: Boolean,
default: false
},
async: {
// 是否开启异步关闭
type: Boolean,
default: false
},
clickModalClose: {
// 点击遮罩层关闭对话窗口
type: Boolean,
default: true
},
customClass: {
type: String,
default: ''
}
},
emits: ['confirm', 'cancel'],
setup(props, { emit }) {
const visible = ref(false)
const handleEvent = (type: 'confirm' | 'cancel') => {
emit(type)
if (!props.async || type === 'cancel') {
close()
}
}
const close = () => {
visible.value = false
}
const open = () => {
if (props.disabled) {
return
}
visible.value = true
}
provide('visible', visible)
return {
visible,
handleEvent,
close,
open
}
}
})
</script>
<style scoped lang="scss">
.dialog-body {
white-space: pre-line;
}
</style>

View File

@@ -0,0 +1,131 @@
<template>
<div class="upload">
<el-upload
ref="uploadRefs"
:action="action"
:multiple="multiple"
:limit="limit"
:show-file-list="false"
:headers="headers"
:data="data"
:on-progress="handleProgress"
:on-success="handleSuccess"
:on-exceed="handleExceed"
:on-error="handleError"
>
<slot></slot>
</el-upload>
<el-dialog
v-if="showProgress && fileList.length"
v-model="visible"
title="上传进度"
:close-on-click-modal="false"
width="500px"
:modal="false"
:before-close="handleClose"
>
<div class="file-list">
<template v-for="(item, index) in fileList" :key="index">
<div class="m-b-20">
<div>{{ item.name }}</div>
<div class="flex-1">
<el-progress :percentage="parseInt(item.percentage)"></el-progress>
</div>
</div>
</template>
</div>
</el-dialog>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, Ref, ref } from 'vue'
import { ElMessage, ElUpload } from 'element-plus'
import { useStore } from '@/store'
import { version } from '@/config/app'
export default defineComponent({
components: {},
props: {
// 上传文件类型
type: {
type: String,
default: 'image'
},
// 是否支持多选
multiple: {
type: Boolean,
default: true
},
// 多选时最多选择几条
limit: {
type: Number,
default: 10
},
// 上传时的额外参数
data: {
type: Object,
default: () => ({})
},
// 是否显示上传进度
showProgress: {
type: Boolean,
default: false
}
},
emits: ['change', 'error'],
setup(props, { emit }) {
const store = useStore()
const uploadRefs: Ref<typeof ElUpload | null> = ref(null)
const action = ref(`${import.meta.env.VITE_APP_BASE_URL}/adminapi/upload/${props.type}`)
const headers = computed(() => ({
token: store.getters.token,
version: version
}))
const visible = ref(false)
const fileList: Ref<any[]> = ref([])
const handleProgress = (event: any, file: any, fileLists: any[]) => {
visible.value = true
fileList.value = fileLists
}
const handleSuccess = (event: any, file: any, fileLists: any[]) => {
const allSuccess = fileLists.every(item => item.status == 'success')
if (allSuccess) {
uploadRefs.value?.clearFiles()
visible.value = false
emit('change')
}
}
const handleError = (event: any, file: any, fileLists: any[]) => {
ElMessage.error(`${file.name}文件上传失败`)
uploadRefs.value?.abort()
visible.value = false
emit('change')
emit('error')
}
const handleExceed = () => {
ElMessage.error('超出上传上限,请重新上传')
}
const handleClose = () => {
uploadRefs.value?.abort()
uploadRefs.value?.clearFiles()
visible.value = false
}
return {
uploadRefs,
action,
headers,
visible,
fileList,
handleProgress,
handleSuccess,
handleError,
handleExceed,
handleClose
}
}
})
</script>
<style lang="scss"></style>

7
admin/src/config/app.ts Normal file
View File

@@ -0,0 +1,7 @@
// 标题
export const title = 'likeadmin'
// terminal
export const terminal = 1
// 版本
export const version = '1.0.0'

View File

@@ -0,0 +1,3 @@
export const TOKEN = 'token'
// 缓存key前缀
export const ACCOUNT = 'account'

View File

@@ -0,0 +1,9 @@
import { App } from 'vue'
const modules = import.meta.globEager('./modules/*.ts')
export default (app: App<Element>) => {
Object.keys(modules).forEach(key => {
const name = key.replace(/^\.\/(.*)\.\w+$/, '$1')
app.directive(name, modules[key].default)
})
}

View File

@@ -0,0 +1,32 @@
/**
* copy 复制指令(用于复制文本)
* 指令用法:
* <el-button v-copy="copyValue">复制</el-button>
* copyValue为需要复制的值
*/
import { ElMessage } from 'element-plus'
import Clipboard from 'clipboard'
;(function copyboard() {
const clipboard = new Clipboard('.copy-btn')
clipboard.on('success', e => {
ElMessage.success('复制成功')
e.clearSelection()
})
clipboard.on('error', err => {
console.error(err)
ElMessage.success('复制失败')
})
})()
export default {
mounted: (el: HTMLElement, binding: any) => {
el.className = el.className + ' copy-btn'
el.setAttribute('data-clipboard-text', binding.value)
},
updated: (el: HTMLElement, binding: any) => {
el.setAttribute('data-clipboard-text', binding.value)
}
}

View File

@@ -0,0 +1,14 @@
import { useStore } from '@/store'
import { useRoute, useRouter } from 'vue-router'
export function useAdmin() {
const store = useStore()
const route = useRoute()
const router = useRouter()
return {
store,
route,
router
}
}

View File

@@ -0,0 +1,68 @@
import { deepClone } from '@/utils/util'
import { reactive, toRaw } from 'vue'
// 分页钩子函数
interface Options {
page?: number
size?: number
callback: (_arg: any) => Promise<any>
params?: Record<any, any>
}
let paramsInit: Record<any, any> = {}
export function usePages(options: Options) {
const { page = 1, size = 15, callback, params = {} } = options
// 记录分页初始参数
paramsInit = Object.assign({}, toRaw(params))
// 分页数据
const pager = reactive({
page,
size,
loading: false,
count: 0,
lists: [] as any[]
})
// 请求分页接口
const requestApi = () => {
// 禁止并发请求
if (pager.loading) {
return Promise.reject()
}
pager.loading = true
return callback({
page_no: pager.page,
page_size: pager.size,
...params
})
.then((res: any) => {
pager.count = res?.count
pager.lists = res?.lists
return Promise.resolve(res)
})
.catch((err: any) => {
return Promise.reject(err)
})
.finally(() => {
pager.loading = false
})
}
// 重置为第一页
const resetPage = () => {
pager.page = 1
requestApi()
}
// 重置参数
const resetParams = () => {
Object.keys(paramsInit).forEach(item => {
params[item] = paramsInit[item]
})
requestApi()
}
return {
pager,
requestApi,
resetParams,
resetPage
}
}

19
admin/src/env.d.ts vendored Normal file
View File

@@ -0,0 +1,19 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import { DefineComponent } from 'vue'
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
const component: DefineComponent<{}, {}, any>
export default component
}
declare module 'vuedraggable/src/vuedraggable' {
const d: any
export default d
}
declare module 'nprogress' {
export function configure(options: any): void
export function start(): void
export function done(): void
}

View File

@@ -0,0 +1,89 @@
<template>
<div class="layout-aside">
<router-link to="/workbench" class="logo flex col-center">
<img class="logo-img" :src="config.web_logo" alt />
<div class="line-1">{{ config.web_name }}</div>
</router-link>
<div class="scrollbar-wrap">
<el-scrollbar style="height: 100%" class="ls-scrollbar">
<el-menu
active-text-color="#fff"
background-color="#2a2c41"
:default-active="currentPath"
text-color="#E5E5E5"
router
>
<template v-for="(item, index) in sidebar" :key="index">
<sub-menu :route="item">
<template v-for="(item, index) in item?.children" :key="index">
<sub-menu :route="item">
<template v-for="(item, index) in item?.children" :key="index">
<sub-menu :route="item"></sub-menu>
</template>
</sub-menu>
</template>
</sub-menu>
</template>
</el-menu>
</el-scrollbar>
</div>
</div>
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue'
import { useAdmin } from '@/core/hooks/app'
import SubMenu from './sub-menu.vue'
export default defineComponent({
components: {
SubMenu
},
setup() {
const { store, route } = useAdmin()
const sidebar = computed(() => store.state.permission.sidebar)
const currentPath = computed(() => route.meta?.parent ?? route.path)
const config = computed(() => store.getters.config)
return {
config,
sidebar,
currentPath
}
}
})
</script>
<style lang="scss" scoped>
.layout-aside {
height: 100%;
width: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
background-color: #2a2c41;
.logo {
height: $layout-header-height;
font-weight: 500;
font-size: 18px;
color: #fff;
padding: 0 20px;
.logo-img {
width: 30px;
height: 30px;
margin-right: 10px;
}
}
.scrollbar-wrap {
flex: 1;
min-height: 0;
.el-menu {
box-sizing: border-box;
padding: 10px 0 20px;
:deep(.el-menu-item) {
&.is-active {
background-color: $color-primary;
}
}
}
}
}
</style>

View File

@@ -0,0 +1,38 @@
<template>
<template v-if="!route.meta.hidden">
<el-sub-menu v-if="hasChildren" :index="route.path">
<template #title>
<i class="iconfont m-r-10" :class="route.meta.icon"></i>
<span>{{ route.meta.title }}</span>
</template>
<slot></slot>
</el-sub-menu>
<el-menu-item v-else :index="route.path">
<i class="iconfont m-r-10" :class="route.meta.icon"></i>
<span>{{ route.meta.title }}</span>
</el-menu-item>
</template>
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue'
import { RouteRecordRaw } from 'vue-router'
export default defineComponent({
components: {},
props: {
route: {
type: Object,
default: () => ({})
}
},
setup(props) {
const hasChildren = computed(() => {
const children: RouteRecordRaw[] = props.route.children ?? []
return !!children.filter(item => !item.meta?.hidden).length
})
return {
hasChildren
}
}
})
</script>

View File

@@ -0,0 +1,67 @@
<template>
<div class="layout-header">
<!-- <input class="search-input" placeholder="请输入搜索内容…" type="text" /> -->
<div class="admin-info flex flex-center m-l-40">
<el-avatar :size="40" :src="userInfo.avatar"></el-avatar>
<div class="m-l-10">
<el-dropdown trigger="hover" @command="handleCommand">
<div class="flex flex-center">
{{ userInfo.name }}
<el-icon class="el-icon--right"><arrow-down /></el-icon>
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="logout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</div>
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue'
import { useAdmin } from '@/core/hooks/app'
export default defineComponent({
setup() {
const { store, router } = useAdmin()
const userInfo = computed(() => store.getters.userInfo)
const handleCommand = (command: string) => {
switch (command) {
case 'logout':
store.dispatch('user/logout').then(() => {
router.push('/login')
store.commit('permission/setPermission', {
auth: null,
root: 0
})
})
}
}
return {
userInfo,
handleCommand
}
}
})
</script>
<style lang="scss" scoped>
.layout-header {
display: flex;
align-items: center;
justify-content: flex-end;
flex: none;
height: $layout-header-height;
background: #fff;
padding: 0 24px;
.search-input {
width: 460px;
height: 40px;
border-radius: 20px;
background: #f6f6f6;
padding: 0 20px;
}
}
</style>

View File

@@ -0,0 +1,26 @@
<template>
<div class="layout-main">
<el-scrollbar>
<div class="p-15">
<perm />
</div>
</el-scrollbar>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import Perm from './perm.vue'
export default defineComponent({
components: {
Perm
}
})
</script>
<style lang="scss" scoped>
.layout-main {
flex: 1;
min-height: 0;
}
</style>

View File

@@ -0,0 +1,57 @@
<template>
<div v-if="permission" class="perm">
<template v-if="hasPermission">
<router-view></router-view>
</template>
<template v-else>
<div class="no-perm flex flex-col flex-center">
<img src="@/assets/images/no_perm.png" />
<div class="muted">暂无查看权限</div>
</div>
</template>
</div>
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue'
import { useAdmin } from '@/core/hooks/app'
export default defineComponent({
components: {},
props: {},
setup(props) {
const { store, route } = useAdmin()
const permission = computed(() => store.getters.permission)
const isAdmin = computed(() => store.getters.isAdmin)
const hasPermission = computed(() => {
const { path, meta } = route
if (isAdmin.value) {
return true
}
const actions = permission.value[path]
console.log(permission.value, path)
if (!actions || !meta?.permission) {
return true
}
return actions.some((item: string) => {
return (meta?.permission as string[]).includes(item)
})
})
return {
permission,
hasPermission
}
}
})
</script>
<style scoped lang="scss">
.perm {
.no-perm {
height: calc(100vh - #{$layout-header-height} - 32px);
img {
width: 152px;
height: 152px;
}
}
}
</style>

View File

@@ -0,0 +1,44 @@
<template>
<div class="layout">
<div class="aside">
<layout-aside />
</div>
<div class="main">
<layout-header />
<layout-main />
</div>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
import LayoutAside from './components/layout-aside/index.vue'
import LayoutMain from './components/layout-main.vue'
import LayoutHeader from './components/layout-header.vue'
export default defineComponent({
components: {
LayoutAside,
LayoutMain,
LayoutHeader
}
})
</script>
<style lang="scss">
.layout {
display: flex;
height: 100vh;
width: 100vw;
min-width: $layout-min-width;
.aside {
flex: none;
width: $layout-aside-width;
}
.main {
display: flex;
flex-direction: column;
flex: 1;
min-width: 0;
}
}
</style>

26
admin/src/main.ts Normal file
View File

@@ -0,0 +1,26 @@
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store, { injectionKey } from './store'
import './permission'
import useElement from './plugins/element'
import useVueEcharts from './plugins/vue-echarts'
import vars, { Variables } from './styles/export.module.scss'
import useDirectives from './core/directives'
const app = createApp(App)
app.config.globalProperties.$variables = vars
// element
useElement(app)
// vue-echarts
useVueEcharts(app)
// 添加自定义指令
useDirectives(app)
app.use(router).use(store, injectionKey).mount('#app')
// 声明vue上的属性
declare module '@vue/runtime-core' {
interface ComponentCustomProperties {
$variables: Variables
}
}

45
admin/src/permission.ts Normal file
View File

@@ -0,0 +1,45 @@
/**
* 权限控制
*/
import NProgress from 'nprogress'
import store from './store'
import router, { asyncRoutes } from './router'
import 'nprogress/nprogress.css'
// NProgress配置
NProgress.configure({ showSpinner: false })
const loginPath = '/login'
const defaultPath = '/'
// 免登录白名单
const whiteList = ['/login']
router.beforeEach(async (to, from, next) => {
NProgress.start()
// 开始 Progress Bar
to.meta?.title && (document.title = to.meta.title as string)
const token = store.getters.token
if (token) {
// 获取用户信息
if (store.getters.permission == null) {
store.commit('permission/setSidebar', asyncRoutes[0].children)
await store.dispatch('user/getUser')
await store.dispatch('permission/getPermission')
}
if (to.path === loginPath) {
next({ path: defaultPath })
} else {
next()
}
} else if (whiteList.includes(to.path as string)) {
// 在免登录白名单,直接进入
next()
} else {
next({ path: loginPath, query: { redirect: to.fullPath } })
}
})
router.afterEach(async (to, from) => {
NProgress.done()
})

View File

@@ -0,0 +1,11 @@
import { App } from '@vue/runtime-core'
import ElementPlus from 'element-plus'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import * as ElIcons from '@element-plus/icons-vue'
export default (app: App<Element>) => {
app.use(ElementPlus, { zIndex: 3000, locale: zhCn })
// 统一注册Icon图标
Object.keys(ElIcons).forEach(item => {
app.component(item, ElIcons[item as keyof typeof ElIcons])
})
}

View File

@@ -0,0 +1,28 @@
import ECharts from 'vue-echarts'
import { use } from 'echarts/core'
import { App } from '@vue/runtime-core'
// 手动引入 ECharts 各模块来减小打包体积
import { CanvasRenderer } from 'echarts/renderers'
import { BarChart, PieChart, LineChart } from 'echarts/charts'
import {
GridComponent,
TooltipComponent,
TitleComponent,
LegendComponent
} from 'echarts/components'
use([
CanvasRenderer,
BarChart,
PieChart,
GridComponent,
TooltipComponent,
TitleComponent,
LegendComponent,
LineChart
])
export default (app: App) => {
app.component('VChart', ECharts)
}

55
admin/src/router/index.ts Normal file
View File

@@ -0,0 +1,55 @@
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import workbench from '@/views/workbench/index.vue'
import Layout from '@/layout/index.vue'
import Error404 from '@/views/error/404.vue'
import Error500 from '@/views/error/500.vue'
// Router modules
import setting from './modules/setting'
import permission from './modules/permission'
import decoration from './modules/decoration'
import content from './modules/content'
import channel from './modules/channel'
import application from './modules/application'
export const asyncRoutes: Array<RouteRecordRaw> = [
{
path: '/',
redirect: 'workbench',
component: Layout,
children: [
{
path: '/workbench',
component: workbench,
meta: { title: '工作台', icon: 'icon-home', permission: ['view'] }
},
decoration, // 装修管理
application,// 应用管理
content, // 内容管理
channel, // 渠道管理
permission,
setting
]
}
]
export const constRoutes: Array<RouteRecordRaw> = [
{
path: '/login',
component: () => import('@/views/account/login.vue')
},
{
path: '/error/500',
component: Error500
},
{ path: '/:pathMatch(.*)*', name: '404', component: Error404 }
]
export const getAsyncRoutes = () => {
return asyncRoutes
}
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [...asyncRoutes, ...constRoutes]
})
export default router

View File

@@ -0,0 +1,40 @@
import { RouteRecordRaw, RouterView } from 'vue-router'
const routes: RouteRecordRaw = {
path: '/application',
redirect: '/application/notification',
component: RouterView,
meta: { title: '应用管理', icon: 'icon-setting' },
children: [
{
path: '/application/notification',
redirect: '/application/notification/index',
component: RouterView,
meta: { title: '消息通知' },
children: [
{
path: '/application/notification/index',
component: () => import('@/views/application/notification/index.vue'),
meta: { title: '通知设置', permission: ['view'] },
},
{
path: '/application/notification/detail',
component: () => import('@/views/application/notification/detail.vue'),
meta: { hidden: true, title: '通知设置', permission: ['view'] },
},
{
path: '/application/sms/index',
component: () => import('@/views/application/sms/index.vue'),
meta: { title: '短信设置', permission: ['view'] },
},
{
path: '/application/sms/detail',
component: () => import('@/views/application/sms/detail.vue'),
meta: { hidden: true, title: '短信设置', permission: ['view'] },
},
],
},
],
}
export default routes

View File

@@ -0,0 +1,106 @@
import { RouteRecordRaw, RouterView } from "vue-router"
const routes: RouteRecordRaw = {
path: '/channel',
name: 'channel',
meta: { title: '渠道管理', icon: 'icon-setting' },
component: RouterView,
children: [{
path: '/channel/mp_wechat',
name: 'mp_wechat',
meta: {
title: '微信公众号',
parentPath: '/channel',
},
component: RouterView,
children: [{
path: '/channel/mp_wechat/index',
name: 'mp_wechat_index',
meta: {
title: '渠道设置',
parentPath: '/channel',
permission: ['view']
},
component: () => import('@/views/channel/mp_wechat/index.vue'),
}, {
path: '/channel/mp_wechat/menu',
name: 'mp_wechat_menu',
meta: {
title: '菜单管理',
parentPath: '/channel',
permission: ['view']
},
component: () => import('@/views/channel/mp_wechat/menu.vue'),
}, {
path: '/channel/mp_wechat/reply/follow_reply',
name: 'follow_reply',
meta: {
title: '关注回复',
parentPath: '/channel',
permission: ['view']
},
component: () => import('@/views/channel/mp_wechat/reply/follow_reply.vue'),
}, {
path: '/channel/mp_wechat/reply/keyword_reply',
name: 'keyword_reply',
meta: {
title: '关键字回复',
parentPath: '/channel',
permission: ['view']
},
component: () => import('@/views/channel/mp_wechat/reply/keyword_reply.vue'),
}, {
path: '/channel/mp_wechat/reply/default_reply',
name: 'default_reply',
meta: {
title: '默认回复',
parentPath: '/channel',
permission: ['view']
},
component: () => import('@/views/channel/mp_wechat/reply/default_reply.vue'),
}, {
path: '/channel/mp_wechat/reply/reply_edit',
name: 'reply_edit',
meta: {
title: '默认编辑',
parentPath: '/channel',
hidden: true,
permission: ['view']
},
component: () => import('@/views/channel/mp_wechat/reply/reply_edit.vue'),
}]
}, {
path: '/channel/wechat_app',
name: 'wechat_app',
meta: {
title: '微信小程序',
parentPath: '/channel'
},
component: () => import('@/views/channel/wechat_app/index.vue')
}, {
path: '/channel/app_store',
name: 'app_store',
meta: {
title: 'APP',
parentPath: '/channel',
},
component: () => import('@/views/channel/app_store/index.vue'),
}, {
path: '/channel/h5_store',
name: 'h5_store',
meta: {
title: 'H5',
parentPath: '/channel',
},
component: () => import('@/views/channel/h5_store/index.vue')
}, {
path: '/wechat/wechat_platform',
name: 'wechat_platform',
meta: {
title: '微信开放平台',
parentPath: '/channel',
},
component: () => import('@/views/channel/wechat_platform/index.vue')
}]
}
export default routes

View File

@@ -0,0 +1,66 @@
import { RouteRecordRaw, RouterView } from 'vue-router'
const routes: RouteRecordRaw = {
path: '/content',
redirect: '/content/advertising',
component: RouterView,
meta: { title: '内容管理', icon: 'icon-setting' },
children: [
// {
// path: '/content/advertising',
// redirect: '/content/advertising/lists',
// component: RouterView,
// meta: { title: '广告管理' },
// children: [
// {
// path: '/content/advertising/lists',
// component: () => import('@/views/content/advertising/advertising.vue'),
// meta: { title: '广告列表' },
// },
// {
// path: '/content/advertising/advertising_edit',
// component: () => import('@/views/decoration/advertising_edit.vue'),
// meta: {
// title: '广告列表',
// parent: '/content/advertising/lists',
// hidden: true,
// },
// },
// {
// path: '/content/advertising/position',
// component: () => import('@/views/content/advertising/position.vue'),
// meta: { title: '广告位' },
// },
// ],
// },
{
path: '/content/information',
redirect: '/content/information/lists',
component: RouterView,
meta: { title: '资讯管理' },
children: [
{
path: '/content/information/lists',
component: () => import('@/views/content/information/lists.vue'),
meta: { title: '资讯列表' },
},
{
path: '/content/information/information_edit',
component: () => import('@/views/content/information/information_edit.vue'),
meta: {
title: '资讯列表',
parent: '/content/information/lists',
hidden: true,
},
},
{
path: '/content/information/position',
component: () => import('@/views/content/information/category.vue'),
meta: { title: '资讯分类' },
},
],
},
],
}
export default routes

View File

@@ -0,0 +1,38 @@
import { RouteRecordRaw, RouterView } from 'vue-router'
const routes: RouteRecordRaw = {
path: '/decoration',
redirect: '/decoration/home',
component: RouterView,
meta: { title: '装修管理', icon: 'icon-setting' },
children: [
{
path: '/decoration/home',
component: () => import('@/views/decoration/home.vue'),
meta: { title: '首页装修' },
},
{
path: '/decoration/home_edit',
component: () => import('@/views/decoration/home_edit.vue'),
meta: { title: '首页装修', parent: '/decoration/home', hidden: true },
},
{
path: '/decoration/tabbar',
component: () => import('@/views/decoration/tabbar.vue'),
meta: { title: '底部标签栏' },
},
{
path: '/decoration/advertising',
component: () => import('@/views/decoration/advertising.vue'),
meta: { title: '广告管理' },
},
{
path: '/decoration/advertising_edit',
component: () => import('@/views/decoration/advertising_edit.vue'),
meta: { title: '广告管理', parent: '/decoration/advertising', hidden: true },
},
],
}
export default routes

View File

@@ -0,0 +1,40 @@
import { RouteRecordRaw, RouterView } from 'vue-router'
const routes: RouteRecordRaw = {
path: '/permission',
redirect: '/permission/admin',
component: RouterView,
meta: { title: '权限管理', icon: 'icon-quanxian' },
children: [
{
path: '/permission/admin',
component: () => import('@/views/permission/admin/index.vue'),
meta: { title: '管理员', permission: ['view'] }
},
{
path: '/permission/admin/edit',
component: () => import('@/views/permission/admin/edit.vue'),
meta: {
title: '管理员',
parent: '/permission/admin',
hidden: true
}
},
{
path: '/permission/role',
component: () => import('@/views/permission/role/index.vue'),
meta: { title: '角色', permission: ['view'] }
},
{
path: '/permission/role/edit',
component: () => import('@/views/permission/role/edit.vue'),
meta: {
title: '角色',
parent: '/permission/role',
hidden: true
}
}
]
}
export default routes

View File

@@ -0,0 +1,99 @@
import { RouteRecordRaw, RouterView } from 'vue-router'
const routes: RouteRecordRaw = {
path: '/setting',
redirect: '/setting/service',
component: RouterView,
meta: { title: '系统设置', icon: 'icon-setting' },
children: [
{
path: '/setting/service',
redirect: '/setting/service/online_service',
component: RouterView,
meta: { title: '客服设置' },
children: [
{
path: '/setting/service/online_service',
component: () => import('@/views/setting/service/online_service.vue'),
meta: {
title: '在线客服',
},
},
],
},
{
path: '/setting/website',
redirect: '/setting/website/information',
component: RouterView,
meta: { title: '网站设置' },
children: [
{
path: '/setting/website/information',
component: () => import('@/views/setting/website/information.vue'),
meta: {
title: '网站信息',
permission: ['view'],
},
},
{
path: '/setting/website/filing',
component: () => import('@/views/setting/website/filing.vue'),
meta: {
title: '备案信息',
permission: ['view'],
},
},
{
path: '/setting/website/protocol',
component: () => import('@/views/setting/website/protocol.vue'),
meta: {
title: '政策/协议',
permission: ['view'],
},
},
],
},
{
path: '/setting/user',
redirect: '/setting/user',
component: RouterView,
meta: { title: '用户设置' },
children: [
{
path: '/setting/user',
component: () => import('@/views/setting/user/index.vue'),
meta: {
title: '用户设置',
permission: ['view'],
},
},
{
path: '/setting/user/login',
component: () => import('@/views/setting/user/login.vue'),
meta: {
title: '登录注册',
permission: ['view'],
},
},
],
},
{
path: '/setting/system',
redirect: '/setting/system/environment',
component: RouterView,
meta: { title: '系统维护' },
children: [
{
path: '/setting/website/environment',
component: () => import('@/views/setting/system/environment.vue'),
meta: {
title: '系统环境',
permission: ['view'],
},
},
],
},
],
}
export default routes

View File

@@ -0,0 +1,16 @@
import { GetterTree } from 'vuex'
import { rootState } from './modules'
const getters: GetterTree<rootState, any> = {
// token
token: state => state.user.token,
// 管理员信息
userInfo: state => state.user.user,
// 通用配置
config: state => state.app.config,
// 权限列表
permission: state => state.permission.permission,
isAdmin: state => state.permission.isAdmin
}
export default getters

17
admin/src/store/index.ts Normal file
View File

@@ -0,0 +1,17 @@
import { createStore, Store, useStore as baseUseStore } from 'vuex'
import { InjectionKey } from 'vue'
import getters from './getters'
import modules, { rootState } from './modules'
const store = createStore<rootState>({
modules: modules,
getters
})
export const injectionKey: InjectionKey<Store<rootState>> = Symbol('vue-store')
// 定义自己的 `useStore` 组合式函数
export function useStore() {
return baseUseStore(injectionKey)
}
export default store

View File

@@ -0,0 +1,29 @@
import { apiConfig } from '@/api/app'
import { Module } from 'vuex'
export interface AppModule {
config: any
}
const app: Module<AppModule, any> = {
namespaced: true,
state: {
config: {}
},
mutations: {
setConfig(state, data) {
state.config = data
}
},
actions: {
getConfig({ commit }) {
return new Promise((resolve, reject) => {
apiConfig().then(data => {
commit('setConfig', data)
resolve(data)
})
})
}
}
}
export default app

View File

@@ -0,0 +1,14 @@
import app, { AppModule } from './app'
import permission, { PermissionModule } from './permission'
import user, { UserModule } from './user'
export interface rootState {
app: AppModule
permission: PermissionModule
user: UserModule
}
export default {
app,
permission,
user
}

View File

@@ -0,0 +1,46 @@
import { Module } from 'vuex'
import { RouteRecordRaw } from 'vue-router'
import { apiConfigGetAuth } from '@/api/auth'
export interface PermissionModule {
sidebar: Array<RouteRecordRaw>
permission: any[] | null
isAdmin: number
}
const permission: Module<PermissionModule, any> = {
namespaced: true,
state: {
// 左侧菜单
sidebar: [],
// 权限列表
permission: null,
// 是否是管理员
isAdmin: 0
},
getters: {},
mutations: {
setSidebar(state, data) {
state.sidebar = data
},
setPermission(state, data) {
state.permission = data.auth
state.isAdmin = data.root
}
},
actions: {
getPermission({ commit }) {
return new Promise((resolve, reject) => {
apiConfigGetAuth()
.then(data => {
commit('setPermission', data)
resolve(data)
})
.catch(err => {
reject(err)
})
})
}
}
}
export default permission

View File

@@ -0,0 +1,73 @@
import { Module } from 'vuex'
import cache from '@/utils/cache'
import { TOKEN } from '@/config/cachekey'
import { apiLogin, apiLogout, apiUserInfo } from '@/api/user'
export interface UserModule {
token: string
user: object
}
const user: Module<UserModule, any> = {
namespaced: true,
state: {
token: cache.get(TOKEN) || '',
user: {}
},
mutations: {
setToken(state, data) {
state.token = data
cache.set(TOKEN, data)
},
setUser(state, data) {
state.user = data
}
},
actions: {
// 登录
login({ commit }, data) {
const { account, password } = data
return new Promise((resolve, reject) => {
apiLogin({
account: account.trim(),
password: password
})
.then((data: any) => {
commit('setToken', data.token)
resolve(data)
})
.catch(error => {
reject(error)
})
})
},
// 退出登录
logout({ commit }) {
return new Promise((resolve, reject) => {
apiLogout()
.then(data => {
commit('setToken', '')
commit('setUser', {})
cache.remove(TOKEN)
resolve(data)
})
.catch(error => {
reject(error)
})
})
},
// 获取管理员信息
getUser({ commit }) {
return new Promise((resolve, reject) => {
apiUserInfo()
.then(data => {
commit('setUser', data)
resolve(data)
})
.catch(error => {
reject(error)
})
})
}
}
}
export default user

View File

@@ -0,0 +1,298 @@
/**
公共样式
*/
/** S 背景颜色 **/
.bg-primary {
background-color: $color-primary;
}
.bg-success {
background-color: $color-success;
}
.bg-warning {
background-color: $color-warning;
}
.bg-danger {
background-color: $color-danger;
}
.bg-white {
background-color: $color-white;
}
.bg-info {
background-color: $color-info;
}
/** E 背景颜色 **/
/** E 字体颜色 **/
.primary {
color: $color-text-primary;
}
.white {
color: $color-white;
}
.black {
color: $color-black;
}
.normal {
color: $color-text-primary;
}
.lighter {
color: $color-text-regular;
}
.muted {
color: $color-text-secondary;
}
.blue {
color: $color-primary;
}
.green {
color: $color-success;
}
/** E 字体颜色 **/
/** S Font **/
// XL
.xl {
font-size: $font-size-xl;
}
// LG
.lg {
font-size: $font-size-lg;
}
// MD
.md {
font-size: $font-size-md;
}
// NR
.nr {
font-size: $font-size-nr;
}
// SM
.sm {
font-size: $font-size-sm;
}
// SM
.xs {
font-size: $font-size-xs;
}
// 字体大小 Example: f-s-[19-40]
@for $i from 19 through 40 {
.f-s-#{$i} {
font-size: $i + px;
}
}
// 字体字重 Example: f-w-[100-900]
@for $i from 100 through 900 {
@if $i % 100==0 {
.f-w-#{$i} {
font-weight: $i;
}
}
}
/** S Font **/
// 内、外边距[1-60]
@for $i from 0 through 60 {
// 只要偶数和能被5整除的数
@if $i % 2==0 or $i % 5==0 {
// 如m-30
.m-#{$i} {
margin: $i + px;
}
// 如p-30
.p-#{$i} {
padding: $i + px;
}
@each $short, $long in l left, t top, r right, b bottom {
//如: m-l-6
// 外边距
.m-#{$short}-#{$i} {
margin-#{$long}: $i + px;
}
//如: p-l-30
// 内边距
.p-#{$short}-#{$i} {
padding-#{$long}: $i + px;
}
}
}
}
/** S 文本行数限制 **/
.line-1 {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.line-2 {
-webkit-line-clamp: 2;
}
.line-3 {
-webkit-line-clamp: 3;
}
.line-4 {
-webkit-line-clamp: 4;
}
.line-2,
.line-3,
.line-4 {
overflow: hidden;
word-break: break-all;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
}
/** E 文本行数限制 **/
/** S 内容排序方式 **/
.text-left {
text-align: left;
}
.text-center {
text-align: center;
}
.text-right {
text-align: right;
}
/** E 内容排序方式 **/
/** S Flex-弹性布局 **/
.flex {
display: flex;
flex-direction: row;
}
.flex-inline {
display: inline-flex;
}
.flex-col {
flex-direction: column;
}
.flex-center {
align-items: center;
justify-content: center;
}
.flex-none {
flex: none;
}
.flex-wrap {
flex-wrap: wrap;
}
.flex-nowrap {
flex-wrap: nowrap;
}
.col-baseline {
align-items: baseline;
}
.col-center {
align-items: center;
}
.col-top {
align-items: flex-start;
}
.col-bottom {
align-items: flex-end;
}
.col-stretch {
align-items: stretch;
}
.row-center {
justify-content: center;
}
.row-left {
justify-content: flex-start;
}
.row-right {
justify-content: flex-end;
}
.row-between {
justify-content: space-between;
}
.row-around {
justify-content: space-around;
}
// Example: flex-[0-24]
@for $i from 0 through 24 {
.flex-#{$i} {
flex: $i;
}
}
/** E Flex-弹性布局 **/
// 行内块元素
.inline {
display: inline-block;
}
// 块元素
.block {
display: block;
}
// 触手
.pointer {
cursor: pointer;
}
// 块卡片
.ls-card {
border-radius: 8px;
background-color: $color-white;
}
.clearfix:after {
content: '';
display: block;
clear: both;
visibility: hidden;
}

View File

@@ -0,0 +1,99 @@
// 引入所有样式
@use 'element-plus/theme-chalk/src/index.scss';
:root {
--el-font-weight-primary: 400;
}
.el-menu {
border-right: none !important;
.el-menu--horizontal {
border-bottom: none;
}
}
.ls-scrollbar.el-scrollbar .el-scrollbar__wrap {
overflow-x: hidden;
}
.ls-view .el-tabs .el-tabs__item {
height: 60px;
line-height: 60px;
}
.el-tabs .el-tabs__nav-wrap::after {
height: 1px;
background-color: #e5e5e5;
}
.el-input-group__prepend {
background-color: #fff;
}
.el-loading-spinner {
font-size: 26px;
}
.el-textarea .el-textarea__inner {
resize: none;
}
.el-image .el-image__error {
font-size: 12px;
}
// 弹窗居中
.el-overlay-dialog {
display: flex;
justify-content: center;
align-items: center;
min-width: 1000px;
min-height: 100%;
position: static;
overflow: hidden;
.el-dialog {
--el-dialog-content-font-size: var(--el-font-size-base);
flex: none;
display: flex;
flex-direction: column;
margin: 20px !important;
border-radius: 5px;
&.body-padding .el-dialog__body {
padding: 0;
}
.el-dialog__body {
flex: 1;
padding: 15px 20px;
}
}
}
.el-card {
border: none;
}
.el-form.ls-form {
.el-form-item__content {
font-size: var(--el-font-size-base);
& > .el-cascader,
& > .el-select,
& > .el-input {
width: 280px;
}
}
}
.el-table {
font-size: var(--el-font-size-base);
thead th {
font-weight: 400;
}
}
.el-icon {
font-size: var(--font-size);
}

View File

@@ -0,0 +1,13 @@
:export {
/* 主题颜色 */
color_primary: $color-primary;
color_success: $color-success;
color_warning: $color-warning;
color_danger: $color-danger;
color_ingo: $color-info;
/* 字体颜色 */
font_color_primary: $color-text-primary;
font_color_regular: $color-text-regular;
font_color_secondary: $color-text-secondary;
}

View File

@@ -0,0 +1,15 @@
export interface Variables {
/* 主题颜色 */
color_primary: string
color_success: string
color_warning: string
color_danger: string
color_ingo: string
/* 字体颜色 */
font_color_primary: string
font_color_regular: string
font_color_secondary: string
}
export const variables: Variables
export default variables

View File

@@ -0,0 +1,10 @@
/**
全局样式
*/
// 初始化默认样式
@import 'reset';
// 公共样式
@import 'common';
// element样式
@import 'element';

View File

@@ -0,0 +1,83 @@
/* http://meyerweb.com/eric/tools/css/reset/ */
/* v1.0 | 20080212 */
html,
body,
div,
span,
h1,
h2,
h3,
h4,
h5,
h6,
p,
a,
b,
u,
i,
dl,
dt,
dd,
ol,
ul,
li,
input,
form,
label,
table,
tbody,
tfoot,
thead,
tr,
th,
td {
font-family: PingFang SC, Arial, Hiragino Sans GB, Microsoft YaHei, sans-serif;
margin: 0;
padding: 0;
border: 0;
outline: 0;
font-size: 100%;
vertical-align: baseline;
background: transparent;
text-decoration: none;
}
ol,
ul {
list-style: none;
}
/* remember to define focus styles! */
:focus {
outline: 0;
}
del {
text-decoration: line-through;
}
/* tables still need 'cellspacing="0"' in the markup */
table {
border-collapse: collapse;
border-spacing: 0;
}
/* 初始化页面 */
body {
font-size: $font-size-base;
background-color: $background-color-base;
color: $color-text-primary;
overflow-y: hidden;
overflow-x: auto;
line-height: 1.3;
}
input::placeholder {
color: $color-text-placeholder;
}
/* NProgress */
#nprogress .bar {
background: $color-primary !important; //自定义颜色
}

View File

@@ -0,0 +1,142 @@
/**
变量
*/
/* Color
-------------------------- */
$color-primary: #4a5dff !default;
$color-success: #67c23a !default;
$color-warning: #fb9400 !default;
$color-danger: #f56c6c !default;
$color-error: #db2828 !default;
$color-info: #909399 !default;
$color-white: #ffffff !default;
$color-black: #333333 !default;
$color-primary-light-1: mix($color-white, $color-primary, 10%) !default;
$color-primary-light-2: mix($color-white, $color-primary, 20%) !default;
$color-primary-light-3: mix($color-white, $color-primary, 30%) !default;
$color-primary-light-4: mix($color-white, $color-primary, 40%) !default;
$color-primary-light-5: mix($color-white, $color-primary, 50%) !default;
$color-primary-light-6: mix($color-white, $color-primary, 60%) !default;
$color-primary-light-7: mix($color-white, $color-primary, 70%) !default;
$color-primary-light-8: mix($color-white, $color-primary, 80%) !default;
$color-primary-light-9: mix($color-white, $color-primary, 90%) !default;
/* Font
-------------------------- */
$color-text-primary: #333333 !default;
$color-text-regular: #666666 !default;
$color-text-secondary: #999999 !default;
$color-text-placeholder: #999999 !default;
$font-size-xl: 17px !default;
$font-size-lg: 16px !default;
$font-size-md: 15px !default;
$font-size-nr: 14px !default;
$font-size-sm: 13px !default;
$font-size-xs: 12px !default;
$font-size-base: $font-size-sm;
/* Background
-------------------------- */
$background-color-base: #f6f6f6 !default;
/* Border
-------------------------- */
$border-color-base: #e5e5e5 !default;
$border-color-light: #f2f2f2 !default;
$border-width-base: 1px !default;
$border-style-base: solid !default;
$border-color-hover: $color-primary !default;
$border-base: $border-width-base $border-style-base $border-color-base !default;
/* Layout
-------------------------- */
$layout-min-width: 1200px !default;
$layout-aside-width: 200px !default;
$layout-header-height: 70px !default;
$colors: (
'primary': (
'base': $color-primary
),
'success': (
'base': $color-success
),
'warning': (
'base': $color-warning
),
'danger': (
'base': $color-danger
),
'error': (
'base': $color-error
),
'info': (
'base': $color-info
)
);
$text-color: (
'primary': $color-text-primary,
'regular': $color-text-regular,
'secondary': $color-text-secondary,
'placeholder': $color-text-placeholder
);
$border-color: (
'base': $border-color-base,
'light': $border-color-light,
'lighter': $border-color-light,
'extra-light': $border-color-light
);
$font-size: (
'extra-large': $font-size-xl,
'large': $font-size-lg,
'medium': $font-size-md,
'base': $font-size-base,
'small': $font-size-sm,
'extra-small': $font-size-xs
);
$button-font-size: (
'default': $font-size-base,
'medium': $font-size-base,
'small': $font-size-base,
'mini': $font-size-sm
);
$button-padding-vertical: (
'default': 12px,
'medium': 10px,
'small': 8px,
'mini': 6px
);
$button-padding-horizontal: (
'default': 25px,
'medium': 25px,
'small': 20px,
'mini': 15px
);
$table: (
'text-color': $color-text-primary,
'header-text-color': $color-text-primary,
'header-bg-color': rgba($color-primary, 0.05)
);
// 替换elementui的变量
@forward 'element-plus/theme-chalk/src/common/var.scss' with (
$colors: $colors,
$text-color: $text-color,
$border-color: $border-color,
$font-size: $font-size,
$button-font-size: $button-font-size,
$button-padding-vertical: $button-padding-vertical,
$table: $table
);

50
admin/src/utils/cache.ts Normal file
View File

@@ -0,0 +1,50 @@
const cache = {
key: 'like_admin_',
//设置缓存(expire为缓存时效)
set(key: string, value: any, expire?: string) {
key = this.getKey(key)
let data: any = {
expire: expire ? this.time() + expire : '',
value
}
if (typeof data === 'object') {
data = JSON.stringify(data)
}
try {
window.localStorage.setItem(key, data)
} catch (e) {
return false
}
},
get(key: string) {
key = this.getKey(key)
try {
const data = window.localStorage.getItem(key)
if (!data) {
return false
}
const { value, expire } = JSON.parse(data)
if (expire && expire < this.time()) {
window.localStorage.removeItem(key)
return false
}
return value
} catch (e) {
return false
}
},
//获取当前时间
time() {
return Math.round(new Date().getTime() / 1000)
},
remove(key: string) {
key = this.getKey(key)
window.localStorage.removeItem(key)
},
getKey(key: string) {
return this.key + key
}
}
export default cache

0
admin/src/utils/enum.ts Normal file
View File

View File

@@ -0,0 +1,86 @@
'use strict'
import axios from 'axios'
import { ElMessage } from 'element-plus'
import { version } from '@/config/app'
import store from '@/store'
import { throttle } from './util'
import router from '@/router'
import cache from './cache'
import { TOKEN } from '@/config/cachekey'
// 事件集
const eventResponse = {
// 成功
success: ({ show, msg, data }: any): Promise<any> => {
if (show * 1) {
ElMessage({ type: 'success', message: msg })
}
return data
},
// 失败
error: ({ show, msg }: any): Promise<any> => {
if (show * 1) {
ElMessage({ type: 'error', message: msg })
}
return Promise.reject(msg)
},
// 重定向
redirect: throttle(() => {
store.commit('user/setToken', '')
store.commit('user/setUser', {})
cache.remove(TOKEN)
router.push('/login')
return Promise.reject()
}),
// 打开新的页面
page: ({ data }: any): Promise<any> => {
window.location.href = data.url
return data
}
}
const request = axios.create({
baseURL: `${import.meta.env.VITE_APP_BASE_URL}/adminapi`,
timeout: 60 * 1000,
headers: {
'Content-Type': 'application/json',
version
}
})
request.interceptors.request.use(
config => {
const token = store.getters.token
// header参入Token
if (config.headers) {
config.headers.token = token
}
return config
},
error => {
return Promise.reject(error)
}
)
// Add a response interceptor
request.interceptors.response.use(
response => {
switch (response.data.code) {
case 1:
return eventResponse.success(response.data)
case 0:
return eventResponse.error(response.data)
case -1:
return eventResponse.redirect()
case 2:
return eventResponse.page(response.data)
}
},
error => {
console.log(error)
ElMessage({ type: 'error', message: error })
return Promise.reject(error)
}
)
export default request

5
admin/src/utils/type.ts Normal file
View File

@@ -0,0 +1,5 @@
// 页面模式
export enum PageMode {
'ADD' = 'add', // 添加
'EDIT' = 'edit' // 编辑
}

170
admin/src/utils/util.ts Normal file
View File

@@ -0,0 +1,170 @@
/**
* 工具方法
* 请谨慎操作,影响全局
*/
/**
* 深拷贝
* @param {any} target 需要深拷贝的对象
* @returns {Object}
*/
export function deepClone(target: any) {
if (typeof target !== 'object' || target === null) {
return target
}
const cloneResult: any = Array.isArray(target) ? [] : {}
for (const key in target) {
if (Object.prototype.hasOwnProperty.call(target, key)) {
const value = target[key]
if (typeof value === 'object' && value !== null) {
cloneResult[key] = deepClone(value)
} else {
cloneResult[key] = value
}
}
}
return cloneResult
}
/**
* 过滤对象属性
* @param { Object } target
* @param { Array } filters
* @return { Object } 过滤后的对象
*/
export function filterObject(target: any, filters: any[]) {
const _target = deepClone(target)
filters.map(key => delete _target[key])
return _target
}
/**
* 节流
* @param { Function } func
* @param { Number } time
* @param context
* @return { Function }
*/
export function throttle(func: () => any, time = 1000, context?: any): any {
let previous = new Date(0).getTime()
return function (...args: []) {
const now = new Date().getTime()
if (now - previous > time) {
previous = now
return func.apply(context, args)
}
}
}
/**
* Query语法格式化为对象
* @param { String } str
* @return { Object }
*/
export function queryToObject(str: string) {
const params: any = {}
for (const item of str.split('&')) {
params[item.split('=')[0]] = item.split('=')[1]
}
return params
}
/**
* 对象格式化为Query语法
* @param { Object } params
* @return {string} Query语法
*/
export function objectToQuery(params: any) {
let p = ''
if (typeof params === 'object') {
p = '?'
for (const props in params) {
p += `${props}=${params[props]}&`
}
p = p.slice(0, -1)
}
return p
}
/**
* @description 获取不重复的id
* @param length { Number } id的长度
* @return { String } id
*/
export const getNonDuplicateID = (length = 8) => {
let idStr = Date.now().toString(36)
idStr += Math.random().toString(36).substr(3, length)
return idStr
}
/**
* @description 时间格式化
* @param dateTime { number } 时间戳
* @param fmt { string } 时间格式
* @return { string }
*/
// yyyy:mm:dd|yyyy:mm|yyyy年mm月dd日|yyyy年mm月dd日 hh时MM分等,可自定义组合
export const timeFormat = (dateTime: number, fmt = 'yyyy-mm-dd') => {
// 如果为null,则格式化当前时间
if (!dateTime) {
dateTime = Number(new Date())
}
// 如果dateTime长度为10或者13则为秒和毫秒的时间戳如果超过13位则为其他的时间格式
if (dateTime.toString().length == 10) {
dateTime *= 1000
}
const date = new Date(dateTime)
let ret
const opt: any = {
'y+': date.getFullYear().toString(), // 年
'm+': (date.getMonth() + 1).toString(), // 月
'd+': date.getDate().toString(), // 日
'h+': date.getHours().toString(), // 时
'M+': date.getMinutes().toString(), // 分
's+': date.getSeconds().toString() // 秒
}
for (const k in opt) {
ret = new RegExp('(' + k + ')').exec(fmt)
if (ret) {
fmt = fmt.replace(
ret[1],
ret[1].length == 1 ? opt[k] : opt[k].padStart(ret[1].length, '0')
)
}
}
return fmt
}
// /**
// *
// * @param {*} tree
// * @param {*} arr
// * @returns
// */
// export function flatten(tree = [], arr = []) {
// tree.forEach((item) => {
// const { children } = item
// arr.push(item)
// if (children) flatten(children, arr)
// })
// return arr
// }
/**
* @description 树状数组扁平化
* @param { Array } tree 树状结构数组
* @param { Array } arr 扁平化后的数组
* @param { String } childrenKey 子节点键名
* @return { Array } 扁平化后的数组
*/
export function flatten(tree = [], arr = [], childrenKey = 'children') {
tree.forEach(item => {
const children = item[childrenKey]
children ? flatten(children, arr, childrenKey) : arr.push(item)
})
return arr
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 594 KiB

View File

@@ -0,0 +1,183 @@
<template>
<div class="login flex flex-col">
<div class="flex-1 flex flex-center">
<div class="login-card bg-white flex">
<div
class="login-img"
:style="{
'background-image': `url(${config.login_image})`
}"
></div>
<div class="login-form flex flex-col">
<div class="f-s-24 f-w-500 text-center m-b-40">
{{ config.web_name }}
</div>
<el-form ref="loginFormRefs" :model="loginForm" status-icon :rules="rules">
<el-form-item prop="account">
<el-input
v-model="loginForm.account"
placeholder="请输入账号"
@keyup.enter="handleEnter"
>
<template #prepend>
<el-icon><avatar /></el-icon>
</template>
</el-input>
</el-form-item>
<el-form-item prop="password">
<el-input
ref="passwordRefs"
v-model="loginForm.password"
show-password
placeholder="请输入密码"
@keyup.enter="handleLogin"
>
<template #prepend>
<el-icon><lock /></el-icon>
</template>
</el-input>
</el-form-item>
</el-form>
<div class="m-b-20">
<el-checkbox v-model="remAccount" label="记住账号"></el-checkbox>
</div>
<el-button type="primary" :loading="loginLoading" @click="handleLogin"
>登录</el-button
>
</div>
</div>
</div>
<div class="login-footer">
<div class="flex flex-center muted xs m-t-20">
<span class="m-r-10">{{ config.copyright_info }}</span>
<a class="link muted" :href="config.icp_link" target="_blank">{{
config.icp_number
}}</a>
</div>
</div>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, onMounted, reactive, Ref, ref } from 'vue'
import { useAdmin } from '@/core/hooks/app'
import { ACCOUNT } from '@/config/cachekey'
import cache from '@/utils/cache'
import { ElInput, ElForm } from 'element-plus'
export default defineComponent({
setup() {
const { store, router, route } = useAdmin()
const passwordRefs: Ref<typeof ElInput | null> = ref(null)
const loginFormRefs: Ref<typeof ElForm | null> = ref(null)
const remAccount = ref(false)
const loginLoading = ref(false)
const config = computed(() => store.getters.config)
const loginForm = reactive({
account: '',
password: ''
})
const rules = {
account: [
{
required: true,
message: '请输入账号',
trigger: ['blur']
}
],
password: [
{
required: true,
message: '请输入密码',
trigger: ['blur']
}
]
}
const handleEnter = () => {
if (!loginForm.password) {
return passwordRefs.value?.focus()
}
handleLogin()
}
const handleLogin = () => {
loginFormRefs.value?.validate((valid: boolean) => {
if (!valid) {
return
}
loginLoading.value = true
// 记住账号,缓存
cache.set(ACCOUNT, {
remember: remAccount.value,
account: loginForm.account
})
store
.dispatch('user/login', loginForm)
.then(() => {
const {
query: { redirect }
} = route
const path = typeof redirect === 'string' ? redirect : '/'
router.replace(path)
})
.catch(err => {
console.log(err)
})
.finally(() => {
loginLoading.value = false
})
})
}
onMounted(() => {
const value = cache.get(ACCOUNT)
if (value.remember) {
remAccount.value = value.remember
loginForm.account = value.account
}
})
return {
config,
passwordRefs,
loginFormRefs,
loginForm,
loginLoading,
rules,
handleEnter,
handleLogin,
remAccount
}
}
})
</script>
<style lang="scss" scoped>
.login {
min-height: 100vh;
background-image: url('./images/login_bg.png');
background-repeat: no-repeat;
background-position: center;
background-size: cover;
.login-card {
width: 800px;
height: 400px;
border-radius: 10px;
overflow: hidden;
.login-img {
height: 100%;
width: 50%;
box-sizing: border-box;
background-size: cover;
background-repeat: no-repeat;
background-position: center;
}
.login-form {
width: 50%;
box-sizing: border-box;
padding: 30px 40px 0;
}
}
.login-footer {
padding: 20px 0;
}
}
</style>

View File

@@ -0,0 +1,365 @@
<template>
<!-- Header Start -->
<el-card shadow="never">
<el-page-header @back="$router.back()" content="编辑消息通知" />
</el-card>
<!-- Header End -->
<!-- Header Start -->
<el-card shadow="never" class="m-t-15">
<div class="xxl m-b-20">通知名称</div>
<div class="m-l-30 m-t-10">通知名称 {{ formData.scene_name }}</div>
<div class="m-l-30 m-t-10">通知类型 业务通知</div>
<div class="m-l-30 m-t-10">通知业务 {{ formData.scene_desc }}</div>
</el-card>
<!-- Header End -->
<!-- 系统通知 Start -->
<el-card shadow="never" class="m-t-15" v-if="formData.system_notice.is_show">
<div class="xxl m-b-20">系统通知</div>
<div class style="width: 90%">
<el-form ref="form" label-width="135px">
<el-form-item label="开启状态" required>
<el-radio v-model="formData.system_notice.status" :label="'0'">关闭</el-radio>
<el-radio v-model="formData.system_notice.status" :label="'1'">开启</el-radio>
</el-form-item>
<el-form-item label="通知标题" size="mini" required>
<el-input v-model="formData.system_notice.title"></el-input>
</el-form-item>
<el-form-item label="通知内容" size="mini" required>
<el-input
type="textarea"
class="text"
style="width: 300px"
placeholder="请输入内容"
v-model="formData.system_notice.content"
></el-input>
</el-form-item>
</el-form>
<div class="desc m-t-20" style="margin-left: 135px">
<div v-for="(item, index) in formData.system_notice.tips" :key="index">
{{ item }}
</div>
</div>
</div>
</el-card>
<!-- 系统通知 End -->
<!-- 短信通知 Start -->
<el-card shadow="never" class="m-t-15" v-if="formData.sms_notice.is_show">
<div class="xxl m-b-20">短信通知</div>
<div style="width: 90%">
<el-form ref="form" label-width="135px">
<el-form-item label="开启状态" required>
<el-radio v-model="formData.sms_notice.status" :label="'0'">关闭</el-radio>
<el-radio v-model="formData.sms_notice.status" :label="'1'">开启</el-radio>
</el-form-item>
<el-form-item label="模板ID" size="mini" required>
<el-input v-model="formData.sms_notice.template_id"></el-input>
</el-form-item>
<el-form-item label="短信内容" size="mini" required>
<el-input
type="textarea"
class="text"
style="width: 300px"
placeholder="请输入内容"
v-model="formData.sms_notice.content"
></el-input>
</el-form-item>
</el-form>
<div class="desc m-t-20" style="margin-left: 135px">
<div v-for="(item, index) in formData.sms_notice.tips" :key="index">{{ item }}</div>
</div>
</div>
</el-card>
<!-- 短信通知 End -->
<!-- 微信模板消息 Start -->
<el-card shadow="never" class="m-t-15" v-if="formData.oa_notice.is_show">
<div class="xxl m-b-20">微信模板消息</div>
<el-alert
title="温馨提示:
1. 请前往微信公众平台将【主营行业IT科技/互联网|电子商务 副营行业:消费品】类目添加至您的服务类目,否则将影响订阅通知的正常发送。
2. 查找订阅通知并选用调整关键词的顺序后复制模板ID粘贴在此页面对应的模板ID输入框中"
type="success"
show-icon
:closable="false"
/>
<div class style="width: 90%">
<el-form ref="form" label-width="135px">
<el-form-item label="开启状态" required>
<el-radio v-model="formData.oa_notice.status" :label="'0'">关闭</el-radio>
<el-radio v-model="formData.oa_notice.status" :label="'1'">开启</el-radio>
</el-form-item>
<el-form-item label="模板ID" size="mini" required>
<el-input v-model="formData.oa_notice.template_id"></el-input>
</el-form-item>
<el-form-item label="模板字段first内容" size="mini" required>
<el-input v-model="formData.oa_notice.first"></el-input>
</el-form-item>
<el-form-item label="模板字段remrk内容" size="mini" required>
<el-input v-model="formData.oa_notice.remark"></el-input>
</el-form-item>
<el-form-item label="模板内容" size="mini" required>
<el-button type="primary" size="mini" @click="onAddModeField"
>新增模板字段</el-button
>
<el-table
class="m-t-12"
:data="formData.oa_notice.tpl"
style="width: 100%"
size="mini"
>
<el-table-column label="字段名" width="120">
<template #="scope">
<el-input
v-model="formData.oa_notice.tpl[scope.$index].tpl_name"
placeholder="例如:订单编号"
></el-input>
</template>
</el-table-column>
<el-table-column label="字段值" width="120">
<template #="scope">
<el-input
v-model="formData.oa_notice.tpl[scope.$index].tpl_keyword"
placeholder="例如:keyword1.DT"
></el-input>
</template>
</el-table-column>
<el-table-column label="字段内容" width="180">
<template #="scope">
<el-input
v-model="formData.oa_notice.tpl[scope.$index].tpl_content"
placeholder="例如:${order.sn}"
></el-input>
</template>
</el-table-column>
<el-table-column label="操作" width="120">
<template #="scope">
<el-button
type="text"
size="mall"
@click="formData.oa_notice.tpl.splice(scope.$index, 1)"
>删除</el-button
>
</template>
</el-table-column>
</el-table>
</el-form-item>
</el-form>
<div class="desc m-t-20" style="margin-left: 135px">
<div v-for="(item, index) in formData.oa_notice.tips" :key="index">{{ item }}</div>
</div>
</div>
</el-card>
<!-- 微信模板消息 End -->
<!-- 微信小程序提醒 Start -->
<el-card shadow="never" class="m-t-15" v-if="formData.mnp_notice.is_show">
<div class="xxl m-b-20">微信小程序提醒</div>
<el-alert
title="温馨提示:
1. 请前往微信公众平台将【主营行业IT科技/互联网|电子商务 副营行业:消费品】类目添加至您的服务类目,否则将影响订阅通知的正常发送。
2. 查找订阅通知并选用调整关键词的顺序后复制模板ID粘贴在此页面对应的模板ID输入框中"
type="success"
show-icon
:closable="false"
/>
<div class style="width: 90%">
<el-form ref="form" label-width="135px">
<el-form-item label="开启状态" required>
<el-radio v-model="formData.mnp_notice.status" :label="'0'">关闭</el-radio>
<el-radio v-model="formData.mnp_notice.status" :label="'1'">开启</el-radio>
</el-form-item>
<el-form-item label="模板ID" size="mini" required>
<el-input v-model="formData.mnp_notice.template_id"></el-input>
</el-form-item>
<el-form-item label="模板内容" size="mini" required>
<el-button type="primary" size="mini" @click="onAddWeChatModeField"
>新增模板字段</el-button
>
<el-table
class="m-t-12"
:data="formData.mnp_notice.tpl"
style="width: 100%"
size="mini"
>
<el-table-column label="字段名" width="120">
<template #="scope">
<el-input
v-model="formData.mnp_notice.tpl[scope.$index].tpl_name"
placeholder="例如:订单编号"
></el-input>
</template>
</el-table-column>
<el-table-column label="字段值" width="120">
<template #="scope">
<el-input
v-model="formData.mnp_notice.tpl[scope.$index].tpl_keyword"
placeholder="例如:keyword1.DT"
></el-input>
</template>
</el-table-column>
<el-table-column label="字段内容" width="180">
<template #="scope">
<el-input
v-model="formData.mnp_notice.tpl[scope.$index].tpl_content"
placeholder="例如:${order.sn}"
></el-input>
</template>
</el-table-column>
<el-table-column label="操作" width="120">
<template #="scope">
<el-button
type="text"
size="mall"
@click="formData.mnp_notice.tpl.splice(scope.$index, 1)"
>删除</el-button
>
</template>
</el-table-column>
</el-table>
</el-form-item>
</el-form>
<div class="desc m-t-20" style="margin-left: 135px">
<div v-for="(item, index) in formData.mnp_notice.tips" :key="index">{{ item }}</div>
</div>
</div>
</el-card>
<!-- 微信小程序提醒 End -->
<!-- Footer Start -->
<footer-btns>
<el-button type="primary" size="small" @click="onSubmit()">保存</el-button>
</footer-btns>
<!-- Footer End -->
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { apiNoticeDetail, apiNoticeEdit } from '@/api/application'
import { useAdmin } from '@/core/hooks/app'
import FooterBtns from '@/components/footer-btns/index.vue'
/** Data Start **/
const { router, route } = useAdmin()
const id = ref<any>(route.query.id)
const formData = ref<any>({
// 系统通知
system_notice: {
is_show: false,
content: '',
status: '',
title: '',
},
// 短信通知
sms_notice: {
content: '',
is_show: true,
status: '',
template_id: '',
},
// 微信模板
oa_notice: {
first: '',
is_show: false,
name: '',
remark: '',
status: '',
template_id: '',
template_sn: '',
tpl: [],
},
// 微信小程序
mnp_notice: {
is_show: false,
name: '',
status: '',
template_id: '',
template_sn: '',
tpl: [],
},
})
/** Data End **/
/** Methods Start **/
// 提交保存
const onSubmit = async (): Promise<void> => {
const params = {
id: id.value,
template: [
{
type: 'system',
...formData.value.system_notice,
},
{
type: 'sms',
...formData.value.sms_notice,
},
{
type: 'oa',
...formData.value.oa_notice,
},
{
type: 'mnp',
...formData.value.mnp_notice,
},
],
}
await apiNoticeEdit({ ...params })
router.back()
}
// 获取详情
const getNoticeDetail = async (): Promise<void> => {
formData.value = await apiNoticeDetail({ id: id.value })
}
// 新增微信模板字段
const onAddModeField = (): void => {
formData.value.oa_notice.tpl.push({
tpl_name: '',
tpl_keyword: '',
tpl_content: '',
})
}
// 新增微信小程序模板字段
const onAddWeChatModeField = (): void => {
formData.value.mnp_notice.tpl.push({
tpl_name: '',
tpl_keyword: '',
tpl_content: '',
})
}
/** Methods End **/
/** Life Cycle Start **/
if (id.value) getNoticeDetail()
/** Life Cycle End **/
</script>
<style lang="scss" scoped>
::v-deep .text .el-textarea__inner {
height: 100px !important;
}
</style>

View File

@@ -0,0 +1,105 @@
<template>
<el-card shadow="never">
<!-- Header Alert Start -->
<el-alert
title="温馨提示: 1.平台配置在各个场景下的通知发送方式和内容模板。"
type="primary"
:closable="false"
/>
<!-- Header Alert End -->
</el-card>
<el-card shadow="never" class="m-t-15">
<!-- Main TableData Start -->
<el-tabs v-model="formData.recipient" @click="changeTabs">
<el-tab-pane label="通知用户" :name="1"></el-tab-pane>
<el-tab-pane label="通知平台" :name="2"></el-tab-pane>
</el-tabs>
<el-table
ref="tableDataRef"
class="m-t-15"
:data="pager.lists"
style="width: 100%"
size="mini"
>
<el-table-column property="scene_name" label="通知名称" max-width="200" />
<el-table-column property="type_desc" label="通知类型" max-width="355" />
<el-table-column label="短信通知" max-width="300">
<template #default="scope">
<!-- 短信通知的当前状态 -->
<el-tag
:type="scope.row.sms_status_desc == '停用' ? 'danger' : 'success'"
effect="plain"
>{{ scope.row.sms_status_desc }}</el-tag
>
</template>
</el-table-column>
<el-table-column label="操作" max-width="300">
<template #default="scope">
<router-link
class="m-r-10"
:to="{
path: '/application/notification/detail',
query: {
id: scope.row.id,
},
}"
>
<el-button type="text">设置</el-button>
</router-link>
</template>
</el-table-column>
</el-table>
<!-- Main TableData End -->
<!-- Footer Pagination Start -->
<div class="flex row-right">
<pagination
v-model="pager"
@change="requestApi"
layout="total, prev, pager, next, jumper"
/>
</div>
<!-- Footer Pagination End -->
</el-card>
</template>
<script lang="ts" setup>
import { apiNoticeLists } from '@/api/application'
import { ref } from 'vue'
import Pagination from '@/components/pagination/index.vue'
import { usePages } from '@/core/hooks/pages'
/** Data Start **/
let formData = ref({
recipient: 1 as number,
})
const { pager, requestApi } = usePages({
callback: apiNoticeLists,
params: formData.value,
})
/** Data End **/
/** Methods Start **/
const changeTabs = (): void => {
requestApi()
}
/** Methods End **/
/** LifeCycle Start **/
requestApi()
/** LifeCycle End **/
</script>
<style lang="scss">
.el-tabs__nav-wrap {
background: none !important;
}
.el-tabs__nav-wrap::after {
background: none !important;
}
.el-tabs__item {
margin-right: 30px;
}
</style>

View File

@@ -0,0 +1,109 @@
<template>
<!-- Header Start -->
<el-card shadow="never">
<el-page-header @back="$router.back()" content="短信设置" />
</el-card>
<!-- Header End -->
<!-- Main Start -->
<el-card shadow="never" class="m-t-15">
<el-form ref="form" label-width="135px">
<el-form-item label="短信渠道">{{ formData.name }}</el-form-item>
<el-form-item label="开启状态" required>
<el-radio
v-model="formData.status"
:label="typeof formData.status == 'string' ? '0' : 0"
>关闭</el-radio
>
<el-radio
v-model="formData.status"
:label="typeof formData.status == 'string' ? '1' : 1"
>开启</el-radio
>
</el-form-item>
<el-form-item label="短信签名" size="mini" required>
<el-input class="ls-input" v-model="formData.sign"></el-input>
</el-form-item>
<el-form-item v-if="type == 'tencent'" label="APP_ID" size="mini" required>
<el-input class="ls-input" v-model="formData.app_id"></el-input>
</el-form-item>
<el-form-item label="APP_KEY" v-if="type == 'ali'" size="mini" required>
<el-input class="ls-input" v-model="formData.app_key"></el-input>
</el-form-item>
<el-form-item v-if="type == 'tencent'" label="SECRET_ID" size="mini" required>
<el-input class="ls-input" v-model="formData.secret_id"></el-input>
</el-form-item>
<el-form-item label="SECRET_KEY" size="mini" required>
<el-input class="ls-input" v-model="formData.secret_key"></el-input>
</el-form-item>
</el-form>
</el-card>
<!-- Main End -->
<!-- Footer Start -->
<footer-btns>
<el-button type="primary" size="small" @click="onSubmit()">保存</el-button>
</footer-btns>
<!-- Footer End -->
</template>
<script lang="ts" setup>
import { apiSmsDetail, apiSmsEdit } from '@/api/application'
import { ref } from 'vue'
import FooterBtns from '@/components/footer-btns/index.vue'
import { useAdmin } from '@/core/hooks/app'
import { number } from 'echarts/core'
/** Interface Start **/
interface formDataObj {
name: string
app_key: string
status: number
sign: string
secret_id: string
app_id: string
secret_key: string
}
/** Interface End **/
/** Data Start **/
const { router, route } = useAdmin()
const type = ref<any>(route.query.type)
const formData = ref<formDataObj>({
name: '',
app_key: '',
status: 1,
sign: '',
secret_id: '',
app_id: '',
secret_key: '',
})
/** Data End **/
/** Methods Start **/
// 详情
const getSmsDetail = async (): Promise<void> => {
;(formData.value as object) = await apiSmsDetail({ type: type.value })
}
const onSubmit = async (): Promise<void> => {
await apiSmsEdit({ ...formData.value, type: type.value })
router.back()
}
/** Methods End **/
/** Life Cycle Start **/
if (type.value) getSmsDetail()
/** Life Cycle End **/
</script>
<style lang="scss" scoped>
.ls-input {
width: 305px;
}
</style>

View File

@@ -0,0 +1,53 @@
<template>
<el-card shadow="never">
<el-table :data="tableData" style="width: 100%" size="mini">
<el-table-column prop="name" label="短信通道" min-width="180" />
<el-table-column label="状态" min-width="180">
<template #="scope">
<!-- 短信通知的当前状态 -->
<el-tag :type="scope.row.status == 0 ? 'danger' : 'success'" effect="plain">{{
scope.row.status == 0 ? '关闭' : '启用'
}}</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="200">
<template #="scope">
<router-link
class="m-r-10"
:to="{
path: '/application/sms/detail',
query: {
type: scope.$index === 0 ? 'ali' : 'tencent',
},
}"
>
<el-button type="text">设置</el-button>
</router-link>
</template>
</el-table-column>
</el-table>
</el-card>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { apiSmsLists } from '@/api/application'
/** Data Start **/
let tableData = ref<Array<object>>([])
/** Data End **/
/** Methods Start **/
const getSmsLists = async (): Promise<void> => {
const res: any = await apiSmsLists()
tableData.value = [{ ...res.ali }, { ...res.tencent }]
}
/** Methods Start **/
/** Life Cycle Start **/
getSmsLists()
/** Life Cycle Start **/
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,88 @@
<!-- APP商城 -->
<template>
<!-- Main Form Start -->
<el-form ref="formRef" :model="formData" label-width="140px" size="small">
<!-- APP 下载 -->
<el-card shadow="never" class="m-t-16">
<template #header>APP下载</template>
<!-- 提示 -->
<div class="ls-card-alert">
<el-alert
class="xs"
title="苹果APP可通过上架APP至苹果App Store获取下载链接安卓APP可通过上架APP至应用宝获取下载链接下载链接也可使用蒲公英等分发渠道的链接。"
type="primary"
:closable="false"
show-icon
/>
</div>
<el-form-item label="苹果APP下载链接">
<el-input class="ls-input m-r-10" v-model="formData.ios_download_url"></el-input>
</el-form-item>
<el-form-item label="安卓APP下载链接">
<el-input class="ls-input m-r-10" v-model="formData.android_download_url"></el-input>
</el-form-item>
<el-form-item label="APP下载引导文案">
<el-input class="ls-input m-r-10" v-model="formData.download_title"></el-input>
<div class="muted xs m-r-16">分享APP页面打开后H5页面顶部会显示APP下载引导文案</div>
</el-form-item>
</el-card>
</el-form>
<!-- Main Form End -->
<!-- Footer Start -->
<footer-btns>
<el-button type="primary" size="small" @click="onSubmit">保存</el-button>
</footer-btns>
<!-- Footer End -->
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import {
apiAppSettings,
apiAppSettingsSet
} from '@/api/channel/app_store'
import { AppSettings_Req } from '@/api/channel/app_store.d.ts'
import FooterBtns from '@/components/footer-btns/index.vue'
/** Data Start **/
const formData = ref<AppSettings_Req>({
ios_download_url: '', // 苹果APP下载链接
android_download_url: '', // 安卓APP下载链接
download_title: '', // APP下载引导文案
})
/** Data End **/
/** Methods Start **/
// 获取APP配置
const getAppSettings = async (): Promise<void> => {
formData.value = await apiAppSettings()
}
// 编辑App配置
const handleAppStoreEdit = async (): Promise<void> => {
await apiAppSettingsSet({ ...formData.value })
getAppSettings()
}
// 提交数据
const onSubmit = (): void => {
handleAppStoreEdit()
}
/** Methods End **/
/** LifeCycle Start **/
getAppSettings()
/** LifeCycle End **/
</script>
<style lang="scss" scoped>
.ls-input {
width: 280px;
}
.ls-card-alert {
border-radius: 8px;
background-color: #ffffff;
padding: 0 24px 24px 24px;
flex: 1;
}
</style>

View File

@@ -0,0 +1,73 @@
<!-- H5 -->
<template>
<!-- Main Form Start -->
<el-form ref="formRef" :model="formData" label-width="140px" size="small">
<!-- 微信小程序 -->
<el-card shadow="never" class="ls-card m-t-16">
<el-form-item label="H5状态">
<!-- switch开关 -->
<div class="flex col-center">
<el-switch v-model="formData.status" :active-value="1" :inactive-value="0" />
<span class="m-l-16">{{ formData.status ? '开启' : '关闭' }}</span>
</div>
<div class="muted xs m-r-16">渠道状态为关闭时将不对外提供服务请谨慎操作</div>
</el-form-item>
<el-form-item label="访问链接">
<el-input class="ls-input m-r-10" v-model="formData.url" disabled></el-input>
<el-button v-on:copy="formData.url">复制</el-button>
</el-form-item>
</el-card>
</el-form>
<!-- Main Form End -->
<!-- Footer Start -->
<footer-btns>
<el-button type="primary" size="small" @click="onSubmit">保存</el-button>
</footer-btns>
<!-- Footer End -->
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import {
apiH5Settings,
apiH5SettingsSet
} from '@/api/channel/h5_store'
import FooterBtns from '@/components/footer-btns/index.vue'
/** Interface Start **/
interface formDataObj {
status?: number | string // 状态 0-关闭 1-开启
url: string // 访问链接
}
/** Interface End **/
/** Data Start **/
const formData = ref<formDataObj>({
status: 1,
url: '',
})
/** Data End **/
/** Methods Start **/
// 获取H5设置
const getH5Settings = async (): Promise<void> => {
formData.value = await apiH5Settings()
}
// 修改H5设置
const onSubmit = async (): Promise<void> => {
await apiH5SettingsSet({ status: formData.value.status })
getH5Settings()
}
/** Methods End **/
/** LifeCycle Start **/
getH5Settings()
/** LifeCycle End **/
</script>
<style lang="scss" scoped>
.ls-input {
width: 280px;
}
</style>

View File

@@ -0,0 +1,29 @@
<template>
<div class="home">
<div :style="{color: styleConfig.primary}">{{cartNum}}</div>
<el-button type="primary" @click="addCartNum">主要按钮</el-button>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";
import { Getter, Mutation } from "vuex-class";
@Component({
components: {},
watch: {
cartNum(val) {
console.log(val)
}
}
})
export default class Home extends Vue {
@Getter('cartNum') cartNum!: number
@Mutation('addCartNum') addCartNum!: () => void
created() {
console.log(this.cartNum, )
}
}
</script>

Some files were not shown because too many files have changed in this diff Show More