This commit is contained in:
吴红兵
2025-12-02 10:37:49 +08:00
commit 1f645dad3e
1183 changed files with 147673 additions and 0 deletions

View File

@@ -0,0 +1,204 @@
<template>
<div class="layout-padding-auto layout-padding-view">
<el-card shadow="never">
<el-form ref="dataFormRef" class="form" :model="form" label-width="85px" :rules="dataRules">
<div class="flex">
<div>
<el-form-item label="文章标题" prop="title">
<div class="w-80">
<el-input
v-model="form.title"
placeholder="请输入文章标题"
type="textarea"
:autosize="{ minRows: 3, maxRows: 3 }"
maxlength="64"
show-word-limit
clearable
/>
</div>
</el-form-item>
<el-form-item label="文章简介" prop="intro">
<div class="w-80">
<el-input
v-model="form.intro"
placeholder="请输入文章简介"
type="textarea"
:autosize="{ minRows: 3, maxRows: 6 }"
:maxlength="200"
show-word-limit
clearable
/>
</div>
</el-form-item>
<el-form-item label="摘要" prop="summary">
<div class="w-80">
<el-input type="textarea" :autosize="{ minRows: 6, maxRows: 6 }" v-model="form.summary" maxlength="200"
show-word-limit clearable/>
</div>
</el-form-item>
<el-form-item label="文章封面" prop="image">
<div>
<div>
<upload-img v-model:imageUrl="form.image"/>
</div>
<div class="form-tips">建议尺寸240*180px</div>
</div>
</el-form-item>
</div>
<div class="xl:ml-20">
<el-row class="xl:mb-8">
<el-col :span="12">
<el-form-item label="文章栏目" prop="cid">
<el-select class="w-80" v-model="form.cid" placeholder="请选择文章栏目" clearable>
<el-option v-for="item in articleCateList" :key="item.id" :label="item.name" :value="item.id"/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="作者" prop="author">
<div class="w-80">
<el-input v-model="form.author" placeholder="请输入作者名称"/>
</div>
</el-form-item>
</el-col>
</el-row>
<el-row class="xl:mb-8">
<el-col :span="12">
<el-form-item label="初始浏览量" prop="visit">
<template #label> 浏览量
<tip content="初始值"/>
</template>
<el-input-number class="!w-80" v-model="form.visit" :min="0" :max="9999"/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item prop="sort">
<template #label> 排序
<tip content="默认为0 数值越大越排前"/>
</template>
<el-input-number class="!w-80" v-model="form.sort" :min="0" :max="9999"/>
</el-form-item>
</el-col>
</el-row>
<el-form-item label="文章内容" required prop="content">
<editor v-model:get-html="form.content" height="500" width="600" :disable="form.id !== ''"/>
</el-form-item>
<div style="text-align: center">
<el-button type="primary" @click="onSubmit">保存</el-button>
</div>
</div>
</div>
</el-form>
</el-card>
</div>
</template>
<script setup lang="ts" name="AppArticleDialog">
import mittBus from "/@/utils/mitt";
import {useMessage} from '/@/hooks/message';
import {getObj, addObj, putObj} from '/@/api/app/appArticle';
import {getObjList} from '/@/api/app/appArticleCategory';
const emit = defineEmits(['refresh']);
const route = useRoute();
// 定义变量内容
const dataFormRef = ref();
const visible = ref(false);
const loading = ref(false);
// 定义字典
const articleCateList = ref([]);
// 提交表单数据
const form = reactive({
id: '',
cid: '',
title: '',
intro: '',
summary: '',
image: '',
content: '',
author: '',
visit: 0,
sort: 0,
});
// 定义校验规则
const dataRules = ref({
cid: [{required: true, message: '分类不能为空', trigger: 'blur'}],
title: [{required: true, message: '标题不能为空', trigger: 'blur'}],
intro: [{required: true, message: '简介不能为空', trigger: 'blur'}],
summary: [{required: true, message: '摘要不能为空', trigger: 'blur'}],
image: [{required: true, message: '封面不能为空', trigger: 'blur'}],
content: [{required: true, message: '内容不能为空', trigger: 'blur'}],
author: [{required: true, message: '作者不能为空', trigger: 'blur'}],
visit: [{required: true, message: '浏览不能为空', trigger: 'blur'}],
});
// 提交
const onSubmit = async () => {
const valid = await dataFormRef.value.validate().catch(() => {
});
if (!valid) return false;
try {
loading.value = true;
form.id ? await putObj(form) : await addObj(form);
useMessage().success(form.id ? '修改成功' : '添加成功');
visible.value = false;
emit('refresh');
// 关闭当前tab
mittBus.emit(
"onCurrentContextmenuClick",
Object.assign({}, {contextMenuClickId: 1, ...route})
);
} catch (err: any) {
useMessage().error(err.msg);
} finally {
loading.value = false;
}
};
// 初始化表单数据
const getAppArticleData = (id: string) => {
// 获取数据
loading.value = true;
getObj(id)
.then((res: any) => {
Object.assign(form, res.data);
})
.finally(() => {
loading.value = false;
});
};
// 查询全部的分类
const getAppCateList = () => {
getObjList().then((res: any) => {
articleCateList.value = res.data;
});
};
onMounted(() => {
getAppCateList();
if (route.query?.id) {
getAppArticleData(route.query?.id);
}
});
</script>
<style scoped lang="scss">
.footer-btns {
height: 60px;
&__content {
bottom: 0;
height: 60px;
right: 0;
left: 0;
z-index: 99;
@apply flex justify-center items-center shadow;
}
}
</style>

View File

@@ -0,0 +1,6 @@
export default {
article: {
edit: 'edit article',
add: 'add article',
},
};

View File

@@ -0,0 +1,6 @@
export default {
article: {
edit: '编辑文章',
add: '发布文章',
},
};

View File

@@ -0,0 +1,133 @@
<template>
<div class="layout-padding">
<div class="layout-padding-auto layout-padding-view">
<el-row v-show="showSearch">
<el-form :model="state.queryForm" ref="queryRef" :inline="true" @keyup.enter="getDataList">
<el-form-item label="标题" prop="title">
<el-input placeholder="请输入标题" v-model="state.queryForm.title" />
</el-form-item>
<el-form-item label="作者" prop="author">
<el-input placeholder="请输入作者" v-model="state.queryForm.author" />
</el-form-item>
<el-form-item>
<el-button icon="search" type="primary" @click="getDataList"> 查询 </el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-row>
<el-row>
<div class="mb8" style="width: 100%">
<el-button v-auth="'app_appArticle_add'" icon="folder-add" type="primary" class="ml10" @click="addOrUpdate()"> </el-button>
<el-button plain :disabled="multiple" icon="Delete" type="primary" v-auth="'app_appArticle_del'" @click="handleDelete(selectObjs)">
删除
</el-button>
<right-toolbar
v-model:showSearch="showSearch"
:export="'app_appArticle_export'"
@exportExcel="exportExcel"
class="ml10 mr20"
style="float: right"
@queryTable="getDataList"
></right-toolbar>
</div>
</el-row>
<el-table
:data="state.dataList"
v-loading="state.loading"
@selection-change="handleSelectionChange"
@sort-change="sortChangeHandle"
border
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
>
<el-table-column type="selection" width="40" align="center" />
<el-table-column type="index" label="#" width="40" />
<el-table-column prop="cname" label="分类" show-overflow-tooltip />
<el-table-column prop="title" label="标题" show-overflow-tooltip />
<el-table-column prop="author" label="作者" show-overflow-tooltip />
<el-table-column prop="visit" label="浏览" show-overflow-tooltip />
<el-table-column prop="sort" label="排序" show-overflow-tooltip />
<el-table-column label="操作" width="150">
<template #default="scope">
<el-button icon="edit-pen" text type="primary" v-auth="'app_appArticle_edit'" @click="addOrUpdate(scope.row.id)">编辑</el-button>
<el-button icon="delete" text type="primary" v-auth="'app_appArticle_del'" @click="handleDelete([scope.row.id])">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
</div>
</div>
</template>
<script setup lang="ts" name="systemAppArticle">
import { BasicTableProps, useTable } from '/@/hooks/table';
import { fetchList, delObjs } from '/@/api/app/appArticle';
import { useMessage, useMessageBox } from '/@/hooks/message';
import { useI18n } from 'vue-i18n';
// 定义查询字典
// 定义变量内容
const router = useRouter();
const { t } = useI18n();
// 搜索变量
const queryRef = ref();
const showSearch = ref(true);
// 多选变量
const selectObjs = ref([]) as any;
const multiple = ref(true);
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {},
pageList: fetchList,
});
// table hook
const { getDataList, currentChangeHandle, sizeChangeHandle, sortChangeHandle, downBlobFile, tableStyle } = useTable(state);
// 清空搜索条件
const resetQuery = () => {
// 清空搜索条件
queryRef.value?.resetFields();
// 清空多选
selectObjs.value = [];
getDataList();
};
// 导出excel
const exportExcel = () => {
downBlobFile('/app/appArticle/export', state.queryForm, 'appArticle.xlsx');
};
// 多选事件
const handleSelectionChange = (objs: { id: string }[]) => {
selectObjs.value = objs.map(({ id }) => id);
multiple.value = !objs.length;
};
// 删除操作
const handleDelete = async (ids: string[]) => {
try {
await useMessageBox().confirm('此操作将永久删除');
} catch {
return;
}
try {
await delObjs(ids);
getDataList();
useMessage().success('删除成功');
} catch (err: any) {
useMessage().error(err.msg);
}
};
// 跳转发布页
const addOrUpdate = (id?: string) => {
const tagsViewName = id ? `${t('article.edit')}:${id}` : t('article.add');
router.push({
path: '/biz/app/appArticle/form',
query: { id: id, tagsViewName: tagsViewName },
});
};
</script>

View File

@@ -0,0 +1,104 @@
<template>
<el-dialog :title="form.id ? '编辑' : '新增'" v-model="visible" width="600" :close-on-click-modal="false" draggable>
<el-form ref="dataFormRef" :model="form" :rules="dataRules" formDialogRef label-width="90px" v-loading="loading">
<el-form-item label="名称" prop="name">
<el-input v-model="form.name" placeholder="请输入名称" />
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input-number :min="1" :max="1000" v-model="form.sort" placeholder="请输入排序"></el-input-number>
</el-form-item>
<el-form-item label="是否显示" prop="isShow">
<el-radio-group v-model="form.isShow">
<el-radio :label="Number(item.value)" v-for="(item, index) in yes_no_type" border :key="index">{{ item.label }} </el-radio>
</el-radio-group>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" @click="onSubmit" :disabled="loading">确认</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup lang="ts" name="AppArticleCategoryDialog">
import { useDict } from '/@/hooks/dict';
import { useMessage } from '/@/hooks/message';
import { getObj, addObj, putObj } from '/@/api/app/appArticleCategory';
const emit = defineEmits(['refresh']);
// 定义变量内容
const dataFormRef = ref();
const visible = ref(false);
const loading = ref(false);
// 定义字典
const { yes_no_type } = useDict('yes_no_type');
// 提交表单数据
const form = reactive({
id: '',
name: '',
sort: 0,
isShow: 1,
});
// 定义校验规则
const dataRules = ref({
name: [{ required: true, message: '名称不能为空', trigger: 'blur' }],
isShow: [{ required: true, message: '是否显示', trigger: 'blur' }],
});
// 打开弹窗
const openDialog = (id: string) => {
visible.value = true;
form.id = '';
// 重置表单数据
nextTick(() => {
dataFormRef.value?.resetFields();
});
// 获取appArticleCategory信息
if (id) {
form.id = id;
getappArticleCategoryData(id);
}
};
// 提交
const onSubmit = async () => {
const valid = await dataFormRef.value.validate().catch(() => {});
if (!valid) return false;
try {
loading.value = true;
form.id ? await putObj(form) : await addObj(form);
useMessage().success(form.id ? '修改成功' : '添加成功');
visible.value = false;
emit('refresh');
} catch (err: any) {
useMessage().error(err.msg);
} finally {
loading.value = false;
}
};
// 初始化表单数据
const getappArticleCategoryData = (id: string) => {
// 获取数据
loading.value = true;
getObj(id)
.then((res: any) => {
Object.assign(form, res.data);
})
.finally(() => {
loading.value = false;
});
};
// 暴露变量
defineExpose({
openDialog,
});
</script>

View File

@@ -0,0 +1,131 @@
<template>
<div class="layout-padding">
<div class="layout-padding-auto layout-padding-view">
<el-row v-show="showSearch">
<el-form :model="state.queryForm" ref="queryRef" :inline="true" @keyup.enter="getDataList">
<el-form-item label="名称" prop="name">
<el-input placeholder="请输入名称" v-model="state.queryForm.name" />
</el-form-item>
<el-form-item>
<el-button icon="search" type="primary" @click="getDataList"> 查询 </el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
</el-row>
<el-row>
<div class="mb8" style="width: 100%">
<el-button icon="folder-add" type="primary" class="ml10" @click="formDialogRef.openDialog()" v-auth="'app_appArticleCategory_add'">
</el-button>
<el-button plain :disabled="multiple" icon="Delete" type="primary" v-auth="'app_appArticleCategory_del'" @click="handleDelete(selectObjs)">
删除
</el-button>
<right-toolbar
v-model:showSearch="showSearch"
:export="'app_appArticleCategory_export'"
@exportExcel="exportExcel"
class="ml10 mr20"
style="float: right"
@queryTable="getDataList"
></right-toolbar>
</div>
</el-row>
<el-table
:data="state.dataList"
v-loading="state.loading"
border
@selection-change="handleSelectionChange"
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
>
<el-table-column type="selection" width="40" align="center" />
<el-table-column type="index" label="#" width="40" />
<el-table-column prop="name" label="名称" show-overflow-tooltip />
<el-table-column prop="sort" label="排序" show-overflow-tooltip />
<el-table-column prop="isShow" label="是否显示" show-overflow-tooltip>
<template #default="scope">
<dict-tag :options="yes_no_type" :value="scope.row.isShow"></dict-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="150">
<template #default="scope">
<el-button icon="edit-pen" text type="primary" v-auth="'app_appArticleCategory_edit'" @click="formDialogRef.openDialog(scope.row.id)"
>编辑</el-button
>
<el-button icon="delete" text type="primary" v-auth="'app_appArticleCategory_del'" @click="handleDelete([scope.row.id])">删除</el-button>
</template>
</el-table-column>
</el-table>
<pagination @size-change="sizeChangeHandle" @current-change="currentChangeHandle" v-bind="state.pagination" />
</div>
<!-- 编辑新增 -->
<form-dialog ref="formDialogRef" @refresh="getDataList(false)" />
</div>
</template>
<script setup lang="ts" name="systemAppArticleCategory">
import { BasicTableProps, useTable } from '/@/hooks/table';
import { fetchList, delObjs } from '/@/api/app/appArticleCategory';
import { useMessage, useMessageBox } from '/@/hooks/message';
import { useDict } from '/@/hooks/dict';
// 引入组件
const FormDialog = defineAsyncComponent(() => import('./form.vue'));
// 定义查询字典
const { yes_no_type } = useDict('yes_no_type');
// 定义变量内容
const formDialogRef = ref();
// 搜索变量
const queryRef = ref();
const showSearch = ref(true);
// 多选变量
const selectObjs = ref([]) as any;
const multiple = ref(true);
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {},
pageList: fetchList,
});
// table hook
const { getDataList, currentChangeHandle, sizeChangeHandle, downBlobFile, tableStyle } = useTable(state);
// 清空搜索条件
const resetQuery = () => {
// 清空搜索条件
queryRef.value?.resetFields();
// 清空多选
selectObjs.value = [];
getDataList();
};
// 多选事件
const handleSelectionChange = (objs: { id: string }[]) => {
selectObjs.value = objs.map(({ id }) => id);
multiple.value = !objs.length;
};
// 导出excel
const exportExcel = () => {
downBlobFile('/app/appArticleCategory/export', state.queryForm, 'appArticleCategory.xlsx');
};
// 删除操作
const handleDelete = async (ids: string[]) => {
try {
await useMessageBox().confirm('此操作将永久删除');
} catch {
return;
}
try {
await delObjs(ids);
getDataList();
useMessage().success('删除成功');
} catch (err: any) {
useMessage().error(err.msg);
}
};
</script>

View File

@@ -0,0 +1,159 @@
<template>
<div class="system-user-dialog-container">
<el-dialog v-model="visible" width="60%" center :close-on-click-modal="false" :close-on-press-escape="false" :show-close="true">
<el-form :model="state.queryForm" ref="queryRef" :inline="true" @keyup.enter="getDataList">
<el-form-item label="用户名" prop="username">
<el-input v-model="state.queryForm.username" placeholder="请输入用户名" clearable />
</el-form-item>
<el-form-item>
<el-button icon="Search" type="primary" @click="getDataList">{{ $t('common.queryBtn') }}</el-button>
<el-button icon="Refresh" @click="resetQuery">{{ $t('common.resetBtn') }}</el-button>
</el-form-item>
</el-form>
<el-table
ref="tableRef"
v-loading="state.loading"
:data="state.dataList"
@selection-change="handleSelectionChange"
style="width: 100%"
border
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
>
<el-table-column type="selection" width="55" />
<el-table-column label="#" type="index" width="60" />
<el-table-column label="用户名" prop="username" show-overflow-tooltip />
<el-table-column label="姓名" prop="name" show-overflow-tooltip />
<el-table-column label="手机号" prop="phone" show-overflow-tooltip />
</el-table>
<pagination v-bind="state.pagination" @current-change="currentChangeHandle" @size-change="sizeChangeHandle" />
<template #footer>
<span class="dialog-footer">
<el-button @click="onCancel" size="default">{{ $t('common.cancelButtonText') }}</el-button>
<el-button type="primary" @click="onSubmit" size="default" :disabled="selectedUsers.length === 0">
批量授权 ({{ selectedUsers.length }})
</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts" name="AddUserDialog">
import { batchAddObj } from '/@/api/app/appuserrole';
import { pageList } from '/@/api/admin/user';
import { BasicTableProps, useTable } from '/@/hooks/table';
import { useMessage } from '/@/hooks/message';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const emit = defineEmits(['refresh']);
// 定义组件Props
interface Props {
roleId?: string;
}
const props = withDefaults(defineProps<Props>(), {
roleId: '',
});
// 定义变量内容
const queryRef = ref();
const tableRef = ref();
const visible = ref(false);
const selectedUsers = ref<any[]>([]);
// 定义表格数据
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {
username: '',
phone: '',
},
pageList: pageList,
isPage: true,
});
const { getDataList, currentChangeHandle, sizeChangeHandle, tableStyle } = useTable(state);
// 打开弹窗
const openDialog = () => {
visible.value = true;
resetQuery();
getDataList();
};
// 关闭弹窗
const closeDialog = () => {
visible.value = false;
selectedUsers.value = [];
state.queryForm = {
username: '',
phone: '',
};
};
// 取消
const onCancel = () => {
closeDialog();
};
// 清空搜索条件
const resetQuery = () => {
queryRef.value?.resetFields();
getDataList();
};
// 多选事件
const handleSelectionChange = (selection: any[]) => {
selectedUsers.value = selection;
};
// 提交
const onSubmit = async () => {
if (!props.roleId) {
useMessage().error('请先选择角色');
return;
}
if (selectedUsers.value.length === 0) {
useMessage().error('请选择要插入的用户');
return;
}
try {
// 批量插入用户角色关联
const userRoleList = selectedUsers.value.map(user => ({
userId: user.userId,
roleId: props.roleId
}));
await batchAddObj(userRoleList);
useMessage().success(`成功为 ${selectedUsers.value.length} 个用户分配角色`);
emit('refresh');
closeDialog();
} catch (err: any) {
useMessage().error(err.msg || '批量插入失败');
}
};
// 暴露变量
defineExpose({
openDialog,
closeDialog,
});
</script>
<style scoped lang="scss">
.system-user-dialog-container {
.dialog-footer {
display: flex;
justify-content: flex-end;
align-items: center;
}
}
</style>

View File

@@ -0,0 +1,135 @@
<template>
<el-dialog :close-on-click-modal="false" :title="form.roleId ? $t('common.editBtn') : $t('common.addBtn')" width="600" draggable v-model="visible">
<el-form :model="form" :rules="dataRules" label-width="90px" ref="dataFormRef" v-loading="loading">
<el-form-item :label="$t('approle.roleName')" prop="roleName">
<el-input :placeholder="$t('approle.please_enter_a_role_name')" clearable v-model="form.roleName"></el-input>
</el-form-item>
<el-form-item :label="$t('approle.roleCode')" prop="roleCode">
<el-input :placeholder="$t('approle.please_enter_the_role_Code')" clearable v-model="form.roleCode"></el-input>
</el-form-item>
<el-form-item :label="$t('approle.roleDesc')" prop="roleDesc">
<el-input :placeholder="$t('approle.please_enter_the_role_description')" maxlength="150" type="textarea" v-model="form.roleDesc"></el-input>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">{{ $t('common.cancelButtonText') }}</el-button>
<el-button @click="onSubmit" type="primary" :disabled="loading">{{ $t('common.confirmButtonText') }}</el-button>
</span>
</template>
</el-dialog>
</template>
<script lang="ts" name="systemRoleDialog" setup>
import { rule } from '/@/utils/validate';
import { useMessage } from '/@/hooks/message';
import { addObj, getObj, putObj, validateAppRoleCode, validateApproleName } from '/@/api/app/approle';
import { useI18n } from 'vue-i18n';
// 定义子组件向父组件传值/事件
const emit = defineEmits(['refresh']);
const { t } = useI18n();
// 定义变量内容
const dataFormRef = ref();
const visible = ref(false);
const loading = ref(false);
// 提交表单数据
const form = reactive({
roleId: '',
roleName: '',
roleCode: '',
roleDesc: '',
});
// 页面对应元数据
const dataForm = reactive({
deptData: [],
checkedDsScope: [],
deptProps: {
children: 'children',
label: 'name',
value: 'id',
},
});
// 定义校验规则
const dataRules = ref({
roleName: [
{ required: true, message: '角色名称不能为空', trigger: 'blur' },
{ min: 3, max: 20, message: '长度在 3 到 20 个字符', trigger: 'blur' },
{
validator: (rule: any, value: any, callback: any) => {
validateApproleName(rule, value, callback, form.roleId !== '');
},
trigger: 'blur',
},
],
roleCode: [
{ required: true, message: '角色标识不能为空', trigger: 'blur' },
{ min: 3, max: 20, message: '长度在 3 到 20 个字符', trigger: 'blur' },
{ validator: rule.validatorCapital, trigger: 'blur' },
{
validator: (rule: any, value: any, callback: any) => {
validateAppRoleCode(rule, value, callback, form.roleId !== '');
},
trigger: 'blur',
},
],
roleDesc: [{ max: 128, message: '长度在 128 个字符内', trigger: 'blur' }],
});
// 打开弹窗
const openDialog = (id: string) => {
visible.value = true;
form.roleId = '';
// 重置表单数据
nextTick(() => {
dataFormRef.value?.resetFields();
});
// 获取角色信息
if (id) {
form.roleId = id;
getRoleData(id);
}
};
// 提交
const onSubmit = async () => {
const valid = await dataFormRef.value.validate().catch(() => {});
if (!valid) return false;
try {
loading.value = true;
form.roleId ? await putObj(form) : await addObj(form);
useMessage().success(t(form.roleId ? 'common.editSuccessText' : 'common.addSuccessText'));
visible.value = false;
emit('refresh');
} catch (err: any) {
useMessage().error(err.msg);
} finally {
loading.value = false;
}
};
// 初始化角色数据
const getRoleData = (id: string) => {
// 获取部门数据
getObj(id).then((res: any) => {
Object.assign(form, res.data);
if (res.data.dsScope) {
dataForm.checkedDsScope = res.data.dsScope.split(',');
} else {
dataForm.checkedDsScope = [];
}
});
};
// 暴露变量
defineExpose({
openDialog,
});
</script>

View File

@@ -0,0 +1,16 @@
export default {
approle: {
index: '#',
roleName: 'roleName',
inputRoleNameTip: 'input roleName',
permissionTip: 'grant',
workbenchMenuTip: 'Menu',
roleCode: 'roleCode',
roleDesc: 'role description',
createTime: 'createTime',
please_enter_a_role_name: 'please enter a role name',
please_enter_the_role_Code: 'please enter the role Code',
please_enter_the_role_description: 'please enter the role description',
},
};

View File

@@ -0,0 +1,16 @@
export default {
approle: {
index: '#',
roleName: '角色名',
inputRoleNameTip: '请输入角色名称',
permissionTip: '授权',
workbenchMenuTip: '工作台',
roleCode: '用户标识',
roleDesc: '用户描述',
createTime: '创建时间',
please_enter_a_role_name: '请输入角色名称',
please_enter_the_role_Code: '请输入角色标识',
please_enter_the_role_description: '请输入角色描述',
},
};

View File

