初始化admin pc端

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

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

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

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

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

View File

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

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

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

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

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

View File

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

View File

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

View File

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

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

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

View File

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

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

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

View File

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

View File

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

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

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

View File

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 605 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

View File

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

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

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

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 594 KiB

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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>

View 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*高400pxjpgjpegpng格式</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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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像素支持jpgjpegpng格式</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>

View 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>

View 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