mirror of
https://gitee.com/likeadmin/likeadmin_java.git
synced 2026-06-15 21:02:15 +08:00
初始化admin pc端
This commit is contained in:
49
admin/src/App.vue
Normal file
49
admin/src/App.vue
Normal 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
44
admin/src/api/app.ts
Normal 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')
|
||||
}
|
||||
31
admin/src/api/application.ts
Normal file
31
admin/src/api/application.ts
Normal 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
55
admin/src/api/auth.ts
Normal 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
17
admin/src/api/channel/app_store.d.ts
vendored
Normal 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设置 **/
|
||||
11
admin/src/api/channel/app_store.ts
Normal file
11
admin/src/api/channel/app_store.ts
Normal 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设置 **/
|
||||
10
admin/src/api/channel/h5_store.ts
Normal file
10
admin/src/api/channel/h5_store.ts
Normal 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设置 **/
|
||||
10
admin/src/api/channel/mp_toutiao.ts
Normal file
10
admin/src/api/channel/mp_toutiao.ts
Normal 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
49
admin/src/api/channel/mp_wechat.d.ts
vendored
Normal 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 菜单配置 **/
|
||||
60
admin/src/api/channel/mp_wechat.ts
Normal file
60
admin/src/api/channel/mp_wechat.ts
Normal 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
32
admin/src/api/channel/wechat_app.d.ts
vendored
Normal 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 微信小程序设置 **/
|
||||
11
admin/src/api/channel/wechat_app.ts
Normal file
11
admin/src/api/channel/wechat_app.ts
Normal 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 微信小程序设置 **/
|
||||
11
admin/src/api/channel/wechat_platform.ts
Normal file
11
admin/src/api/channel/wechat_platform.ts
Normal 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
101
admin/src/api/decoration.ts
Normal 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 */
|
||||
70
admin/src/api/information.ts
Normal file
70
admin/src/api/information.ts
Normal 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
65
admin/src/api/setting.ts
Normal 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
17
admin/src/api/user.ts
Normal 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')
|
||||
}
|
||||
6
admin/src/api/workbench.ts
Normal file
6
admin/src/api/workbench.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 工作台主页
|
||||
export function apiWorkbench() {
|
||||
return request.get('/Workbench/index')
|
||||
}
|
||||
539
admin/src/assets/font/demo.css
Normal file
539
admin/src/assets/font/demo.css
Normal 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;
|
||||
}
|
||||
282
admin/src/assets/font/demo_index.html
Normal file
282
admin/src/assets/font/demo_index.html
Normal 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"></span>
|
||||
<div class="name">quanxian</div>
|
||||
<div class="code-name">&#xe658;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">setting</div>
|
||||
<div class="code-name">&#xe659;</div>
|
||||
</li>
|
||||
|
||||
<li class="dib">
|
||||
<span class="icon iconfont"></span>
|
||||
<div class="name">home</div>
|
||||
<div class="code-name">&#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"
|
||||
><span class="iconfont">&#x33;</span>
|
||||
</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"><link rel="stylesheet" href="./iconfont.css">
|
||||
</code></pre>
|
||||
<h3 id="-">第二步:挑选相应图标并获取类名,应用于页面:</h3>
|
||||
<pre><code class="language-html"><span class="iconfont icon-xxx"></span>
|
||||
</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"><script src="./iconfont.js"></script>
|
||||
</code></pre>
|
||||
<h3 id="-css-">第二步:加入通用 CSS 代码(引入一次就行):</h3>
|
||||
<pre><code class="language-html"><style>
|
||||
.icon {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
vertical-align: -0.15em;
|
||||
fill: currentColor;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
</code></pre>
|
||||
<h3 id="-">第三步:挑选相应图标并获取类名,应用于页面:</h3>
|
||||
<pre><code class="language-html"><svg class="icon" aria-hidden="true">
|
||||
<use xlink:href="#icon-xxx"></use>
|
||||
</svg>
|
||||
</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>
|
||||
26
admin/src/assets/font/iconfont.css
Normal file
26
admin/src/assets/font/iconfont.css
Normal 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';
|
||||
}
|
||||
30
admin/src/assets/font/iconfont.json
Normal file
30
admin/src/assets/font/iconfont.json
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
BIN
admin/src/assets/font/iconfont.ttf
Normal file
BIN
admin/src/assets/font/iconfont.ttf
Normal file
Binary file not shown.
BIN
admin/src/assets/font/iconfont.woff
Normal file
BIN
admin/src/assets/font/iconfont.woff
Normal file
Binary file not shown.
BIN
admin/src/assets/font/iconfont.woff2
Normal file
BIN
admin/src/assets/font/iconfont.woff2
Normal file
Binary file not shown.
BIN
admin/src/assets/images/icon_folder.png
Normal file
BIN
admin/src/assets/images/icon_folder.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 605 B |
BIN
admin/src/assets/images/no_perm.png
Normal file
BIN
admin/src/assets/images/no_perm.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
53
admin/src/components/del-wrap/index.vue
Normal file
53
admin/src/components/del-wrap/index.vue
Normal 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>
|
||||
237
admin/src/components/editor/index copy.vue
Normal file
237
admin/src/components/editor/index copy.vue
Normal 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>
|
||||
124
admin/src/components/editor/index.vue
Normal file
124
admin/src/components/editor/index.vue
Normal 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>
|
||||
29
admin/src/components/footer-btns/index.vue
Normal file
29
admin/src/components/footer-btns/index.vue
Normal 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>
|
||||
57
admin/src/components/material-select/file-item.vue
Normal file
57
admin/src/components/material-select/file-item.vue
Normal 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>
|
||||
162
admin/src/components/material-select/hook.ts
Normal file
162
admin/src/components/material-select/hook.ts
Normal 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
|
||||
}
|
||||
}
|
||||
255
admin/src/components/material-select/index.vue
Normal file
255
admin/src/components/material-select/index.vue
Normal 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>
|
||||
347
admin/src/components/material-select/material.vue
Normal file
347
admin/src/components/material-select/material.vue
Normal 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>
|
||||
64
admin/src/components/pagination/index.vue
Normal file
64
admin/src/components/pagination/index.vue
Normal 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>
|
||||
78
admin/src/components/popover-input/index.vue
Normal file
78
admin/src/components/popover-input/index.vue
Normal 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>
|
||||
136
admin/src/components/popup/index.vue
Normal file
136
admin/src/components/popup/index.vue
Normal 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>
|
||||
131
admin/src/components/upload/index.vue
Normal file
131
admin/src/components/upload/index.vue
Normal 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
7
admin/src/config/app.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
// 标题
|
||||
export const title = 'likeadmin'
|
||||
|
||||
// terminal
|
||||
export const terminal = 1
|
||||
// 版本
|
||||
export const version = '1.0.0'
|
||||
3
admin/src/config/cachekey.ts
Normal file
3
admin/src/config/cachekey.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const TOKEN = 'token'
|
||||
// 缓存key前缀
|
||||
export const ACCOUNT = 'account'
|
||||
9
admin/src/core/directives/index.ts
Normal file
9
admin/src/core/directives/index.ts
Normal 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)
|
||||
})
|
||||
}
|
||||
32
admin/src/core/directives/modules/copy.ts
Normal file
32
admin/src/core/directives/modules/copy.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
14
admin/src/core/hooks/app.ts
Normal file
14
admin/src/core/hooks/app.ts
Normal 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
|
||||
}
|
||||
}
|
||||
68
admin/src/core/hooks/pages.ts
Normal file
68
admin/src/core/hooks/pages.ts
Normal 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
19
admin/src/env.d.ts
vendored
Normal 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
|
||||
}
|
||||
89
admin/src/layout/components/layout-aside/index.vue
Normal file
89
admin/src/layout/components/layout-aside/index.vue
Normal 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>
|
||||
38
admin/src/layout/components/layout-aside/sub-menu.vue
Normal file
38
admin/src/layout/components/layout-aside/sub-menu.vue
Normal 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>
|
||||
67
admin/src/layout/components/layout-header.vue
Normal file
67
admin/src/layout/components/layout-header.vue
Normal 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>
|
||||
26
admin/src/layout/components/layout-main.vue
Normal file
26
admin/src/layout/components/layout-main.vue
Normal 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>
|
||||
57
admin/src/layout/components/perm.vue
Normal file
57
admin/src/layout/components/perm.vue
Normal 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>
|
||||
44
admin/src/layout/index.vue
Normal file
44
admin/src/layout/index.vue
Normal 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
26
admin/src/main.ts
Normal 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
45
admin/src/permission.ts
Normal 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()
|
||||
})
|
||||
11
admin/src/plugins/element.ts
Normal file
11
admin/src/plugins/element.ts
Normal 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])
|
||||
})
|
||||
}
|
||||
28
admin/src/plugins/vue-echarts.ts
Normal file
28
admin/src/plugins/vue-echarts.ts
Normal 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
55
admin/src/router/index.ts
Normal 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
|
||||
40
admin/src/router/modules/application.ts
Normal file
40
admin/src/router/modules/application.ts
Normal 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
|
||||
106
admin/src/router/modules/channel.ts
Normal file
106
admin/src/router/modules/channel.ts
Normal 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
|
||||
66
admin/src/router/modules/content.ts
Normal file
66
admin/src/router/modules/content.ts
Normal 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
|
||||
38
admin/src/router/modules/decoration.ts
Normal file
38
admin/src/router/modules/decoration.ts
Normal 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
|
||||
40
admin/src/router/modules/permission.ts
Normal file
40
admin/src/router/modules/permission.ts
Normal 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
|
||||
99
admin/src/router/modules/setting.ts
Normal file
99
admin/src/router/modules/setting.ts
Normal 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
|
||||
16
admin/src/store/getters.ts
Normal file
16
admin/src/store/getters.ts
Normal 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
17
admin/src/store/index.ts
Normal 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
|
||||
29
admin/src/store/modules/app.ts
Normal file
29
admin/src/store/modules/app.ts
Normal 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
|
||||
14
admin/src/store/modules/index.ts
Normal file
14
admin/src/store/modules/index.ts
Normal 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
|
||||
}
|
||||
46
admin/src/store/modules/permission.ts
Normal file
46
admin/src/store/modules/permission.ts
Normal 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
|
||||
73
admin/src/store/modules/user.ts
Normal file
73
admin/src/store/modules/user.ts
Normal 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
|
||||
298
admin/src/styles/common.scss
Normal file
298
admin/src/styles/common.scss
Normal 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;
|
||||
}
|
||||
99
admin/src/styles/element.scss
Normal file
99
admin/src/styles/element.scss
Normal 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);
|
||||
}
|
||||
13
admin/src/styles/export.module.scss
Normal file
13
admin/src/styles/export.module.scss
Normal 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;
|
||||
}
|
||||
15
admin/src/styles/export.module.scss.d.ts
vendored
Normal file
15
admin/src/styles/export.module.scss.d.ts
vendored
Normal 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
|
||||
10
admin/src/styles/index.scss
Normal file
10
admin/src/styles/index.scss
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
全局样式
|
||||
*/
|
||||
|
||||
// 初始化默认样式
|
||||
@import 'reset';
|
||||
// 公共样式
|
||||
@import 'common';
|
||||
// element样式
|
||||
@import 'element';
|
||||
83
admin/src/styles/reset.scss
Normal file
83
admin/src/styles/reset.scss
Normal 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; //自定义颜色
|
||||
}
|
||||
142
admin/src/styles/variables.scss
Normal file
142
admin/src/styles/variables.scss
Normal 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
50
admin/src/utils/cache.ts
Normal 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
0
admin/src/utils/enum.ts
Normal file
86
admin/src/utils/request.ts
Normal file
86
admin/src/utils/request.ts
Normal 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
5
admin/src/utils/type.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
// 页面模式
|
||||
export enum PageMode {
|
||||
'ADD' = 'add', // 添加
|
||||
'EDIT' = 'edit' // 编辑
|
||||
}
|
||||
170
admin/src/utils/util.ts
Normal file
170
admin/src/utils/util.ts
Normal 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
|
||||
}
|
||||
BIN
admin/src/views/account/images/login_bg.png
Normal file
BIN
admin/src/views/account/images/login_bg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 594 KiB |
183
admin/src/views/account/login.vue
Normal file
183
admin/src/views/account/login.vue
Normal 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>
|
||||
365
admin/src/views/application/notification/detail.vue
Normal file
365
admin/src/views/application/notification/detail.vue
Normal 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>
|
||||
105
admin/src/views/application/notification/index.vue
Normal file
105
admin/src/views/application/notification/index.vue
Normal 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>
|
||||
109
admin/src/views/application/sms/detail.vue
Normal file
109
admin/src/views/application/sms/detail.vue
Normal 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>
|
||||
53
admin/src/views/application/sms/index.vue
Normal file
53
admin/src/views/application/sms/index.vue
Normal 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>
|
||||
88
admin/src/views/channel/app_store/index.vue
Normal file
88
admin/src/views/channel/app_store/index.vue
Normal 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>
|
||||
73
admin/src/views/channel/h5_store/index.vue
Normal file
73
admin/src/views/channel/h5_store/index.vue
Normal 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>
|
||||
29
admin/src/views/channel/index.vue
Normal file
29
admin/src/views/channel/index.vue
Normal 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>
|
||||
|
||||
126
admin/src/views/channel/mp_wechat/components/menu-form.vue
Normal file
126
admin/src/views/channel/mp_wechat/components/menu-form.vue
Normal file
@@ -0,0 +1,126 @@
|
||||
<template>
|
||||
<el-form :inline="true" label-position="top" :model="form" :rules="rules" ref="menuFormRef">
|
||||
<el-form-item label="菜单名称" prop="name" v-if="mode !== 'index'">
|
||||
<el-input class="ls-input-menu" v-model="form.name"></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="mode !== 'index' ? '菜单类型' : ''" prop="type">
|
||||
<el-select v-model="form.type" placeholder="请选择">
|
||||
<el-option label="网页" value="view"></el-option>
|
||||
<el-option label="小程序" value="miniprogram"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<div v-if="form.type === 'view'">
|
||||
<el-form-item label="网址" prop="url" required>
|
||||
<el-input class="ls-input-menu" v-model="form.url"></el-input>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div v-if="form.type === 'miniprogram'">
|
||||
<el-form-item label="网址" prop="url" required>
|
||||
<el-input class="ls-input-menu" v-model="form.url"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="AppID" prop="appid" required>
|
||||
<el-input class="ls-input-menu" v-model="form.appid"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="路径" prop="pagepath" required>
|
||||
<el-input class="ls-input-menu" v-model="form.pagepath"></el-input>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Ref, ref, watch, watchEffect, withDefaults } from 'vue'
|
||||
|
||||
/** Emit Start **/
|
||||
const emit = defineEmits(['update:name', 'update:type', 'update:url', 'update:appid', 'update:pagepath'])
|
||||
/** Emit End **/
|
||||
|
||||
/** Props Start **/
|
||||
const props = withDefaults(defineProps<{
|
||||
mode: any
|
||||
name?: string
|
||||
type?: string
|
||||
url?: string
|
||||
appid?: string
|
||||
pagepath?: string
|
||||
}>(), {
|
||||
mode: 'normal',
|
||||
name: '',
|
||||
type: '',
|
||||
url: '',
|
||||
appid: '',
|
||||
pagepath: ''
|
||||
})
|
||||
/** Props End **/
|
||||
|
||||
/** Data Start **/
|
||||
const menuFormRef = ref<any>(null)
|
||||
// 表单数据
|
||||
let form = ref({...props})
|
||||
|
||||
// 表单检验
|
||||
const rules = ref<object>({
|
||||
name: [
|
||||
{ required: true, message: '必填项不能为空', trigger: ['blur', 'change'] },
|
||||
{ min: 1, max: 18, message: '长度限制18个字符', trigger: ['blur', 'change'] }
|
||||
],
|
||||
type: [{ required: true, message: '必填项不能为空', trigger: ['blur', 'change'] }],
|
||||
url: [
|
||||
{ required: true, message: '必填项不能为空', trigger: ['blur', 'change'] },
|
||||
{
|
||||
pattern: /[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(\.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+\.?/,
|
||||
message: '请输入合法链接',
|
||||
trigger: ['blur', 'change']
|
||||
}
|
||||
],
|
||||
appid: [
|
||||
{
|
||||
validator: (rule: object, value: string, callback: Function) => {
|
||||
if (value || form.value.type !== 'miniprogram') callback()
|
||||
else callback(new Error())
|
||||
},
|
||||
message: '必填项不能为空',
|
||||
trigger: ['blur', 'change']
|
||||
}
|
||||
],
|
||||
pagepath: [
|
||||
{
|
||||
validator: (rule: object, value: string, callback: Function) => {
|
||||
if (value || form.value.type !== 'miniprogram') callback()
|
||||
else callback(new Error())
|
||||
},
|
||||
message: '必填项不能为空',
|
||||
trigger: ['blur', 'change']
|
||||
}
|
||||
],
|
||||
})
|
||||
/** Data End **/
|
||||
|
||||
/** Watch Start **/
|
||||
watchEffect(() => {
|
||||
form.value = props
|
||||
if(props.mode === 'index') {
|
||||
emit('update:appid', form.value.appid)
|
||||
emit('update:url', form.value.url)
|
||||
emit('update:type', form.value.type)
|
||||
emit('update:pagepath', form.value.pagepath)
|
||||
}
|
||||
})
|
||||
/** Watch End **/
|
||||
|
||||
defineExpose({
|
||||
menuFormRef,
|
||||
form,
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
:deep .ls-input-menu {
|
||||
width: 280px;
|
||||
}
|
||||
|
||||
</style>
|
||||
239
admin/src/views/channel/mp_wechat/index.vue
Normal file
239
admin/src/views/channel/mp_wechat/index.vue
Normal file
@@ -0,0 +1,239 @@
|
||||
<template>
|
||||
<!-- Header Alert Start -->
|
||||
<el-card shadow="never">
|
||||
<el-alert
|
||||
title="温馨提示:请先前往微信公众号后台申请认证微信公众号-服务号。"
|
||||
type="primary"
|
||||
:closable="false"
|
||||
show-icon
|
||||
/>
|
||||
</el-card>
|
||||
<!-- Header Alert End -->
|
||||
|
||||
<el-form ref="formRef" :model="formData" :rules="rules" label-width="120px" size="small">
|
||||
<!-- 微信公众号 Start -->
|
||||
<el-card shadow="never" class="m-t-16">
|
||||
<template #header>
|
||||
<span class="nr f-w-500">微信公众号</span>
|
||||
</template>
|
||||
<div class="m-t-24">
|
||||
<el-form-item label="名称" prop="name">
|
||||
<el-input class="ls-input" v-model="formData.name" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item label="原始ID" prop="original_id">
|
||||
<el-input class="ls-input" v-model="formData.original_id" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item label="二维码" prop="qr_code">
|
||||
<material-select :limit="1" v-model="formData.qr_code" />
|
||||
<div class="muted xs m-r-16">建议尺寸:宽400px*高400px。jpg,jpeg,png格式</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-card>
|
||||
<!-- 微信公众号 End -->
|
||||
|
||||
<!-- 公众号开发者信息 Start -->
|
||||
<el-card shadow="never" class="m-t-16">
|
||||
<template #header>
|
||||
<span class="nr f-w-500">公众号开发者信息</span>
|
||||
</template>
|
||||
<div class="m-t-24">
|
||||
<el-form-item label="AppID" prop="app_id">
|
||||
<el-input class="ls-input" v-model="formData.app_id" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-form-item label="AppSecret" prop="app_secret">
|
||||
<el-input class="ls-input" v-model="formData.app_secret" show-word-limit />
|
||||
<div class="muted xs m-r-16">登录微信公众平台,点击开发>基本配置>公众号开发信息,设置AppID和AppSecret</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-card>
|
||||
<!-- 公众号开发者信息 End -->
|
||||
|
||||
<!-- 服务器配置 Start -->
|
||||
<el-card shadow="never" class="m-t-16">
|
||||
<template #header>
|
||||
<span class="nr f-w-500">服务器配置</span>
|
||||
</template>
|
||||
<div class="m-t-24">
|
||||
<el-form-item label="URL">
|
||||
<el-input
|
||||
class="ls-input m-r-16"
|
||||
v-model="formData.url"
|
||||
show-word-limit
|
||||
disabled
|
||||
/>
|
||||
<el-button v-on:copy="formData.url">复制</el-button>
|
||||
<div class="muted xs">登录微信公众平台,点击开发>基本配置>服务器配置,填写服务器地址(URL)</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="Token" prop="token">
|
||||
<el-input class="ls-input" v-model="formData.token" show-word-limit />
|
||||
<div class="muted xs">登录微信公众平台,点击开发>基本配置>服务器配置,设置令牌Token。不填默认为“likeshop”</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="EncodingAESKey" prop="encoding_aes_key">
|
||||
<el-input class="ls-input" v-model="formData.encoding_aes_key" show-word-limit />
|
||||
<div class="muted xs">消息加密密钥由43位字符组成,字符范围为A-Z,a-z,0-9</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="消息加密方式" prop="encryption_type">
|
||||
<el-radio-group v-model="formData.encryption_type">
|
||||
<el-radio :label="1" class="form__item-encryption">明文模式 (不使用消息体加解密功能,安全系数较低)</el-radio>
|
||||
<el-radio
|
||||
:label="2"
|
||||
class="form__item-encryption"
|
||||
>兼容模式 (明文、密文将共存,方便开发者调试和维护)</el-radio>
|
||||
<el-radio
|
||||
:label="3"
|
||||
class="form__item-encryption"
|
||||
>安全模式(推荐) (消息包为纯密文,需要开发者加密和解密,安全系数高)</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-card>
|
||||
<!-- 服务器配置 End -->
|
||||
|
||||
<!-- 功能设置 Start -->
|
||||
<el-card shadow="never" class="m-t-16">
|
||||
<template #header>
|
||||
<span class="nr f-w-500">功能设置</span>
|
||||
</template>
|
||||
<div class="m-t-24">
|
||||
<el-form-item label="业务域名">
|
||||
<el-input
|
||||
class="ls-input m-r-16"
|
||||
v-model="formData.business_domain"
|
||||
show-word-limit
|
||||
disabled
|
||||
/>
|
||||
<el-button class="m-l-16" v-on:copy="formData.business_domain">复制</el-button>
|
||||
<div class="muted xs">登录微信公众平台,点击设置>公众号设置>功能设置,填写业务域名</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="JS接口安全域名">
|
||||
<el-input
|
||||
class="ls-input m-r-16"
|
||||
v-model="formData.js_secure_domain"
|
||||
show-word-limit
|
||||
disabled
|
||||
/>
|
||||
<el-button class="m-l-16" v-on:copy="formData.js_secure_domain">复制</el-button>
|
||||
<div class="muted xs">登录微信公众平台,点击设置>公众号设置>功能设置,填写JS接口安全域名</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="网页授权域名">
|
||||
<el-input
|
||||
class="ls-input m-r-16"
|
||||
v-model="formData.web_auth_domain"
|
||||
show-word-limit
|
||||
disabled
|
||||
/>
|
||||
<el-button v-on:copy="formData.web_auth_domain">复制</el-button>
|
||||
<div class="muted xs">登录微信公众平台,点击设置>公众号设置>功能设置,填写网页授权域名</div>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-card>
|
||||
<!-- 功能设置 Start -->
|
||||
</el-form>
|
||||
|
||||
<!-- Footer Start -->
|
||||
<footer-btns>
|
||||
<el-button size="small" @click="onResetFrom">重置</el-button>
|
||||
<el-button type="primary" size="small" @click="onSubmitFrom(formRef)">保存</el-button>
|
||||
</footer-btns>
|
||||
<!-- Footer End -->
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from 'vue'
|
||||
import MaterialSelect from '@/components/material-select/index.vue'
|
||||
import { apiMpWeChatConfigEdit, apiMPWeChatConfigInfo } from '@/api/channel/mp_wechat'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { ElForm } from "element-plus";
|
||||
import FooterBtns from '@/components/footer-btns/index.vue'
|
||||
|
||||
/** Interface Start **/
|
||||
interface formDataObj {
|
||||
name: string
|
||||
original_id: string
|
||||
qr_code: string
|
||||
app_id: string
|
||||
app_secret: string
|
||||
url?: string
|
||||
token: string
|
||||
encoding_aes_key: string
|
||||
encryption_type: string
|
||||
business_domain?: string
|
||||
js_secure_domain?: string
|
||||
web_auth_domain?: string
|
||||
}
|
||||
|
||||
type FormInstance = InstanceType<typeof ElForm>
|
||||
const formRef = ref<FormInstance>();
|
||||
/** Interface End **/
|
||||
|
||||
/** Data Start **/
|
||||
// 表单数据
|
||||
let formData = ref<formDataObj>({
|
||||
name: '', // 公众号名称
|
||||
original_id: '', // 原始id
|
||||
qr_code: '', // 二维码
|
||||
app_id: '', // APP ID
|
||||
app_secret: '', // App Secret
|
||||
url: '', // URL
|
||||
token: '', // Token
|
||||
encoding_aes_key: '', // Encoding AES Key
|
||||
encryption_type: '', // 消息加密方式: 1-明文模式 2-兼容模式 3-安全模式
|
||||
business_domain: '', // 业务域名
|
||||
js_secure_domain: '', // JS接口安全域名
|
||||
web_auth_domain: '' // 网页授权域名
|
||||
})
|
||||
// 表单验证
|
||||
const rules = reactive<object>({
|
||||
app_id: [{ required: true, message: '必填项不能为空', trigger: 'blur' }],
|
||||
app_secret: [{ required: true, message: '必填项不能为空', trigger: 'blur' }],
|
||||
encryption_type: [{ required: true, message: '必填项不能为空', trigger: 'blur' }],
|
||||
})
|
||||
/** Data End **/
|
||||
|
||||
|
||||
/** S Methods **/
|
||||
// 初始化表单数据
|
||||
const initFormData = async (): Promise<void> => {
|
||||
formData.value = await apiMPWeChatConfigInfo()
|
||||
console.log(formData.value)
|
||||
}
|
||||
|
||||
// 重置表单数据
|
||||
const onResetFrom = () => {
|
||||
initFormData()
|
||||
ElMessage.info('已重置数据')
|
||||
}
|
||||
|
||||
// 编辑数据
|
||||
const handleFormEdit = async (): Promise<void> => {
|
||||
await apiMpWeChatConfigEdit({ ...formData.value })
|
||||
initFormData()
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const onSubmitFrom = async (formEl: FormInstance | undefined): Promise<void> => {
|
||||
if (!formEl) return
|
||||
formEl.validate((valid): boolean | undefined => {
|
||||
if (!valid) return false
|
||||
handleFormEdit()
|
||||
})
|
||||
}
|
||||
/** E Methods **/
|
||||
|
||||
|
||||
/** S Life Cycle **/
|
||||
initFormData()
|
||||
/** E Life Cycle **/
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.ls-input {
|
||||
width: 280px;
|
||||
}
|
||||
.form__item-encryption {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 3em;
|
||||
}
|
||||
</style>
|
||||
392
admin/src/views/channel/mp_wechat/menu.vue
Normal file
392
admin/src/views/channel/mp_wechat/menu.vue
Normal file
@@ -0,0 +1,392 @@
|
||||
<template>
|
||||
<!-- Header Alert Start -->
|
||||
<el-card shadow="never">
|
||||
<el-alert
|
||||
title="温馨提示:点击保存并发布菜单后,菜单才会发布至微信公众号,需提前设置好公众号相关配置。"
|
||||
type="primary"
|
||||
:closable="false"
|
||||
show-icon
|
||||
/>
|
||||
</el-card>
|
||||
<!-- Header Alert End -->
|
||||
|
||||
<div class="flex m-t-16">
|
||||
<!-- Phone Start -->
|
||||
<el-card shadow="never">
|
||||
<div class="mp_wechat__phone">
|
||||
<div class="mp_wechat__phone-menu mp_wechat__phone-active">
|
||||
<div
|
||||
class="mp_wechat__phone-menu-item"
|
||||
v-for="(item, index) in formData.menu"
|
||||
:key="index"
|
||||
>
|
||||
<div class="mp_wechat__phone-menu-item--title">
|
||||
<span>{{ item.name }}</span>
|
||||
</div>
|
||||
<div class="mp_wechat__phone-submenu" v-show="item.has_menu">
|
||||
<div
|
||||
class="mp_wechat__phone-submenu-item"
|
||||
v-for="(item2, index2) in item.sub_button"
|
||||
:key="index2"
|
||||
>{{ item2.name }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
<!-- Phone End -->
|
||||
|
||||
<!-- Menu Config Start -->
|
||||
<el-card shadow="never" class="m-l-16 mp_wechat__form">
|
||||
<div class="mp_wechat__form--title">菜单配置</div>
|
||||
<div class="m-t-16">
|
||||
<el-button type="primary" plain size="small" @click="onMenuAdd">
|
||||
<i class="el-icon-plus"></i>
|
||||
<span>新增主菜单({{ formData.menu.length || 0 }}/3)</span>
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="mp_wechat__form--content m-t-24">
|
||||
<div class="menu-item" v-for="(item, index) in formData.menu" :key="index">
|
||||
<!-- 删除菜单 -->
|
||||
<div class="menu-item__delete">
|
||||
<popup class="m-l-10 inline" top="35vh" @confirm="handleMenuDel(index)">
|
||||
<template #trigger>
|
||||
<el-icon size="16">
|
||||
<close />
|
||||
</el-icon>
|
||||
</template>
|
||||
</popup>
|
||||
</div>
|
||||
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData.menu[index]"
|
||||
:rules="rules"
|
||||
label-position="top"
|
||||
size="small"
|
||||
>
|
||||
<!-- 主菜单名称 -->
|
||||
<el-form-item label="主菜单" prop="name">
|
||||
<el-input
|
||||
class="ls-input"
|
||||
v-model="formData.menu[index].name"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
<!-- 主菜单类型 -->
|
||||
<el-form-item label="主菜单类型">
|
||||
<el-radio-group v-model="formData.menu[index].has_menu">
|
||||
<el-radio :label="false">不配置子菜单</el-radio>
|
||||
<el-radio :label="true">配置子菜单</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<!-- 没有子菜单 -->
|
||||
<div v-if="!formData.menu[index].has_menu">
|
||||
<MPWechatMenuForm
|
||||
mode="index"
|
||||
v-model:url="formData.menu[index].url"
|
||||
v-model:appid="formData.menu[index].appid"
|
||||
v-model:type="formData.menu[index].type"
|
||||
v-model:pagepath="formData.menu[index].pagepath"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 存在子菜单 -->
|
||||
<div v-if="formData.menu[index].has_menu">
|
||||
<ul>
|
||||
<li
|
||||
class="flex"
|
||||
v-for="(subItem, subIndex) in formData.menu[index].sub_button"
|
||||
:key="subIndex"
|
||||
style="padding: 8px;"
|
||||
>
|
||||
<span style="margin-right: auto">{{ subItem.name }}</span>
|
||||
<!-- 编辑子菜单 -->
|
||||
<popup
|
||||
top="40vh"
|
||||
title="子菜单"
|
||||
:async="true"
|
||||
:clickModalClose="false"
|
||||
@close="handleSubMenuReset"
|
||||
@confirm="onSubMenuEdit(index, subIndex)"
|
||||
:ref="(el: Event | any) => subMenuFormEditPopupRef = el"
|
||||
>
|
||||
<MPWechatMenuForm
|
||||
:name="subItem.name"
|
||||
:url="subItem.url"
|
||||
:appid="subItem.appid"
|
||||
:type="subItem.type"
|
||||
:pagepath="subItem.pagepath"
|
||||
:ref="(el: Event | any) => subMenuFormEditRef = el"
|
||||
/>
|
||||
<template #trigger>
|
||||
<el-icon size="16">
|
||||
<Edit />
|
||||
</el-icon>
|
||||
</template>
|
||||
</popup>
|
||||
<!-- 删除子菜单 -->
|
||||
<popup
|
||||
class="m-l-10 inline"
|
||||
top="35vh"
|
||||
@confirm="handleSubMenuDel(index, subIndex)"
|
||||
>
|
||||
<template #trigger>
|
||||
<el-icon size="16">
|
||||
<close />
|
||||
</el-icon>
|
||||
</template>
|
||||
</popup>
|
||||
</li>
|
||||
</ul>
|
||||
<popup
|
||||
top="40vh"
|
||||
:async="true"
|
||||
:clickModalClose="false"
|
||||
title="新增子菜单"
|
||||
:ref="(el: Event | any) => subMenuFormAddPopupRef = el"
|
||||
@confirm="handleSubMenuAdd(index)"
|
||||
>
|
||||
<MPWechatMenuForm
|
||||
:ref="(el: Event | any) => subMenuFormAddRef = el"
|
||||
/>
|
||||
<template #trigger>
|
||||
<el-button
|
||||
type="text"
|
||||
size="small"
|
||||
>添加子菜单({{ formData.menu[index].sub_button.length || 0 }}/5)</el-button>
|
||||
</template>
|
||||
</popup>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
<!-- Menu Config Start -->
|
||||
</div>
|
||||
|
||||
<!-- Footer Start -->
|
||||
<footer-btns>
|
||||
<el-button size="small" @click="onFromSave">保存</el-button>
|
||||
<el-button type="primary" size="small" @click="onFromPublish">保存并发布</el-button>
|
||||
</footer-btns>
|
||||
<!-- Footer End -->
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from 'vue'
|
||||
import { apiMpWeChatMenuDetail, apiMpWeChatMenuSave, apiMpWeChatMenuPublish } from '@/api/channel/mp_wechat'
|
||||
import { MPWeChatMenu } from '@/api/channel/mp_wechat.d.ts'
|
||||
import Popup from '@/components/popup/index.vue'
|
||||
import FooterBtns from '@/components/footer-btns/index.vue'
|
||||
import { ElMessage } from "element-plus";
|
||||
import MPWechatMenuForm from './components/menu-form.vue'
|
||||
import { deepClone } from '@/utils/util'
|
||||
|
||||
/** Interface Start **/
|
||||
interface MenuFrom {
|
||||
menu: Array<MPWeChatMenu>
|
||||
}
|
||||
/** Interface Start **/
|
||||
|
||||
/** Data Start **/
|
||||
// 子菜单添加的Ref
|
||||
const subMenuFormAddRef = ref<InstanceType<typeof MPWechatMenuForm> | null>(null)
|
||||
// 子菜单添加的弹出窗口
|
||||
const subMenuFormAddPopupRef = ref<InstanceType<typeof Popup> | null>(null)
|
||||
// 子菜单编辑的Ref
|
||||
const subMenuFormEditRef = ref<InstanceType<typeof MPWechatMenuForm> | null>(null)
|
||||
// 子菜单编辑的弹出窗口
|
||||
const subMenuFormEditPopupRef = ref<InstanceType<typeof Popup> | null>(null)
|
||||
// 菜单数据
|
||||
const formData = ref<MenuFrom>({
|
||||
menu: []
|
||||
})
|
||||
// 校验
|
||||
const rules = reactive<object>({
|
||||
name: [
|
||||
{ required: true, message: '必填项不能为空', trigger: ['blur', 'change'] },
|
||||
{ min: 1, max: 12, message: '长度限制12个字符', trigger: ['blur', 'change'] }
|
||||
],
|
||||
})
|
||||
/** Data End **/
|
||||
|
||||
|
||||
/** Methods Start **/
|
||||
// 添加主菜单
|
||||
const onMenuAdd = () => {
|
||||
if (formData.value.menu.length >= 3)
|
||||
return ElMessage.info('主菜单仅限有3项!')
|
||||
formData.value.menu.push({
|
||||
name: '',
|
||||
type: '',
|
||||
has_menu: false,
|
||||
key: '', // 关键字
|
||||
url: '', // 网页URL
|
||||
appid: '', // 小程序AppID
|
||||
pagepath: '', // 小程序路径
|
||||
sub_button: [] as Array<object>, // 二级菜单
|
||||
})
|
||||
}
|
||||
|
||||
// 添加子菜单
|
||||
const handleSubMenuAdd = async (index: number): Promise<void> => {
|
||||
await subMenuFormAddRef.value?.menuFormRef.validate()
|
||||
|
||||
formData.value.menu[index].sub_button.push({ ...deepClone(subMenuFormAddRef.value?.menuFormRef.model) })
|
||||
ElMessage.success('添加成功')
|
||||
|
||||
subMenuFormAddPopupRef.value?.close()
|
||||
subMenuFormAddRef.value?.menuFormRef.resetFields()
|
||||
}
|
||||
|
||||
// 子菜单编辑
|
||||
const onSubMenuEdit = async (index: number, subIndex: number): Promise<void> => {
|
||||
await subMenuFormEditRef.value?.menuFormRef.validate()
|
||||
|
||||
formData.value.menu[index].sub_button[subIndex] = { ...deepClone(subMenuFormEditRef.value?.menuFormRef.model) }
|
||||
ElMessage.success('修改成功')
|
||||
|
||||
subMenuFormEditRef.value?.menuFormRef.resetFields()
|
||||
subMenuFormEditPopupRef.value?.close()
|
||||
}
|
||||
|
||||
// 取消编辑恢复数据
|
||||
const handleSubMenuReset = (): void => {
|
||||
subMenuFormEditRef.value?.menuFormRef.resetFields()
|
||||
}
|
||||
|
||||
// 删除菜单
|
||||
const handleMenuDel = (index: number): void => {
|
||||
formData.value?.menu.splice(index, 1)
|
||||
}
|
||||
|
||||
// 删除子菜单
|
||||
const handleSubMenuDel = (index: number, subIndex: number): void => {
|
||||
const menu: MPWeChatMenu = formData.value.menu[index]
|
||||
menu.sub_button.splice(subIndex, 1)
|
||||
}
|
||||
|
||||
// 初始化菜单数据
|
||||
const initMPWeChatMenuData = async (): Promise<void> => {
|
||||
formData.value.menu = await apiMpWeChatMenuDetail()
|
||||
}
|
||||
|
||||
// 表单保存
|
||||
const onFromSave = async (): Promise<void> => {
|
||||
await apiMpWeChatMenuSave({ ...formData.value })
|
||||
initMPWeChatMenuData()
|
||||
}
|
||||
|
||||
// 保存并发布
|
||||
const onFromPublish = async (): Promise<void> => {
|
||||
await apiMpWeChatMenuPublish({ ...formData.value })
|
||||
initMPWeChatMenuData()
|
||||
}
|
||||
/** Methods End **/
|
||||
|
||||
/** Life Cycle Start **/
|
||||
initMPWeChatMenuData()
|
||||
/** Life Cycle End **/
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.mp_wechat__phone {
|
||||
position: relative;
|
||||
width: 312.5px;
|
||||
height: 676.67px;
|
||||
background: url("../../../assets/images/mobile.png") no-repeat;
|
||||
background-size: 100% 100%;
|
||||
|
||||
&-menu {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 40px;
|
||||
display: flex;
|
||||
height: 60px;
|
||||
margin: 0 16px;
|
||||
border-top: 1px solid #e4e4e4;
|
||||
background-color: rgb(247, 247, 247);
|
||||
|
||||
&-item {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&:nth-child(n + 2)::before {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
height: 50%;
|
||||
display: block;
|
||||
content: "";
|
||||
border-left: 1px solid #e4e4e4;
|
||||
}
|
||||
|
||||
&--title {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.mp_wechat__phone-submenu {
|
||||
position: absolute;
|
||||
z-index: 99;
|
||||
bottom: calc(100% + 10px);
|
||||
min-width: 100px;
|
||||
background-color: rgb(247, 247, 247);
|
||||
box-shadow: 0 0 4px 1px rgba(0, 0, 0, 0.3);
|
||||
|
||||
&-item {
|
||||
padding: 16px 4px;
|
||||
}
|
||||
|
||||
&-item:nth-child(n + 2) {
|
||||
border-top: 1px solid #e4e4e4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-active {
|
||||
border: 2px dashed $color-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.mp_wechat__form {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex: 1;
|
||||
|
||||
&--title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&--content {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
.menu-item {
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
width: 340px;
|
||||
padding: 24px;
|
||||
border-radius: 8px;
|
||||
margin-right: 16px;
|
||||
margin-bottom: 16px;
|
||||
background-color: #efefef;
|
||||
overflow: hidden;
|
||||
|
||||
&__delete {
|
||||
position: absolute;
|
||||
top: 14px;
|
||||
right: 20px;
|
||||
color: $color-primary;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
129
admin/src/views/channel/mp_wechat/reply/default_reply.vue
Normal file
129
admin/src/views/channel/mp_wechat/reply/default_reply.vue
Normal file
@@ -0,0 +1,129 @@
|
||||
<template>
|
||||
<!-- Header Alert Start -->
|
||||
<el-card shadow="never">
|
||||
<el-alert
|
||||
class="xxl"
|
||||
title="温馨提示:1.粉丝在公众号发送内容时,系统无法匹配情况下发送启用的默认文本回复;2.同时只能启用一个默认回复。"
|
||||
type="primary"
|
||||
:closable="false"
|
||||
show-icon
|
||||
></el-alert>
|
||||
</el-card>
|
||||
<!-- Header Alert End -->
|
||||
|
||||
<el-card shadow="never" class="m-t-20">
|
||||
<!-- Header BtnGroup Start -->
|
||||
<el-button size="small" type="primary" @click="onReplyAdd()">新增默认回复</el-button>
|
||||
<!-- Header BtnGroup Start -->
|
||||
|
||||
<!-- Main TableData Start -->
|
||||
<div class="m-t-16">
|
||||
<el-table
|
||||
:data="pager.lists"
|
||||
style="width: 100%"
|
||||
v-loading="pager.loading"
|
||||
:default-sort="{ prop: 'level', order: 'ascending' }"
|
||||
:header-cell-style="{ 'background': '#f5f8ff' }"
|
||||
size="mini"
|
||||
>
|
||||
<el-table-column prop="name" label="规则名称" min-width="100px"></el-table-column>
|
||||
<el-table-column prop="content_type_desc" label="回复类型" min-width="100px"></el-table-column>
|
||||
<el-table-column prop="status" label="启用状态" min-width="100px">
|
||||
<template #default="scope">
|
||||
<el-switch
|
||||
v-model="scope.row.status"
|
||||
:active-value="1"
|
||||
:inactive-value="0"
|
||||
@change="putMpWeChatReplyStatus(scope.row)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column fixed="right" label="操作" min-width="120px">
|
||||
<template #default="scope">
|
||||
<!-- <el-button type="text" size="small" @click="onUserLevelEdit(scope.row)">详情</el-button> -->
|
||||
<el-button type="text" size="small" @click="onReplyEdit(scope.row)">编辑</el-button>
|
||||
<popup class="m-l-10 inline" @confirm="onMpWeChatReplyDelete(scope.row)">
|
||||
<el-button type="text" size="small" slot="trigger">删除</el-button>
|
||||
</popup>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<!-- 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 { ref } from "vue"
|
||||
import {
|
||||
apiMpWeChatReplyLists,
|
||||
apiMpWeChatReplyDelete,
|
||||
apiMpWeChatReplyStatus
|
||||
} from "@/api/channel/mp_wechat"
|
||||
import { PageMode } from "@/utils/type"
|
||||
import Popup from "@/components/popup/index.vue"
|
||||
import { useAdmin } from "@/core/hooks/app"
|
||||
import { usePages } from "@/core/hooks/pages"
|
||||
import Pagination from "@/components/pagination/index.vue"
|
||||
|
||||
/** Data Start **/
|
||||
const { router } = useAdmin();
|
||||
|
||||
let formData = ref({ reply_type: '3' as string | undefined | number }) // 回复类型 1-关注回复 2-关键词回复 3-默认回复
|
||||
const {
|
||||
pager,
|
||||
requestApi
|
||||
} = usePages({
|
||||
callback: apiMpWeChatReplyLists,
|
||||
params: formData.value
|
||||
});
|
||||
|
||||
/** Data End **/
|
||||
|
||||
/** S Methods **/
|
||||
// 新增用户等级
|
||||
const onReplyAdd = (): void => {
|
||||
router.push({
|
||||
path: "/channel/mp_wechat/reply/reply_edit",
|
||||
query: {
|
||||
mode: PageMode["ADD"],
|
||||
replyType: formData.value.reply_type,
|
||||
},
|
||||
});
|
||||
}
|
||||
// 编辑
|
||||
const onReplyEdit = (event: Event | any): void => {
|
||||
router.push({
|
||||
path: "/channel/mp_wechat/reply/reply_edit",
|
||||
query: {
|
||||
mode: PageMode["EDIT"],
|
||||
id: event.id,
|
||||
replyType: formData.value.reply_type,
|
||||
},
|
||||
});
|
||||
}
|
||||
// 修改状态
|
||||
const putMpWeChatReplyStatus = async (event: Event | any): Promise<void> => {
|
||||
await apiMpWeChatReplyStatus({ id: event.id as number })
|
||||
requestApi();
|
||||
}
|
||||
// 删除
|
||||
const onMpWeChatReplyDelete = async (event: Event | any): Promise<void> => {
|
||||
await apiMpWeChatReplyDelete({ id: event.id as number })
|
||||
requestApi();
|
||||
}
|
||||
/** Methods End **/
|
||||
|
||||
/** Life Cycle Start **/
|
||||
requestApi();
|
||||
/** Life Cycle End **/
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
128
admin/src/views/channel/mp_wechat/reply/follow_reply.vue
Normal file
128
admin/src/views/channel/mp_wechat/reply/follow_reply.vue
Normal file
@@ -0,0 +1,128 @@
|
||||
<template>
|
||||
<!-- Header Alert Start -->
|
||||
<el-card shadow="never">
|
||||
<el-alert
|
||||
class="xxl"
|
||||
title="温馨提示:1.粉丝关注公众号时,会自动发送启用的关注回复;2.同时只能启用一个关注回复。"
|
||||
type="primary"
|
||||
:closable="false"
|
||||
show-icon
|
||||
></el-alert>
|
||||
</el-card>
|
||||
<!-- Header Alert End -->
|
||||
|
||||
<el-card shadow="never" class="m-t-20">
|
||||
<!-- Header BtnGroup Start -->
|
||||
<el-button size="small" type="primary" @click="onReplyAdd()">新增关注回复</el-button>
|
||||
<!-- Header BtnGroup Start -->
|
||||
|
||||
<!-- Main TableData Start -->
|
||||
<div class="m-t-16">
|
||||
<el-table
|
||||
:data="pager.lists"
|
||||
style="width: 100%"
|
||||
v-loading="pager.loading"
|
||||
:default-sort="{ prop: 'level', order: 'ascending' }"
|
||||
:header-cell-style="{ 'background': '#f5f8ff' }"
|
||||
size="mini"
|
||||
>
|
||||
<el-table-column prop="name" label="规则名称" min-width="100px"></el-table-column>
|
||||
<el-table-column prop="content_type_desc" label="回复类型" min-width="100px"></el-table-column>
|
||||
<el-table-column prop="status" label="启用状态" min-width="100px">
|
||||
<template #default="scope">
|
||||
<el-switch
|
||||
v-model="scope.row.status"
|
||||
:active-value="1"
|
||||
:inactive-value="0"
|
||||
@change="putMpWeChatReplyStatus(scope.row)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column fixed="right" label="操作" min-width="120px">
|
||||
<template #default="scope">
|
||||
<!-- <el-button type="text" size="small" @click="onUserLevelEdit(scope.row)">详情</el-button> -->
|
||||
<el-button type="text" size="small" @click="onReplyEdit(scope.row)">编辑</el-button>
|
||||
<popup class="m-l-10 inline" @confirm="onMpWeChatReplyDelete(scope.row)">
|
||||
<el-button type="text" size="small" slot="trigger">删除</el-button>
|
||||
</popup>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<!-- 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 { ref } from "vue"
|
||||
import {
|
||||
apiMpWeChatReplyLists,
|
||||
apiMpWeChatReplyDelete,
|
||||
apiMpWeChatReplyStatus
|
||||
} from "@/api/channel/mp_wechat"
|
||||
import { PageMode } from "@/utils/type"
|
||||
import Popup from "@/components/popup/index.vue"
|
||||
import { useAdmin } from "@/core/hooks/app"
|
||||
import { usePages } from "@/core/hooks/pages";
|
||||
import Pagination from "@/components/pagination/index.vue"
|
||||
|
||||
/** Data Start **/
|
||||
const { router } = useAdmin();
|
||||
let formData = ref({ reply_type: '1' as string | undefined | number }) // 回复类型 1-关注回复 2-关键词回复 3-默认回复
|
||||
const {
|
||||
pager,
|
||||
requestApi
|
||||
} = usePages({
|
||||
callback: apiMpWeChatReplyLists,
|
||||
params: formData.value
|
||||
});
|
||||
|
||||
/** Data End **/
|
||||
|
||||
/** S Methods **/
|
||||
// 新增用户等级
|
||||
const onReplyAdd = (): void => {
|
||||
router.push({
|
||||
path: "/channel/mp_wechat/reply/reply_edit",
|
||||
query: {
|
||||
mode: PageMode["ADD"],
|
||||
replyType: formData.value.reply_type,
|
||||
},
|
||||
});
|
||||
}
|
||||
// 编辑
|
||||
const onReplyEdit = (event: Event | any): void => {
|
||||
router.push({
|
||||
path: "/channel/mp_wechat/reply/reply_edit",
|
||||
query: {
|
||||
mode: PageMode["EDIT"],
|
||||
id: event.id,
|
||||
replyType: formData.value.reply_type,
|
||||
},
|
||||
});
|
||||
}
|
||||
// 修改状态
|
||||
const putMpWeChatReplyStatus = async (event: Event | any): Promise<void> => {
|
||||
await apiMpWeChatReplyStatus({ id: event.id as number })
|
||||
requestApi();
|
||||
}
|
||||
// 删除
|
||||
const onMpWeChatReplyDelete = async (event: Event | any): Promise<void> => {
|
||||
await apiMpWeChatReplyDelete({ id: event.id as number })
|
||||
requestApi();
|
||||
}
|
||||
/** Methods End **/
|
||||
|
||||
/** Life Cycle Start **/
|
||||
requestApi();
|
||||
/** Life Cycle End **/
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
127
admin/src/views/channel/mp_wechat/reply/keyword_reply.vue
Normal file
127
admin/src/views/channel/mp_wechat/reply/keyword_reply.vue
Normal file
@@ -0,0 +1,127 @@
|
||||
<template>
|
||||
<!-- Header Alert Start -->
|
||||
<el-card shadow="never">
|
||||
<el-alert
|
||||
class="xxl"
|
||||
title="温馨提示:1.粉丝在公众号发送内容时,通过关键词可触发关键词回复;2.同时可启用多个关键词回复,有多条关键词匹配时优选选择排序靠前的一条。"
|
||||
type="primary"
|
||||
:closable="false"
|
||||
show-icon
|
||||
></el-alert>
|
||||
</el-card>
|
||||
<!-- Header Alert End -->
|
||||
|
||||
<el-card shadow="never" class="m-t-20">
|
||||
<!-- Header BtnGroup Start -->
|
||||
<el-button size="small" type="primary" @click="onReplyAdd()">新增关键字回复</el-button>
|
||||
<!-- Header BtnGroup Start -->
|
||||
|
||||
<!-- Main TableData Start -->
|
||||
<div class="m-t-16">
|
||||
<el-table
|
||||
:data="pager.lists"
|
||||
style="width: 100%"
|
||||
v-loading="pager.loading"
|
||||
:default-sort="{ prop: 'level', order: 'ascending' }"
|
||||
:header-cell-style="{ 'background': '#f5f8ff' }"
|
||||
size="mini"
|
||||
>
|
||||
<el-table-column prop="name" label="规则名称" min-width="100px"></el-table-column>
|
||||
<el-table-column prop="content_type_desc" label="回复类型" min-width="100px"></el-table-column>
|
||||
<el-table-column prop="status" label="启用状态" min-width="100px">
|
||||
<template #default="scope">
|
||||
<el-switch
|
||||
v-model="scope.row.status"
|
||||
:active-value="1"
|
||||
:inactive-value="0"
|
||||
@change="putMpWeChatReplyStatus(scope.row)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column fixed="right" label="操作" min-width="120px">
|
||||
<template #default="scope">
|
||||
<!-- <el-button type="text" size="small" @click="onUserLevelEdit(scope.row)">详情</el-button> -->
|
||||
<el-button type="text" size="small" @click="onReplyEdit(scope.row)">编辑</el-button>
|
||||
<popup class="m-l-10 inline" @confirm="onMpWeChatReplyDelete(scope.row)">
|
||||
<el-button type="text" size="small" slot="trigger">删除</el-button>
|
||||
</popup>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<!-- 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 { ref } from "vue"
|
||||
import {
|
||||
apiMpWeChatReplyLists,
|
||||
apiMpWeChatReplyDelete,
|
||||
apiMpWeChatReplyStatus
|
||||
} from "@/api/channel/mp_wechat"
|
||||
import { PageMode } from "@/utils/type"
|
||||
import Popup from "@/components/popup/index.vue"
|
||||
import { useAdmin } from "@/core/hooks/app"
|
||||
import { usePages } from "@/core/hooks/pages";
|
||||
import Pagination from "@/components/pagination/index.vue"
|
||||
|
||||
/** Data Start **/
|
||||
const { router } = useAdmin();
|
||||
let formData = ref({ reply_type: '2' as string | undefined | number }) // 回复类型 1-关注回复 2-关键词回复 3-默认回复
|
||||
const {
|
||||
pager,
|
||||
requestApi
|
||||
} = usePages({
|
||||
callback: apiMpWeChatReplyLists,
|
||||
params: formData.value
|
||||
});
|
||||
/** Data End **/
|
||||
|
||||
/** S Methods **/
|
||||
// 新增用户等级
|
||||
const onReplyAdd = (): void => {
|
||||
router.push({
|
||||
path: "/channel/mp_wechat/reply/reply_edit",
|
||||
query: {
|
||||
mode: PageMode["ADD"],
|
||||
replyType: formData?.value?.reply_type,
|
||||
},
|
||||
});
|
||||
}
|
||||
// 编辑
|
||||
const onReplyEdit = (event: Event | any): void => {
|
||||
router.push({
|
||||
path: "/channel/mp_wechat/reply/reply_edit",
|
||||
query: {
|
||||
mode: PageMode["EDIT"],
|
||||
id: event.id,
|
||||
replyType: formData.value.reply_type,
|
||||
},
|
||||
});
|
||||
}
|
||||
// 修改状态
|
||||
const putMpWeChatReplyStatus = async (event: Event | any): Promise<void> => {
|
||||
await apiMpWeChatReplyStatus({ id: event.id as number })
|
||||
requestApi();
|
||||
}
|
||||
// 删除
|
||||
const onMpWeChatReplyDelete = async (event: Event | any): Promise<void> => {
|
||||
await apiMpWeChatReplyDelete({ id: event.id as number })
|
||||
requestApi();
|
||||
}
|
||||
/** Methods End **/
|
||||
|
||||
/** Life Cycle Start **/
|
||||
requestApi();
|
||||
/** Life Cycle End **/
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
</style>
|
||||
192
admin/src/views/channel/mp_wechat/reply/reply_edit.vue
Normal file
192
admin/src/views/channel/mp_wechat/reply/reply_edit.vue
Normal file
@@ -0,0 +1,192 @@
|
||||
<!-- 新增/编辑-->
|
||||
<template>
|
||||
<!-- Header Start -->
|
||||
<el-card shadow="never">
|
||||
<el-page-header
|
||||
v-if="formData.reply_type == 1"
|
||||
@back="$router.back()"
|
||||
:content="mode === 'add' ? '新增关注回复' : '编辑关注回复'"
|
||||
/>
|
||||
<el-page-header
|
||||
v-if="formData.reply_type == 2"
|
||||
@back="$router.back()"
|
||||
:content="mode === 'add' ? '新增关键字回复' : '编辑关键字回复'"
|
||||
/>
|
||||
<el-page-header
|
||||
v-if="formData.reply_type == 3"
|
||||
@back="$router.back()"
|
||||
:content="mode === 'add' ? '新增默认回复' : '编辑默认回复'"
|
||||
/>
|
||||
</el-card>
|
||||
<!-- Header End -->
|
||||
|
||||
<!-- Main Start -->
|
||||
<el-form :rules="rules" ref="formRef" :model="formData" label-width="120px" size="small">
|
||||
<el-card shadow="never" class="m-t-16">
|
||||
<div class="card-title">关注回复</div>
|
||||
<div class="card-content m-t-24">
|
||||
<el-form-item label="规则名称" prop="name">
|
||||
<el-input class="ls-input" v-model="formData.name" placeholder="请输入规则名称"></el-input>
|
||||
<div class="muted xs">方便通过名称管理关注回复内容</div>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="formData.reply_type == 2" label="关键词" prop="keyword">
|
||||
<el-input class="ls-input" v-model="formData.keyword" placeholder="请输入关键词"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="formData.reply_type == 2" label="排序值" prop="sort">
|
||||
<el-input class="ls-input" v-model.number="formData.sort" placeholder="请输入排序值"></el-input>
|
||||
<div class="muted xs">关键词排序值</div>
|
||||
</el-form-item>
|
||||
<el-form-item v-if="formData.reply_type == 2" label="匹配方式" prop="matching_type">
|
||||
<el-select class="ls-select" v-model="formData.matching_type" placeholder="请选择匹配方式">
|
||||
<el-option label="全匹配" :value="1"></el-option>
|
||||
<el-option label="模糊匹配" :value="2"></el-option>
|
||||
</el-select>
|
||||
<div class="muted xs">模糊匹配时,关键词部分匹配用户输入的内容即可</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="内容类型" prop="content_type">
|
||||
<el-select class="ls-select" v-model="formData.content_type" placeholder="请选择内容类型">
|
||||
<el-option label="文本" :value="1"></el-option>
|
||||
</el-select>
|
||||
<div class="muted xs">暂时支持文本类型</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="回复内容" prop="content">
|
||||
<el-input
|
||||
class="ls-input-textarea"
|
||||
v-model="formData.content"
|
||||
placeholder="请输入回复内容"
|
||||
type="textarea"
|
||||
:rows="6"
|
||||
></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="启用状态">
|
||||
<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>
|
||||
</el-form-item>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-form>
|
||||
<!-- Main End -->
|
||||
|
||||
<!-- Footer Start -->
|
||||
<footer-btns>
|
||||
<el-button size="small" @click="$router.back()">取消</el-button>
|
||||
<el-button type="primary" size="small" @click="onSubmit(formRef)">保存</el-button>
|
||||
</footer-btns>
|
||||
<!-- Footer End -->
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from "vue"
|
||||
import {
|
||||
PageMode
|
||||
} from '@/utils/type'
|
||||
import {
|
||||
apiMpWeChatReplyAdd,
|
||||
apiMpWeChatReplyEdit,
|
||||
apiMpWeChatReplyDetail
|
||||
} from "@/api/channel/mp_wechat"
|
||||
import FooterBtns from '@/components/footer-btns/index.vue'
|
||||
import { useAdmin } from '@/core/hooks/app'
|
||||
import type { ElForm } from "element-plus";
|
||||
|
||||
/** Interface Start **/
|
||||
interface formDataObj {
|
||||
reply_type?: string | boolean | number
|
||||
name: string | null
|
||||
content_type: number
|
||||
content: string
|
||||
status: number
|
||||
keyword: string
|
||||
matching_type: number
|
||||
sort: string | number
|
||||
reply_num: number
|
||||
}
|
||||
|
||||
type FormInstance = InstanceType<typeof ElForm>
|
||||
const formRef = ref<FormInstance>();
|
||||
/** Interface End **/
|
||||
|
||||
/** Data Start **/
|
||||
const { router, route } = useAdmin()
|
||||
const mode = ref<string>(PageMode['ADD']) // 当前页面【add: 添加用户等级 | edit: 编辑用户等级】
|
||||
const identity = ref(route.query.id as string) // 当前编辑回复ID valid: mode = 'edit'
|
||||
let formData = ref<formDataObj>({
|
||||
reply_type: route.query.replyType as string, // 回复类型 1-关注回复 2-关键词回复 3-默认回复
|
||||
name: '', // 规格名称
|
||||
content_type: 1, // 内容类型 1-文本
|
||||
content: '', // 内容
|
||||
status: 1, // 启用状态 0-禁用 1-开启
|
||||
keyword: '', // 关键词 *关键词回复必填
|
||||
matching_type: 1, // 匹配方式 1-全匹配 2-模糊匹配 *关键词回复必填
|
||||
sort: '', // 排序值 *关键词回复必填
|
||||
reply_num: 1, // 回复数量 *关键词回复必填 1-回复匹配首条
|
||||
})
|
||||
const rules = reactive<object>({
|
||||
name: [{ required: true, message: '请输入规则名称', trigger: 'blur' }],
|
||||
keyword: [{ required: true, message: '请输入关键词', trigger: 'blur' }],
|
||||
sort: [
|
||||
{ required: true, message: '请输入排序值', trigger: 'blur' },
|
||||
{ type: 'number', min: 1, message: '请输入大于0的数字值', trigger: 'blur' }
|
||||
],
|
||||
matching_type: [{ required: true, message: '请选择匹配方式', trigger: 'change' }],
|
||||
content_type: [{ required: true, message: '请选择内容类型', trigger: 'change' }],
|
||||
content: [{ required: true, message: '请输入回复内容', trigger: 'blur' }]
|
||||
})
|
||||
/** Data End **/
|
||||
|
||||
/** Methods Start **/
|
||||
// 表单提交
|
||||
const onSubmit = (formEl: FormInstance | undefined): void => {
|
||||
if (!formEl) return
|
||||
formEl.validate((valid): boolean | Promise<void> | undefined => {
|
||||
if (!valid) return false
|
||||
switch (mode.value) {
|
||||
case PageMode['ADD']:
|
||||
return handleMpWeChatReplyAdd()
|
||||
case PageMode['EDIT']:
|
||||
return handleMpWeChatReplyEdit()
|
||||
}
|
||||
})
|
||||
}
|
||||
// 新增
|
||||
const handleMpWeChatReplyAdd = async (): Promise<void> => {
|
||||
await apiMpWeChatReplyAdd(formData.value)
|
||||
setTimeout(() => router.go(-1), 500)
|
||||
}
|
||||
// 编辑
|
||||
const handleMpWeChatReplyEdit = async (): Promise<void> => {
|
||||
apiMpWeChatReplyEdit({ ...formData.value, id: identity.value })
|
||||
setTimeout(() => router.go(-1), 500)
|
||||
}
|
||||
// 表单初始化数据 [编辑模式] mode => edit
|
||||
const initMpWeChatReplyDetail = async (): Promise<void> => {
|
||||
formData.value = await apiMpWeChatReplyDetail({ id: identity.value })
|
||||
}
|
||||
/** Methods Start **/
|
||||
|
||||
/** Life Cycle Start **/
|
||||
// 编辑模式:初始化数据
|
||||
if (route.query.mode === PageMode['EDIT']) {
|
||||
mode.value = PageMode['EDIT']
|
||||
initMpWeChatReplyDetail()
|
||||
}
|
||||
/** Life Cycle End **/
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.ls-input,
|
||||
.ls-select {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.ls-input-textarea {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
164
admin/src/views/channel/wechat_app/index.vue
Normal file
164
admin/src/views/channel/wechat_app/index.vue
Normal file
@@ -0,0 +1,164 @@
|
||||
<!-- 微信小程序 -->
|
||||
<template>
|
||||
<!-- Header Alert Start -->
|
||||
<el-card shadow="never">
|
||||
<el-alert title="温馨提示:请先前往微信小程序后台申请认证微信小程序。" type="primary" :closable="false" show-icon />
|
||||
</el-card>
|
||||
<!-- Header Alert End -->
|
||||
|
||||
<!-- Main Form Start -->
|
||||
<el-form ref="formRef" :model="formData" :rules="rules" label-width="140px" size="small">
|
||||
<!-- 微信小程序 Start -->
|
||||
<el-card shadow="never" class="m-t-16">
|
||||
<template #header>微信小程序</template>
|
||||
|
||||
<el-form-item label="小程序名称">
|
||||
<el-input class="ls-input" v-model="formData.name" size="small"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="原始ID">
|
||||
<el-input class="ls-input" v-model="formData.original_id" size="small"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="小程序码">
|
||||
<material-select :limit="1" v-model="formData.qr_code" />
|
||||
<div class="muted xs m-r-16">建议尺寸:400*400像素,支持jpg,jpeg,png格式</div>
|
||||
</el-form-item>
|
||||
</el-card>
|
||||
<!-- 微信小程序 End -->
|
||||
|
||||
<!-- 开发者ID Start -->
|
||||
<el-card shadow="never" class="m-t-16">
|
||||
<template #header>开发者ID</template>
|
||||
<el-form-item label="AppID" prop="app_id">
|
||||
<el-input class="ls-input" v-model="formData.app_id" size="small"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="AppSecret" prop="app_secret">
|
||||
<el-input class="ls-input" v-model="formData.app_secret" size="small"></el-input>
|
||||
<div class="muted xs m-r-16">小程序账号登录微信公众平台,点击开发>开发设置->开发者ID,设置AppID和AppSecret</div>
|
||||
</el-form-item>
|
||||
</el-card>
|
||||
<!-- 开发者ID End -->
|
||||
|
||||
<!-- 服务器域名 Start -->
|
||||
<el-card shadow="never" class="m-t-16">
|
||||
<template #header>服务器域名</template>
|
||||
<el-form-item label="request合法域名">
|
||||
<el-input class="ls-input m-r-10" v-model="formData.request_domain" size="small" disabled></el-input>
|
||||
<el-button size="small" v-on:copy="formData.request_domain">复制</el-button>
|
||||
<div class="muted xs m-r-16">小程序账号登录微信公众平台,点击开发>开发设置->服务器域名,填写https协议域名</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="socket合法域名">
|
||||
<el-input class="ls-input m-r-10" v-model="formData.socket_domain" size="small" disabled></el-input>
|
||||
<el-button size="small" v-on:copy="formData.socket_domain">复制</el-button>
|
||||
<div class="muted xs m-r-16">小程序账号登录微信公众平台,点击开发>开发设置->服务器域名,填写wss协议域名</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="uploadFile合法域名">
|
||||
<el-input class="ls-input m-r-10" v-model="formData.upload_file_domain" size="small" disabled></el-input>
|
||||
<el-button size="small" v-on:copy="formData.upload_file_domain">复制</el-button>
|
||||
<div class="muted xs m-r-16">小程序账号登录微信公众平台,点击开发>开发设置->服务器域名,填写https协议域名</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="downloadFile合法域名">
|
||||
<el-input class="ls-input m-r-10" v-model="formData.download_file_domain" size="small" disabled></el-input>
|
||||
<el-button size="small" v-on:copy="formData.download_file_domain">复制</el-button>
|
||||
<div class="muted xs m-r-16">小程序账号登录微信公众平台,点击开发>开发设置->服务器域名,填写https协议域名</div>
|
||||
</el-form-item>
|
||||
<el-form-item label="udp合法域名">
|
||||
<el-input class="ls-input m-r-10" v-model="formData.udp_domain" size="small" disabled></el-input>
|
||||
<el-button size="small" v-on:copy="formData.udp_domain">复制</el-button>
|
||||
<div class="muted xs m-r-16">小程序账号登录微信公众平台,点击开发>开发设置->服务器域名,填写udp协议域名</div>
|
||||
</el-form-item>
|
||||
</el-card>
|
||||
<!-- 服务器域名 End -->
|
||||
|
||||
<!-- 业务域名 Start -->
|
||||
<el-card shadow="never" class="m-t-16">
|
||||
<template #header>业务域名</template>
|
||||
<el-form-item label="业务域名">
|
||||
<el-input class="ls-input m-r-10" v-model="formData.business_domain" size="small" disabled></el-input>
|
||||
<el-button size="small" v-on:copy="formData.business_domain">复制</el-button>
|
||||
<div class="muted xs m-r-16">小程序账号登录微信公众平台,点击开发>开发设置->业务域名,填写业务域名</div>
|
||||
</el-form-item>
|
||||
</el-card>
|
||||
<!-- 业务域名 End -->
|
||||
</el-form>
|
||||
<!-- Main Form End -->
|
||||
|
||||
<!-- Footer Start -->
|
||||
<footer-btns>
|
||||
<el-button type="primary" size="small" @click="onSubmit(formRef)">保存</el-button>
|
||||
</footer-btns>
|
||||
<!-- Footer End -->
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from 'vue'
|
||||
import MaterialSelect from '@/components/material-select/index.vue'
|
||||
import FooterBtns from '@/components/footer-btns/index.vue'
|
||||
import {
|
||||
apiWechatMiniSetting,
|
||||
apiWechatMiniSettingSet
|
||||
} from '@/api/channel/wechat_app'
|
||||
import {
|
||||
WechatMiniSetting_Res,
|
||||
WechatMiniSetting_Req
|
||||
} from '@/api/channel/wechat_app.d'
|
||||
import type { ElForm } from "element-plus";
|
||||
|
||||
type FormInstance = InstanceType<typeof ElForm>
|
||||
const formRef = ref<FormInstance>();
|
||||
|
||||
/** S Data **/
|
||||
const formData = ref<WechatMiniSetting_Res>({
|
||||
name: '', // 小程序名称
|
||||
original_id: '', // 原始id
|
||||
qr_code: '', // 二维码
|
||||
app_id: '',
|
||||
app_secret: '',
|
||||
request_domain: '', // request合法域名
|
||||
socket_domain: '', // socket合法域名
|
||||
upload_file_domain: '', // uploadFile合法域名
|
||||
download_file_domain: '', // downloadFile合法域名
|
||||
udp_domain: '', // udp合法域名
|
||||
business_domain: '', // 业务域名
|
||||
url: '',
|
||||
token: '',
|
||||
encoding_aes_key: '',
|
||||
encryption_type: 1, // 消息加密方式 1-明文模式 2-兼容模式 3-安全模式
|
||||
data_format: 1 // 数据格式 1-JSON 2-XML
|
||||
})
|
||||
// 表单验证
|
||||
const rules = reactive<object>({
|
||||
app_id: [{ required: true, message: '必填项不能为空', trigger: 'blur' }],
|
||||
app_secret: [{ required: true, message: '必填项不能为空', trigger: 'blur' }],
|
||||
})
|
||||
/** Data End **/
|
||||
|
||||
/** Methods Start **/
|
||||
// 获取小程序配置
|
||||
const getWechatAppSetting = async (): Promise<void> => {
|
||||
formData.value = await apiWechatMiniSetting()
|
||||
}
|
||||
// 编辑小程序配置
|
||||
const handleWechatAppEdit = async (): Promise<void> => {
|
||||
await apiWechatMiniSettingSet({ ...formData.value })
|
||||
getWechatAppSetting()
|
||||
}
|
||||
// 提交数据
|
||||
const onSubmit = (formEl: FormInstance | undefined): void => {
|
||||
if (!formEl) return
|
||||
formEl.validate((valid): boolean | undefined => {
|
||||
if (!valid) return false
|
||||
handleWechatAppEdit()
|
||||
})
|
||||
}
|
||||
/** Methods End **/
|
||||
|
||||
/** LifeCycle Start **/
|
||||
getWechatAppSetting()
|
||||
/** LifeCycle End **/
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.ls-input {
|
||||
width: 280px;
|
||||
}
|
||||
</style>
|
||||
98
admin/src/views/channel/wechat_platform/index.vue
Normal file
98
admin/src/views/channel/wechat_platform/index.vue
Normal file
@@ -0,0 +1,98 @@
|
||||
<!-- 微信开放平台 -->
|
||||
<template>
|
||||
<!-- Main Form Start -->
|
||||
<el-form :rules="formRules" ref="formRef" :model="form" label-width="140px" size="small">
|
||||
<!-- 微信开放平台 -->
|
||||
<el-card shadow="never" class="m-t-16">
|
||||
<template #header>微信开放平台</template>
|
||||
<!-- 提示 -->
|
||||
<div class="ls-card-alert">
|
||||
<el-alert
|
||||
title="APP需要使用微信授权登录、微信支付等微信生态能力时,需要设置关联微信开发平台;请填写APP在微信开发平台申请的应用ID等信息。"
|
||||
type="primary"
|
||||
:closable="false"
|
||||
show-icon
|
||||
/>
|
||||
</div>
|
||||
<el-form-item label="AppID" prop="app_id">
|
||||
<el-input class="ls-input m-r-10" size="small" v-model="form.app_id"></el-input>
|
||||
</el-form-item>
|
||||
<el-form-item label="AppSecret" prop="app_secret">
|
||||
<el-input class="ls-input m-r-10" size="small" v-model="form.app_secret"></el-input>
|
||||
</el-form-item>
|
||||
</el-card>
|
||||
</el-form>
|
||||
<!-- Main Form End -->
|
||||
|
||||
<!-- Footer Start -->
|
||||
<footer-btns>
|
||||
<el-button type="primary" size="small" @click="onSubmit(formRef)">保存</el-button>
|
||||
</footer-btns>
|
||||
<!-- Footer End -->
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { reactive, ref } from 'vue'
|
||||
import {
|
||||
apiWechatPlatformGet,
|
||||
apiWechatPlatformSet
|
||||
} from '@/api/channel/wechat_platform'
|
||||
import { AppSettings_Req } from '@/api/channel/app_store.d.ts'
|
||||
import type { ElForm } from "element-plus";
|
||||
import FooterBtns from '@/components/footer-btns/index.vue'
|
||||
|
||||
/** Interface Start **/
|
||||
type FormInstance = InstanceType<typeof ElForm>
|
||||
const formRef = ref<FormInstance>();
|
||||
/** Interface Start **/
|
||||
|
||||
/** Data Start **/
|
||||
const form = ref<AppSettings_Req>({
|
||||
app_id: '', // 开放平台appid
|
||||
app_secret: '' // 开放平台appSecrets
|
||||
})
|
||||
|
||||
// 表单验证
|
||||
const formRules = reactive<object>({
|
||||
app_id: [{ required: true, message: '必填项不能为空', trigger: 'blur' }],
|
||||
app_secret: [{ required: true, message: '必填项不能为空', trigger: 'blur' }],
|
||||
})
|
||||
/** Data End **/
|
||||
|
||||
/** Methods Start **/
|
||||
// 获取微信公众平台配置
|
||||
const getAppSettings = async (): Promise<void> => {
|
||||
form.value = await apiWechatPlatformGet()
|
||||
}
|
||||
// 编辑微信公众平台配置
|
||||
const handleAppStoreEdit = async (): Promise<void> => {
|
||||
await apiWechatPlatformSet({ ...form.value })
|
||||
getAppSettings()
|
||||
}
|
||||
// 提交数据
|
||||
const onSubmit = (formEl: FormInstance | undefined): void => {
|
||||
if (!formEl) return
|
||||
formEl.validate((valid): boolean | undefined=> {
|
||||
if (!valid) return false
|
||||
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>
|
||||
99
admin/src/views/content/advertising/position.vue
Normal file
99
admin/src/views/content/advertising/position.vue
Normal file
@@ -0,0 +1,99 @@
|
||||
<template>
|
||||
<div class="estate">
|
||||
<el-card shadow="never">
|
||||
<el-form class="ls-form" :model="formData" label-width="80px" size="small" inline>
|
||||
<el-form-item label="广告位名称">
|
||||
<el-input
|
||||
placeholder="请输入广告位标题"
|
||||
class="ls-input"
|
||||
v-model="formData.username"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="广告位属性">
|
||||
<el-select v-model="formData.house_type" placeholder="全部">
|
||||
<el-option label="全部" value=""></el-option>
|
||||
<el-option label="系统默认" :value="1"></el-option>
|
||||
<el-option label="自定义" :value="2"></el-option>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<div class="m-l-20">
|
||||
<el-button type="primary" @click="resetPage">查询</el-button>
|
||||
<el-button @click="resetParams">重置</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<el-card class="m-t-15" shadow="never" v-loading="pager.loading">
|
||||
<router-link to="/content/advertising/advertising_edit">
|
||||
<el-button type="primary" size="small"> 新增广告位 </el-button>
|
||||
</router-link>
|
||||
|
||||
<div>
|
||||
<el-table class="m-t-20" :data="pager.lists">
|
||||
<el-table-column label="ID" prop="id" min-width="30"></el-table-column>
|
||||
<el-table-column label="广告位名称" prop="username"> </el-table-column>
|
||||
<el-table-column label="广告位属性" prop="mobile"></el-table-column>
|
||||
<el-table-column label="图片建议尺寸" prop="budget"> </el-table-column>
|
||||
<el-table-column label="排序" prop="area"></el-table-column>
|
||||
<el-table-column label="广告位状态" prop="plot"></el-table-column>
|
||||
<el-table-column label="添加时间" prop="floor"> </el-table-column>
|
||||
<el-table-column label="操作" width="150" fixed="right">
|
||||
<template v-slot="{ row }">
|
||||
<router-link
|
||||
class="m-r-10"
|
||||
:to="{
|
||||
path: '',
|
||||
query: {
|
||||
id: row.id,
|
||||
},
|
||||
}"
|
||||
>
|
||||
<el-button type="text">编辑</el-button>
|
||||
</router-link>
|
||||
<popup class="m-r-10 inline">
|
||||
<template #trigger>
|
||||
<el-button type="text">删除</el-button>
|
||||
</template>
|
||||
</popup>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
<div class="flex row-right">
|
||||
<pagination
|
||||
v-model="pager"
|
||||
@change="requestApi"
|
||||
layout="total, prev, pager, next, jumper"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { apiBuyHouseList } from '@/api/entrust'
|
||||
import Pagination from '@/components/pagination/index.vue'
|
||||
import Popup from '@/components/Popup/index.vue'
|
||||
import { usePages } from '@/core/hooks/pages'
|
||||
|
||||
const formData = reactive({
|
||||
username: '',
|
||||
mobile: '',
|
||||
house_type: '',
|
||||
status: '',
|
||||
start: '',
|
||||
end: '',
|
||||
})
|
||||
|
||||
const { pager, requestApi, resetParams, resetPage } = usePages({
|
||||
callback: apiBuyHouseList,
|
||||
params: formData,
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user