@@ -0,0 +1,391 @@
<template>
<div class="layout-padding">
<splitpanes>
<!-- 左侧角色管理 -->
<pane size="45">
<div class="layout-padding-auto layout-padding-view">
<el-row class="ml10" v-show="showSearch">
<el-form :inline="true" :model="state.queryForm" @keyup.enter="getDataList" ref="queryRef">
<el-form-item :label="$t('approle.roleName')" prop="roleName">
<el-input :placeholder="$t('approle.inputRoleNameTip')" style="max-width: 180px" v-model="state.queryForm.roleName" />
</el-form-item>
<el-form-item>
<el-button @click="getDataList" icon="search" type="primary">
{{ $t('common.queryBtn') }}
</el-button>
<el-button @click="resetQuery" icon="Refresh">{{ $t('common.resetBtn') }}</el-button>
</el-form-item>
</el-form>
</el-row>
<el-row>
<div class="mb8" style="width: 100%">
<el-button @click="roleDialogRef.openDialog()" class="ml10" icon="folder-add" type="primary" v-auth="'app_approle_add'">
{{ $t('common.addBtn') }}
</el-button>
<el-button plain @click="excelUploadRef.show()" class="ml10" icon="upload-filled" type="primary" v-auth="'app_approle_export'">
{{ $t('common.importBtn') }}
</el-button>
<el-button
plain
:disabled="multiple"
@click="handleDelete(selectObjs)"
class="ml10"
icon="Delete"
type="primary"
v-auth="'app_approle_del'"
>
{{ $t('common.delBtn') }}
</el-button>
<right-toolbar
:export="'app_approle_export'"
@exportExcel="exportExcel"
@queryTable="getDataList"
class="ml10"
style="float: right; margin-right: 20px"
v-model:showSearch="showSearch"
></right-toolbar>
</div>
</el-row>
<el-table
:data="state.dataList"
@selection-change="handleSelectionChange"
@row-click="handleRoleClick"
style="width: 100%"
v-loading="state.loading"
border
highlight-current-row
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
>
<el-table-column align="center" type="selection" width="40" />
<el-table-column :label="$t('approle.index')" type="index" width="60" />
<el-table-column :label="$t('approle.roleName')" prop="roleName" show-overflow-tooltip></el-table-column>
<el-table-column :label="$t('approle.roleCode')" prop="roleCode" show-overflow-tooltip></el-table-column>
<el-table-column :label="$t('common.action')" width="300">
<template #default="scope">
<el-button icon="edit-pen" @click="roleDialogRef.openDialog(scope.row.roleId)" text type="primary" v-auth="'app_approle_edit'"
>{{ $t('common.editBtn') }}
</el-button>
<el-button icon="setting" @click="workbenchMenuDialogRef.openDialog(scope.row.roleId)" text type="primary" v-auth="'app_approle_edit'"
>{{ $t('approle.workbenchMenuTip') }}
</el-button>
<el-button icon="delete" @click="handleDelete([scope.row.roleId])" text type="primary" v-auth="'app_approle_del'"
>{{ $t('common.delBtn') }}
</el-button>
</template>
</el-table-column>
</el-table>
<pagination @current-change="currentChangeHandle" @size-change="sizeChangeHandle" v-bind="state.pagination" />
</div>
</pane>
<!-- 右侧用户角色管理 -->
<pane size="55" class="ml8">
<div class="layout-padding-auto layout-padding-view">
<el-card shadow="never" style="height: 100%">
<template #header>
<div class="card-header">
<div class="card-title">
<el-icon class="title-icon" v-if="selectedRoleId">
<User />
</el-icon>
<el-icon class="title-icon" v-else>
<InfoFilled />
</el-icon>
<span class="title-text">
{{ selectedRoleName ? `${selectedRoleName} - 授权用户` : '用户角色管理' }}
</span>
</div>
<div class="card-subtitle" v-if="selectedRoleId">
<el-tag type="info" size="small">
{{ userState.pagination?.total || 0 }} 个授权用户
</el-tag>
</div>
</div>
</template>
<div v-if="!selectedRoleId" class="empty-state">
<el-empty description="请在左侧选择角色以查看对应的用户列表">
<template #image>
<el-icon size="100" color="var(--el-color-info)">
<User />
</el-icon>
</template>
<template #description>
<p>选择左侧角色后这里将显示该角色的授权用户</p>
</template>
</el-empty>
</div>
<div v-else>
<!-- 搜索栏 -->
<el-row class="mb8" v-show="showUserSearch">
<el-form :inline="true" :model="userState.queryForm" @keyup.enter="getUserDataList" ref="userQueryRef">
<el-form-item label="用户名" prop="username">
<el-input placeholder="请输入用户名" style="max-width: 180px" v-model="userState.queryForm.username" />
</el-form-item>
<el-form-item>
<el-button @click="getUserDataList" icon="search" type="primary">
{{ $t('common.queryBtn') }}
</el-button>
<el-button @click="resetUserQuery" icon="Refresh">{{ $t('common.resetBtn') }}</el-button>
</el-form-item>
</el-form>
</el-row>
<!-- 操作按钮 -->
<el-row>
<div class="mb8" style="width: 100%">
<el-button @click="addUserDialogRef.openDialog()" icon="folder-add" type="primary">
新增用户
</el-button>
<right-toolbar
@queryTable="getUserDataList"
class="ml10"
style="float: right; margin-right: 20px"
v-model:showSearch="showUserSearch"
></right-toolbar>
</div>
</el-row>
<!-- 用户列表 -->
<el-table
:data="userState.dataList"
style="width: 100%"
v-loading="userState.loading"
border
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
>
<el-table-column label="#" type="index" width="60" />
<el-table-column label="用户名" prop="username" show-overflow-tooltip></el-table-column>
<el-table-column label="姓名" prop="name" show-overflow-tooltip></el-table-column>
<el-table-column label="操作" width="120">
<template #default="scope">
<el-button icon="delete" @click="handleRemoveUser(scope.row.userId)" text type="danger">
取消授权
</el-button>
</template>
</el-table-column>
<template #empty>
<el-empty description="该角色暂无授权用户">
<template #image>
<el-icon size="60" color="var(--el-color-info-light-5)">
<User />
</el-icon>
</template>
<template #description>
<p>点击上方"新增用户"按钮为该角色添加用户</p>
</template>
</el-empty>
</template>
</el-table>
<pagination @current-change="userCurrentChangeHandle" @size-change="userSizeChangeHandle" v-bind="userState.pagination" />
</div>
</el-card>
</div>
</pane>
</splitpanes>
<!-- 角色编辑新增 -->
<RoleDialog @refresh="getDataList()" ref="roleDialogRef" />
<!-- 分配工作台菜单 -->
<WorkbenchMenuDialog @refresh="getDataList()" ref="workbenchMenuDialogRef" />
<!-- 新增用户对话框 -->
<AddUserDialog @refresh="getUserDataList()" ref="addUserDialogRef" :roleId="selectedRoleId" />
<!-- 导入角色 -->
<upload-excel
:title="$t('sysuser.importUserTip')"
@refreshDataList="getDataList"
ref="excelUploadRef"
temp-url="/admin/sys-file/local/file/approle.xlsx"
url="/admin/approle/import"
/>
</div>
</template>
<script lang="ts" name="systemRole" setup>
import { BasicTableProps, useTable } from '/@/hooks/table';
import { delObj, fetchList } from '/@/api/app/approle';
import { fetchList as fetchUserRoleList, delObj as delUserRole } from '/@/api/app/appuserrole';
import { useMessage, useMessageBox } from '/@/hooks/message';
import { useI18n } from 'vue-i18n';
import { User, InfoFilled } from '@element-plus/icons-vue';
// 引入组件
const RoleDialog = defineAsyncComponent(() => import('./form.vue'));
const WorkbenchMenuDialog = defineAsyncComponent(() => import('./workbench-menu-dialog.vue'));
const AddUserDialog = defineAsyncComponent(() => import('./add-user-dialog.vue'));
const { t } = useI18n();
// 定义变量内容
const roleDialogRef = ref();
const workbenchMenuDialogRef = ref();
const addUserDialogRef = ref();
const excelUploadRef = ref();
const queryRef = ref();
const userQueryRef = ref();
const showSearch = ref(true);
const showUserSearch = ref(true);
// 角色相关状态
const selectObjs = ref([]) as any;
const multiple = ref(true);
// 用户相关状态
const selectedRoleId = ref('');
const selectedRoleName = ref('');
// 角色表格状态
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {
roleName: '',
},
pageList: fetchList,
descs: ['create_time'],
});
// 用户角色关联表格状态
const userState: BasicTableProps = reactive<BasicTableProps>({
queryForm: {
roleId: '',
username: '',
},
pageList: fetchUserRoleList
});
// 角色 table hook
const { getDataList, currentChangeHandle, sizeChangeHandle, downBlobFile, tableStyle } = useTable(state);
// 用户角色关联 table hook
const {
getDataList: getUserDataList,
currentChangeHandle: userCurrentChangeHandle,
sizeChangeHandle: userSizeChangeHandle
} = useTable(userState);
// 清空角色搜索条件
const resetQuery = () => {
queryRef.value.resetFields();
getDataList();
};
// 清空用户搜索条件
const resetUserQuery = () => {
userQueryRef.value.resetFields();
getUserDataList();
};
// 导出excel
const exportExcel = () => {
downBlobFile('/app/approle/export', state.queryForm, 'approle.xlsx');
};
// 角色多选事件
const handleSelectionChange = (objs: { roleId: string }[]) => {
selectObjs.value = objs.map(({ roleId }) => roleId);
multiple.value = !objs.length;
};
// 点击角色行事件
const handleRoleClick = (row: any) => {
selectedRoleId.value = row.roleId;
selectedRoleName.value = row.roleName;
userState.queryForm.roleId = row.roleId;
getUserDataList();
};
// 删除角色操作
const handleDelete = async (ids: string[]) => {
try {
await useMessageBox().confirm(t('common.delConfirmText'));
} catch {
return;
}
try {
await delObj(ids);
getDataList();
useMessage().success(t('common.delSuccessText'));
// 如果删除的角色是当前选中的角色,清空右侧
if (ids.includes(selectedRoleId.value)) {
selectedRoleId.value = '';
selectedRoleName.value = '';
userState.dataList = [];
}
} catch (err: any) {
useMessage().error(err.msg);
}
};
// 单个取消用户授权
const handleRemoveUser = async (userId: string) => {
try {
await useMessageBox().confirm('确认要取消该用户的角色授权吗?');
} catch {
return;
}
try {
await delUserRole(userId);
getUserDataList();
useMessage().success('取消授权成功');
} catch (err: any) {
useMessage().error(err.msg);
}
};
</script>
<style scoped>
.empty-state {
display: flex;
justify-content: center;
align-items: center;
height: 300px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 8px;
}
.card-title {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
}
.title-icon {
font-size: 18px;
color: var(--el-color-primary);
}
.title-text {
font-size: 16px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.card-subtitle {
display: flex;
align-items: center;
}
@media (max-width: 768px) {
.card-header {
flex-direction: column;
align-items: flex-start;
}
.card-subtitle {
width: 100%;
justify-content: flex-start;
}
}
</style>

View File

@@ -0,0 +1,361 @@
<template>
<el-dialog
:close-on-click-modal="false"
title="分配工作台菜单"
width="800px"
draggable
v-model="visible"
>
<div v-loading="loading">
<div class="mb-4">
<el-alert
title="请选择该角色可以访问的工作台菜单项"
type="info"
:closable="false"
show-icon
/>
</div>
<div v-if="workbenchData.length > 0" class="workbench-menu-list">
<div
v-for="item in workbenchData"
:key="item.id"
class="menu-item"
>
<div class="menu-header">
<div class="menu-info">
<el-checkbox
:model-value="isModuleAllSelected(item)"
:indeterminate="isModuleIndeterminate(item)"
@change="toggleModuleSelection(item)"
class="menu-checkbox"
/>
<div class="menu-title">{{ item.title }}</div>
<div class="menu-name">{{ item.name }}</div>
</div>
</div>
<div v-if="item.content && item.content.data" class="menu-content">
<div class="content-grid">
<div
v-for="(dataItem, index) in item.content.data"
:key="dataItem.id || index"
class="content-item"
:class="{ 'selected': selectedMenuIds.includes(dataItem.id) }"
@click="toggleMenuItem(dataItem.id)"
>
<el-checkbox
:model-value="selectedMenuIds.includes(dataItem.id)"
@change="toggleMenuItem(dataItem.id)"
class="item-checkbox"
@click.stop
/>
<div class="item-icon">
<el-icon v-if="dataItem.icon" :size="20">
<component :is="dataItem.icon" />
</el-icon>
<div v-else class="default-icon">
<el-icon :size="20"><Menu /></el-icon>
</div>
</div>
<div class="item-name">{{ dataItem.name }}</div>
<div v-if="dataItem.badge" class="item-badge">{{ dataItem.badge }}</div>
</div>
</div>
</div>
</div>
</div>
<div v-else class="empty-state">
<el-empty description="暂无工作台菜单数据" />
</div>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">{{ $t('common.cancelButtonText') }}</el-button>
<el-button @click="onSubmit" type="primary" :disabled="loading">{{ $t('common.confirmButtonText') }}</el-button>
</span>
</template>
</el-dialog>
</template>
<script lang="ts" name="workbenchMenuDialog" setup>
import { useMessage } from '/@/hooks/message';
import { getWorkbenchDecorate, updateRoleWorkbenchMenus, getObj } from '/@/api/app/approle';
import { useI18n } from 'vue-i18n';
import { Menu } from '@element-plus/icons-vue';
// 定义子组件向父组件传值/事件
const emit = defineEmits(['refresh']);
const { t } = useI18n();
// 定义变量内容
const visible = ref(false);
const loading = ref(false);
const currentRoleId = ref('');
// 工作台数据
const workbenchData = ref<any[]>([]);
const selectedMenuIds = ref<string[]>([]);
// 打开弹窗
const openDialog = async (roleId: string) => {
visible.value = true;
currentRoleId.value = roleId;
selectedMenuIds.value = [];
try {
loading.value = true;
// 获取工作台装饰数据
await getWorkbenchData();
// 获取角色当前的菜单配置
if (roleId) {
await getRoleMenuData(roleId);
}
} catch (err: any) {
useMessage().error(err.msg || '获取数据失败');
} finally {
loading.value = false;
}
};
// 获取工作台装饰数据
const getWorkbenchData = async () => {
try {
const response = await getWorkbenchDecorate();
const { data } = response;
if (data && data.pageData) {
const pageData = JSON.parse(data.pageData);
workbenchData.value = pageData
}
} catch (err: any) {
console.error('获取工作台数据失败:', err);
throw err;
}
};
// 获取角色当前的菜单数据
const getRoleMenuData = async (roleId: string) => {
try {
const { data } = await getObj(roleId);
if (data && data.menuId) {
selectedMenuIds.value = data.menuId.split(',').filter((id: string) => id.trim());
}
} catch (err: any) {
console.error('获取角色菜单数据失败:', err);
// 不抛出错误,允许继续操作
}
};
// 切换菜单项选择状态
const toggleMenuItem = (menuId: string) => {
const index = selectedMenuIds.value.indexOf(menuId);
if (index > -1) {
selectedMenuIds.value.splice(index, 1);
} else {
selectedMenuIds.value.push(menuId);
}
};
// 检查模块是否全选
const isModuleAllSelected = (module: any) => {
if (!module.content || !module.content.data || module.content.data.length === 0) {
return false;
}
return module.content.data.every((item: any) => selectedMenuIds.value.includes(item.id));
};
// 检查模块是否部分选中
const isModuleIndeterminate = (module: any) => {
if (!module.content || !module.content.data || module.content.data.length === 0) {
return false;
}
const selectedCount = module.content.data.filter((item: any) => selectedMenuIds.value.includes(item.id)).length;
return selectedCount > 0 && selectedCount < module.content.data.length;
};
// 切换模块选择状态(全选/取消全选)
const toggleModuleSelection = (module: any) => {
if (!module.content || !module.content.data) {
return;
}
const isAllSelected = isModuleAllSelected(module);
if (isAllSelected) {
// 取消选择该模块下的所有项
module.content.data.forEach((item: any) => {
const index = selectedMenuIds.value.indexOf(item.id);
if (index > -1) {
selectedMenuIds.value.splice(index, 1);
}
});
} else {
// 选择该模块下的所有项
module.content.data.forEach((item: any) => {
if (!selectedMenuIds.value.includes(item.id)) {
selectedMenuIds.value.push(item.id);
}
});
}
};
// 提交
const onSubmit = async () => {
try {
loading.value = true;
const menuId = selectedMenuIds.value.join(',');
await updateRoleWorkbenchMenus(currentRoleId.value, menuId);
useMessage().success('工作台菜单分配成功');
visible.value = false;
emit('refresh');
} catch (err: any) {
useMessage().error(err.msg || '分配失败');
} finally {
loading.value = false;
}
};
// 暴露变量
defineExpose({
openDialog,
});
</script>
<style lang="scss" scoped>
.workbench-menu-list {
max-height: 500px;
overflow-y: auto;
}
.menu-item {
border: 1px solid #e4e7ed;
border-radius: 8px;
margin-bottom: 16px;
padding: 16px;
transition: all 0.3s ease;
}
.menu-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.menu-info {
display: flex;
align-items: center;
gap: 12px;
}
.menu-checkbox {
pointer-events: auto;
}
.menu-title {
font-size: 16px;
font-weight: 600;
color: #303133;
}
.menu-name {
font-size: 12px;
color: #909399;
background-color: #f5f7fa;
padding: 2px 8px;
border-radius: 4px;
}
.menu-content {
border-top: 1px solid #f0f0f0;
padding-top: 12px;
}
.content-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
gap: 12px;
}
.content-item {
display: flex;
flex-direction: column;
align-items: center;
padding: 12px 8px;
border-radius: 6px;
background-color: #fafafa;
position: relative;
cursor: pointer;
transition: all 0.3s ease;
border: 1px solid transparent;
&:hover {
border-color: #409eff;
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.1);
}
&.selected {
border-color: #409eff;
background-color: #f0f9ff;
box-shadow: 0 2px 8px rgba(64, 158, 255, 0.2);
}
}
.item-checkbox {
position: absolute;
top: 4px;
left: 4px;
z-index: 1;
}
.item-icon {
margin-bottom: 4px;
margin-top: 16px;
color: #409eff;
}
.default-icon {
color: #909399;
}
.item-name {
font-size: 12px;
color: #606266;
text-align: center;
line-height: 1.2;
margin-top: 4px;
}
.item-badge {
position: absolute;
top: -4px;
right: -4px;
background-color: #f56c6c;
color: white;
font-size: 10px;
padding: 1px 4px;
border-radius: 8px;
min-width: 16px;
text-align: center;
}
.empty-state {
text-align: center;
padding: 40px 0;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
}
</style>

View File

@@ -0,0 +1,151 @@
<template>
<el-dialog :close-on-click-modal="false" :title="form.id ? $t('common.editBtn') : $t('common.addBtn')" draggable v-model="visible">
<el-form :model="form" :rules="dataRules" formDialogRef label-width="90px" ref="dataFormRef" v-loading="loading">
<el-row :gutter="20">
<el-col :span="12" class="mb20">
<el-form-item :label="t('appsocial.type')" prop="type">
<el-select :placeholder="t('appsocial.inputTypeTip')" v-model="form.type">
<el-option :key="index" :label="item.label" :value="item.value" v-for="(item, index) in app_social_type"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('appsocial.remark')" prop="remark">
<el-input :placeholder="t('appsocial.inputRemarkTip')" v-model="form.remark" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('appsocial.appId')" prop="appId">
<el-input :placeholder="t('appsocial.inputAppIdTip')" v-model="form.appId" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('appsocial.appSecret')" prop="appSecret">
<el-input :placeholder="t('appsocial.inputAppSecretTip')" v-model="form.appSecret" />
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item :label="t('appsocial.redirectUrl')" prop="redirectUrl">
<el-input :placeholder="t('appsocial.inputRedirectUrlTip')" type="textarea" v-model="form.redirectUrl" />
</el-form-item>
</el-col>
<el-col :span="24" class="mb20">
<el-form-item :label="t('appsocial.ext')" prop="ext">
<el-input :placeholder="t('appsocial.inputExtTip')" type="textarea" v-model="form.ext" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">{{ $t('common.cancelButtonText') }}</el-button>
<el-button @click="onSubmit" type="primary" :disabled="loading">{{ $t('common.confirmButtonText') }}</el-button>
</span>
</template>
</el-dialog>
</template>
<script lang="ts" name="AppSocialDetailsDialog" setup>
// 定义子组件向父组件传值/事件
import { useDict } from '/@/hooks/dict';
import { useMessage } from '/@/hooks/message';
import { addObj, getObj, putObj } from '/@/api/app/appsocial';
import { useI18n } from 'vue-i18n';
import { rule } from '/@/utils/validate';
const emit = defineEmits(['refresh']);
const { t } = useI18n();
// 定义变量内容
const dataFormRef = ref();
const visible = ref(false);
const loading = ref(false);
// 定义字典
const { app_social_type } = useDict('app_social_type');
// 提交表单数据
const form = reactive({
id: '',
type: '',
remark: '',
appId: '' as string | undefined,
appSecret: '' as string | undefined,
redirectUrl: '',
ext: '',
});
// 定义校验规则
const dataRules = ref({
type: [{ required: true, message: '类型不能为空', trigger: 'blur' }],
appId: [{ required: true, message: 'appId不能为空', trigger: 'blur' }],
appSecret: [{ required: true, message: 'app秘钥不能为空', trigger: 'blur' }],
redirectUrl: [
{ required: true, message: '回调地址不能为空', trigger: 'blur' },
{ validator: rule.url, trigger: 'blur' },
],
});
// 打开弹窗
const openDialog = (id: string) => {
visible.value = true;
form.id = '';
// 重置表单数据
nextTick(() => {
dataFormRef.value?.resetFields();
});
// 获取appSocialDetails信息
if (id) {
form.id = id;
getappSocialDetailsData(id);
}
};
// 提交
const onSubmit = async () => {
const valid = await dataFormRef.value.validate().catch(() => {});
if (!valid) return false;
// 隐藏敏感信息
form.appSecret = form.appSecret?.includes('******') ? undefined : form.appSecret;
form.appId = form.appId?.includes('******') ? undefined : form.appId;
try {
loading.value = true;
if (form.id) {
await putObj(form);
useMessage().success(t('common.editSuccessText'));
} else {
await addObj(form);
useMessage().success(t('common.addSuccessText'));
}
visible.value = false; // 关闭弹窗
emit('refresh');
} catch (err: any) {
useMessage().error(err.msg);
} finally {
loading.value = false;
}
};
// 初始化表单数据
const getappSocialDetailsData = (id: string) => {
// 获取数据
getObj(id).then((res: any) => {
Object.assign(form, res.data);
});
};
// 暴露变量
defineExpose({
openDialog,
});
</script>

View File

@@ -0,0 +1,32 @@
export default {
appsocial: {
index: '#',
importappSocialDetailsTip: 'import AppSocialDetails',
id: 'id',
type: 'type',
remark: 'remark',
appId: 'appId',
appSecret: 'appSecret',
redirectUrl: 'redirectUrl',
ext: 'ext',
createBy: 'createBy',
updateBy: 'updateBy',
createTime: 'createTime',
updateTime: 'updateTime',
delFlag: 'delFlag',
tenantId: 'tenantId',
inputIdTip: 'input id',
inputTypeTip: 'input type',
inputRemarkTip: 'input remark',
inputAppIdTip: 'input appId',
inputAppSecretTip: 'input appSecret',
inputRedirectUrlTip: 'input redirectUrl',
inputExtTip: 'input ext',
inputCreateByTip: 'input createBy',
inputUpdateByTip: 'input updateBy',
inputCreateTimeTip: 'input createTime',
inputUpdateTimeTip: 'input updateTime',
inputDelFlagTip: 'input delFlag',
inputTenantIdTip: 'input tenantId',
},
};

View File

@@ -0,0 +1,32 @@
export default {
appsocial: {
index: '#',
importappSocialDetailsTip: '导入系统社交登录账号表',
id: '主鍵',
type: '类型',
remark: '描述',
appId: 'appId',
appSecret: 'app秘钥',
redirectUrl: '回调地址',
ext: '拓展字段',
createBy: '创建人',
updateBy: '修改人',
createTime: '创建时间',
updateTime: '更新时间',
delFlag: '${field.fieldComment}',
tenantId: '所属租户',
inputIdTip: '请输入主鍵',
inputTypeTip: '请输入类型',
inputRemarkTip: '请输入描述',
inputAppIdTip: '请输入appId',
inputAppSecretTip: '请输入appSecret',
inputRedirectUrlTip: '请输入回调地址',
inputExtTip: '请输入拓展字段',
inputCreateByTip: '请输入创建人',
inputUpdateByTip: '请输入修改人',
inputCreateTimeTip: '请输入创建时间',
inputUpdateTimeTip: '请输入更新时间',
inputDelFlagTip: '请输入${field.fieldComment}',
inputTenantIdTip: '请输入所属租户',
},
};

View File

@@ -0,0 +1,162 @@
<template>
<div class="layout-padding">
<div class="layout-padding-auto layout-padding-view">
<el-row class="ml10" v-show="showSearch">
<el-form :inline="true" :model="state.queryForm" @keyup.enter="getDataList" ref="queryRef">
<el-form-item :label="t('appsocial.type')" class="ml2" prop="type">
<el-select :placeholder="t('appsocial.inputTypeTip')" v-model="state.queryForm.type">
<el-option :key="index" :label="item.label" :value="item.value" v-for="(item, index) in app_social_type"></el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-button @click="getDataList" formDialogRef icon="search" type="primary">
{{ $t('common.queryBtn') }}
</el-button>
<el-button @click="resetQuery" formDialogRef icon="Refresh">{{ $t('common.resetBtn') }} </el-button>
</el-form-item>
</el-form>
</el-row>
<el-row>
<div class="mb8" style="width: 100%">
<el-button
@click="formDialogRef.openDialog()"
class="ml10"
formDialogRef
icon="folder-add"
type="primary"
v-auth="'app_social_details_add'"
>
{{ $t('common.addBtn') }}
</el-button>
<el-button
plain
:disabled="multiple"
@click="handleDelete(selectObjs)"
class="ml10"
formDialogRef
icon="Delete"
type="primary"
v-auth="'app_social_details_del'"
>
{{ $t('common.delBtn') }}
</el-button>
<right-toolbar
:export="'app_social_details_del'"
@exportExcel="exportExcel"
@queryTable="getDataList"
class="ml10"
style="float: right; margin-right: 20px"
v-model:showSearch="showSearch"
></right-toolbar>
</div>
</el-row>
<el-table
:data="state.dataList"
@selection-change="handleSelectionChange"
@sort-change="sortChangeHandle"
style="width: 100%"
v-loading="state.loading"
border
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
>
<el-table-column align="center" type="selection" width="40" />
<el-table-column :label="t('appsocial.index')" type="index" width="60" />
<el-table-column :label="t('appsocial.type')" prop="type" show-overflow-tooltip>
<template #default="scope">
<dict-tag :options="app_social_type" :value="scope.row.type"></dict-tag>
</template>
</el-table-column>
<el-table-column :label="t('appsocial.remark')" prop="remark" show-overflow-tooltip />
<el-table-column :label="t('appsocial.appId')" prop="appId" show-overflow-tooltip />
<el-table-column :label="t('appsocial.appSecret')" prop="appSecret" show-overflow-tooltip />
<el-table-column :label="t('appsocial.createTime')" prop="createTime" show-overflow-tooltip />
<el-table-column :label="$t('common.action')" width="150">
<template #default="scope">
<el-button icon="edit-pen" @click="formDialogRef.openDialog(scope.row.id)" text type="primary" v-auth="'app_social_details_edit'"
>{{ $t('common.editBtn') }}
</el-button>
<el-button icon="delete" @click="handleDelete([scope.row.id])" text type="primary" v-auth="'app_social_details_del'"
>{{ $t('common.delBtn') }}
</el-button>
</template>
</el-table-column>
</el-table>
<pagination @current-change="currentChangeHandle" @size-change="sizeChangeHandle" v-bind="state.pagination" />
</div>
<!-- 编辑新增 -->
<form-dialog @refresh="getDataList()" ref="formDialogRef" />
</div>
</template>
<script lang="ts" name="systemAppSocialDetails" setup>
import { BasicTableProps, useTable } from '/@/hooks/table';
import { delObj, fetchList } from '/@/api/app/appsocial';
import { useMessage, useMessageBox } from '/@/hooks/message';
import { useDict } from '/@/hooks/dict';
import { useI18n } from 'vue-i18n';
// 引入组件
const FormDialog = defineAsyncComponent(() => import('./form.vue'));
const { t } = useI18n();
// 定义查询字典
const { app_social_type } = useDict('app_social_type');
// 定义变量内容
const formDialogRef = ref();
// 搜索变量
const queryRef = ref();
const showSearch = ref(true);
// 多选变量
const selectObjs = ref([]) as any;
const multiple = ref(true);
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {
type: '',
},
pageList: fetchList,
descs: ['create_time'],
});
// table hook
const { getDataList, currentChangeHandle, sizeChangeHandle, sortChangeHandle, downBlobFile, tableStyle } = useTable(state);
// 清空搜索条件
const resetQuery = () => {
// 清空搜索条件
queryRef.value.resetFields();
// 清空多选
selectObjs.value = [];
getDataList();
};
// 导出excel
const exportExcel = () => {
downBlobFile('/app/appsocial/export', state.queryForm, 'appsocial.xlsx');
};
// 多选事件
const handleSelectionChange = (objs: { id: string }[]) => {
selectObjs.value = objs.map(({ id }) => id);
multiple.value = !objs.length;
};
// 删除操作
const handleDelete = async (ids: string[]) => {
try {
await useMessageBox().confirm(t('common.delConfirmText'));
} catch {
return;
}
try {
await delObj(ids);
getDataList();
useMessage().success(t('common.delSuccessText'));
} catch (err: any) {
useMessage().error(err.msg);
}
};
</script>

View File

@@ -0,0 +1,218 @@
<template>
<div class="system-user-dialog-container">
<el-dialog v-model="visible" :close-on-click-modal="false" :title="dataForm.userId ? $t('common.editBtn') : $t('common.addBtn')" draggable>
<el-form ref="dataFormRef" v-loading="loading" :model="dataForm" :rules="dataRules" label-width="90px">
<el-row :gutter="20">
<el-col :span="12" class="mb20">
<el-form-item :label="$t('appuser.username')" prop="username">
<el-input v-model="dataForm.username" :disabled="dataForm.userId !== ''" :placeholder="$t('appuser.inputUserNameTip')"></el-input>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="$t('appuser.password')" prop="password">
<el-input v-model="dataForm.password" :placeholder="$t('appuser.inputPasswordTip')" clearable type="password"></el-input>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="$t('appuser.name')" prop="name">
<el-input v-model="dataForm.name" :placeholder="$t('appuser.inputNameTip')" clearable></el-input>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="$t('appuser.phone')" prop="phone">
<el-input v-model="dataForm.phone" :placeholder="$t('appuser.inputPhoneTip')" clearable></el-input>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="$t('appuser.role')" prop="role">
<el-select v-model="dataForm.role" :placeholder="$t('appuser.inputRoleTip')" clearable multiple>
<el-option v-for="item in roleData" :key="item.roleId" :label="item.roleName" :value="item.roleId" />
</el-select>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="$t('appuser.email')" prop="email">
<el-input v-model="dataForm.email" :placeholder="$t('appuser.inputEmailTip')" clearable></el-input>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="$t('appuser.nickname')" prop="nickname">
<el-input v-model="dataForm.nickname" :placeholder="$t('appuser.inputNickNameTip')" clearable></el-input>
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="$t('appuser.lockFlag')" prop="lockFlag">
<el-radio-group v-model="dataForm.lockFlag">
<el-radio v-for="(item, index) in lock_flag" :key="index" :label="item.value" border>{{ item.label }} </el-radio>
</el-radio-group>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">{{ $t('common.cancelButtonText') }}</el-button>
<el-button type="primary" @click="onSubmit" :disabled="loading">{{ $t('common.confirmButtonText') }}</el-button>
</span>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" name="systemUserDialog" setup>
import { addObj, getObj, putObj, validatePhone, validateUsername } from '/@/api/app/appuser';
import { list as roleList } from '/@/api/app/approle';
import { useDict } from '/@/hooks/dict';
import { useI18n } from 'vue-i18n';
import { useMessage } from '/@/hooks/message';
import { rule } from '/@/utils/validate';
const { t } = useI18n();
// 定义刷新表格emit
const emit = defineEmits(['refresh']);
// @ts-ignore
const { lock_flag } = useDict('lock_flag');
// 定义变量内容
const dataFormRef = ref();
const visible = ref(false);
const roleData = ref<any[]>([]);
const loading = ref(false);
const dataForm = reactive({
userId: '',
username: '',
password: '' as String | undefined,
salt: '',
wxOpenid: '',
qqOpenid: '',
lockFlag: '0',
phone: '' as String | undefined,
deptId: '',
roleList: [],
postList: [],
nickname: '',
name: '',
email: '',
post: [] as string[],
role: [] as string[],
});
const dataRules = ref({
username: [
{ required: true, message: '用户名不能为空', trigger: 'blur' },
{ min: 3, max: 20, message: '用户名称长度必须介于 3 和 20 之间', trigger: 'blur' },
{
validator: (rule: any, value: any, callback: any) => {
validateUsername(rule, value, callback, dataForm.userId !== '');
},
trigger: 'blur',
},
],
password: [
{ required: true, message: '密码不能为空', trigger: 'blur' },
{
min: 6,
max: 20,
message: '用户密码长度必须介于 6 和 20 之间',
trigger: 'blur',
},
],
name: [
{ required: true, message: '姓名不能为空', trigger: 'blur' },
{ validator: rule.chinese, trigger: 'blur' },
],
role: [{ required: true, message: '角色不能为空', trigger: 'blur' }],
phone: [
{ required: true, message: '手机号不能为空', trigger: 'blur' },
{ validator: rule.validatePhone, trigger: 'blur' },
{
validator: (rule: any, value: any, callback: any) => {
validatePhone(rule, value, callback, dataForm.userId !== '');
},
trigger: 'blur',
},
],
email: [{ type: 'email', message: '请输入正确的邮箱地址', trigger: ['blur', 'change'] }],
nickname: [{ required: true, message: '姓名不能为空', nickname: 'blur' }],
});
// 打开弹窗
const openDialog = (id: string) => {
visible.value = true;
dataForm.userId = '';
// 重置表单数据
nextTick(() => {
dataFormRef.value.resetFields();
});
// 修改获取用户信息
if (id) {
dataForm.userId = id;
getUserData(id);
}
getRoleData();
};
// 提交
const onSubmit = async () => {
const valid = await dataFormRef.value.validate().catch(() => {});
if (!valid) return false;
try {
const { userId, phone, password } = dataForm;
if (userId) {
// 清除占位符,避免提交错误的数据
if (phone?.includes('*')) dataForm.phone = undefined;
if (password?.includes('******')) dataForm.password = undefined;
loading.value = true;
await putObj(dataForm);
useMessage().success(t('common.editSuccessText'));
visible.value = false; // 关闭弹窗
emit('refresh');
} else {
loading.value = true;
await addObj(dataForm);
useMessage().success(t('common.addSuccessText'));
visible.value = false; // 关闭弹窗
emit('refresh');
}
} catch (error: any) {
useMessage().error(error.msg);
} finally {
loading.value = false;
}
};
// 初始化用户信息数据
const getUserData = async (id: string) => {
loading.value = true;
try {
const { data } = await getObj(id);
Object.assign(dataForm, data);
dataForm.password = '******';
if (data.roleList) {
dataForm.role = data.roleList.map((item: any) => item.roleId);
}
} finally {
loading.value = false;
}
};
// 角色数据
const getRoleData = () => {
roleList().then((res) => {
roleData.value = res.data;
});
};
// 暴露变量
defineExpose({
openDialog,
});
</script>

View File

@@ -0,0 +1,25 @@
export default {
appuser: {
index: '#',
username: 'username',
name: 'name',
phone: 'phone',
post: 'post',
role: 'role',
lockFlag: 'lockFlag',
createTime: 'createTime',
password: 'password',
dept: 'dept',
email: 'email',
avatar: 'avatar',
nickname: 'nickname',
inputNameTip: 'input name',
inputRoleTip: 'input role',
inputUserNameTip: 'input username',
inputPasswordTip: 'input Password',
inputPhoneTip: 'input phone',
inputEmailTip: 'input Email',
inputNickNameTip: 'input NickName',
importUserTip: 'import user',
},
};

View File

@@ -0,0 +1,25 @@
export default {
appuser: {
index: '#',
username: '用户名',
name: '姓名',
phone: '手机号',
post: '岗位',
role: '角色',
lockFlag: '状态',
createTime: '创建时间',
password: '密码',
dept: '部门',
email: '邮箱',
nickname: '昵称',
avatar: '头像',
inputNameTip: '请输入姓名',
inputRoleTip: '请选择角色',
inputUserNameTip: '请输入用户名',
inputPasswordTip: '请输入密码',
inputEmailTip: '请输入邮箱',
inputPhoneTip: '请输入手机号码',
inputNickNameTip: '请输入昵称',
importUserTip: '导入用户',
},
};

View File

@@ -0,0 +1,168 @@
<template>
<div class="layout-padding">
<div class="layout-padding-auto layout-padding-view">
<el-row class="ml10" v-show="showSearch">
<el-form :inline="true" :model="state.queryForm" @keyup.enter="getDataList" ref="queryRef">
<el-form-item :label="$t('appuser.username')" prop="username">
<el-input :placeholder="$t('appuser.inputUserNameTip')" @keyup.enter="getDataList" clearable v-model="state.queryForm.username" />
</el-form-item>
<el-form-item :label="$t('appuser.phone')" prop="phone">
<el-input :placeholder="$t('appuser.inputPhoneTip')" @keyup.enter="getDataList" clearable v-model="state.queryForm.phone" />
</el-form-item>
<el-form-item>
<el-button @click="getDataList" icon="Search" type="primary">{{ $t('common.queryBtn') }} </el-button>
<el-button @click="resetQuery" icon="Refresh">{{ $t('common.resetBtn') }}</el-button>
</el-form-item>
</el-form>
</el-row>
<el-row>
<div class="mb8" style="width: 100%">
<el-button @click="userDialogRef.openDialog()" class="ml10" icon="folder-add" type="primary" v-auth="'app_appuser_add'">
{{ $t('common.addBtn') }}
</el-button>
<el-button plain @click="excelUploadRef.show()" class="ml10" icon="upload-filled" type="primary" v-auth="'app_appuser_export'">
{{ $t('common.importBtn') }}
</el-button>
<el-button plain :disabled="multiple" @click="handleDelete(selectObjs)" class="ml10" icon="Delete" type="primary" v-auth="'app_appuser_del'">
{{ $t('common.delBtn') }}
</el-button>
<right-toolbar
:export="'app_appuser_export'"
@exportExcel="exportExcel"
@queryTable="getDataList"
class="ml10"
style="float: right; margin-right: 20px"
v-model:showSearch="showSearch"
></right-toolbar>
</div>
</el-row>
<el-table
:data="state.dataList"
@selection-change="handleSelectionChange"
style="width: 100%"
row-key="userId"
v-loading="state.loading"
border
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
>
<el-table-column align="center" type="selection" width="40" />
<el-table-column :label="$t('appuser.index')" type="index" width="60" />
<el-table-column :label="$t('appuser.username')" prop="username" show-overflow-tooltip></el-table-column>
<el-table-column :label="$t('appuser.nickname')" prop="nickname" show-overflow-tooltip></el-table-column>
<el-table-column :label="$t('appuser.avatar')" prop="avatar" show-overflow-tooltip>
<template #default="scope">
<div style="display: flex; justify-content: center">
<ImageUpload v-model:imageUrl="scope.row.avatar" height="50px" width="50px" disabled />
</div>
</template>
</el-table-column>
<el-table-column :label="$t('appuser.role')" show-overflow-tooltip>
<template #default="scope">
<el-tag :key="index" v-for="(item, index) in scope.row.roleList">{{ item.roleName }} </el-tag>
</template>
</el-table-column>
<el-table-column :label="$t('appuser.lockFlag')" show-overflow-tooltip>
<template #default="scope">
<dict-tag :options="lock_flag" :value="scope.row.lockFlag"></dict-tag>
</template>
</el-table-column>
<el-table-column :label="$t('appuser.createTime')" prop="createTime" show-overflow-tooltip></el-table-column>
<el-table-column :label="$t('common.action')" width="150">
<template #default="scope">
<el-button icon="edit-pen" @click="userDialogRef.openDialog(scope.row.userId)" text type="primary" v-auth="'app_appuser_edit'">
{{ $t('common.editBtn') }}
</el-button>
<el-button icon="delete" @click="handleDelete([scope.row.userId])" text type="primary" v-auth="'app_appuser_del'">
{{ $t('common.delBtn') }}
</el-button>
</template>
</el-table-column>
</el-table>
<pagination @current-change="currentChangeHandle" @size-change="sizeChangeHandle" v-bind="state.pagination"> </pagination>
</div>
<user-form @refresh="getDataList()" ref="userDialogRef" />
<upload-excel
:title="$t('appuser.importUserTip')"
@refreshDataList="getDataList"
ref="excelUploadRef"
temp-url="/admin/sys-file/local/file/appuser.xlsx"
url="/admin/appuser/import"
/>
</div>
</template>
<script lang="ts" name="systemUser" setup>
import { delObj, fetchList } from '/@/api/app/appuser';
import { BasicTableProps, useTable } from '/@/hooks/table';
import { useDict } from '/@/hooks/dict';
import { useMessage, useMessageBox } from '/@/hooks/message';
import { useI18n } from 'vue-i18n';
// 动态引入组件
const UserForm = defineAsyncComponent(() => import('./form.vue'));
const ImageUpload = defineAsyncComponent(() => import('/@/components/Upload/Image.vue'));
const { lock_flag } = useDict('lock_flag');
const { t } = useI18n();
// 定义变量内容
const userDialogRef = ref();
const excelUploadRef = ref();
const queryRef = ref();
const showSearch = ref(true);
// 多选rows
const selectObjs = ref([]) as any;
// 是否可以多选
const multiple = ref(true);
// 定义表格查询、后台调用的API
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {
username: '',
phone: '',
},
pageList: fetchList,
descs: ['create_time'],
});
// table hook
const { getDataList, currentChangeHandle, sizeChangeHandle, downBlobFile, tableStyle } = useTable(state);
// 清空搜索条件
const resetQuery = () => {
queryRef.value.resetFields();
getDataList();
};
// 导出excel
const exportExcel = () => {
downBlobFile('/app/appuser/export',Object.assign( state.queryForm,{ids:selectObjs}), 'users.xlsx');
};
// 多选事件
const handleSelectionChange = (objs: { userId: string }[]) => {
selectObjs.value = objs.map(({ userId }) => userId);
multiple.value = !objs.length;
};
// 删除用户
const handleDelete = async (ids: string[]) => {
try {
await useMessageBox().confirm(t('common.delConfirmText'));
} catch {
return;
}
try {
await delObj(ids);
getDataList();
useMessage().success(t('common.delSuccessText'));
} catch (err: any) {
useMessage().error(err.msg);
}
};
</script>

View File

@@ -0,0 +1,80 @@
<template>
<div>
<div>
<draggable class="draggable" v-model="navLists" item-key="index" animation="300">
<template v-slot:item="{ element: item, index }">
<del-wrap class="max-w-[400px]" :key="index" @close="handleDelete(index)">
<div class="flex items-center w-full p-4 mb-4 cursor-move bg-fill-light">
<upload-img v-model:imageUrl="item.image" height="50px" width="50px" :iconSize="12" />
<div class="flex-1 ml-3">
<div class="flex">
<span class="flex-none mr-3 text-tx-regular">名称</span>
<el-input v-model="item.name" placeholder="请输入名称" />
</div>
<div class="flex mt-[18px]">
<span class="flex-none mr-3 text-tx-regular">链接</span>
<link-picker v-model="item.link" />
</div>
</div>
</div>
</del-wrap>
</template>
</draggable>
</div>
<div>
<el-button type="primary" @click="handleAdd">添加</el-button>
</div>
</div>
</template>
<script lang="ts" setup>
import type { PropType } from 'vue';
import Draggable from 'vuedraggable';
import { useMessage } from '/@/hooks/message';
const LinkPicker = defineAsyncComponent(() => import('/@/components/Link/picker.vue'));
const props = defineProps({
modelValue: {
type: Array as PropType<any[]>,
default: () => [],
},
max: {
type: Number,
default: 10,
},
min: {
type: Number,
default: 1,
},
});
const emit = defineEmits(['update:modelValue']);
const navLists = computed({
get() {
return props.modelValue;
},
set(value) {
emit('update:modelValue', value);
},
});
const handleAdd = () => {
if (props.modelValue?.length < props.max) {
navLists.value.push({
image: '',
name: '导航名称',
link: {},
});
} else {
useMessage().error(`最多添加${props.max}`);
}
};
const handleDelete = (index: number) => {
if (props.modelValue?.length <= props.min) {
return useMessage().error(`最少保留${props.min}`);
}
navLists.value.splice(index, 1);
};
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,112 @@
<template>
<div>
<div>
<draggable class="draggable" v-model="workbenchLists" item-key="index" animation="300">
<template v-slot:item="{ element: item, index }">
<del-wrap class="max-w-[400px]" :key="index" @close="handleDelete(index)">
<div class="flex items-start w-full p-4 mb-4 cursor-move bg-fill-light">
<upload-img v-model:imageUrl="item.icon" height="50px" width="50px" :iconSize="12" />
<div class="flex-1 ml-3">
<div class="flex">
<span class="flex-none mr-3 text-tx-regular w-12">名称</span>
<el-input v-model="item.name" placeholder="请输入名称" />
</div>
<div class="flex mt-[18px]">
<span class="flex-none mr-3 text-tx-regular w-12">链接</span>
<link-picker v-model="item.link" />
</div>
<div v-if="showColor" class="flex mt-[18px]">
<span class="flex-none mr-3 text-tx-regular w-12">颜色</span>
<el-color-picker v-model="item.color" />
</div>
<div v-if="showBadge" class="flex mt-[18px]">
<span class="flex-none mr-3 text-tx-regular w-12">徽章</span>
<el-input v-model="item.badge" placeholder="可选,显示数字徽章" />
</div>
</div>
</div>
</del-wrap>
</template>
</draggable>
</div>
<div>
<el-button type="primary" @click="handleAdd">添加</el-button>
</div>
</div>
</template>
<script lang="ts" setup>
import type { PropType } from 'vue';
import Draggable from 'vuedraggable';
import { useMessage } from '/@/hooks/message';
import { useTimestamp } from '@vueuse/core';
import other from '/@/utils/other';
const LinkPicker = defineAsyncComponent(() => import('/@/components/Link/picker.vue'));
const props = defineProps({
modelValue: {
type: Array as PropType<any[]>,
default: () => [],
},
max: {
type: Number,
default: 8,
},
min: {
type: Number,
default: 1,
},
showColor: {
type: Boolean,
default: false,
},
showBadge: {
type: Boolean,
default: false,
},
});
const emit = defineEmits(['update:modelValue']);
const timestamp = useTimestamp();
const workbenchLists = computed({
get() {
return props.modelValue;
},
set(value) {
emit('update:modelValue', value);
},
});
const handleAdd = () => {
if (props.modelValue?.length < props.max) {
const newItem: any = {
id: other.getNonDuplicateID(),
icon: '',
name: '功能名称',
link: {},
};
if (props.showColor) {
newItem.color = '#007aff';
}
if (props.showBadge) {
newItem.badge = '';
}
workbenchLists.value.push(newItem);
} else {
useMessage().error(`最多添加${props.max}`);
}
};
const handleDelete = (index: number) => {
if (props.modelValue?.length <= props.min) {
return useMessage().error(`最少保留${props.min}`);
}
workbenchLists.value.splice(index, 1);
};
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,57 @@
<template>
<el-image :style="styles" v-bind="props" :src="src.includes('http') ? src : baseURL + src">
<template #placeholder>
<div class="image-slot"></div>
</template>
<template #error>
<div class="image-slot">
<el-icon><Picture /></el-icon>
</div>
</template>
</el-image>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import type { CSSProperties } from 'vue';
import { imageProps } from 'element-plus';
import other from '/@/utils/other';
const props = defineProps({
width: {
type: [String, Number],
default: 'auto',
},
height: {
type: [String, Number],
default: 'auto',
},
radius: {
type: [String, Number],
default: 0,
},
...imageProps,
});
const styles = computed<CSSProperties>(() => {
return {
width: other.addUnit(props.width),
height: other.addUnit(props.height),
borderRadius: other.addUnit(props.radius),
};
});
</script>
<style lang="scss" scoped>
.el-image {
display: block;
.image-slot {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
background: #fafafa;
color: #909399;
}
}
</style>

View File

@@ -0,0 +1,25 @@
<template>
<div class="pages-setting">
<div class="title flex items-center before:w-[3px] before:h-[14px] before:block before:bg-primary before:mr-2">
{{ widget?.title }}
</div>
<keep-alive>
<component class="pt-5 pr-4" :is="widgets[widget?.name]?.attr" :content="widget?.content" :styles="widget?.styles" :type="type" />
</keep-alive>
</div>
</template>
<script lang="ts" setup>
import type { PropType } from 'vue';
import widgets from '../widgets';
defineProps({
widget: {
type: Object as PropType<Record<string, any>>,
default: () => ({}),
},
type: {
type: String as PropType<'mobile' | 'pc'>,
default: 'mobile',
},
});
</script>

View File

@@ -0,0 +1,40 @@
<template>
<el-menu :default-active="modelValue" class="w-[160px] min-h-[668px] pages-menu" @select="handleSelect">
<el-menu-item v-for="(item, key) in menus" :index="key" :key="item.id">
<span>{{ item.name }}</span>
</el-menu-item>
</el-menu>
</template>
<script lang="ts" setup>
import type { PropType } from 'vue';
defineProps({
menus: {
type: Object as PropType<Record<string, any>>,
default: () => ({}),
},
modelValue: {
type: String,
default: '1',
},
});
const emit = defineEmits<{
(event: 'update:modelValue', value: string): void;
}>();
const handleSelect = (index: string) => {
emit('update:modelValue', index);
};
</script>
<style lang="scss" scoped>
.pages-menu {
:deep(.el-menu-item) {
border-color: transparent;
&.is-active {
border-right-width: 2px;
border-color: var(--el-color-primary);
background-color: var(--el-color-primary-light-9);
}
}
}
</style>

View File

@@ -0,0 +1,132 @@
<template>
<div class="shadow mx-[30px] pages-preview" :class="{ workbench: isWorkbench }">
<!-- 工作台顶部静态信息 -->
<div v-if="isWorkbench" class="workbench-header bg-gradient-to-r from-blue-500 to-blue-600 text-white p-4 mb-4">
<!-- 用户信息区域 -->
<div class="user-info flex items-center mb-4">
<div class="avatar w-12 h-12 bg-white/20 rounded-full flex items-center justify-center mr-3">
<svg width="24" height="24" viewBox="0 0 24 24" fill="white">
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/>
</svg>
</div>
<div>
<div class="text-sm opacity-80">夜深了</div>
<div class="font-medium">管理员</div>
</div>
</div>
<!-- 任务统计 -->
<div class="task-stats grid grid-cols-3 gap-4">
<div class="stat-item text-center">
<div class="stat-icon w-8 h-8 bg-orange-400 rounded-full mx-auto mb-2 flex items-center justify-center">
<svg width="16" height="16" viewBox="0 0 24 24" fill="white">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
</svg>
</div>
<div class="text-xl font-bold">0</div>
<div class="text-xs opacity-80">待办任务</div>
</div>
<div class="stat-item text-center">
<div class="stat-icon w-8 h-8 bg-blue-400 rounded-full mx-auto mb-2 flex items-center justify-center">
<svg width="16" height="16" viewBox="0 0 24 24" fill="white">
<path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-5 14H7v-2h7v2zm3-4H7v-2h10v2zm0-4H7V7h10v2z"/>
</svg>
</div>
<div class="text-xl font-bold">0</div>
<div class="text-xs opacity-80">秒送任务</div>
</div>
<div class="stat-item text-center">
<div class="stat-icon w-8 h-8 bg-green-400 rounded-full mx-auto mb-2 flex items-center justify-center">
<svg width="16" height="16" viewBox="0 0 24 24" fill="white">
<path d="M9 16.2L4.8 12l-1.4 1.4L9 19 21 7l-1.4-1.4L9 16.2z"/>
</svg>
</div>
<div class="text-xl font-bold">0</div>
<div class="text-xs opacity-80">已办任务</div>
</div>
</div>
</div>
<!-- 装修组件内容 -->
<div
v-for="(widget, index) in pageData"
:key="widget.id"
class="relative"
:class="{
'cursor-pointer': !widget?.disabled,
}"
@click="handleClick(widget, index)"
>
<div
class="absolute inset-0 pointer-events-none"
:class="{
'widget-selected': index === modelValue,
'widget-hoverable': !widget?.disabled,
}"
></div>
<slot>
<component :is="widgets[widget?.name]?.content" :content="widget.content" :styles="widget.styles" :key="widget.id" />
</slot>
</div>
<slot name="footer" />
</div>
</template>
<script lang="ts" setup>
import widgets from '../widgets';
import type { PropType } from 'vue';
const props = defineProps({
pageData: {
type: Array as PropType<any[]>,
default: () => [],
},
modelValue: {
type: Number,
default: 0,
},
pageType: {
type: Number,
default: 1,
},
});
const emit = defineEmits<{
(event: 'update:modelValue', value: number): void;
}>();
const isWorkbench = computed(() => {
return props.pageType === 4;
});
const handleClick = (widget: any, index: number) => {
if (widget.disabled) return;
emit('update:modelValue', index);
};
</script>
<style lang="scss" scoped>
.pages-preview {
background-color: #f8f8f8;
width: 360px;
height: 680px;
color: #333;
overflow-y: auto;
&.workbench {
height: 800px;
}
.widget-hoverable {
@apply border-2 border-dashed border-[#dcdfe6] z-[100];
}
.widget-selected {
@apply border-2 border-solid border-primary z-[101];
box-shadow: 0 0 0 2px rgba(var(--color-primary), 0.1);
}
}
.workbench-header {
border-radius: 12px 12px 0 0;
}
</style>

View File

@@ -0,0 +1,84 @@
<template>
<div>
<el-scrollbar style="height: 550px">
<el-form label-width="70px">
<el-form-item label="是否启用" v-if="type == 'mobile'">
<el-radio-group v-model="content.enabled">
<el-radio :label="1">开启</el-radio>
<el-radio :label="0">停用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="图片设置">
<div class="flex-1">
<div class="form-tips">最多添加5张建议图片尺寸750px*340px</div>
<draggable class="draggable" v-model="content.data" item-key="index" animation="300">
<template v-slot:item="{ element: item, index }">
<del-wrap :key="index" @close="handleDelete(index)" class="max-w-[640px]">
<div class="flex items-center w-full p-1 mt-4 cursor-move bg-fill-light">
<upload-img v-model:imageUrl="item.image" />
<div class="flex-1 ml-3">
<el-form-item label="图片名称">
<el-input v-model="item.name" placeholder="请输入名称" />
</el-form-item>
<el-form-item class="mt-[18px]" label="图片链接">
<link-picker v-if="type == 'mobile'" v-model="item.link" />
<el-input v-if="type == 'pc'" placeholder="请输入链接" v-model="item.link.path" />
</el-form-item>
</div>
</div>
</del-wrap>
</template>
</draggable>
</div>
</el-form-item>
<el-form-item v-if="content.data?.length < limit">
<el-button type="primary" @click="handleAdd">添加图片</el-button>
</el-form-item>
</el-form>
</el-scrollbar>
</div>
</template>
<script lang="ts" setup>
import type { PropType } from 'vue';
import type options from './options';
import { useMessage } from '/@/hooks/message';
import Draggable from 'vuedraggable';
const LinkPicker = defineAsyncComponent(() => import('/@/components/Link/picker.vue'));
const limit = 5;
type OptionsType = ReturnType<typeof options>;
const props = defineProps({
content: {
type: Object as PropType<OptionsType['content']>,
default: () => ({}),
},
styles: {
type: Object as PropType<OptionsType['styles']>,
default: () => ({}),
},
type: {
type: String as PropType<'mobile' | 'pc'>,
default: 'mobile',
},
});
const handleAdd = () => {
if (props.content.data?.length < limit) {
props.content.data.push({
image: '',
name: '',
link: {},
});
} else {
useMessage().error(`最多添加${limit}张图片`);
}
};
const handleDelete = (index: number) => {
if (props.content.data?.length <= 1) {
return useMessage().error('最少保留一张图片');
}
props.content.data.splice(index, 1);
};
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,37 @@
<template>
<div class="banner" :style="styles">
<div class="banner-image w-full h-full">
<decoration-img width="100%" :height="styles.height || height" :src="getImage" fit="contain" />
</div>
</div>
</template>
<script lang="ts" setup>
import type { PropType } from 'vue';
import type options from './options';
import DecorationImg from '../../decoration-img.vue';
type OptionsType = ReturnType<typeof options>;
const props = defineProps({
content: {
type: Object as PropType<OptionsType['content']>,
default: () => ({}),
},
styles: {
type: Object as PropType<OptionsType['styles']>,
default: () => ({}),
},
height: {
type: String,
default: '170px',
},
});
const getImage = computed(() => {
const { data } = props.content;
if (Array.isArray(data)) {
return data[0] ? data[0].image : '';
}
return '';
});
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,8 @@
import attr from './attr.vue';
import content from './content.vue';
import options from './options';
export default {
attr,
content,
options,
};

View File

@@ -0,0 +1,15 @@
export default () => ({
title: '首页轮播图',
name: 'banner',
content: {
enabled: 1,
data: [
{
image: '',
name: '',
link: {},
},
],
},
styles: {},
});

View File

@@ -0,0 +1,38 @@
<template>
<div>
<el-form label-width="90px">
<el-form-item label="客服标题">
<el-input class="w-[400px]" v-model="content.title" />
</el-form-item>
<el-form-item label="服务时间">
<el-input class="w-[400px]" v-model="content.time" />
</el-form-item>
<el-form-item label="联系电话">
<el-input class="w-[400px]" v-model="content.mobile" />
</el-form-item>
<el-form-item label="客服二维码">
<div>
<upload-img v-model:imageUrl="content.qrcode" />
<div class="form-tips">建议图片尺寸200*200像素图片格式jpgpngjpeg</div>
</div>
</el-form-item>
</el-form>
</div>
</template>
<script lang="ts" setup>
import type { PropType } from 'vue';
import type options from './options';
type OptionsType = ReturnType<typeof options>;
defineProps({
content: {
type: Object as PropType<OptionsType['content']>,
default: () => ({}),
},
styles: {
type: Object as PropType<OptionsType['styles']>,
default: () => ({}),
},
});
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,35 @@
<template>
<div class="customer-service">
<decoration-img width="140px" height="140px" :src="content.qrcode" alt="" />
<div class="text-[15px] mt-[7px] font-medium">{{ content.title }}</div>
<div class="text-[#666] mt-[20px]">服务时间{{ content.time }}</div>
<div class="text-[#666] mt-[7px]">客服电话{{ content.mobile }}</div>
<div class="text-white text-[16px] rounded-[42px] bg-[#4173FF] w-full h-[42px] flex justify-center items-center mt-[50px]">保存二维码图片</div>
</div>
</template>
<script lang="ts" setup>
import type { PropType } from 'vue';
import type options from './options';
import DecorationImg from '../../decoration-img.vue';
type OptionsType = ReturnType<typeof options>;
defineProps({
content: {
type: Object as PropType<OptionsType['content']>,
default: () => ({}),
},
styles: {
type: Object as PropType<OptionsType['styles']>,
default: () => ({}),
},
});
</script>
<style lang="scss" scoped>
.customer-service {
margin: 10px 18px;
border-radius: 10px;
padding: 50px 55px 80px;
background: #fff;
@apply flex flex-col justify-center items-center;
}
</style>

View File

@@ -0,0 +1,8 @@
import attr from './attr.vue';
import content from './content.vue';
import options from './options';
export default {
attr,
content,
options,
};

View File

@@ -0,0 +1,11 @@
export default () => ({
title: '客服设置',
name: 'customer-service',
content: {
title: '添加客服二维码',
time: '',
mobile: '',
qrcode: '',
},
styles: {},
});

View File

@@ -0,0 +1,13 @@
const widgets: Record<string, any> = import.meta.glob('./**/index.ts', { eager: true });
interface Widget {
attr: any;
content: any;
options: any;
}
const exportWidgets: Record<string, Widget> = {};
Object.keys(widgets).forEach((key) => {
const widgetName = key.replace(/^\.\/([\w-]+).*/gi, '$1');
exportWidgets[widgetName] = widgets[key]?.default;
});
export default exportWidgets;

View File

@@ -0,0 +1,40 @@
<template>
<div>
<el-scrollbar style="height: 700px">
<el-form label-width="70px">
<el-form-item label="排版样式">
<el-radio-group v-model="content.style">
<el-radio :label="1">横排</el-radio>
<el-radio :label="2">竖排</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="标题名称">
<el-input class="!w-[400px]" v-model="content.title" />
</el-form-item>
<el-form-item label="菜单设置">
<div class="flex-1">
<AddNav v-model="content.data" />
</div>
</el-form-item>
</el-form>
</el-scrollbar>
</div>
</template>
<script lang="ts" setup>
import type { PropType } from 'vue';
import type options from './options';
import AddNav from '../../add-nav.vue';
type OptionsType = ReturnType<typeof options>;
defineProps({
content: {
type: Object as PropType<OptionsType['content']>,
default: () => ({}),
},
styles: {
type: Object as PropType<OptionsType['styles']>,
default: () => ({}),
},
});
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,51 @@
<template>
<div class="my-service">
<div v-if="content.title" class="title px-[15px] py-[10px]">
<div>{{ content.title }}</div>
</div>
<div v-if="content.style == 1" class="flex flex-wrap pt-[20px] pb-[10px]">
<div v-for="(item, index) in content.data" :key="index" class="flex flex-col items-center w-1/4 mb-[15px]">
<decoration-img width="26px" height="26px" :src="item.image" alt="" />
<div class="mt-[7px]">{{ item.name }}</div>
</div>
</div>
<div v-if="content.style == 2">
<div v-for="(item, index) in content.data" :key="index" class="flex items-center border-b border-[#e5e5e5] h-[50px] px-[12px]">
<decoration-img width="24px" height="24px" :src="item.image" alt="" />
<div class="ml-[10px] flex-1">{{ item.name }}</div>
<div>
<el-icon><ArrowRight /></el-icon>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import type { PropType } from 'vue';
import type options from './options';
import DecorationImg from '../../decoration-img.vue';
type OptionsType = ReturnType<typeof options>;
defineProps({
content: {
type: Object as PropType<OptionsType['content']>,
default: () => ({}),
},
styles: {
type: Object as PropType<OptionsType['styles']>,
default: () => ({}),
},
});
</script>
<style lang="scss" scoped>
.my-service {
margin: 10px 10px 0;
background-color: #fff;
border-radius: 7px;
.title {
border-bottom: 1px solid #e5e5e5;
font-size: 16px;
font-weight: 500;
}
}
</style>

View File

@@ -0,0 +1,8 @@
import attr from './attr.vue';
import content from './content.vue';
import options from './options';
export default {
attr,
content,
options,
};

View File

@@ -0,0 +1,16 @@
export default () => ({
title: '我的服务',
name: 'my-service',
content: {
style: 1,
title: '我的服务',
data: [
{
image: '',
name: '导航名称',
link: {},
},
],
},
styles: {},
});

View File

@@ -0,0 +1,40 @@
<template>
<div>
<el-scrollbar style="height: 700px">
<el-form label-width="70px">
<el-form-item label="是否启用">
<el-radio-group v-model="content.enabled">
<el-radio :label="1">开启</el-radio>
<el-radio :label="0">停用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="菜单设置">
<div class="flex-1">
<div class="mb-4 form-tips">最多可添加10个建议图片尺寸100px*100px</div>
<el-scrollbar style="height: 500px">
<AddNav v-model="content.data" />
</el-scrollbar>
</div>
</el-form-item>
</el-form>
</el-scrollbar>
</div>
</template>
<script lang="ts" setup>
import type { PropType } from 'vue';
import type options from './options';
import AddNav from '../../add-nav.vue';
type OptionsType = ReturnType<typeof options>;
defineProps({
content: {
type: Object as PropType<OptionsType['content']>,
default: () => ({}),
},
styles: {
type: Object as PropType<OptionsType['styles']>,
default: () => ({}),
},
});
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,28 @@
<template>
<div class="nav bg-white pt-[15px] pb-[8px]">
<div class="flex flex-wrap">
<div v-for="(item, index) in content.data" :key="index" class="flex flex-col items-center w-1/5 mb-[15px]">
<decoration-img width="41px" height="41px" :src="item.image" alt="" />
<div class="mt-[7px]">{{ item.name }}</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import type { PropType } from 'vue';
import type options from './options';
import DecorationImg from '../../decoration-img.vue';
type OptionsType = ReturnType<typeof options>;
defineProps({
content: {
type: Object as PropType<OptionsType['content']>,
default: () => ({}),
},
styles: {
type: Object as PropType<OptionsType['styles']>,
default: () => ({}),
},
});
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,8 @@
import attr from './attr.vue';
import content from './content.vue';
import options from './options';
export default {
attr,
content,
options,
};

View File

@@ -0,0 +1,15 @@
export default () => ({
title: '导航菜单',
name: 'nav',
content: {
enabled: 1,
data: [
{
image: '',
name: '导航名称',
link: {},
},
],
},
styles: {},
});

View File

@@ -0,0 +1,20 @@
<template>
<div></div>
</template>
<script lang="ts" setup>
import type { PropType } from 'vue';
import type options from './options';
type OptionsType = ReturnType<typeof options>;
defineProps({
content: {
type: Object as PropType<OptionsType['content']>,
default: () => ({}),
},
styles: {
type: Object as PropType<OptionsType['styles']>,
default: () => ({}),
},
});
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,62 @@
<template>
<div class="news">
<div class="flex items-center news-title mx-[10px] my-[15px] text-[17px] font-medium">最新资讯</div>
<div v-for="item in newsList" :key="item.id" class="news-card flex bg-white px-[10px] py-[16px] text-[#333] border-[#f2f2f2] border-b">
<div class="mr-[10px]" v-if="item.image">
<img :src="item.image.includes('http') ? item.image : baseURL + item.image" class="w-[120px] h-[90px]" />
</div>
<div class="flex flex-col justify-between flex-1">
<div class="text-[15px] font-medium line-clamp-2">{{ item.title }}</div>
<div class="line-clamp-1 text-sm mt-[8px]">
{{ item.intro }}
</div>
<div class="text-[#999] text-xs w-full flex justify-between mt-[8px]">
<div>{{ item.createTime }}</div>
<div class="flex items-center">
<el-icon><View /></el-icon>
<div class="ml-[5px]">{{ item.visit }}</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import type { PropType } from 'vue';
import { fetchList } from '/@/api/app/appArticle';
import type options from './options';
type OptionsType = ReturnType<typeof options>;
defineProps({
content: {
type: Object as PropType<OptionsType['content']>,
default: () => ({}),
},
styles: {
type: Object as PropType<OptionsType['styles']>,
default: () => ({}),
},
});
const newsList = ref<any[]>([]);
const getData = async () => {
const { data } = await fetchList();
newsList.value = data.records;
};
getData();
</script>
<style lang="scss" scoped>
.news {
.news-title {
&::before {
content: '';
width: 4px;
height: 17px;
display: block;
margin-right: 5px;
background: #4173ff;
}
}
}
</style>

View File

@@ -0,0 +1,8 @@
import attr from './attr.vue';
import content from './content.vue';
import options from './options';
export default {
attr,
content,
options,
};

View File

@@ -0,0 +1,7 @@
export default () => ({
title: '资讯',
name: 'news',
disabled: 1,
content: {},
styles: {},
});

View File

@@ -0,0 +1,20 @@
<template>
<div></div>
</template>
<script lang="ts" setup>
import type { PropType } from 'vue';
import type options from './options';
type OptionsType = ReturnType<typeof options>;
defineProps({
content: {
type: Object as PropType<OptionsType['content']>,
default: () => ({}),
},
styles: {
type: Object as PropType<OptionsType['styles']>,
default: () => ({}),
},
});
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,23 @@
<template>
<div class="search">
<div class="search-con flex items-center px-[15px]">
<el-icon><Search /></el-icon>
<span class="ml-[5px]">请输入关键词搜索</span>
</div>
</div>
</template>
<script lang="ts" setup></script>
<style lang="scss" scoped>
.search {
background-color: #fff;
padding: 7px 12px;
.search-con {
height: 100%;
height: 36px;
border-radius: 36px;
background: #f4f4f4;
color: #999999;
}
}
</style>

View File

@@ -0,0 +1,8 @@
import attr from './attr.vue';
import content from './content.vue';
import options from './options';
export default {
attr,
content,
options,
};

View File

@@ -0,0 +1,7 @@
export default () => ({
title: '搜索',
name: 'search',
disabled: 1,
content: {},
styles: {},
});

View File

@@ -0,0 +1,79 @@
<template>
<div>
<el-scrollbar style="height: 550px">
<el-form label-width="70px">
<el-form-item label="是否启用">
<el-radio-group v-model="content.enabled">
<el-radio :label="1">开启</el-radio>
<el-radio :label="0">停用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="图片设置">
<div class="flex-1">
<div class="form-tips">最多添加5张建议图片尺寸750px*200px</div>
<draggable class="draggable" v-model="content.data" item-key="index" animation="300">
<template v-slot:item="{ element: item, index }">
<del-wrap :key="index" @close="handleDelete(index)" class="max-w-[400px]">
<div class="flex items-center w-full p-4 mt-4 cursor-move bg-fill-light">
<upload-img v-model:imageUrl="item.image" />
<div class="flex-1 ml-3">
<el-form-item label="图片名称">
<el-input v-model="item.name" placeholder="请输入名称" />
</el-form-item>
<el-form-item class="mt-[18px]" label="图片链接">
<link-picker v-model="item.link" />
</el-form-item>
</div>
</div>
</del-wrap>
</template>
</draggable>
</div>
</el-form-item>
<el-form-item v-if="content.data?.length < limit">
<el-button type="primary" @click="handleAdd">添加图片</el-button>
</el-form-item>
</el-form>
</el-scrollbar>
</div>
</template>
<script lang="ts" setup>
import { useMessage } from '/@/hooks/message';
import type { PropType } from 'vue';
import type options from './options';
import Draggable from 'vuedraggable';
const LinkPicker = defineAsyncComponent(() => import('/@/components/Link/picker.vue'));
const limit = 5;
type OptionsType = ReturnType<typeof options>;
const props = defineProps({
content: {
type: Object as PropType<OptionsType['content']>,
default: () => ({}),
},
styles: {
type: Object as PropType<OptionsType['styles']>,
default: () => ({}),
},
});
const handleAdd = () => {
if (props.content.data?.length < limit) {
props.content.data.push({
image: '',
name: '',
link: {},
});
} else {
useMessage().error(`最多添加${limit}张图片`);
}
};
const handleDelete = (index: number) => {
if (props.content.data?.length <= 1) {
return useMessage().error('最少保留一张图片');
}
props.content.data.splice(index, 1);
};
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,32 @@
<template>
<div class="banner mx-[10px] mt-[10px]">
<div class="banner-image">
<decoration-img width="100%" height="100px" :src="getImage" fit="contain" />
</div>
</div>
</template>
<script lang="ts" setup>
import type { PropType } from 'vue';
import type options from './options';
import DecorationImg from '../../decoration-img.vue';
type OptionsType = ReturnType<typeof options>;
const props = defineProps({
content: {
type: Object as PropType<OptionsType['content']>,
default: () => ({}),
},
styles: {
type: Object as PropType<OptionsType['styles']>,
default: () => ({}),
},
});
const getImage = computed(() => {
const { data } = props.content;
if (Array.isArray(data)) {
return data[0] ? data[0].image : '';
}
return '';
});
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,8 @@
import attr from './attr.vue';
import content from './content.vue';
import options from './options';
export default {
attr,
content,
options,
};

View File

@@ -0,0 +1,15 @@
export default () => ({
title: '个人中心广告图',
name: 'user-banner',
content: {
enabled: 1,
data: [
{
image: '',
name: '',
link: {},
},
],
},
styles: {},
});

View File

@@ -0,0 +1,20 @@
<template>
<div></div>
</template>
<script lang="ts" setup>
import type { PropType } from 'vue';
import type options from './options';
type OptionsType = ReturnType<typeof options>;
defineProps({
content: {
type: Object as PropType<OptionsType['content']>,
default: () => ({}),
},
styles: {
type: Object as PropType<OptionsType['styles']>,
default: () => ({}),
},
});
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,16 @@
<template>
<div class="user-info flex items-center px-[25px]">
<img src="./images/default_avatar.png" class="w-[60px] h-[60px]" alt="" />
<div class="text-white text-[18px] ml-[10px]">未登录</div>
</div>
</template>
<script lang="ts" setup></script>
<style lang="scss" scoped>
.user-info {
background: url(./images/my_topbg.png);
height: 115px;
background-position: bottom;
background-size: 100% auto;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

View File

@@ -0,0 +1,8 @@
import attr from './attr.vue';
import content from './content.vue';
import options from './options';
export default {
attr,
content,
options,
};

View File

@@ -0,0 +1,7 @@
export default () => ({
title: '用户信息',
name: 'user-info',
disabled: 1,
content: {},
styles: {},
});

View File

@@ -0,0 +1,40 @@
<template>
<div>
<el-scrollbar style="height: 700px">
<el-form label-width="70px">
<el-form-item label="是否启用">
<el-radio-group v-model="content.enabled">
<el-radio :label="1">开启</el-radio>
<el-radio :label="0">停用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="快捷设置">
<div class="flex-1">
<div class="mb-4 form-tips">最多可添加8个建议图标尺寸24px*24px</div>
<el-scrollbar style="height: 500px">
<AddWorkbenchItem v-model="content.data" :max="8" :show-color="true" />
</el-scrollbar>
</div>
</el-form-item>
</el-form>
</el-scrollbar>
</div>
</template>
<script lang="ts" setup>
import type { PropType } from 'vue';
import type options from './options';
import AddWorkbenchItem from '../../add-workbench-item.vue';
type OptionsType = ReturnType<typeof options>;
defineProps({
content: {
type: Object as PropType<OptionsType['content']>,
default: () => ({}),
},
styles: {
type: Object as PropType<OptionsType['styles']>,
default: () => ({}),
},
});
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,53 @@
<template>
<div class="shortcuts-section bg-white p-4 mb-4">
<div class="grid grid-cols-4 gap-4">
<div v-for="(item, index) in content.data" :key="index" class="flex flex-col items-center p-4 hover:bg-gray-50 rounded-lg cursor-pointer">
<div class="icon-wrapper mb-3" :style="{ backgroundColor: item.color || '#007aff' }">
<decoration-img v-if="item.icon" width="24px" height="24px" :src="item.icon" alt="" />
<div v-else class="w-6 h-6 flex items-center justify-center">
<svg width="16" height="16" viewBox="0 0 16 16" fill="white">
<path d="M8 1L8 15M1 8L15 8" stroke="white" stroke-width="2" stroke-linecap="round"/>
</svg>
</div>
</div>
<span class="text-sm text-gray-700 text-center font-medium">{{ item.name }}</span>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import type { PropType } from 'vue';
import type options from './options';
import DecorationImg from '../../decoration-img.vue';
type OptionsType = ReturnType<typeof options>;
defineProps({
content: {
type: Object as PropType<OptionsType['content']>,
default: () => ({}),
},
styles: {
type: Object as PropType<OptionsType['styles']>,
default: () => ({}),
},
});
</script>
<style lang="scss" scoped>
.shortcuts-section {
border-radius: 12px;
}
.icon-wrapper {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.icon-wrapper:hover {
transform: scale(1.05);
}
</style>

View File

@@ -0,0 +1,8 @@
import attr from './attr.vue';
import content from './content.vue';
import options from './options';
export default {
attr,
content,
options,
};

View File

@@ -0,0 +1,22 @@
export default () => ({
title: '快捷操作',
name: 'workbench-shortcuts',
content: {
enabled: 1,
data: [
{
icon: '',
name: '扫一扫',
link: {},
color: '#ff9500',
},
{
icon: '',
name: '系统信息',
link: {},
color: '#007aff',
},
],
},
styles: {},
});

View File

@@ -0,0 +1,46 @@
<template>
<div>
<el-scrollbar style="height: 700px">
<el-form label-width="70px">
<el-form-item label="是否启用">
<el-radio-group v-model="content.enabled">
<el-radio :label="1">开启</el-radio>
<el-radio :label="0">停用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="标题">
<el-input class="!w-[400px]" v-model="content.title" placeholder="请输入标题" />
</el-form-item>
<el-form-item label="副标题">
<el-input class="!w-[400px]" v-model="content.subtitle" placeholder="请输入副标题" />
</el-form-item>
<el-form-item label="功能设置">
<div class="flex-1">
<div class="mb-4 form-tips">最多可添加8个建议图标尺寸24px*24px</div>
<el-scrollbar style="height: 500px">
<AddWorkbenchItem v-model="content.data" :max="8" />
</el-scrollbar>
</div>
</el-form-item>
</el-form>
</el-scrollbar>
</div>
</template>
<script lang="ts" setup>
import type { PropType } from 'vue';
import type options from './options';
import AddWorkbenchItem from '../../add-workbench-item.vue';
type OptionsType = ReturnType<typeof options>;
defineProps({
content: {
type: Object as PropType<OptionsType['content']>,
default: () => ({}),
},
styles: {
type: Object as PropType<OptionsType['styles']>,
default: () => ({}),
},
});
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,62 @@
<template>
<div class="workbench-section bg-white p-4 mb-4">
<div class="section-header flex items-center justify-between mb-3">
<div class="flex items-center">
<h3 class="text-lg font-medium text-gray-900">{{ content.title }}</h3>
<div class="ml-2 text-sm text-gray-500">{{ content.subtitle }}</div>
</div>
<div class="expand-icon">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M6 12L10 8L6 4" stroke="#999" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
</div>
<div class="grid grid-cols-4 gap-3">
<div v-for="(item, index) in content.data" :key="index" class="flex flex-col items-center p-3 hover:bg-gray-50 rounded-lg cursor-pointer">
<div class="icon-wrapper mb-2">
<decoration-img v-if="item.icon" width="24px" height="24px" :src="item.icon" alt="" />
<div v-else class="w-6 h-6 bg-blue-500 rounded flex items-center justify-center">
<svg width="16" height="16" viewBox="0 0 16 16" fill="white">
<path d="M8 1L8 15M1 8L15 8" stroke="white" stroke-width="2" stroke-linecap="round"/>
</svg>
</div>
</div>
<span class="text-sm text-gray-700 text-center">{{ item.name }}</span>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import type { PropType } from 'vue';
import type options from './options';
import DecorationImg from '../../decoration-img.vue';
type OptionsType = ReturnType<typeof options>;
defineProps({
content: {
type: Object as PropType<OptionsType['content']>,
default: () => ({}),
},
styles: {
type: Object as PropType<OptionsType['styles']>,
default: () => ({}),
},
});
</script>
<style lang="scss" scoped>
.workbench-section {
border-radius: 12px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.section-header {
border-bottom: 1px solid #f0f0f0;
padding-bottom: 12px;
}
.icon-wrapper {
display: flex;
align-items: center;
justify-content: center;
}
</style>

View File

@@ -0,0 +1,8 @@
import attr from './attr.vue';
import content from './content.vue';
import options from './options';
export default {
attr,
content,
options,
};

View File

@@ -0,0 +1,32 @@
export default () => ({
title: '系统管理',
name: 'workbench-system',
content: {
enabled: 1,
title: '系统管理',
subtitle: '系统功能配置',
data: [
{
icon: '',
name: '用户管理',
link: {},
},
{
icon: '',
name: '缓存管理',
link: {},
},
{
icon: '',
name: '在线用户',
link: {},
},
{
icon: '',
name: '路由管理',
link: {},
},
],
},
styles: {},
});

View File

@@ -0,0 +1,46 @@
<template>
<div>
<el-scrollbar style="height: 700px">
<el-form label-width="70px">
<el-form-item label="是否启用">
<el-radio-group v-model="content.enabled">
<el-radio :label="1">开启</el-radio>
<el-radio :label="0">停用</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="标题">
<el-input class="!w-[400px]" v-model="content.title" placeholder="请输入标题" />
</el-form-item>
<el-form-item label="副标题">
<el-input class="!w-[400px]" v-model="content.subtitle" placeholder="请输入副标题" />
</el-form-item>
<el-form-item label="工具设置">
<div class="flex-1">
<div class="mb-4 form-tips">最多可添加6个建议图标尺寸32px*32px</div>
<el-scrollbar style="height: 500px">
<AddWorkbenchItem v-model="content.data" :max="6" :show-badge="true" />
</el-scrollbar>
</div>
</el-form-item>
</el-form>
</el-scrollbar>
</div>
</template>
<script lang="ts" setup>
import type { PropType } from 'vue';
import type options from './options';
import AddWorkbenchItem from '../../add-workbench-item.vue';
type OptionsType = ReturnType<typeof options>;
defineProps({
content: {
type: Object as PropType<OptionsType['content']>,
default: () => ({}),
},
styles: {
type: Object as PropType<OptionsType['styles']>,
default: () => ({}),
},
});
</script>
<style lang="scss" scoped></style>

View File

@@ -0,0 +1,65 @@
<template>
<div class="workbench-section bg-white p-4 mb-4">
<div class="section-header flex items-center justify-between mb-3">
<div class="flex items-center">
<h3 class="text-lg font-medium text-gray-900">{{ content.title }}</h3>
<div class="ml-2 text-sm text-gray-500">{{ content.subtitle }}</div>
</div>
<div class="expand-icon">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M6 12L10 8L6 4" stroke="#999" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
</div>
<div class="grid grid-cols-3 gap-4">
<div v-for="(item, index) in content.data" :key="index" class="relative flex flex-col items-center p-4 hover:bg-gray-50 rounded-lg cursor-pointer">
<div class="icon-wrapper mb-2 relative">
<decoration-img v-if="item.icon" width="32px" height="32px" :src="item.icon" alt="" />
<div v-else class="w-8 h-8 bg-green-500 rounded flex items-center justify-center">
<svg width="20" height="20" viewBox="0 0 16 16" fill="white">
<path d="M8 1L8 15M1 8L15 8" stroke="white" stroke-width="2" stroke-linecap="round"/>
</svg>
</div>
<div v-if="item.badge" class="absolute -top-2 -right-2 bg-red-500 text-white text-xs rounded-full min-w-[16px] h-4 flex items-center justify-center px-1">
{{ item.badge }}
</div>
</div>
<span class="text-sm text-gray-700 text-center">{{ item.name }}</span>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import type { PropType } from 'vue';
import type options from './options';
import DecorationImg from '../../decoration-img.vue';
type OptionsType = ReturnType<typeof options>;
defineProps({
content: {
type: Object as PropType<OptionsType['content']>,
default: () => ({}),
},
styles: {
type: Object as PropType<OptionsType['styles']>,
default: () => ({}),
},
});
</script>
<style lang="scss" scoped>
.workbench-section {
border-radius: 12px;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.section-header {
border-bottom: 1px solid #f0f0f0;
padding-bottom: 12px;
}
.icon-wrapper {
display: flex;
align-items: center;
justify-content: center;
}
</style>

View File

@@ -0,0 +1,8 @@
import attr from './attr.vue';
import content from './content.vue';
import options from './options';
export default {
attr,
content,
options,
};

View File

@@ -0,0 +1,28 @@
export default () => ({
title: '效率工具',
name: 'workbench-tools',
content: {
enabled: 1,
title: '效率工具',
subtitle: '提升工作效率',
data: [
{
icon: '',
name: '日程管理',
link: {},
badge: '2',
},
{
icon: '',
name: '定时任务',
link: {},
},
{
icon: '',
name: '帮助文档',
link: {},
},
],
},
styles: {},
});

View File

@@ -0,0 +1,115 @@
<template>
<div class="layout-padding decoration-pages min-w-[1100px]">
<el-card shadow="never" class="!border-none flex-1 flex" :body-style="{ flex: 1 }">
<div class="flex items-start h-full">
<Menu v-model="activeMenu" :menus="menus" />
<preview v-model="selectWidgetIndex" :pageData="getPageData" :pageType="menus[activeMenu]?.pageType">
<template #footer>
<div class="flex justify-center mt-4">
<el-button type="primary" @click="setData">保存</el-button>
</div>
</template>
</preview>
<attr-setting class="flex-1" :widget="getSelectWidget" />
</div>
</el-card>
</div>
</template>
<script lang="ts" setup name="decorationPages">
import Menu from './component/pages/menu.vue';
import Preview from './component/pages/preview.vue';
import AttrSetting from './component/pages/attr-setting.vue';
import widgets from './component/widgets';
import { getObj, putObj } from '/@/api/app/page';
import { useMessage } from '/@/hooks/message';
import other from '/@/utils/other';
enum pagesTypeEnum {
HOME = '1',
USER = '2',
SERVICE = '3',
WORKBENCH = '4',
}
const generatePageData = (widgetNames: string[]) => {
return widgetNames.map((widgetName) => {
const options = {
id: other.getNonDuplicateID(),
...(widgets[widgetName]?.options() || {}),
};
return options;
});
};
const menus: Record<
string,
{
pageType: number;
name: string;
pageData: any[];
}
> = reactive({
[pagesTypeEnum.HOME]: {
pageType: 1,
name: '首页装修',
pageData: generatePageData(['search', 'banner', 'nav', 'news']),
},
[pagesTypeEnum.USER]: {
pageType: 2,
name: '个人中心',
pageData: generatePageData(['user-info', 'my-service', 'user-banner']),
},
[pagesTypeEnum.SERVICE]: {
pageType: 3,
name: '客服设置',
pageData: generatePageData(['customer-service']),
},
[pagesTypeEnum.WORKBENCH]: {
pageType: 4,
name: '工作台装修',
pageData: generatePageData(['workbench-shortcuts','workbench-system', 'workbench-tools']),
},
});
const activeMenu = ref('4');
const selectWidgetIndex = ref(-1);
const getPageData = computed(() => {
return menus[activeMenu.value]?.pageData ?? [];
});
const getSelectWidget = computed(() => {
return menus[activeMenu.value]?.pageData[selectWidgetIndex.value] ?? '';
});
const getData = async () => {
const { data } = await getObj(activeMenu.value);
menus[String(data.pageType)].pageData = JSON.parse(data.pageData);
};
const setData = async () => {
await putObj({
...menus[activeMenu.value],
pageData: JSON.stringify(menus[activeMenu.value].pageData),
});
getData();
useMessage().success('保存成功');
};
watch(
activeMenu,
() => {
selectWidgetIndex.value = getPageData.value.findIndex((item) => !item.disabled);
getData();
},
{
immediate: true,
}
);
</script>
<style lang="scss" scoped>
.decoration-pages {
min-height: calc(100vh - var(--navbar-height) - 80px);
@apply flex flex-col;
}
.el-menu {
width: 15% !important;
}
</style>

View File

@@ -0,0 +1,147 @@
<template>
<div class="layout-padding decoration-tabbar min-w-[800px] h-full">
<el-card shadow="never" class="!border-none flex-1 h-full dark:bg-gray-800" :body-style="{ height: '100%' }">
<div class="flex items-start h-full">
<div class="pages-preview mx-[30px]">
<div class="flex tabbar dark:bg-gray-700">
<div class="flex flex-col flex-1 justify-center items-center tabbar-item" v-for="(item, index) in tabbar.list" :key="index">
<img class="w-[22px] h-[22px]" :src="item.unselected.includes('http') ? item.unselected: baseURL + item.unselected" alt="" />
<div class="leading-3 text-[12px] mt-[4px] dark:text-gray-300">{{ item.name }}</div>
</div>
</div>
</div>
<div class="overflow-y-auto flex-1">
<div class="title flex items-center before:w-[3px] before:h-[14px] before:block before:bg-primary before:mr-2 dark:text-gray-200">
底部导航设置
<span class="form-tips ml-[10px] !mt-0 dark:text-gray-400"> 至少添加2个导航最多添加5个导航 </span>
</div>
<el-form label-width="70px">
<div class="mb-[18px]">
<el-scrollbar class="h-[calc(100vh-300px)]">
<draggable class="draggable" v-model="tabbar.list" animation="300" draggable=".draggable" itemKey="index" :move="onMove">
<template v-slot:item="{ element, index }">
<del-wrap @close="handleDelete(index)" class="max-w-[400px]" :class="{ draggable: index != 0 }">
<div class="p-4 mt-4 w-full bg-fill-light">
<el-form-item label="导航图标">
<upload-img v-model:imageUrl="element.unselected" height="60px" width="60px" />
<upload-img v-model:imageUrl="element.selected" height="60px" width="60px" class="ml-2" />
</el-form-item>
<el-form-item label="导航名称">
<el-input v-model="element.name" placeholder="请输入名称" />
</el-form-item>
<el-form-item label="链接地址">
<link-picker v-model="element.link" :disabled="index == 0" />
</el-form-item>
</div>
</del-wrap>
</template>
</draggable>
</el-scrollbar>
</div>
<el-form-item v-if="tabbar.list?.length < max" label-width="0">
<el-button type="primary" @click="setData">保存导航</el-button>
<el-button type="primary" @click="handleAdd"> 添加导航 </el-button>
</el-form-item>
</el-form>
</div>
</div>
</el-card>
</div>
</template>
<script lang="ts" setup name="decorationTabbar">
import { fetchList, putObj } from '/@/api/app/tabbar';
import Draggable from 'vuedraggable';
import { useMessage } from '/@/hooks/message';
const LinkPicker = defineAsyncComponent(() => import('/@/components/Link/picker.vue'));
const max = 5;
const min = 2;
const tabbar = reactive({
list: [
{
name: '',
selected: '',
unselected: '',
link: {},
},
{
name: '',
selected: '',
unselected: '',
link: {},
},
],
});
const handleAdd = () => {
if (tabbar.list?.length < max) {
tabbar.list.push({
name: '',
selected: '',
unselected: '',
link: {},
});
} else {
useMessage().error(`最多添加${max}`);
}
};
const handleDelete = (index: number) => {
if (tabbar.list?.length <= min) {
return useMessage().error(`最少保留${min}`);
}
tabbar.list.splice(index, 1);
};
const onMove = (e: any) => {
if (e.relatedContext.index == 0) {
return false;
}
return true;
};
const getData = async () => {
const { data } = await fetchList();
tabbar.list = data;
};
const setData = async () => {
const data = toRaw(tabbar.list).map((item, index) => {
return {
id: item.id,
name: item.name,
selected: item.selected,
unselected: item.unselected,
link: JSON.stringify(item.link), // 将link转为字符串
sortOrder: index,
};
});
await putObj(data);
getData();
useMessage().success('保存成功');
};
getData();
// Add this to handle baseURL
const baseURL = import.meta.env.VITE_APP_BASE_API || '';
</script>
<style lang="scss" scoped>
.decoration-tabbar {
height: calc(100vh - var(--navbar-height) - 80px);
@apply flex flex-col;
.pages-preview {
@apply bg-gray-100 dark:bg-gray-700;
width: 360px;
height: 600px;
@apply text-gray-800 dark:text-gray-200;
position: relative;
.tabbar {
position: absolute;
height: 50px;
@apply bg-white dark:bg-gray-600;
bottom: 0;
width: 100%;
border: 2px solid var(--el-color-primary);
}
}
}
</style>

View File

@@ -0,0 +1,149 @@
<template>
<el-dialog v-model="visible" :close-on-click-modal="false" :title="form.id ? $t('common.editBtn') : $t('common.addBtn')" draggable>
<el-form ref="dataFormRef" v-loading="loading" :model="form" :rules="dataRules" label-width="90px">
<el-row :gutter="24">
<el-col :span="12" class="mb20">
<el-form-item :label="t('fans.wxAccountName')" prop="wxAccountName">
<el-input v-model="form.wxAccountName" disabled />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('fans.wxAccountAppid')" prop="wxAccountAppid">
<el-input v-model="form.wxAccountAppid" disabled />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('fans.openid')" prop="openid">
<el-input v-model="form.openid" disabled />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('fans.nickname')" prop="nickname">
<el-input v-model="form.nickname" disabled />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('fans.remark')" prop="remark">
<el-input v-model="form.remark" :placeholder="t('fans.inputremarkTip')" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('fans.tagIds')" prop="tagIds">
<el-select v-model="form.tagIds" :placeholder="t('fans.inputTagTip')" clearable multiple>
<el-option v-for="item in tagOption" :key="item.tagId" :label="item.tag" :value="item.tagId" />
</el-select>
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">{{ $t('common.cancelButtonText') }}</el-button>
<el-button type="primary" @click="onSubmit">{{ $t('common.confirmButtonText') }}</el-button>
</span>
</template>
</el-dialog>
</template>
<script lang="ts" name="wx-fans" setup>
import { addObj, getObj, putObj } from '/@/api/mp/wx-account-fans';
import { list } from '/@/api/mp/wx-account-tag';
import { useMessage } from '/@/hooks/message';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const emit = defineEmits(['refresh']);
// 定义变量内容
const dataFormRef = ref();
const visible = ref(false);
const loading = ref(false);
const wxAccountAppid = ref();
// 提交表单数据
const form = reactive({
id: '',
});
const dataRules = ref([]);
// 打开弹窗
const openDialog = (row: any, accountId: string) => {
visible.value = true;
form.id = row.id;
wxAccountAppid.value = accountId;
// 重置表单数据
if (dataFormRef.value) {
dataFormRef.value.resetFields();
}
if (form.id) {
getFansData();
}
getTagList();
};
const getFansData = () => {
loading.value = true;
getObj(form.id)
.then((res) => {
Object.assign(form, res.data);
})
.finally(() => {
loading.value = false;
});
};
// 提交
const onSubmit = () => {
dataFormRef.value.validate((valid: boolean) => {
if (!valid) {
return false;
}
loading.value = true;
if (form.id) {
putObj(form)
.then(() => {
useMessage().success(t('common.editSuccessText'));
visible.value = false; // 关闭弹窗
emit('refresh');
})
.catch((err: any) => {
useMessage().error(err.msg);
})
.finally(() => {
loading.value = false;
});
} else {
addObj(form)
.then(() => {
useMessage().success(t('common.addSuccessText'));
visible.value = false; // 关闭弹窗
emit('refresh');
})
.catch((err: any) => {
useMessage().error(err.msg);
})
.finally(() => {
loading.value = false;
});
}
});
};
const tagOption = ref([]);
const getTagList = () => {
list(wxAccountAppid.value).then((res) => {
tagOption.value = res.data;
});
};
// 暴露变量
defineExpose({
openDialog,
});
</script>
<style scoped></style>

View File

@@ -0,0 +1,24 @@
export default {
fans: {
index: '#',
importwxAccountFansTip: 'import WxAccountFans',
id: 'id',
openid: 'openid',
subscribeStatus: 'subscribeStatus',
subscribeTime: 'subscribeTime',
nickname: 'nickname',
gender: 'gender',
language: 'language',
country: 'country',
province: 'province',
isBlack: 'black',
city: 'city',
tagIds: 'tagIds',
headimgUrl: 'headimgUrl',
remark: 'remark',
wxAccountId: 'wxAccountId',
wxAccountName: 'wxAccountName',
wxAccountAppid: 'wxAccountAppid',
inputNicknameTip: 'input nickname',
},
};

View File

@@ -0,0 +1,26 @@
export default {
fans: {
index: '#',
importwxAccountFansTip: '导入微信公众号粉丝表',
id: '主键',
openid: '用户标识',
subscribeStatus: '订阅状态',
subscribeTime: '订阅时间',
nickname: '昵称',
gender: '性别',
language: '语言',
country: '国家',
province: '省份',
isBlack: '黑名单',
city: '城市',
tagIds: '分组',
headimgUrl: ' headimgUrl',
remark: '备注',
wxAccountId: '微信公众号ID',
wxAccountName: '微信公众号',
wxAccountAppid: '公众号appid',
inputremarkTip: '请输入备注',
inputTagTip: '请选择分组',
inputNicknameTip: '请输入粉丝昵称',
},
};

View File

@@ -0,0 +1,222 @@
<template>
<div class="layout-padding">
<div class="layout-padding-auto layout-padding-view">
<el-row v-show="showSearch">
<el-form ref="queryRef" :inline="true" :model="state.queryForm" @keyup.enter="getDataList">
<el-form-item :label="$t('fans.nickname')" prop="nickname">
<el-input v-model="state.queryForm.nickname" style="max-width: 180px" :placeholder="$t('fans.inputNicknameTip')" />
</el-form-item>
<el-form-item :label="$t('fans.wxAccountName')" prop="wxAccountAppid">
<el-select v-model="state.queryForm.wxAccountAppid" :placeholder="$t('fans.wxAccountName')" clearable>
<el-option v-for="item in accountList" :key="item.appid" :label="item.name" :value="item.appid" />
</el-select>
</el-form-item>
<el-form-item>
<el-button icon="search" type="primary" @click="getDataList">
{{ $t('common.queryBtn') }}
</el-button>
<el-button icon="Refresh" @click="resetQuery">{{ $t('common.resetBtn') }}</el-button>
</el-form-item>
</el-form>
</el-row>
<el-row>
<div class="mb8" style="width: 100%">
<el-button type="primary" class="ml10" icon="Sort" @click="asyncFans" v-auth="'mp_wxaccountfans_sync'">同步</el-button>
<right-toolbar
:export="'mp_wxaccountfans_sync'"
@exportExcel="exportExcel"
v-model:showSearch="showSearch"
class="ml10"
style="float: right; margin-right: 20px"
@queryTable="getDataList"
></right-toolbar>
</div>
</el-row>
<el-table
v-loading="state.loading"
:data="state.dataList"
style="width: 100%"
@sort-change="sortChangeHandle"
border
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
>
<el-table-column :label="t('fans.index')" type="index" width="60" />
<el-table-column :label="t('fans.openid')" prop="openid" show-overflow-tooltip />
<el-table-column :label="t('fans.subscribeStatus')" prop="subscribeStatus" show-overflow-tooltip>
<template #default="scope">
<dict-tag :options="subscribe" :value="scope.row.subscribeStatus"></dict-tag>
</template>
</el-table-column>
<el-table-column :label="t('fans.subscribeTime')" prop="subscribeTime" show-overflow-tooltip />
<el-table-column :label="t('fans.nickname')" prop="nickname" show-overflow-tooltip />
<el-table-column :label="t('fans.language')" prop="language" show-overflow-tooltip />
<el-table-column :label="t('fans.isBlack')" prop="isBlack" show-overflow-tooltip>
<template #default="scope">
<dict-tag :options="blackList" :value="scope.row.isBlack"></dict-tag>
</template>
</el-table-column>
<el-table-column :label="t('fans.tagIds')" prop="tagIds" show-overflow-tooltip>
<template #default="scope">
<span v-for="(tag, index) in scope.row.tagList" :key="index">
<el-tag>{{ tag.tag }} </el-tag>&nbsp;&nbsp;
</span>
</template>
</el-table-column>
<el-table-column :label="t('fans.remark')" prop="remark" show-overflow-tooltip />
<el-table-column :label="t('fans.wxAccountName')" prop="wxAccountName" show-overflow-tooltip />
<el-table-column :label="$t('common.action')" width="250" fixed="right">
<template #default="scope">
<el-button icon="edit-pen" text type="primary" @click="formDialogRef.openDialog(scope.row, state.queryForm.wxAccountAppid)"
>{{ $t('common.editBtn') }}
</el-button>
<el-button icon="delete" text type="primary" @click="handleDelete([scope.row.id])">{{ $t('common.delBtn') }} </el-button>
<el-button icon="CircleCheck" text type="primary" @click="handelUnBlack([scope.row.id])" v-if="scope.row.isBlack"> 取消拉黑 </el-button>
<el-button icon="warning" text type="primary" @click="handelBlack([scope.row.id])" v-else> 拉黑 </el-button>
</template>
</el-table-column>
</el-table>
<pagination v-bind="state.pagination" @size-change="sizeChangeHandle" @current-change="currentChangeHandle" />
<form-dialog ref="formDialogRef" @refresh="getDataList"></form-dialog>
</div>
</div>
</template>
<script lang="ts" name="systemWxAccountFans" setup>
import { BasicTableProps, useTable } from '/@/hooks/table';
import { black, delObjs, fetchList, sync, unblack } from '/@/api/mp/wx-account-fans';
import { fetchAccountList } from '/@/api/mp/wx-account';
import { useMessage, useMessageBox } from '/@/hooks/message';
import { useI18n } from 'vue-i18n';
import { useDict } from '/@/hooks/dict';
const FormDialog = defineAsyncComponent(() => import('./form.vue'));
const { subscribe } = useDict('subscribe');
// 引入组件
const { t } = useI18n();
// 定义查询字典
const blackList = ref([
{
label: '是',
value: '1',
},
{
label: '否',
value: '0',
},
]);
// 定义变量内容
const formDialogRef = ref();
// 搜索变量
const queryRef = ref();
const showSearch = ref(true);
// 多选变量
const selectObjs = ref([]) as any;
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {},
pageList: fetchList,
createdIsNeed: false,
});
// table hook
const { getDataList, currentChangeHandle, sizeChangeHandle, sortChangeHandle, downBlobFile, tableStyle } = useTable(state);
const accountList = ref([]);
const getAccountList = () => {
fetchAccountList().then((res) => {
accountList.value = res.data;
if (accountList.value.length > 0) {
state.queryForm.wxAccountAppid = accountList.value[0].appid;
getDataList();
}
});
};
watch(
() => state.queryForm.wxAccountAppid,
() => {
getDataList();
}
);
const asyncFans = () => {
if (state.queryForm.wxAccountAppid) {
sync(state.queryForm.wxAccountAppid).then(() => {
useMessage().success('已开始从微信同步粉丝信息,建议等待后查询');
getDataList();
});
} else {
useMessage().error('请选择公众号');
}
};
onMounted(() => {
getAccountList();
});
// 清空搜索条件
const resetQuery = () => {
// 清空搜索条件
queryRef.value.resetFields();
// 清空多选
selectObjs.value = [];
getDataList();
};
// 导出excel
const exportExcel = () => {
downBlobFile('/mp/fans/export', state.queryForm, 'fans.xlsx');
};
// 删除操作
const handleDelete = (ids: string[]) => {
useMessageBox()
.confirm(t('common.delConfirmText'))
.then(() => {
delObjs(ids)
.then(() => {
getDataList();
useMessage().success(t('common.delSuccessText'));
})
.catch((err: any) => {
useMessage().error(err.msg);
});
});
};
const handelBlack = (ids: string[]) => {
useMessageBox()
.confirm('是否要拉黑用户')
.then(() => {
black(ids, state.queryForm.wxAccountAppid)
.then(() => {
getDataList();
useMessage().success('拉黑用户成功');
})
.catch((err: any) => {
useMessage().error(err.msg);
});
});
};
const handelUnBlack = (ids: string[]) => {
useMessageBox()
.confirm('是否要取消拉黑用户')
.then(() => {
unblack(ids, state.queryForm.wxAccountAppid)
.then(() => {
getDataList();
useMessage().success('设置成功');
})
.catch((err: any) => {
useMessage().error(err.msg);
});
});
};
</script>

View File

@@ -0,0 +1,91 @@
<template>
<el-dialog v-model="visible" :close-on-click-modal="false" :title="form.id ? $t('common.editBtn') : $t('common.addBtn')" draggable>
<el-form ref="dataFormRef" v-loading="loading" :model="form" :rules="dataRules" label-width="90px">
<el-row :gutter="24">
<el-col :span="24" class="mb20">
<el-form-item :label="t('wxAccountTag.tag')" prop="tag">
<el-input v-model="form.tag" :placeholder="t('wxAccountTag.inputTagTip')" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">{{ $t('common.cancelButtonText') }}</el-button>
<el-button type="primary" @click="onSubmit" :disabled="loading">{{ $t('common.confirmButtonText') }}</el-button>
</span>
</template>
</el-dialog>
</template>
<script lang="ts" name="WxAccountTagDialog" setup>
import { useMessage } from '/@/hooks/message';
import { addObj, putObj } from '/@/api/mp/wx-account-tag';
import { useI18n } from 'vue-i18n';
// 定义子组件向父组件传值/事件
const emit = defineEmits(['refresh']);
const { t } = useI18n();
// 定义变量内容
const dataFormRef = ref();
const visible = ref(false);
const loading = ref(false);
// 定义字典
// 提交表单数据
const form = reactive({
wxAccountAppid: '',
tag: '',
id: '',
});
// 定义校验规则
const dataRules = ref({});
// 打开弹窗
const openDialog = (row: any, appid: string) => {
visible.value = true;
form.wxAccountAppid = '';
form.tag = '';
form.id = '';
// 重置表单数据
if (dataFormRef.value) {
dataFormRef.value.resetFields();
}
if (row) {
Object.assign(form, row);
}
form.wxAccountAppid = appid;
};
// 提交
const onSubmit = async () => {
const valid = await dataFormRef.value.validate().catch(() => {});
if (!valid) return false;
loading.value = true;
try {
if (form.id) {
await putObj(form);
useMessage().success(t('common.editSuccessText'));
} else {
await addObj(form);
useMessage().success(t('common.addSuccessText'));
}
visible.value = false;
emit('refresh');
} catch (err: any) {
useMessage().error(err.msg);
} finally {
loading.value = false;
}
};
// 暴露变量
defineExpose({
openDialog,
});
</script>

View File

@@ -0,0 +1,18 @@
export default {
wxAccountTag: {
index: '#',
importwxAccountTagTip: 'import WxAccountTag',
id: 'id',
tag: 'tag',
wxAccountId: 'wxAccountId',
wxAccountName: 'wxAccountName',
wxAccountAppid: 'wxAccountAppid',
tagId: 'tagId',
inputIdTip: 'input id',
inputTagTip: 'input tag',
inputWxAccountIdTip: 'input wxAccountId',
inputWxAccountNameTip: 'input wxAccountName',
inputWxAccountAppidTip: 'input wxAccountAppid',
inputTagIdTip: 'input tagId',
},
};

View File

@@ -0,0 +1,18 @@
export default {
wxAccountTag: {
index: '#',
importwxAccountTagTip: '导入标签管理',
id: '主键',
tag: '标签',
wxAccountId: '微信账号ID',
wxAccountName: '微信账号名称',
wxAccountAppid: 'appID',
tagId: '标签ID',
inputIdTip: '请输入主键',
inputTagTip: '请输入标签',
inputWxAccountIdTip: '请输入微信账号ID',
inputWxAccountNameTip: '请输入微信账号名称',
inputWxAccountAppidTip: '请输入appID',
inputTagIdTip: '请输入标签ID',
},
};

View File

@@ -0,0 +1,188 @@
<template>
<div class="layout-padding">
<div class="layout-padding-auto layout-padding-view">
<el-row v-show="showSearch">
<el-form ref="queryRef" :inline="true" :model="state.queryForm" @keyup.enter="getDataList">
<el-form-item :label="$t('wxAccountTag.tag')" prop="tag">
<el-input v-model="state.queryForm.tag" :placeholder="t('wxAccountTag.inputTagTip')" style="max-width: 180px" />
</el-form-item>
<el-form-item :label="$t('wxAccountTag.wxAccountAppid')" prop="wxAccountAppid">
<el-select v-model="state.queryForm.wxAccountAppid" :placeholder="t('wxAccountTag.inputWxAccountAppidTip')" clearable>
<el-option v-for="item in accountList" :key="item.appid" :label="item.name" :value="item.appid" />
</el-select>
</el-form-item>
<el-form-item>
<el-button formDialogRef icon="search" type="primary" @click="getDataList">
{{ $t('common.queryBtn') }}
</el-button>
<el-button formDialogRef icon="Refresh" @click="resetQuery">{{ $t('common.resetBtn') }}</el-button>
</el-form-item>
</el-form>
</el-row>
<el-row>
<div class="mb8" style="width: 100%">
<el-button
v-auth="'mp_wx_account_tag_add'"
class="ml10"
formDialogRef
icon="folder-add"
type="primary"
@click="formDialogRef.openDialog(null, state.queryForm.wxAccountAppid)"
>
{{ $t('common.addBtn') }}
</el-button>
<el-button v-auth="'mp_wx_account_tag_export'" class="ml10" formDialogRef icon="Download" type="primary" @click="exportExcel">
{{ $t('common.exportBtn') }}
</el-button>
<el-button
v-auth="'mp_wx_account_tag_del'"
:disabled="multiple"
class="ml10"
formDialogRef
icon="Delete"
type="primary"
@click="handleDelete(selectObjs)"
>
{{ $t('common.delBtn') }}
</el-button>
<el-button plain type="primary" icon="Sort" @click="asyncTag">同步</el-button>
<right-toolbar
v-model:showSearch="showSearch"
class="ml10"
style="float: right; margin-right: 20px"
@queryTable="getDataList"
></right-toolbar>
</div>
</el-row>
<el-table
v-loading="state.loading"
:data="state.dataList"
style="width: 100%"
@selection-change="handleSelectionChange"
@sort-change="sortChangeHandle"
border
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
>
<el-table-column align="center" type="selection" width="40" />
<el-table-column :label="t('wxAccountTag.index')" type="index" width="60" />
<el-table-column :label="t('wxAccountTag.tag')" prop="tag" show-overflow-tooltip />
<el-table-column :label="t('wxAccountTag.wxAccountId')" prop="wxAccountId" show-overflow-tooltip />
<el-table-column :label="t('wxAccountTag.wxAccountName')" prop="wxAccountName" show-overflow-tooltip />
<el-table-column :label="t('wxAccountTag.wxAccountAppid')" prop="wxAccountAppid" show-overflow-tooltip />
<el-table-column :label="t('wxAccountTag.tagId')" prop="tagId" show-overflow-tooltip />
<el-table-column :label="$t('common.action')" width="150">
<template #default="scope">
<el-button icon="edit-pen" text type="primary" @click="formDialogRef.openDialog(scope.row, state.queryForm.wxAccountAppid)">{{
$t('common.editBtn')
}}</el-button>
<el-button icon="delete" text type="primary" @click="handleDelete([scope.row.id])">{{ $t('common.delBtn') }}</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-bind="state.pagination" @size-change="sizeChangeHandle" @current-change="currentChangeHandle" />
</div>
<!-- 编辑新增 -->
<form-dialog ref="formDialogRef" @refresh="getDataList(false)" />
</div>
</template>
<script lang="ts" name="systemWxAccountTag" setup>
import { BasicTableProps, useTable } from '/@/hooks/table';
import { delObjs, getPage, sync } from '/@/api/mp/wx-account-tag';
import { useMessage, useMessageBox } from '/@/hooks/message';
import { useI18n } from 'vue-i18n';
import { fetchAccountList } from '/@/api/mp/wx-account';
// 引入组件
const FormDialog = defineAsyncComponent(() => import('./form.vue'));
const { t } = useI18n();
// 定义查询字典
// 定义变量内容
const formDialogRef = ref();
// 搜索变量
const queryRef = ref();
const showSearch = ref(true);
// 多选变量
const selectObjs = ref([]) as any;
const multiple = ref(true);
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {},
pageList: getPage,
createdIsNeed: false,
});
const accountList = ref([]);
const getAccountList = () => {
fetchAccountList().then((res) => {
accountList.value = res.data;
if (accountList.value.length > 0) {
state.queryForm.wxAccountAppid = accountList.value[0].appid;
getDataList();
}
});
};
onMounted(() => {
getAccountList();
});
watch(
() => state.queryForm.wxAccountAppid,
() => {
getDataList();
}
);
// table hook
const { getDataList, currentChangeHandle, sizeChangeHandle, sortChangeHandle, downBlobFile, tableStyle } = useTable(state);
// 清空搜索条件
const resetQuery = () => {
// 清空搜索条件
queryRef.value.resetFields();
// 清空多选
selectObjs.value = [];
getDataList();
};
// 导出excel
const exportExcel = () => {
downBlobFile('/mp/wxAccountTag/export', state.queryForm, 'wxAccountTag.xlsx');
};
// 多选事件
const handleSelectionChange = (objs: { id: string }[]) => {
selectObjs.value = objs.map(({ id }) => id);
multiple.value = !objs.length;
};
// 删除操作
const handleDelete = (ids: string[]) => {
useMessageBox()
.confirm(t('common.delConfirmText'))
.then(() => {
delObjs({
ids: ids,
wxAccountAppid: state.queryForm.wxAccountAppid,
})
.then(() => {
getDataList();
useMessage().success(t('common.delSuccessText'));
})
.catch((err: any) => {
useMessage().error(err.msg);
});
});
};
const asyncTag = () => {
sync(state.queryForm.wxAccountAppid).then(() => {
getDataList();
});
};
</script>

View File

@@ -0,0 +1,158 @@
<template>
<el-dialog v-model="visible" :title="form.id ? $t('common.editBtn') : $t('common.addBtn')" size="50%">
<el-form ref="dataFormRef" v-loading="loading" :model="form" :rules="dataRules" label-width="90px">
<el-row :gutter="24">
<el-col :span="12" class="mb20">
<el-form-item :label="t('account.name')" prop="name">
<el-input v-model="form.name" :placeholder="t('account.inputNameTip')" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('account.account')" prop="account">
<el-input v-model="form.account" :placeholder="t('account.inputAccountTip')" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('account.appid')" prop="appid">
<el-input v-model="form.appid" :placeholder="t('account.inputAppidTip')" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('account.appsecret')" prop="appsecret">
<el-input v-model="form.appsecret" :placeholder="t('account.inputAppsecretTip')" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('account.url')" prop="url">
<el-input v-model="form.url" :placeholder="t('account.inputUrlTip')" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('account.token')" prop="token">
<el-input v-model="form.token" :placeholder="t('account.inputTokenTip')" />
</el-form-item>
</el-col>
<el-col :span="12" class="mb20">
<el-form-item :label="t('account.aeskey')" prop="aeskey">
<el-input v-model="form.aeskey" :placeholder="t('account.inputAeskeyTip')" />
</el-form-item>
</el-col>
</el-row>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="visible = false">{{ $t('common.cancelButtonText') }}</el-button>
<el-button type="primary" @click="onSubmit" :disabled="loading">{{ $t('common.confirmButtonText') }}</el-button>
</span>
</template>
</el-dialog>
</template>
<script lang="ts" name="WxAccountDialog" setup>
// 定义子组件向父组件传值/事件
import { useMessage } from '/@/hooks/message';
import { addObj, getObj, putObj } from '/@/api/mp/wx-account';
import { useI18n } from 'vue-i18n';
import { rule } from '/@/utils/validate';
const emit = defineEmits(['refresh']);
const { t } = useI18n();
// 定义变量内容
const dataFormRef = ref();
const visible = ref(false);
const loading = ref(false);
// 提交表单数据
const form = reactive({
id: '',
name: '',
account: '',
appid: '',
appsecret: '' as string | undefined,
url: '',
token: '',
aeskey: '',
});
// 定义校验规则
const dataRules = ref({
name: [{ required: true, message: '名称不能为空', trigger: 'blur' }],
account: [{ required: true, message: '微信号不能为空', trigger: 'blur' }],
appid: [{ required: true, message: 'appid不能为空', trigger: 'blur' }],
appsecret: [{ required: true, message: '密钥不能为空', trigger: 'blur' }],
url: [
{ required: true, message: 'url不能为空', trigger: 'blur' },
{ validator: rule.url, trigger: 'blur' },
],
token: [{ required: true, message: 'token不能为空', trigger: 'blur' }],
aeskey: [{ required: true, message: '加密密钥不能为空', trigger: 'blur' }],
});
// 打开弹窗
const openDialog = (id: string) => {
visible.value = true;
form.id = '';
// 重置表单数据
if (dataFormRef.value) {
dataFormRef.value.resetFields();
}
// 获取wxAccount信息
if (id) {
form.id = id;
getwxAccountData(id);
}
};
// 提交
const onSubmit = async () => {
const valid = await dataFormRef.value.validate().catch(() => {});
if (!valid) return false;
if (form.appsecret && form.appsecret.includes('*')) {
form.appsecret = undefined;
}
loading.value = true;
try {
if (form.id) {
await putObj(form);
useMessage().success(t('common.editSuccessText'));
} else {
await addObj(form);
useMessage().success(t('common.addSuccessText'));
}
visible.value = false;
emit('refresh');
} catch (err: any) {
useMessage().error(err.msg);
} finally {
loading.value = false;
}
};
// 初始化表单数据
const getwxAccountData = (id: string) => {
// 获取数据
loading.value = true;
getObj(id)
.then((res: any) => {
Object.assign(form, res.data);
})
.finally(() => {
loading.value = false;
});
};
// 暴露变量
defineExpose({
openDialog,
});
</script>

View File

@@ -0,0 +1,25 @@
export default {
account: {
index: '#',
importwxAccountTip: 'import WxAccount',
id: 'id',
name: 'name',
account: 'account',
appid: 'appid',
appsecret: 'appsecret',
url: 'url',
export: 'export',
token: 'token',
aeskey: 'aeskey',
qrUrl: 'qrUrl',
inputIdTip: 'input id',
inputNameTip: 'input name',
inputAccountTip: 'input account',
inputAppidTip: 'input appid',
inputAppsecretTip: 'input appsecret',
inputUrlTip: 'input url',
inputTokenTip: 'input token',
inputAeskeyTip: 'input aeskey',
inputQrUrlTip: 'input qrUrl',
},
};

View File

@@ -0,0 +1,25 @@
export default {
account: {
index: '#',
importwxAccountTip: '导入公众号账户表',
id: '主键',
name: '名称',
account: '微信号',
appid: 'appid',
appsecret: '密钥',
export: '导出',
url: ' url',
token: 'token',
aeskey: '加密密钥',
qrUrl: '图片',
inputIdTip: '请输入主键',
inputNameTip: '请输入名称',
inputAccountTip: '请输入微信号',
inputAppidTip: '请输入appid',
inputAppsecretTip: '请输入密钥',
inputUrlTip: '请输入 url',
inputTokenTip: '请输入token',
inputAeskeyTip: '请输入加密密钥',
inputQrUrlTip: '请输入图片',
},
};

View File

@@ -0,0 +1,218 @@
<template>
<div class="layout-padding">
<div class="layout-padding-auto layout-padding-view">
<el-row v-show="showSearch">
<el-form ref="queryRef" :inline="true" :model="state.queryForm">
<el-form-item :label="$t('account.name')" prop="name">
<el-input v-model="state.queryForm.name" :placeholder="t('account.inputNameTip')" style="max-width: 180px" />
</el-form-item>
<el-form-item :label="$t('account.account')" prop="account">
<el-input v-model="state.queryForm.account" :placeholder="t('account.inputAccountTip')" style="max-width: 180px" />
</el-form-item>
<el-form-item>
<el-button formDialogRef icon="search" type="primary" @click="getDataList">
{{ $t('common.queryBtn') }}
</el-button>
<el-button formDialogRef icon="Refresh" @click="resetQuery">{{ $t('common.resetBtn') }}</el-button>
</el-form-item>
</el-form>
</el-row>
<el-row>
<div class="mb8" style="width: 100%">
<el-button v-auth="'mp_wxaccount_add'" class="ml10" icon="folder-add" type="primary" @click="formDialogRef.openDialog()">
{{ $t('common.addBtn') }}
</el-button>
<el-button
plain
v-auth="'mp_wxaccount_add'"
class="ml10"
icon="Delete"
type="primary"
:disabled="multiple"
@click="handleDelete(selectObjs)"
>
{{ $t('common.delBtn') }}
</el-button>
<right-toolbar
:export="'app_social_details_del'"
@exportExcel="exportExcel"
v-model:showSearch="showSearch"
class="ml10"
style="float: right; margin-right: 20px"
@queryTable="getDataList"
></right-toolbar>
</div>
</el-row>
<el-table
v-loading="state.loading"
:data="state.dataList"
style="width: 100%"
@selection-change="handleSelectionChange"
@sort-change="sortChangeHandle"
border
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
>
<el-table-column align="center" type="selection" width="60" />
<el-table-column :label="t('account.index')" type="index" width="60" />
<el-table-column :label="t('account.name')" prop="name" show-overflow-tooltip />
<el-table-column :label="t('account.account')" prop="account" show-overflow-tooltip />
<el-table-column :label="t('account.appid')" prop="appid" show-overflow-tooltip />
<el-table-column :label="t('account.appsecret')" prop="appsecret" show-overflow-tooltip />
<el-table-column :label="t('account.url')" prop="url" show-overflow-tooltip />
<el-table-column :label="t('account.token')" prop="token" show-overflow-tooltip />
<el-table-column :label="t('account.aeskey')" prop="aeskey" show-overflow-tooltip />
<el-table-column :label="t('account.qrUrl')" prop="qrUrl" show-overflow-tooltip>
<template #default="scope">
<ImageUpload v-model:imageUrl="scope.row.qrUrl" height="80px" width="80px" disabled />
</template>
</el-table-column>
<el-table-column :label="$t('common.action')" width="100" fixed="right">
<template #default="scope">
<el-dropdown>
<span class="el-dropdown-link">
更多功能
<el-icon class="el-icon--right">
<arrow-down />
</el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>
<el-button icon="edit-pen" v-auth="'mp_wxaccount_edit'" text type="primary" @click="formDialogRef.openDialog(scope.row.id)"
>{{ $t('common.editBtn') }}
</el-button>
</el-dropdown-item>
<el-dropdown-item>
<el-button icon="delete" v-auth="'mp_wxaccount_del'" text type="primary" @click="handleDelete([scope.row.id])"
>{{ $t('common.delBtn') }}
</el-button>
</el-dropdown-item>
<el-dropdown-item>
<el-button icon="DArrowRight" v-auth="'mp_wxaccount_del'" text type="primary" @click="access(scope.row)">接入 </el-button>
</el-dropdown-item>
<el-dropdown-item>
<el-button icon="Grid" v-auth="'mp_wxaccount_del'" text type="primary" @click="generate(scope.row)">二维码 </el-button>
</el-dropdown-item>
<el-dropdown-item>
<el-button icon="refresh" v-auth="'mp_wxaccount_del'" text type="primary" @click="quota(scope.row)">quota </el-button>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
</el-table-column>
</el-table>
<pagination v-bind="state.pagination" @size-change="sizeChangeHandle" @current-change="currentChangeHandle" />
</div>
<!-- 编辑新增 -->
<form-dialog ref="formDialogRef" @refresh="getDataList(false)" />
<el-dialog v-model="dialogFormVisible" title="接入">
<el-input v-model="wxurl">
<template #append>
<el-button @click="copyText(wxurl)">复制链接</el-button>
</template>
</el-input>
</el-dialog>
</div>
</template>
<script lang="ts" name="systemWxAccount" setup>
import { BasicTableProps, useTable } from '/@/hooks/table';
import { clearQuota, delObjs, fetchList, generateQr } from '/@/api/mp/wx-account';
import { useMessage, useMessageBox } from '/@/hooks/message';
import { useI18n } from 'vue-i18n';
import commonFunction from '/@/utils/commonFunction';
import other from '/@/utils/other';
const { proxy } = getCurrentInstance();
const { copyText } = commonFunction();
// 引入组件
const ImageUpload = defineAsyncComponent(() => import('/@/components/Upload/Image.vue'));
const FormDialog = defineAsyncComponent(() => import('./form.vue'));
const { t } = useI18n();
// 定义查询字典
// 定义变量内容
const formDialogRef = ref();
// 搜索变量
const queryRef = ref();
const showSearch = ref(true);
// 多选变量
const selectObjs = ref([]) as any;
const multiple = ref(true);
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {},
pageList: fetchList,
});
// table hook
const { getDataList, currentChangeHandle, sizeChangeHandle, sortChangeHandle, downBlobFile, tableStyle } = useTable(state);
// 清空搜索条件
const resetQuery = () => {
// 清空搜索条件
queryRef.value.resetFields();
// 清空多选
selectObjs.value = [];
getDataList();
};
// 导出excel
const exportExcel = () => {
downBlobFile('/mp/account/export', state.queryForm, 'account.xlsx');
};
// 多选事件
const handleSelectionChange = (objs: { id: string }[]) => {
selectObjs.value = objs.map(({ id }) => id);
multiple.value = !objs.length;
};
// 删除操作
const handleDelete = (ids: string[]) => {
useMessageBox()
.confirm(t('common.delConfirmText'))
.then(() => {
delObjs(ids)
.then(() => {
getDataList();
useMessage().success(t('common.delSuccessText'));
})
.catch((err: any) => {
useMessage().error(err.msg);
});
});
};
const dialogFormVisible = ref(false);
const wxurl = ref();
const access = (row: any) => {
dialogFormVisible.value = true;
let url = '/mp/wx-portal/' + row.appid;
wxurl.value = row.url + proxy.baseURL + other.adaptationUrl(url);
};
const generate = (row: any) => {
generateQr(row.appid)
.then(() => {
useMessage().success('获取成功');
getDataList();
})
.catch((err) => {
useMessage().error(err.msg);
});
};
const quota = (row) => {
clearQuota(row.appid)
.then(() => {
useMessage().success('清空api的调用quota成功');
})
.catch(() => {
useMessage().error('清空api的调用quota失败');
});
};
</script>

View File

@@ -0,0 +1,353 @@
<template>
<div class="layout-padding">
<splitpanes>
<pane size="20">
<div class="layout-padding-auto layout-padding-view">
<el-scrollbar>
<query-tree class="mt10" :query="deptData.queryList" @node-click="handleNodeClick" placeholder="请输入微信公众号名称" />
</el-scrollbar>
</div>
</pane>
<pane size="80">
<div class="layout-padding-auto layout-padding-view">
<el-tabs v-model="type" @tab-click="handleClick">
<el-tab-pane name="1" label="1">
<template #label>关注时回复</template>
<el-row>
<div class="mb8" style="width: 100%">
<el-button class="ml10" icon="folder-add" type="primary" @click="handleAdd">
{{ $t('common.addBtn') }}
</el-button>
</div>
</el-row>
<el-table v-loading="state.loading" :data="state.dataList" style="width: 100%" max-height="600px" @sort-change="sortChangeHandle">
<el-table-column label="序号" type="index" width="60" />
<el-table-column label="回复消息类型" prop="repType" show-overflow-tooltip>
<template #default="scope">
<dict-tag :options="dicDataRepType" :value="scope.row.repType"></dict-tag>
</template>
</el-table-column>
<el-table-column label="操作" prop="action" show-overflow-tooltip>
<template #default="scope">
<el-button link icon="edit" @click="handleEdit(scope.row)">编辑 </el-button>
<el-button link icon="delete" @click="handleDel(scope.row)">删除 </el-button>
</template>
</el-table-column>
</el-table>
<pagination v-bind="state.pagination" @size-change="sizeChangeHandle" @current-change="currentChangeHandle" />
</el-tab-pane>
<el-tab-pane name="2" label="2">
<template #label>消息回复</template>
<el-row>
<div class="mb8" style="width: 100%">
<el-button class="ml10" icon="folder-add" type="primary" @click="handleAdd">
{{ $t('common.addBtn') }}
</el-button>
</div>
</el-row>
<el-table v-loading="state.loading" :data="state.dataList" style="width: 100%" max-height="600px" @sort-change="sortChangeHandle">
<el-table-column label="序号" type="index" width="60" />
<el-table-column label="请求消息类型" prop="reqType" show-overflow-tooltip>
<template #default="scope">
<dict-tag :options="dicDataReqType" :value="scope.row.reqType"></dict-tag>
</template>
</el-table-column>
<el-table-column label="回复消息类型" prop="repType" show-overflow-tooltip>
<template #default="scope">
<dict-tag :options="dicDataRepType" :value="scope.row.repType"></dict-tag>
</template>
</el-table-column>
<el-table-column label="操作" prop="action" show-overflow-tooltip>
<template #default="scope">
<el-button icon="edit" link @click="handleEdit(scope.row)">编辑 </el-button>
<el-button icon="delete" link @click="handleDel(scope.row)">删除 </el-button>
</template>
</el-table-column>
</el-table>
<pagination v-bind="state.pagination" @size-change="sizeChangeHandle" @current-change="currentChangeHandle" />
</el-tab-pane>
<el-tab-pane name="3" label="3">
<template #label>关键词回复</template>
<el-row>
<div class="mb8" style="width: 100%">
<el-button class="ml10" icon="folder-add" type="primary" @click="handleAdd">
{{ $t('common.addBtn') }}
</el-button>
</div>
</el-row>
<el-table
v-loading="state.loading"
:data="state.dataList"
style="width: 100%"
max-height="600px"
@sort-change="sortChangeHandle"
border
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
>
<el-table-column label="序号" type="index" width="60" />
<el-table-column label="关键词" prop="reqKey" show-overflow-tooltip> </el-table-column>
<el-table-column label="匹配类型" prop="repMate" show-overflow-tooltip>
<template #default="scope">
<dict-tag :options="dicRepMate" :value="scope.row.repMate"></dict-tag>
</template>
</el-table-column>
<el-table-column label="匹配类型" prop="repMate" show-overflow-tooltip>
<template #default="scope">
<dict-tag :options="dicDataRepType" :value="scope.row.repType"></dict-tag>
</template>
</el-table-column>
<el-table-column label="操作" prop="action" show-overflow-tooltip>
<template #default="scope">
<el-button icon="edit" link @click="handleEdit(scope.row)">编辑 </el-button>
<el-button icon="delete" link @click="handleDel(scope.row)">删除 </el-button>
</template>
</el-table-column>
</el-table>
<pagination v-bind="state.pagination" @size-change="sizeChangeHandle" @current-change="currentChangeHandle" />
</el-tab-pane>
</el-tabs>
</div>
</pane>
</splitpanes>
<el-dialog :title="handleType === 'add' ? '新增回复消息' : '修改回复消息'" v-model="dialog1Visible" width="50%">
<el-form label-width="100px">
<el-form-item v-if="type === '2'" label="请求消息类型">
<el-select v-model="objData.reqType" placeholder="请选择">
<template v-for="item in dicDataReqType">
<el-option v-if="item.value !== 'event'" :key="item.value" :label="item.label" :value="item.value" :disabled="item.disabled">
</el-option>
</template>
</el-select>
</el-form-item>
<el-form-item v-if="type === '3'" label="匹配类型">
<el-select v-model="objData.repMate" placeholder="请选择" style="width: 100px">
<el-option v-for="item in dicRepMate" :key="item.value" :label="item.label" :value="item.value"> </el-option>
</el-select>
</el-form-item>
<el-form-item v-if="type === '3'" label="关键词">
<el-input v-model="objData.reqKey" placeholder="请输入内容" clearable></el-input>
</el-form-item>
<el-form-item label="回复消息">
<WxReply v-if="hackResetWxReplySelect" :obj-data="objData"></WxReply>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialog1Visible = false"> </el-button>
<el-button type="primary" @click="handleSubmit"> </el-button>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts" name="wx-auto-reply">
import {fetchAccountList} from '/@/api/mp/wx-account';
import {BasicTableProps, useTable} from '/@/hooks/table';
import {useMessage, useMessageBox} from '/@/hooks/message';
import {addObj, delObj, getPage, putObj} from '/@/api/mp/wx-auto-reply';
const QueryTree = defineAsyncComponent(() => import('/@/components/QueryTree/index.vue'));
const WxReply = defineAsyncComponent(() => import('/@/components/Wechat/wx-reply/index.vue'));
// 点击树
const handleNodeClick = (node: any) => {
accountId.value = node.appid;
state.queryForm.appId = accountId.value;
getDataList();
};
const dicDataRepType = ref([
{
label: '文本',
value: 'text',
},
{
label: '图片',
value: 'image',
},
{
label: '语音',
value: 'voice',
},
{
label: '视频',
value: 'video',
},
{
label: '图文',
value: 'news',
},
]);
const dicDataReqType = ref([
{
value: 'text',
label: '文本',
},
{
value: 'image',
label: '图片',
},
{
value: 'voice',
label: '语音',
},
{
value: 'video',
label: '视频',
},
{
value: 'shortvideo',
label: '小视频',
},
{
value: 'location',
label: '地理位置',
},
{
value: 'link',
label: '链接消息',
},
{
value: 'event',
label: '事件推送',
},
]);
const dicRepMate = ref([
{
value: '1',
label: '全匹配',
},
{
value: '2',
label: '半匹配',
},
]);
const deptData = reactive({
queryList: (name?: string) => {
return fetchAccountList({
name: name,
});
},
});
const accountId = ref();
const type = ref('1');
const handleClick = (e: any) => {
type.value = e.paneName;
state.queryForm.type = type.value;
state.queryForm.appId = accountId.value;
getDataList();
};
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {
type: '1',
appId: '',
},
pageList: getPage,
createdIsNeed: false,
});
const { getDataList, currentChangeHandle, sizeChangeHandle, sortChangeHandle, tableStyle } = useTable(state);
const dialog1Visible = ref(false);
const handleType = ref('add');
const hackResetWxReplySelect = ref(true);
const objData = ref() as any;
const handleEdit = (row: any) => {
hackResetWxReplySelect.value = false;
nextTick(() => {
hackResetWxReplySelect.value = true;
});
handleType.value = 'edit';
dialog1Visible.value = true;
if (row.content && typeof row.content === 'string') {
row.content = JSON.parse(row.content);
}
objData.value = Object.assign({}, row);
};
const handleDel = (row) => {
useMessageBox()
.confirm('是否确认删除此数据?')
.then(() => {
delObj(row.id).then(() => {
useMessage().success('删除成功');
getDataList();
});
})
.catch((err) => {
useMessage().error(err.msg);
});
};
const handleSubmit = () => {
if (objData.repType === 'news') {
objData.content = JSON.stringify(objData.content);
}
if (handleType.value === 'add') {
addObj(
Object.assign(
{
type: type.value,
appId: accountId.value,
},
objData.value
)
)
.then(() => {
useMessage().success('添加成功');
getDataList();
dialog1Visible.value = false;
})
.catch((err) => {
useMessage().error(err.msg);
});
}
if (handleType.value === 'edit') {
putObj(objData.value)
.then(() => {
useMessage().success('修改成功');
getDataList();
dialog1Visible.value = false;
})
.catch((err) => {
useMessage().error(err.msg);
});
}
};
const handleAdd = () => {
hackResetWxReplySelect.value = false; //销毁组件
nextTick(() => {
hackResetWxReplySelect.value = true; //重建组件
});
handleType.value = 'add';
dialog1Visible.value = true;
objData.value = {
repType: 'text',
appId: accountId.value,
};
};
// 默认选择第一个公众号
onMounted(async () => {
const { data } = await deptData.queryList();
if (data?.length > 0) {
handleNodeClick(data[0]);
}
});
</script>
<style scoped></style>

View File

@@ -0,0 +1,64 @@
export default {
wxFansMsg: {
index: '#',
importwxMsgTip: 'import WxMsg',
id: 'id',
appName: 'appName',
appLogo: 'appLogo',
wxUserId: 'wxUserId',
nickName: 'nickName',
headimgUrl: 'headimgUrl',
type: 'type',
repType: 'repType',
repEvent: 'repEvent',
repContent: 'repContent',
repMediaId: 'repMediaId',
repName: 'repName',
repDesc: 'repDesc',
repUrl: 'repUrl',
repHqUrl: 'repHqUrl',
content: 'content',
repThumbMediaId: 'repThumbMediaId',
repThumbUrl: 'repThumbUrl',
repLocationX: 'repLocationX',
repLocationY: 'repLocationY',
repScale: 'repScale',
readFlag: 'readFlag',
appId: 'appId',
openId: 'openId',
remark: 'remark',
delFlag: 'delFlag',
createTime: 'createTime',
updateTime: 'updateTime',
tenantId: 'tenantId',
inputIdTip: 'input id',
inputAppNameTip: 'input appName',
inputAppLogoTip: 'input appLogo',
inputWxUserIdTip: 'input wxUserId',
inputNickNameTip: 'input nickName',
inputHeadimgUrlTip: 'input headimgUrl',
inputTypeTip: 'input type',
inputRepTypeTip: 'input repType',
inputRepEventTip: 'input repEvent',
inputRepContentTip: 'input repContent',
inputRepMediaIdTip: 'input repMediaId',
inputRepNameTip: 'input repName',
inputRepDescTip: 'input repDesc',
inputRepUrlTip: 'input repUrl',
inputRepHqUrlTip: 'input repHqUrl',
inputContentTip: 'input content',
inputRepThumbMediaIdTip: 'input repThumbMediaId',
inputRepThumbUrlTip: 'input repThumbUrl',
inputRepLocationXTip: 'input repLocationX',
inputRepLocationYTip: 'input repLocationY',
inputRepScaleTip: 'input repScale',
inputReadFlagTip: 'input readFlag',
inputAppIdTip: 'input appId',
inputOpenIdTip: 'input openId',
inputRemarkTip: 'input remark',
inputDelFlagTip: 'input delFlag',
inputCreateTimeTip: 'input createTime',
inputUpdateTimeTip: 'input updateTime',
inputTenantIdTip: 'input tenantId',
},
};

View File

@@ -0,0 +1,64 @@
export default {
wxFansMsg: {
index: '#',
importwxMsgTip: '导入微信消息',
id: '主键',
appName: '公众号名称',
appLogo: '公众号logo',
wxUserId: '微信用户ID',
nickName: '微信用户昵称',
headimgUrl: '微信用户头像',
type: '消息分类',
repType: '消息类型',
repEvent: '事件类型',
repContent: '内容',
repMediaId: '回复类型',
repName: '回复的素材名、视频和音乐的标题',
repDesc: '视频和音乐的描述',
repUrl: '链接',
repHqUrl: '高质量链接',
content: '图文消息的内容',
repThumbMediaId: '缩略图的媒体id',
repThumbUrl: '缩略图url',
repLocationX: '地理位置维度',
repLocationY: '地理位置经度',
repScale: '地图缩放大小',
readFlag: '已读标记',
appId: '公众号ID',
openId: '微信唯一标识',
remark: '备注',
delFlag: '逻辑删除标记0显示1隐藏',
createTime: '创建时间',
updateTime: '更新时间',
tenantId: '租户ID',
inputIdTip: '请输入主键',
inputAppNameTip: '请输入公众号名称',
inputAppLogoTip: '请输入公众号logo',
inputWxUserIdTip: '请输入微信用户ID',
inputNickNameTip: '请输入微信用户昵称',
inputHeadimgUrlTip: '请输入微信用户头像',
inputTypeTip: '请输入消息分类',
inputRepTypeTip: '请输入消息类型',
inputRepEventTip: '请输入事件类型',
inputRepContentTip: '请输入回复类型文本保存文字、地理位置信息',
inputRepMediaIdTip: '请输入回复类型',
inputRepNameTip: '请输入回复的素材名、视频和音乐的标题',
inputRepDescTip: '请输入视频和音乐的描述',
inputRepUrlTip: '请输入链接',
inputRepHqUrlTip: '请输入高质量链接',
inputContentTip: '请输入图文消息的内容',
inputRepThumbMediaIdTip: '请输入缩略图的媒体id',
inputRepThumbUrlTip: '请输入缩略图url',
inputRepLocationXTip: '请输入地理位置维度',
inputRepLocationYTip: '请输入地理位置经度',
inputRepScaleTip: '请输入地图缩放大小',
inputReadFlagTip: '请输入已读标记10',
inputAppIdTip: '请输入公众号ID',
inputOpenIdTip: '请输入微信唯一标识',
inputRemarkTip: '请输入备注',
inputDelFlagTip: '请输入逻辑删除标记0显示1隐藏',
inputCreateTimeTip: '请输入创建时间',
inputUpdateTimeTip: '请输入更新时间',
inputTenantIdTip: '请输入租户ID',
},
};

View File

@@ -0,0 +1,194 @@
<template>
<div class="layout-padding">
<div class="layout-padding-auto layout-padding-view">
<el-row v-show="showSearch">
<el-form ref="queryRef" :inline="true" :model="state.queryForm" @keyup.enter="getDataList">
<el-form-item :label="$t('wxFansMsg.appName')" prop="wxAccountAppid">
<el-select v-model="state.queryForm.wxAccountAppid" :placeholder="$t('wxFansMsg.appName')" clearable >
<el-option v-for="item in accountList" :key="item.appid" :label="item.name" :value="item.appid" />
</el-select>
</el-form-item>
<el-form-item :label="$t('wxFansMsg.nickName')" prop="nickName">
<el-input v-model="state.queryForm.nickName" :placeholder="t('wxFansMsg.inputNickNameTip')" style="max-width: 180px" />
</el-form-item>
<el-form-item :label="$t('wxFansMsg.repType')" prop="repType">
<el-select v-model="state.queryForm.repType" :placeholder="$t('wxFansMsg.repType')" clearable>
<el-option v-for="item in repType" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</el-form-item>
<el-form-item>
<el-button formDialogRef icon="search" type="primary" @click="getDataList">
{{ $t('common.queryBtn') }}
</el-button>
<el-button formDialogRef icon="Refresh" @click="resetQuery">{{ $t('common.resetBtn') }}</el-button>
</el-form-item>
</el-form>
</el-row>
<el-row>
<div class="mb8" style="width: 100%">
<el-button v-auth="'mp_wxFansMsg_export'" class="ml10" formDialogRef icon="Download" type="primary" @click="exportExcel">
{{ $t('common.exportBtn') }}
</el-button>
<el-button
v-auth="'mp_wxmsg_del'"
:disabled="multiple"
class="ml10"
formDialogRef
icon="Delete"
type="primary"
@click="handleDelete(selectObjs)"
>
{{ $t('common.delBtn') }}
</el-button>
<right-toolbar
v-model:showSearch="showSearch"
class="ml10"
style="float: right; margin-right: 20px"
@queryTable="getDataList"
></right-toolbar>
</div>
</el-row>
<el-table
v-loading="state.loading"
:data="state.dataList"
style="width: 100%"
@sort-change="sortChangeHandle"
border
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
>
<el-table-column :label="t('wxFansMsg.index')" type="index" width="60" />
<el-table-column :label="t('wxFansMsg.appName')" prop="appName" show-overflow-tooltip />
<el-table-column :label="t('wxFansMsg.repType')" prop="repType" show-overflow-tooltip />
<el-table-column :label="t('wxFansMsg.nickName')" prop="nickName" show-overflow-tooltip />
<el-table-column :label="t('wxFansMsg.openId')" prop="openId" show-overflow-tooltip />
<el-table-column :label="t('wxFansMsg.repContent')" prop="repContent" show-overflow-tooltip>
<template #default="scope">
<div v-if="scope.row.repType === 'event' && scope.row.repEvent === 'subscribe'"><el-tag type="success">关注</el-tag></div>
<div v-if="scope.row.repType === 'event' && scope.row.repEvent === 'unsubscribe'">
<el-tag type="danger">取消关注</el-tag>
</div>
<div v-if="scope.row.repType === 'event' && scope.row.repEvent === 'CLICK'"><el-tag>点击菜单</el-tag>{{ scope.row.repName }}</div>
<div v-if="scope.row.repType === 'event' && scope.row.repEvent === 'VIEW'"><el-tag>点击菜单链接</el-tag>{{ scope.row.repUrl }}</div>
<div v-if="scope.row.repType === 'event' && scope.row.repEvent === 'scancode_waitmsg'">
<el-tag>扫码结果</el-tag>{{ scope.row.repContent }}
</div>
<div v-if="scope.row.repType === 'text'">{{ scope.row.repContent }}</div>
<div v-if="scope.row.repType === 'image'">
<a target="_blank" :href="scope.row.repUrl"><img :src="scope.row.repUrl" style="width: 100px" /></a>
</div>
<div v-if="['video', 'voice', 'link', 'shortvideo'].includes(scope.row.repType)">
<el-tag>链接</el-tag><a :href="scope.row.repUrl" target="_blank">{{ scope.row.repName }}</a>
</div>
</template>
</el-table-column>
<el-table-column :label="t('wxFansMsg.readFlag')" prop="readFlag" show-overflow-tooltip>
<template #default="scope">
<dict-tag :options="readFlag" :value="scope.row.readFlag"></dict-tag>
</template>
</el-table-column>
<el-table-column :label="t('wxFansMsg.createTime')" prop="createTime" show-overflow-tooltip />
<el-table-column :label="$t('common.action')" width="150">
<template #default="scope">
<el-button icon="ChatSquare" link type="primary" @click="wxMsgDo(scope.row, scope.index)">消息</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-bind="state.pagination" @size-change="sizeChangeHandle" @current-change="currentChangeHandle" />
</div>
<wx-msg ref="WxmsgRef"></wx-msg>
</div>
</template>
<script lang="ts" name="systemWxMsg" setup>
import { BasicTableProps, useTable } from '/@/hooks/table';
import { fetchList } from '/@/api/mp/wx-fans-msg';
import { useMessage } from '/@/hooks/message';
import { useDict } from '/@/hooks/dict';
import { useI18n } from 'vue-i18n';
import { fetchAccountList } from '/@/api/mp/wx-account';
const WxMsg = defineAsyncComponent(() => import('/@/components/Wechat/wx-msg/index.vue'));
const { t } = useI18n();
// 定义查询字典
const { repType } = useDict('repType');
const readFlag = ref([
{
value: '1',
label: '是',
},
{
value: '0',
label: '否',
},
]);
const WxmsgRef = ref();
// 搜索变量
const queryRef = ref();
const showSearch = ref(true);
// 多选变量
const selectObjs = ref([]) as any;
const multiple = ref(true);
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {},
pageList: fetchList,
descs: ['create_time'],
});
// table hook
const { getDataList, currentChangeHandle, sizeChangeHandle, sortChangeHandle, downBlobFile, tableStyle } = useTable(state);
// 清空搜索条件
const resetQuery = () => {
// 清空搜索条件
queryRef.value.resetFields();
// 清空多选
selectObjs.value = [];
getDataList();
};
const accountList = ref([]);
const getAccountList = () => {
fetchAccountList()
.then((res) => {
accountList.value = res.data;
if (accountList.value.length > 0) {
state.queryForm.wxAccountAppid = accountList.value[0].appid;
getDataList();
}
})
.catch((err) => {
useMessage().error(err.msg);
});
};
watch(
() => state.queryForm.wxAccountAppid,
() => {
getDataList();
}
);
onMounted(() => {
getAccountList();
});
// 导出excel
const exportExcel = () => {
downBlobFile('/act/wxFansMsg/export', state.queryForm, 'wxFansMsg.xlsx');
};
const wxMsgDo = (row) => {
WxmsgRef.value.openDialog({
wxUserId: row.wxUserId,
appId: row.appId,
});
};
</script>

View File

@@ -0,0 +1,539 @@
<template>
<el-dialog
:title="operateMaterial === 'add' ? '新建图文' : '修改图文'"
:before-close="dialogNewsClose"
:close-on-click-modal="false"
v-model="dialogNewsVisible"
:destroy-on-close="true"
width="80%"
top="20px"
>
<div class="left">
<div class="select-item">
<div v-for="(news, index) in articlesAdd" :key="news.id">
<div v-if="index == 0" class="news-main father" :class="{ activeAddNews: isActiveAddNews === index }" @click="activeNews(index)">
<div class="news-content">
<img v-if="news.thumbUrl" class="material-img" :src="news.thumbUrl" />
<div class="news-content-title">{{ news.title }}</div>
</div>
<div v-if="articlesAdd.length > 1" class="child">
<el-button icon="top" @click="downNews(index)">下移</el-button>
<el-button v-if="operateMaterial == 'add'" icon="delete" @click="minusNews(index)">删除 </el-button>
</div>
</div>
<div v-if="index > 0" class="news-main-item father" :class="{ activeAddNews: isActiveAddNews === index }" @click="activeNews(index)">
<div class="news-content-item">
<div class="news-content-item-title">{{ news.title }}</div>
<div class="news-content-item-img">
<img v-if="news.thumbUrl" class="material-img" :src="news.thumbUrl" height="100%" />
</div>
</div>
<div class="child">
<el-button v-if="articlesAdd.length > index + 1" icon="sort-down" @click="downNews(index)">下移 </el-button>
<el-button icon="sort-up" @click="upNews(index)">上移</el-button>
<el-button v-if="operateMaterial == 'add'" icon="delete" @click="minusNews(index)">删除 </el-button>
</div>
</div>
</div>
<div v-if="articlesAdd.length < 8 && operateMaterial == 'add'" class="news-main-plus" @click="plusNews">
<el-icon><Plus /></el-icon>
</div>
</div>
</div>
<div v-loading="addMaterialLoading" class="right">
<!--富文本编辑器组件-->
<el-row>
<editor v-model:get-html="articlesAdd[isActiveAddNews].content" style="margin-top: 20px"></editor>
</el-row>
<br /><br /><br /><br />
<div class="input-tt">封面和摘要</div>
<div>
<div class="thumb-div">
<img
v-if="articlesAdd[isActiveAddNews].thumbUrl"
class="material-img"
:src="articlesAdd[isActiveAddNews].thumbUrl"
:class="isActiveAddNews === 0 ? 'avatar' : 'avatar1'"
/>
<i v-else class="el-icon-plus avatar-uploader-icon" :class="isActiveAddNews === 0 ? 'avatar' : 'avatar1'"></i>
<div class="thumb-but">
<wx-file-upload :uploadData="uploadData" @success="handleImageChange"></wx-file-upload>
<el-button type="primary" @click="openMaterial">素材库选择</el-button>
</div>
</div>
<el-input
v-model="articlesAdd[isActiveAddNews].digest"
:rows="6"
type="textarea"
placeholder="请输入摘要"
class="digest"
maxlength="120"
></el-input>
</div>
<div class="input-tt">标题</div>
<el-input v-model="articlesAdd[isActiveAddNews].title" placeholder="请输入标题"></el-input>
<div class="input-tt">作者</div>
<el-input v-model="articlesAdd[isActiveAddNews].author" placeholder="请输入作者"></el-input>
<div class="input-tt">原文地址</div>
<el-input v-model="articlesAdd[isActiveAddNews].contentSourceUrl" placeholder="请输入原文地址"></el-input>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogNewsVisible = false">{{ $t('common.cancelButtonText') }}</el-button>
<el-button type="primary" @click="onSubmit">{{ $t('common.confirmButtonText') }}</el-button>
</span>
</template>
</el-dialog>
<wx-material-select ref="WxMaterialSelectRef"></wx-material-select>
</template>
<script setup lang="ts" name="wx-news-form">
import { useMessageBox } from '/@/hooks/message';
import { addObj, materialNewsUpdate } from '/@/api/mp/wx-material';
const WxMaterialSelect = defineAsyncComponent(() => import('/@/components/Wechat/wx-material-select/main.vue'));
const WxFileUpload = defineAsyncComponent(() => import('/@/components/Wechat/fileUpload/index.vue'));
const WxMaterialSelectRef = ref();
const dialogNewsVisible = ref(false);
const operateMaterial = ref('add');
const addMaterialLoading = ref(false);
// 定义刷新表格emit
const emit = defineEmits(['ok']);
const dialogNewsClose = () => {
useMessageBox()
.confirm('修改内容可能还未保存,确定关闭吗?')
.then(() => {
dialogNewsVisible.value = false;
});
};
// 公众号id
const accountId = ref();
// 文章数据
const articlesAdd = ref([
{
title: '',
thumbMediaId: '',
author: '',
digest: '',
showCoverPic: '',
content: '',
contentSourceUrl: '',
needOpenComment: '',
onlyFansCanComment: '',
thumbUrl: '',
},
]);
// 激活文章
const isActiveAddNews = ref(0);
// 编辑媒体的id
const articlesMediaId = ref();
const openDialog = (data: any, item?: any, mediaId?: any, type: any = 'add') => {
// 设置组件内不用账号
accountId.value = data.accountId;
uploadData.appId = data.accountId;
dialogNewsVisible.value = true;
operateMaterial.value = 'add';
if (item) {
articlesAdd.value = item;
}
if (mediaId) {
articlesMediaId.value = mediaId || '';
}
if (type) {
operateMaterial.value = type;
}
};
const uploadData = reactive({
mediaType: 'image',
title: '',
introduction: '',
appId: '',
});
const openMaterial = () => {
WxMaterialSelectRef.value.openDialog({
type: 'image',
accountId: accountId.value,
});
};
const handleImageChange = (response) => {
articlesAdd.value[isActiveAddNews.value].thumbMediaId = response.data.mediaId;
articlesAdd.value[isActiveAddNews.value].thumbUrl = response.data.url;
};
const onSubmit = () => {
addMaterialLoading.value = true;
if (operateMaterial.value === 'add') {
addObj({
articles: articlesAdd.value,
appId: accountId.value,
})
.then(() => {
addMaterialLoading.value = false;
dialogNewsVisible.value = false;
isActiveAddNews.value = 0;
articlesAdd.value = [
{
title: '',
thumbMediaId: '',
author: '',
digest: '',
showCoverPic: '',
content: '',
contentSourceUrl: '',
needOpenComment: '',
onlyFansCanComment: '',
thumbUrl: '',
},
];
emit('ok');
})
.finally(() => {
addMaterialLoading.value = false;
});
}
if (operateMaterial.value === 'edit') {
materialNewsUpdate({
articles: articlesAdd.value,
mediaId: articlesMediaId.value,
appId: accountId.value,
})
.then(() => {
addMaterialLoading.value = false;
dialogNewsVisible.value = false;
isActiveAddNews.value = 0;
articlesAdd.value = [
{
title: '',
thumbMediaId: '',
author: '',
digest: '',
showCoverPic: '',
content: '',
contentSourceUrl: '',
needOpenComment: '',
onlyFansCanComment: '',
thumbUrl: '',
},
];
emit('ok');
})
.finally(() => {
addMaterialLoading.value = false;
});
}
};
const activeNews = (index) => {
isActiveAddNews.value = index;
};
const minusNews = (index) => {
useMessageBox()
.confirm('确定删除该图文吗?')
.then(() => {
articlesAdd.value.splice(index, 1);
if (isActiveAddNews.value === index) {
isActiveAddNews.value = 0;
}
});
};
const plusNews = () => {
articlesAdd.value.push({
title: '',
thumbMediaId: '',
author: '',
digest: '',
showCoverPic: '',
content: '',
contentSourceUrl: '',
needOpenComment: '',
onlyFansCanComment: '',
thumbUrl: '',
});
isActiveAddNews.value = articlesAdd.value.length - 1;
};
const downNews = (index) => {
const temp = articlesAdd.value[index];
articlesAdd.value[index] = articlesAdd.value[index + 1];
articlesAdd.value[index + 1] = temp;
isActiveAddNews.value = index + 1;
};
const upNews = (index) => {
const temp = articlesAdd[index];
articlesAdd[index] = articlesAdd[index - 1];
articlesAdd[index - 1] = temp;
isActiveAddNews.value = index - 1;
};
// 暴露变量
defineExpose({
openDialog,
});
</script>
<style lang="scss" scoped>
.tree-position {
margin: 12px 20px 0 0;
}
.pagination {
float: right;
margin-right: 25px;
}
.add_but {
padding: 10px;
}
.ope-row {
margin-top: 5px;
text-align: center;
border-top: 1px solid #eaeaea;
padding-top: 5px;
}
.item-name {
font-size: 12px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: center;
}
.el-upload__tip {
margin-left: 5px;
}
/*新增图文*/
.left {
display: inline-block;
width: 35%;
vertical-align: top;
margin-top: 200px;
}
.right {
display: inline-block;
width: 60%;
margin-top: -40px;
}
.avatar-uploader {
width: 20%;
display: inline-block;
}
.avatar-uploader .el-upload {
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
text-align: unset !important;
}
.avatar-uploader .el-upload:hover {
border-color: #409eff;
}
.avatar-uploader-icon {
border: 1px solid #d9d9d9;
font-size: 28px;
color: #8c939d;
width: 120px;
height: 120px;
line-height: 120px;
text-align: center;
}
.avatar {
width: 230px;
height: 120px;
}
.avatar1 {
width: 120px;
height: 120px;
}
.digest {
width: 60%;
display: inline-block;
vertical-align: top;
}
/*新增图文*/
/*瀑布流样式*/
.waterfall {
width: 100%;
column-gap: 10px;
column-count: 5;
margin: 0 auto;
}
.waterfall-item {
padding: 10px;
margin-bottom: 10px;
break-inside: avoid;
border: 1px solid #eaeaea;
}
p {
line-height: 30px;
}
@media (min-width: 992px) and (max-width: 1300px) {
.waterfall {
column-count: 3;
}
p {
color: red;
}
}
@media (min-width: 768px) and (max-width: 991px) {
.waterfall {
column-count: 2;
}
p {
color: orange;
}
}
@media (max-width: 767px) {
.waterfall {
column-count: 1;
}
}
/*瀑布流样式*/
.news-main {
background-color: #ffffff;
width: 100%;
margin: auto;
height: 120px;
}
.news-content {
background-color: #acadae;
width: 100%;
height: 120px;
position: relative;
}
.news-content-title {
display: inline-block;
font-size: 15px;
color: #ffffff;
position: absolute;
left: 0px;
bottom: 0px;
background-color: black;
width: 98%;
padding: 1%;
opacity: 0.65;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
height: 25px;
}
.news-main-item {
background-color: #ffffff;
padding: 5px 0px;
border-top: 1px solid #eaeaea;
width: 100%;
margin: auto;
}
.news-content-item {
position: relative;
margin-left: -3px;
}
.news-content-item-title {
display: inline-block;
font-size: 12px;
width: 70%;
}
.news-content-item-img {
display: inline-block;
width: 25%;
background-color: #acadae;
}
.input-tt {
padding: 5px;
}
.activeAddNews {
border: 5px solid #2bb673;
}
.news-main-plus {
width: 280px;
text-align: center;
margin: auto;
height: 50px;
}
.icon-plus {
margin: 10px;
font-size: 25px;
}
.select-item {
width: 60%;
padding: 10px;
margin: 0 auto 10px auto;
border: 1px solid #eaeaea;
}
.father .child {
display: none;
text-align: center;
position: relative;
bottom: 25px;
}
.father:hover .child {
display: block;
}
.thumb-div {
display: inline-block;
width: 30%;
text-align: center;
}
.thumb-but {
display: flex;
margin: 5px;
justify-content: space-between;
}
.material-img {
width: 100%;
height: 100%;
}
</style>

View File

@@ -0,0 +1,553 @@
<template>
<div class="layout-padding">
<splitpanes>
<pane size="20">
<div class="layout-padding-auto layout-padding-view">
<el-scrollbar>
<query-tree class="mt10" :query="deptData.queryList" @node-click="handleNodeClick" placeholder="请输入微信公众号名称" />
</el-scrollbar>
</div>
</pane>
<pane size="80">
<div class="layout-padding-auto layout-padding-view">
<el-tabs v-model="materialType" @tab-click="handleClick">
<el-tab-pane name="image" label="image">
<template #label><i class="picture"></i> 图片</template>
<div class="add_but">
<wx-file-upload
@success="getDataList"
:uploadData="uploadData"
:type="['image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/jpg']"
></wx-file-upload>
</div>
<div v-loading="state.loading" class="waterfall">
<div v-for="item in state.dataList" :key="item.id" class="waterfall-item">
<a target="_blank" :href="item.url">
<img class="material-img" :src="item.url" />
<div class="item-name">{{ item.name }}</div>
</a>
<el-row class="ope-row">
<el-button type="danger" icon="delete" circle @click="delMaterial(item)"></el-button>
</el-row>
</div>
</div>
<div v-if="state.dataList.length <= 0 && !state.loading" class="el-table__empty-block">
<span class="el-table__empty-text">暂无数据</span>
</div>
<pagination v-bind="state.pagination" @size-change="sizeChangeHandle" @current-change="currentChangeHandle" />
</el-tab-pane>
<el-tab-pane name="voice" label="voice">
<template #label><i class="microphone"></i> 语音</template>
<div class="add_but">
<wx-file-upload @success="getDataList" :uploadData="uploadData" :type="['mp3', 'wma', 'wav', 'amr']"></wx-file-upload>
</div>
<el-table v-loading="state.loading" :data="state.dataList" stripe border max-height="600px">
<el-table-column prop="mediaId" label="media_id"> </el-table-column>
<el-table-column prop="name" label="名称"> </el-table-column>
<el-table-column prop="updateTime" label="更新时间"> </el-table-column>
<el-table-column fixed="right" label="操作">
<template v-slot="scope">
<el-button type="text" icon="download" plain @click="handleDown(scope.row)">下载 </el-button>
<el-button type="text" icon="delete" plain @click="delMaterial(scope.row)">删除 </el-button>
</template>
</el-table-column>
</el-table>
<pagination v-bind="state.pagination" @size-change="sizeChangeHandle" @current-change="currentChangeHandle" />
</el-tab-pane>
<el-tab-pane name="video" label="video">
<template #label><i class="video-play"></i> 视频</template>
<div class="add_but">
<el-button type="primary" @click="handleAddVideo">新建</el-button>
</div>
<el-dialog title="新建视频" v-model="dialogVideoVisible">
<wx-file-upload
@success="getDataList"
:uploadData="uploadData"
:auto-upload="false"
ref="uploadFileVideo"
:type="['video/mp4']"
></wx-file-upload>
<el-form ref="uploadForm" :model="uploadData" v-loading="addMaterialLoading" :rules="uploadRules">
<el-form-item label="标题" prop="title">
<el-input v-model="uploadData.title" placeholder="标题将展示在相关播放页面,建议填写清晰、准确、生动的标题"></el-input>
</el-form-item>
<el-form-item label="描述" prop="introduction">
<el-input
v-model="uploadData.introduction"
:rows="3"
type="textarea"
placeholder="介绍语将展示在相关播放页面,建议填写简洁明确、有信息量的内容"
></el-input>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVideoVisible = false"> </el-button>
<el-button type="primary" @click="subVideo"> </el-button>
</template>
</el-dialog>
<el-table
v-loading="state.loading"
:data="state.dataList"
stripe
border
max-height="600px"
:cell-style="tableStyle.cellStyle"
:header-cell-style="tableStyle.headerCellStyle"
>
<el-table-column prop="mediaId" label="media_id"> </el-table-column>
<el-table-column prop="name" label="名称"> </el-table-column>
<el-table-column prop="updateTime" label="更新时间"> </el-table-column>
<el-table-column fixed="right" label="操作">
<template v-slot="scope">
<el-button type="text" icon="view" @click="handleInfo(scope.row)">查看 </el-button>
<el-button type="text" icon="delete" @click="delMaterial(scope.row)">删除 </el-button>
</template>
</el-table-column>
</el-table>
<pagination v-bind="state.pagination" @size-change="sizeChangeHandle" @current-change="currentChangeHandle" />
</el-tab-pane>
<el-tab-pane name="news" label="news">
<template #label><i class="news"></i> 图文</template>
<div class="add_but">
<el-button type="primary" @click="handleAddNews"> </el-button>
</div>
<news-form ref="dialogNewsRef" @ok="getDataList"></news-form>
<div v-loading="state.loading" class="waterfall">
<div v-for="item in state.dataList" :key="item.id" class="waterfall-item">
<wx-news :obj-data="item.content.newsItem"></wx-news>
<el-row class="ope-row">
<el-button type="primary" icon="edit" circle @click="handleEditNews(item)"></el-button>
<el-button type="danger" icon="delete" circle @click="delMaterial(item)"></el-button>
</el-row>
</div>
</div>
<div v-if="state.dataList.length <= 0 && !state.loading" class="el-table__empty-block">
<span class="el-table__empty-text">暂无数据</span>
</div>
</el-tab-pane>
</el-tabs>
</div>
</pane>
</splitpanes>
</div>
</template>
<script setup lang="ts" name="wx-material">
import { fetchAccountList } from '/@/api/mp/wx-account';
import { useMessage, useMessageBox } from '/@/hooks/message';
import { BasicTableProps, useTable } from '/@/hooks/table';
import { delObj, getMaterialOther, getMaterialVideo, getPage } from '/@/api/mp/wx-material';
const QueryTree = defineAsyncComponent(() => import('/@/components/QueryTree/index.vue'));
const NewsForm = defineAsyncComponent(() => import('./components/news-form.vue'));
const WxFileUpload = defineAsyncComponent(() => import('/@/components/Wechat/fileUpload/index.vue'));
const WxNews = defineAsyncComponent(() => import('/@/components/Wechat/wx-news/index.vue'));
const deptData = reactive({
queryList: (name?: string) => {
return fetchAccountList({
name: name,
});
},
});
const checkAppId = ref();
const uploadData = ref({
appId: '',
mediaType: 'image',
title: '',
introduction: '',
});
const materialType = ref('image');
// 点击树
const handleNodeClick = (data: any) => {
checkAppId.value = data.appid;
uploadData.value.appId = data.appid;
state.queryForm.appId = data.appid;
state.queryForm.type = materialType.value;
getDataList();
};
const handleClick = (tab) => {
if (checkAppId.value) {
// getPage(this.page)
} else {
useMessage().error('请选择公众号');
}
materialType.value = tab.paneName;
uploadData.value.mediaType = tab.paneName;
state.queryForm.type = materialType.value;
getDataList();
};
const state: BasicTableProps = reactive<BasicTableProps>({
queryForm: {
appId: '',
type: '',
},
pageList: getPage,
createdIsNeed: false,
props: {
item: 'items',
totalCount: 'totalCount',
},
});
const { getDataList, currentChangeHandle, sizeChangeHandle, tableStyle } = useTable(state);
const delMaterial = (item: any) => {
useMessageBox()
.confirm('此操作将永久删除该文件, 是否继续?')
.then(() => {
delObj({
id: item.mediaId,
appId: checkAppId.value,
})
.then(() => {
getDataList();
})
.catch((err) => {
useMessage().error(err.msg);
});
});
};
// 视频
const dialogVideoVisible = ref(false);
const addMaterialLoading = ref(false);
const handleAddVideo = () => {
dialogVideoVisible.value = true;
};
const uploadRules = reactive({
title: [{ required: true, message: '请输入标题', trigger: 'blur' }],
introduction: [{ required: true, message: '请输入描述', trigger: 'blur' }],
});
const uploadForm = ref();
const uploadFileVideo = ref();
const subVideo = () => {
uploadForm.value.validate((valid: boolean) => {
if (!valid) {
return false;
}
uploadFileVideo.value.submit().then(() => {
dialogVideoVisible.value = false;
});
});
};
const handleDown = (row: any) => {
getMaterialOther({
mediaId: row.mediaId,
fileName: row.name,
appId: checkAppId.value,
}).then((response) => {
const url = window.URL.createObjectURL(new Blob([response.data]));
const link = document.createElement('a');
link.style.display = 'none';
link.href = url;
link.setAttribute('download', row.name);
document.body.appendChild(link);
link.click();
});
};
// 图文
const dialogNewsRef = ref();
const handleAddNews = () => {
dialogNewsRef.value.openDialog({
accountId: checkAppId.value,
});
};
const handleEditNews = (item) => {
dialogNewsRef.value.openDialog(
{
accountId: checkAppId.value,
},
JSON.parse(JSON.stringify(item.content.newsItem)),
item.mediaId,
'edit'
);
};
const handleInfo = (row) => {
getMaterialVideo({
mediaId: row.mediaId,
appId: checkAppId.value,
})
.then((response) => {
const downUrl = response.data.downUrl;
window.open(downUrl, '_blank');
})
.catch((err) => {
useMessage().error(err.msg);
});
};
// 默认选择第一个公众号
onMounted(async () => {
const { data } = await deptData.queryList();
if (data?.length > 0) {
handleNodeClick(data[0]);
}
});
</script>
<style lang="scss" scoped>
.tree-position {
margin: 12px 20px 0 0;
}
.pagination {
float: right;
margin-right: 25px;
}
.add_but {
padding: 10px;
}
.ope-row {
margin-top: 5px;
text-align: center;
border-top: 1px solid #eaeaea;
padding-top: 5px;
}
.item-name {
font-size: 12px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: center;
}
.el-upload__tip {
margin-left: 5px;
}
/*新增图文*/
.left {
display: inline-block;
width: 35%;
vertical-align: top;
margin-top: 200px;
}
.right {
display: inline-block;
width: 60%;
margin-top: -40px;
}
.avatar-uploader {
width: 20%;
display: inline-block;
}
.avatar-uploader .el-upload {
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
text-align: unset !important;
}
.avatar-uploader .el-upload:hover {
border-color: #409eff;
}
.avatar-uploader-icon {
border: 1px solid #d9d9d9;
font-size: 28px;
color: #8c939d;
width: 120px;
height: 120px;
line-height: 120px;
text-align: center;
}
.avatar {
width: 230px;
height: 120px;
}
.avatar1 {
width: 120px;
height: 120px;
}
.digest {
width: 60%;
display: inline-block;
vertical-align: top;
}
/*新增图文*/
/*瀑布流样式*/
.waterfall {
width: 100%;
column-gap: 10px;
column-count: 5;
margin: 0 auto;
}
.waterfall-item {
padding: 10px;
margin-bottom: 10px;
break-inside: avoid;
border: 1px solid #eaeaea;
}
p {
line-height: 30px;
}
@media (min-width: 992px) and (max-width: 1300px) {
.waterfall {
column-count: 3;
}
p {
color: red;
}
}
@media (min-width: 768px) and (max-width: 991px) {
.waterfall {
column-count: 2;
}
p {
color: orange;
}
}
@media (max-width: 767px) {
.waterfall {
column-count: 1;
}
}
/*瀑布流样式*/
.news-main {
background-color: #ffffff;
width: 100%;
margin: auto;
height: 120px;
}
.news-content {
background-color: #acadae;
width: 100%;
height: 120px;
position: relative;
}
.news-content-title {
display: inline-block;
font-size: 15px;
color: #ffffff;
position: absolute;
left: 0px;
bottom: 0px;
background-color: black;
width: 98%;
padding: 1%;
opacity: 0.65;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
height: 25px;
}
.news-main-item {
background-color: #ffffff;
padding: 5px 0px;
border-top: 1px solid #eaeaea;
width: 100%;
margin: auto;
}
.news-content-item {
position: relative;
margin-left: -3px;
}
.news-content-item-title {
display: inline-block;
font-size: 12px;
width: 70%;
}
.news-content-item-img {
display: inline-block;
width: 25%;
background-color: #acadae;
}
.input-tt {
padding: 5px;
}
.activeAddNews {
border: 5px solid #2bb673;
}
.news-main-plus {
width: 280px;
text-align: center;
margin: auto;
height: 50px;
}
.icon-plus {
margin: 10px;
font-size: 25px;
}
.select-item {
width: 60%;
padding: 10px;
margin: 0 auto 10px auto;
border: 1px solid #eaeaea;
}
.father .child {
display: none;
text-align: center;
position: relative;
bottom: 25px;
}
.father:hover .child {
display: block;
}
.thumb-div {
display: inline-block;
width: 30%;
text-align: center;
}
.thumb-but {
margin: 5px;
}
.material-img {
width: 100%;
height: 100%;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,221 @@
.clearfix::after {
content: '';
display: table;
clear: both;
}
div {
text-align: left;
}
.weixin-hd {
color: #fff;
text-align: center;
position: relative;
bottom: 426px;
left: 0px;
width: 300px;
height: 64px;
background: transparent url('./assets/menu_head.png') no-repeat 0 0;
background-position: 0 0;
background-size: 100%;
}
.weixin-title {
color: #fff;
font-size: 14px;
width: 100%;
text-align: center;
position: absolute;
top: 33px;
left: 0px;
}
.weixin-menu {
background: transparent url('./assets/menu_foot.png') no-repeat 0 0;
padding-left: 43px;
font-size: 12px;
}
.menu_option {
width: 40% !important;
}
.public-account-management {
min-width: 1200px;
width: 1200px;
margin: 0 auto;
.left {
float: left;
display: inline-block;
width: 350px;
height: 715px;
background: url('./assets/iphone_backImg.png') no-repeat;
background-size: 100% auto;
padding: 518px 25px 88px;
position: relative;
box-sizing: border-box;
/*第一级菜单*/
.menu_main {
.menu_bottom {
position: relative;
float: left;
display: inline-block;
box-sizing: border-box;
width: 85.5px;
text-align: center;
border: 1px solid #ebedee;
background-color: #fff;
cursor: pointer;
&.menu_addicon {
height: 46px;
line-height: 46px;
}
.menu_item {
height: 44px;
line-height: 44px;
text-align: center;
box-sizing: border-box;
width: 100%;
&.active {
border: 1px solid #2bb673;
}
}
.menu_subItem {
height: 44px;
line-height: 44px;
text-align: center;
box-sizing: border-box;
&.active {
border: 1px solid #2bb673;
}
}
}
i {
color: #2bb673;
}
/*第二级菜单*/
.submenu {
position: absolute;
width: 85.5px;
bottom: 45px;
.subtitle {
background-color: #fff;
box-sizing: border-box;
}
}
}
.save_div {
margin-top: 15px;
text-align: center;
.save_btn {
bottom: 20px;
left: 100px;
}
}
}
/*右边菜单内容*/
.right {
float: left;
width: 63%;
background-color: #e8e7e7;
padding: 20px;
margin-left: 20px;
-webkit-box-sizing: border-box;
box-sizing: border-box;
.configure_page {
.delete_btn {
text-align: right;
margin-bottom: 15px;
}
.menu_content {
margin-top: 20px;
}
.configur_content {
margin-top: 20px;
background-color: #fff;
padding: 20px 10px;
border-radius: 5px;
}
.blue {
color: #29b6f6;
margin-top: 10px;
}
.applet {
margin-bottom: 20px;
span {
width: 20%;
}
}
.input_width {
width: 40%;
}
.material {
.input_width {
width: 30%;
}
.el-textarea {
width: 80%;
}
}
}
}
.el-input {
width: 70%;
margin-right: 2%;
}
}
.pagination {
text-align: right;
margin-right: 25px;
}
.select-item {
width: 280px;
padding: 10px;
margin: 0 auto 10px auto;
border: 1px solid #eaeaea;
}
.select-item2 {
padding: 10px;
margin: 0 auto 10px auto;
border: 1px solid #eaeaea;
}
.ope-row {
padding-top: 10px;
text-align: center;
}
.item-name {
font-size: 12px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: center;
}

View File

@@ -0,0 +1,416 @@
<template>
<div class="layout-padding">
<splitpanes>
<pane size="20">
<div class="layout-padding-auto layout-padding-view">
<el-scrollbar>
<query-tree class="mt10" :query="deptData.queryList" @node-click="handleNodeClick" placeholder="请输入微信公众号名称" />
</el-scrollbar>
</div>
</pane>
<pane>
<div class="layout-padding-auto layout-padding-view">
<el-scrollbar>
<div v-loading="loading" class="clearfix public-account-management">
<div class="left">
<div class="weixin-hd">
<div class="weixin-title">{{ name }}</div>
</div>
<div class="clearfix weixin-menu menu_main">
<div v-for="(item, i) of menuList" :key="i" class="menu_bottom">
<div :class="{ active: isActive === i }" class="menu_item el-icon-s-fold" @click="menuClick(i, item)">
{{ item.name }}
</div>
<!-- 以下为二级菜单-->
<div v-if="isSubMenuFlag === i" class="submenu">
<template v-for="(subItem, k) in item.sub_button">
<div v-if="item.sub_button" :key="k" class="subtitle menu_bottom">
<div :class="{ active: isSubMenuActive === i + '' + k }" class="menu_subItem" @click="subMenuClick(subItem, i, k)">
{{ subItem.name }}
</div>
</div>
</template>
<!-- 二级菜单加号 当长度 小于 5 才显示二级菜单的加号 -->
<div v-if="!item.sub_button || item.sub_button.length < 5" class="menu_bottom menu_addicon" @click="addSubMenu(i, item)">
<el-icon>
<el-icon><Plus /></el-icon>
</el-icon>
</div>
</div>
</div>
<!-- 一级菜单加号 -->
<div v-if="menuList.length < 3" class="menu_bottom menu_addicon" @click="addMenu">
<el-icon>
<el-icon><Plus /></el-icon>
</el-icon>
</div>
</div>
<div class="flex items-center justify-center gap-4 mt-4 mb-6 save_div">
<el-button
class="save_btn !px-6 !h-9 hover:scale-105 transition-transform"
type="primary"
size="small"
@click="handleSave"
>
<el-icon class="mr-1"><Check /></el-icon>
保存发布
</el-button>
<el-button
class="save_btn !px-6 !h-9 hover:scale-105 transition-transform"
type="warning"
size="small"
@click="handleDelete"
>
<el-icon class="mr-1"><Delete /></el-icon>
清空菜单
</el-button>
</div>
</div>
<div v-if="showRightFlag" class="right">
<div class="configure_page">
<div class="delete_btn">
<el-button icon="Delete" size="mini" type="danger" @click="deleteMenu(tempObj)">删除当前菜单 </el-button>
</div>
<div>
<span>菜单名称</span>
<el-input v-model="tempObj.name" class="input_width" clearable placeholder="请输入菜单名称" />
</div>
<div v-if="showConfigureContent">
<div class="menu_content">
<span>菜单标识</span>
<el-input v-model="tempObj.key" class="input_width" clearable placeholder="请输入菜单 KEY" />
</div>
<div class="menu_content">
<span>菜单内容</span>
<el-select v-model="tempObj.type" class="menu_option" clearable placeholder="请选择">
<el-option v-for="item in menuOptions" :key="item.value" :label="item.label" :value="item.value" />
</el-select>
</div>
<div class="configur_content" v-if="tempObj.type === 'view'">
<span>跳转链接</span>
<el-input class="input_width" v-model="tempObj.url" placeholder="请输入链接" clearable />
</div>
<div class="configur_content" v-if="tempObj.type === 'miniprogram'">
<div class="applet">
<span>小程序的 appid </span>
<el-input class="input_width" v-model="tempObj.miniProgramAppId" placeholder="请输入小程序的appid" clearable />
</div>
<div class="applet">
<span>小程序的页面路径</span>
<el-input
class="input_width"
v-model="tempObj.miniProgramPagePath"
placeholder="请输入小程序的页面路径pages/index"
clearable
/>
</div>
<div class="applet">
<span>小程序的备用网页</span>
<el-input class="input_width" v-model="tempObj.url" placeholder="不支持小程序的老版本客户端将打开本网页" clearable />
</div>
<p class="blue">tips:需要和公众号进行关联才可以把小程序绑定带微信菜单上哟</p>
</div>
<div class="configur_content" v-if="tempObj.type === 'article_view_limited'">
<el-row>
<div class="select-item" v-if="tempObj && tempObj.replyArticles">
<wx-news :objData="tempObj.replyArticles" />
<el-row class="ope-row">
<el-button type="danger" icon="delete" circle @click="deleteMaterial" />
</el-row>
</div>
<div v-else>
<el-row>
<el-col :span="24" style="text-align: center">
<el-button type="success" @click="openMaterial"> 素材库选择<i class="fansel-icon--right"></i> </el-button>
</el-col>
</el-row>
</div>
<wx-material-select ref="dialogNewsRef" @selectMaterial="selectMaterial" />
</el-row>
</div>
<div class="configur_content" v-if="tempObj.type === 'click' || tempObj.type === 'scancode_waitmsg'">
<wx-reply :objData="tempObj" v-if="hackResetWxReplySelect" />
</div>
</div>
</div>
</div>
</div>
</el-scrollbar>
</div>
</pane>
</splitpanes>
</div>
</template>
<script lang="ts" name="wx-menu" setup>
import {getObj, publishObj, saveObj} from '/@/api/mp/wx-menu';
// 部门树使用的数据
import {fetchAccountList} from '/@/api/mp/wx-account';
import {useMessage, useMessageBox} from '/@/hooks/message';
const WxMaterialSelect = defineAsyncComponent(() => import('/@/components/Wechat/wx-material-select/main.vue'));
const WxReply = defineAsyncComponent(() => import('/@/components/Wechat/wx-reply/index.vue'));
const QueryTree = defineAsyncComponent(() => import('/@/components/QueryTree/index.vue'));
const WxNews = defineAsyncComponent(() => import('/@/components/Wechat/wx-news/index.vue'));
// 点击树
const handleNodeClick = (node: any) => {
accountId.value = node.appid;
name.value = node.name;
getMenuFun();
};
const deptData = reactive({
queryList: (name?: string) => {
return fetchAccountList({
name: name,
});
},
});
const loading = ref(false);
const name = ref('测试公众号');
const accountId = ref(''); // 公众号id
// 一级菜单点中样式
const isActive = ref(-1);
// 一级菜单点中样式
const isSubMenuActive = ref('-1');
// 二级菜单显示标志
const isSubMenuFlag = ref(-1);
const menuList = reactive([
{
name: '菜单名称',
sub_button: [],
},
] as any);
const hackResetWxReplySelect = ref(false);
const menuOptions = ref([
{
value: 'view',
label: '跳转网页',
},
{
value: 'miniprogram',
label: '跳转小程序',
},
{
value: 'click',
label: '点击回复',
},
{
value: 'article_view_limited',
label: '跳转图文消息',
},
{
value: 'scancode_push',
label: '扫码直接返回结果',
},
{
value: 'scancode_waitmsg',
label: '扫码回复',
},
{
value: 'pic_sysphoto',
label: '系统拍照发图',
},
{
value: 'pic_photo_or_album',
label: '拍照或者相册',
},
{
value: 'pic_weixin',
label: '微信相册',
},
{
value: 'location_select',
label: '选择地理位置',
},
]);
const showRightFlag = ref(false);
let tempObj = ref({
replyArticles: [] as any,
articleId: '',
appId: '',
});
const tempSelfObj = reactive({
grand: '', // 表示二级菜单
index: '', // 表示一级菜单索引
secondIndex: '', // 表示二级菜单索引
});
const getMenuFun = () => {
getObj(accountId.value).then((res) => {
if (res.data) {
const data = JSON.parse(res.data);
if (data && data.button) {
Object.assign(menuList, data.button);
}
} else {
menuList.length = 0;
Object.assign(menuList, {
name: '菜单名称',
sub_button: [],
});
}
});
};
const showConfigureContent = ref(true);
// 一级菜单点击事件
const menuClick = (i, item) => {
hackResetWxReplySelect.value = false;
nextTick(() => {
hackResetWxReplySelect.value = true;
});
showRightFlag.value = true; // 右边菜单
tempObj.value = item;
tempObj.value.appId = accountId.value;
showConfigureContent.value = !(item.sub_button && item.sub_button.length > 0); // 有子菜单,就不显示配置内容
isActive.value = i;
isSubMenuFlag.value = i;
isSubMenuActive.value = '-1';
tempSelfObj.grand = '1'; //表示一级菜单
tempSelfObj.index = i; //表示一级菜单索引
};
// 点击二级菜单
const subMenuClick = (subItem, index, k) => {
hackResetWxReplySelect.value = false;
nextTick(() => {
hackResetWxReplySelect.value = true;
});
showRightFlag.value = true; // 右边菜单
// Object.assign(tempObj, subItem) // 这个如果放在顶部flag 会没有。因为重新赋值了。
tempObj.value = subItem;
tempObj.value.appId = accountId.value;
showConfigureContent.value = true;
isActive.value = -1; // 一级菜单去除样式
isSubMenuActive.value = index + '' + k; // 二级菜单选中样式
tempSelfObj.grand = '2'; //表示二级菜单
tempSelfObj.index = index; //表示一级菜单索引
tempSelfObj.secondIndex = k; //表示二级菜单索引
};
// 添加横向二级菜单item 表示要操作的父菜单
const addSubMenu = (i, item) => {
if (!item.sub_button || item.sub_button.length <= 0) {
item['sub_button'] = [];
showConfigureContent.value = false;
}
let addButton = {
name: '子菜单名称',
reply: {
// 用于存储回复内容
type: 'text',
accountId: accountId.value, // 保证组件里,可以使用到对应的公众号
},
};
item.sub_button.push(addButton);
};
// 添加横向一级菜单
const addMenu = () => {
const addButton = {
name: '菜单名称',
sub_button: [],
reply: {
// 用于存储回复内容
type: 'text',
accountId: accountId.value, // 保证组件里,可以使用到对应的公众号
},
};
menuList.push(addButton);
};
const deleteMenu = () => {
useMessageBox()
.confirm('确定要删除吗?')
.then(() => {
if (tempSelfObj.grand === '1') {
menuList.splice(tempSelfObj.index, 1);
} else if (tempSelfObj.grand === '2') {
menuList[tempSelfObj.index].sub_button.splice(tempSelfObj.secondIndex, 1);
}
useMessage().success('删除成功');
Object.assign(tempObj, {});
showRightFlag.value = false;
isActive.value = -1;
isSubMenuActive.value = '-1';
})
.catch((err) => {
useMessage().error(err.msg);
});
};
const handleSave = async () => {
try {
await useMessageBox().confirm('确定要保存该菜单吗?');
await saveObj(accountId.value, { button: menuList });
await publishObj(accountId.value);
useMessage().success('发布成功');
} catch (err: any) {
useMessage().error(err.msg);
}
};
const deleteMaterial = () => {
tempObj.value.replyArticles = [];
tempObj.value.articleId = '';
};
const dialogNewsRef = ref();
const openMaterial = () => {
dialogNewsRef.value.openDialog({ type: 'news', accountId: accountId.value });
};
const selectMaterial = (item) => {
const articleId = item.articleId;
const articles = item.content.newsItem;
// 提示,针对多图文
if (articles.length > 1) {
// this.$alert('您选择的是多图文,将默认跳转第一篇', '提示', {
// confirmButtonText: '确定'
// })
}
// 设置菜单的回复
tempObj.value.articleId = articleId;
tempObj.value.replyArticles = [];
articles.forEach((article) => {
tempObj.value.replyArticles.push({
title: article.title,
description: article.digest,
picUrl: article.picUrl,
url: article.url,
});
});
};
const handleDelete = () => {};
// 默认选择第一个公众号
onMounted(async () => {
const { data } = await deptData.queryList();
if (data?.length > 0) {
handleNodeClick(data[0]);
}
});
</script>
<style lang="scss" scoped>
@import './assets/wx-menu.scss';
</style>

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