xingyuv 2023-04-04 09:13:37 +08:00
commit 1f75f376f6
28 changed files with 2582 additions and 2150 deletions

View File

@ -13,11 +13,8 @@
## 开发文档
[开发文档](./dev.md)
### 说明
- 本项目为ruoyi-vue-pro vue3 antdv 版本ui
- 基于vben2.9.0版本并升级到最新的依赖后续将升级antdv4
- 目前开发中
</div>
## 交流群
<img alt="index.vue" width="400px" src="./docimg/wx.jpg"></img>
## 开发进度
- 系统管理 页面适配 99%
@ -94,9 +91,6 @@ pnpm front
pnpm build
```
## 交流群
<img alt="index.vue" width="400px" src="./docimg/code.jpg"></img>
## Git 贡献提交规范
- 参考 [vue](https://github.com/vuejs/vue/blob/dev/.github/COMMIT_CONVENTION.md) 规范 ([Angular](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular))

View File

@ -14,7 +14,7 @@ import { configVisualizerConfig } from './visualizer'
import { configThemePlugin } from './theme'
import { configSvgIconsPlugin } from './svgSprite'
export function createVitePlugins(viteEnv: ViteEnv, isBuild: boolean) {
export async function createVitePlugins(viteEnv: ViteEnv, isBuild: boolean) {
const { VITE_BUILD_COMPRESS, VITE_BUILD_COMPRESS_DELETE_ORIGIN_FILE } = viteEnv
const vitePlugins: (PluginOption | PluginOption[])[] = [

View File

@ -5,6 +5,7 @@
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
import path from 'path'
import { PluginOption } from 'vite'
export function configSvgIconsPlugin(isBuild: boolean) {
const svgIconsPlugin = createSvgIconsPlugin({
@ -13,5 +14,5 @@ export function configSvgIconsPlugin(isBuild: boolean) {
// default
symbolId: 'icon-[dir]-[name]'
})
return svgIconsPlugin
return svgIconsPlugin as PluginOption
}

View File

@ -3,6 +3,7 @@
*/
import visualizer from 'rollup-plugin-visualizer'
import { isReportMode } from '../../utils'
import { PluginOption } from 'vite'
export function configVisualizerConfig() {
if (isReportMode()) {
@ -11,7 +12,7 @@ export function configVisualizerConfig() {
open: true,
gzipSize: true,
brotliSize: true
}) as Plugin
}) as PluginOption
}
return []
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

BIN
docimg/wx.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

View File

@ -140,8 +140,10 @@
"url": "https://github.com/xingyuv/issues"
},
"homepage": "https://github.com/xingyuv",
"packageManager": "pnpm@8.1.0",
"engines": {
"node": ">= 16.0.0"
"node": ">= 16.0.0",
"pnpm": ">=7.30.0"
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": [

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,4 @@
import { defHttp } from '@/utils/http/axios'
import { UploadFileParams } from '@/types/axios'
export interface ProfileDept {
id: number
@ -87,10 +86,7 @@ export function updateUserPwdApi(oldPassword: string, newPassword: string) {
// 用户头像上传
export function uploadAvatarApi(data) {
const params: UploadFileParams = {
file: data
}
return defHttp.uploadFile({ url: Api.uploadAvatarApi }, params)
return defHttp.put({ url: Api.uploadAvatarApi, data: { file: data } })
}
// 社交绑定,使用 code 授权码

View File

@ -7,5 +7,6 @@ export default {
delete: 'Delete',
detail: 'Detail',
export: 'Export',
import: 'Import'
import: 'Import',
sync: 'Sync'
}

View File

@ -7,5 +7,6 @@ export default {
delete: '删除',
detail: '详情',
export: '导出',
import: '导入'
import: '导入',
sync: '同步'
}

View File

@ -14,4 +14,54 @@ export function formatToDate(date?: dayjs.ConfigType, format = DATE_FORMAT): str
return dayjs(date).format(format)
}
export function beginOfDay(date) {
return new Date(date.getFullYear(), date.getMonth(), date.getDate())
}
export function endOfDay(date) {
return new Date(date.getFullYear(), date.getMonth(), date.getDate(), 23, 59, 59, 999)
}
export function betweenDay(date1, date2) {
date1 = convertDate(date1)
date2 = convertDate(date2)
// 计算差值
return Math.floor((date2.getTime() - date1.getTime()) / (24 * 3600 * 1000))
}
export function formatDate(date, fmt) {
date = convertDate(date)
const o = {
'M+': date.getMonth() + 1, //月份
'd+': date.getDate(), //日
'H+': date.getHours(), //小时
'm+': date.getMinutes(), //分
's+': date.getSeconds(), //秒
'q+': Math.floor((date.getMonth() + 3) / 3), //季度
S: date.getMilliseconds() //毫秒
}
if (/(y+)/.test(fmt)) {
// 年份
fmt = fmt.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length))
}
for (const k in o) {
if (new RegExp('(' + k + ')').test(fmt)) {
fmt = fmt.replace(RegExp.$1, RegExp.$1.length === 1 ? o[k] : ('00' + o[k]).substr(('' + o[k]).length))
}
}
return fmt
}
export function addTime(date, time) {
date = convertDate(date)
return new Date(date.getTime() + time)
}
export function convertDate(date) {
if (typeof date === 'string') {
return new Date(date)
}
return date
}
export const dateUtil = dayjs

View File

@ -9,6 +9,7 @@
<div class="mb-2">头像</div>
<CropperAvatar
:value="avatar"
:uploadApi="uploadAvatarApi as any"
btnText="更换头像"
:btnProps="{ preIcon: 'ant-design:cloud-upload-outlined' }"
@change="updateAvatar"
@ -56,7 +57,6 @@ async function updateAvatar({ src, data }) {
const userinfo = userStore.getUserInfo
userinfo.user.avatar = src
userStore.setUserInfo(userinfo)
console.log('data', data)
}
async function handleSubmit() {

View File

@ -12,6 +12,30 @@ export interface ListItem {
color?: string
}
// tab的list
export const settingList = [
{
key: '1',
name: '基本设置',
component: 'BaseSetting'
},
{
key: '2',
name: '安全设置',
component: 'SecureSetting'
},
{
key: '3',
name: '账号绑定',
component: 'AccountBind'
},
{
key: '4',
name: '新消息通知',
component: 'MsgNotify'
}
]
// 基础设置 form
export const baseSetschemas: FormSchema[] = [
{

View File

@ -15,7 +15,7 @@
{ icon: IconEnum.DOWNLOAD, label: '生成', auth: 'infra:codegen:download', onClick: handleGenTable.bind(null, record) },
{
icon: IconEnum.RESET,
label: '同步',
label: t('action.sync'),
auth: 'infra:codegen:update',
popConfirm: {
title: '确认要强制同步' + record.tableName + '表结构吗?',

View File

@ -8,7 +8,7 @@ import { ref, unref } from 'vue'
import { BasicModal, useModalInner } from '@/components/Modal'
import { BasicForm, useForm } from '@/components/Form'
import { formSchema } from './dataSourceConfig.data'
import { createPost, getPost, updatePost } from '@/api/system/post'
import { createDataSourceConfig, getDataSourceConfig, updateDataSourceConfig } from '@/api/infra/dataSourceConfig'
const emit = defineEmits(['success', 'register'])
const isUpdate = ref(true)
@ -26,7 +26,7 @@ const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data
setModalProps({ confirmLoading: false })
isUpdate.value = !!data?.isUpdate
if (unref(isUpdate)) {
const res = await getPost(data.record.id)
const res = await getDataSourceConfig(data.record.id)
setFieldsValue({ ...res })
}
})
@ -36,9 +36,9 @@ async function handleSubmit() {
const values = await validate()
setModalProps({ confirmLoading: true })
if (unref(isUpdate)) {
await updatePost(values)
await updateDataSourceConfig(values)
} else {
await createPost(values)
await createDataSourceConfig(values)
}
closeModal()
emit('success')

View File

@ -9,8 +9,8 @@
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'">
<TableAction
:actions="[
{ icon: IconEnum.EDIT, label: t('action.edit'), auth: 'mp:account:update', onClick: handleEdit.bind(null, record) },
:actions="[{ icon: IconEnum.EDIT, label: t('action.edit'), auth: 'mp:account:update', onClick: handleEdit.bind(null, record) }]"
:drop-down-actions="[
{
icon: IconEnum.RESET,
label: '生成二维码',

View File

@ -3,7 +3,7 @@
<BasicTable @register="registerTable">
<template #toolbar>
<a-button type="primary" v-auth="['mp:user:sync']" :preIcon="IconEnum.RESET" @click="handleSync">
{{ t('action.create') }}
{{ t('action.sync') }}
</a-button>
</template>
<template #bodyCell="{ column }">
@ -49,7 +49,7 @@ const [registerTable, { getForm, reload }] = useTable({
/** 同步按钮操作 */
async function handleSync() {
createConfirm({
title: '同步粉丝',
title: t('action.sync'),
iconType: 'warning',
content: '是否确认同步粉丝?',
async onOk() {

View File

@ -0,0 +1,70 @@
<!-- <template>
<Card title="接口分析数据" :loading="loading">
<div ref="chartRef" :style="{ width, height }"></div>
</Card>
</template>
<script lang="ts" setup>
import { Ref, ref, watch } from 'vue'
import { Card } from 'ant-design-vue'
import { useECharts } from '@/hooks/web/useECharts'
import { propTypes } from '@/utils/propTypes'
const props = defineProps({
loading: Boolean,
newUser: propTypes.array,
cancelUser: propTypes.array,
width: propTypes.string.def('100%'),
height: propTypes.string.def('300px')
})
const chartRef = ref<HTMLDivElement | null>(null)
const { setOptions } = useECharts(chartRef as Ref<HTMLDivElement>)
const newUserData = ref<any[]>(props.newUser)
const cancelUserData = ref<any[]>(props.cancelUser)
watch(
() => props.loading,
() => {
if (props.loading) {
return
}
setOptions({
color: ['#67C23A', '#e5323e', '#E6A23C', '#409EFF'],
legend: {
data: ['被动回复用户消息的次数', '失败次数', '最大耗时', '总耗时']
},
tooltip: {},
xAxis: {
data: [] // X
},
yAxis: {},
series: [
{
name: '被动回复用户消息的次数',
type: 'bar',
barGap: 0,
data: [] //
},
{
name: '失败次数',
type: 'bar',
data: [] //
},
{
name: '最大耗时',
type: 'bar',
data: [] //
},
{
name: '总耗时',
type: 'bar',
data: [] //
}
]
})
},
{ immediate: true }
)
</script> -->
<template><div>开发中</div></template>

View File

@ -0,0 +1,63 @@
<!-- <template>
<Card title="消息概况数据" :loading="loading">
<div ref="chartRef" :style="{ width, height }"></div>
</Card>
</template>
<script lang="ts" setup>
import { Ref, ref, watch } from 'vue'
import { Card } from 'ant-design-vue'
import { useECharts } from '@/hooks/web/useECharts'
import { propTypes } from '@/utils/propTypes'
const props = defineProps({
loading: Boolean,
newUser: propTypes.array,
cancelUser: propTypes.array,
width: propTypes.string.def('100%'),
height: propTypes.string.def('300px')
})
const chartRef = ref<HTMLDivElement | null>(null)
const { setOptions } = useECharts(chartRef as Ref<HTMLDivElement>)
const newUserData = ref<any[]>(props.newUser)
const cancelUserData = ref<any[]>(props.cancelUser)
watch(
() => props.loading,
() => {
if (props.loading) {
return
}
setOptions({
color: ['#67C23A', '#e5323e'],
legend: {
data: ['用户发送人数', '用户发送条数']
},
tooltip: {},
xAxis: {
data: [] // X
},
yAxis: {
minInterval: 1
},
series: [
{
name: '用户发送人数',
type: 'line',
smooth: true,
data: newUserData.value //
},
{
name: '用户发送条数',
type: 'line',
smooth: true,
data: cancelUserData.value //
}
]
})
},
{ immediate: true }
)
</script> -->
<template><div>开发中</div></template>

View File

@ -0,0 +1,74 @@
<!-- <template>
<Card title="累计用户数据" :loading="loading">
<div ref="chartRef" :style="{ width, height }"></div>
</Card>
</template>
<script lang="ts" setup>
import { Ref, onMounted, ref, watch } from 'vue'
import { Card } from 'ant-design-vue'
import { useECharts } from '@/hooks/web/useECharts'
import { propTypes } from '@/utils/propTypes'
import { getUserCumulate } from '@/api/mp/statistics'
import { beginOfDay, endOfDay, formatToDateTime } from '@/utils/dateUtil'
const props = defineProps({
loading: Boolean,
accountId: propTypes.number,
dataTime: {
type: Array as PropType<Date[]>,
default: () => [
beginOfDay(new Date(new Date().getTime() - 3600 * 1000 * 24 * 7)), // -7
endOfDay(new Date(new Date().getTime() - 3600 * 1000 * 24))
]
},
width: propTypes.string.def('100%'),
height: propTypes.string.def('300px')
})
const chartRef = ref<HTMLDivElement | null>(null)
const { setOptions } = useECharts(chartRef as Ref<HTMLDivElement>)
const optionsData = ref()
async function initData() {
const res = await getUserCumulate({
accountId: props.accountId,
date: [formatToDateTime(props.dataTime[0]), formatToDateTime(props.dataTime[1])]
})
optionsData.value = res.cumulateUser
}
watch(
() => props.loading,
() => {
if (props.loading) {
return
}
setOptions({
legend: {
data: ['累计用户量']
},
xAxis: {
type: 'category',
data: optionsData.value
},
yAxis: {
minInterval: 1
},
series: [
{
name: '累计用户量',
data: [], //
type: 'line',
smooth: true
}
]
})
},
{ immediate: true }
)
onMounted(async () => {
await initData()
})
</script> -->
<template><div>开发中</div></template>

View File

@ -0,0 +1,87 @@
<!-- <template>
<Card title="用户增减数据" :loading="loading">
<div ref="chartRef" :style="{ width, height }"></div>
</Card>
</template>
<script lang="ts" setup>
import { Ref, onMounted, ref, watch } from 'vue'
import { Card } from 'ant-design-vue'
import { useECharts } from '@/hooks/web/useECharts'
import { propTypes } from '@/utils/propTypes'
import { getUserSummary } from '@/api/mp/statistics'
import { addTime, beginOfDay, betweenDay, endOfDay, formatDate, formatToDateTime } from '@/utils/dateUtil'
const props = defineProps({
loading: Boolean,
accountId: propTypes.number,
dataTime: {
type: Array as PropType<Date[]>,
default: () => [
beginOfDay(new Date(new Date().getTime() - 3600 * 1000 * 24 * 7)), // -7
endOfDay(new Date(new Date().getTime() - 3600 * 1000 * 24))
]
},
width: propTypes.string.def('100%'),
height: propTypes.string.def('300px')
})
const chartRef = ref<HTMLDivElement | null>(null)
const { setOptions } = useECharts(chartRef as Ref<HTMLDivElement>)
const optionsData = ref()
const xAxisDate = ref()
async function initData() {
const res = await getUserSummary({
accountId: props.accountId,
date: [formatToDateTime(props.dataTime[0]), formatToDateTime(props.dataTime[1])]
})
optionsData.value = res
xAxisDate.value = []
const days = betweenDay(props.dataTime[0], props.dataTime[1]) //
for (let i = 0; i <= days; i++) {
xAxisDate.value.push(formatDate(addTime(props.dataTime[0], 3600 * 1000 * 24 * i), 'yyyy-MM-dd'))
}
}
watch(
() => props.loading,
() => {
if (props.loading) {
return
}
setOptions({
color: ['#67C23A', '#e5323e'],
legend: {
data: ['新增用户', '取消关注的用户']
},
tooltip: {},
xAxis: {
data: xAxisDate.value // X
},
yAxis: {
minInterval: 1
},
series: [
{
name: '新增用户',
type: 'bar',
barGap: 0,
data: optionsData.value.newUserData //
},
{
name: '取消关注的用户',
type: 'bar',
data: optionsData.value.cancelUserData //
}
]
})
},
{ immediate: true }
)
onMounted(async () => {
await initData()
})
</script> -->
<template><div>开发中</div></template>

View File

@ -1,3 +1,20 @@
<template>
<div>开发中</div>
<!-- <template>
<div>
<UserSummaryChart class="md:w-1/2 w-full" :loading="loading" :accountId="accountId" />
<UserCumulateChart class="md:w-1/2 w-full" :loading="loading" :accountId="accountId" />
<UpstreamMessageChart class="md:w-1/2 w-full" :loading="loading" :accountId="accountId" />
<InterfaceSummaryChart class="md:w-1/2 w-full" :loading="loading" :accountId="accountId" />
</div>
</template>
<script lang="ts" setup name="Statistics">
import { ref } from 'vue'
import UserSummaryChart from './components/UserSummaryChart.vue'
import UserCumulateChart from './components/UserCumulateChart.vue'
import UpstreamMessageChart from './components/UpstreamMessageChart.vue'
import InterfaceSummaryChart from './components/InterfaceSummaryChart.vue'
const loading = ref(true)
const accountId = ref(1)
</script> -->
<template><div>开发中</div></template>

View File

@ -0,0 +1,49 @@
<template>
<BasicModal v-bind="$attrs" @register="registerModal" :title="isUpdate ? '编辑' : '新增'" @ok="handleSubmit">
<BasicForm @register="registerForm" />
</BasicModal>
</template>
<script lang="ts" setup name="PostModal">
import { ref, unref } from 'vue'
import { BasicModal, useModalInner } from '@/components/Modal'
import { BasicForm, useForm } from '@/components/Form'
import { formSchema } from './tag.data'
import { createTag, updateTag, getTag } from '@/api/mp/tag'
const emit = defineEmits(['success', 'register'])
const isUpdate = ref(true)
const [registerForm, { setFieldsValue, resetFields, validate }] = useForm({
labelWidth: 120,
baseColProps: { span: 24 },
schemas: formSchema,
showActionButtonGroup: false,
actionColOptions: { span: 23 }
})
const [registerModal, { setModalProps, closeModal }] = useModalInner(async (data) => {
resetFields()
setModalProps({ confirmLoading: false })
isUpdate.value = !!data?.isUpdate
if (unref(isUpdate)) {
const res = await getTag(data.record.id)
setFieldsValue({ ...res })
}
})
async function handleSubmit() {
try {
const values = await validate()
setModalProps({ confirmLoading: true })
if (unref(isUpdate)) {
await updateTag(values)
} else {
await createTag(values)
}
closeModal()
emit('success')
} finally {
setModalProps({ confirmLoading: false })
}
}
</script>

View File

@ -1,3 +1,90 @@
<template>
<div>开发中</div>
<div>
<BasicTable @register="registerTable">
<template #toolbar>
<a-button type="primary" v-auth="['mp:tag:create']" :preIcon="IconEnum.ADD" @click="handleCreate">
{{ t('action.create') }}
</a-button>
<a-button type="warning" v-auth="['mp:tag:sync']" :preIcon="IconEnum.RESET" @click="handleSync">
{{ t('action.sync') }}
</a-button>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'">
<TableAction
:actions="[
{ icon: IconEnum.EDIT, label: t('action.edit'), auth: 'mp:tag:update', onClick: handleEdit.bind(null, record) },
{
icon: IconEnum.DELETE,
color: 'error',
label: t('action.delete'),
auth: 'mp:tag:delete',
popConfirm: {
title: t('common.delMessage'),
placement: 'left',
confirm: handleDelete.bind(null, record)
}
}
]"
/>
</template>
</template>
</BasicTable>
<TagModal @register="registerModal" @success="reload()" />
</div>
</template>
<script lang="ts" setup name="Tag">
import { useI18n } from '@/hooks/web/useI18n'
import { useMessage } from '@/hooks/web/useMessage'
import { useModal } from '@/components/Modal'
import TagModal from './TagModal.vue'
import { IconEnum } from '@/enums/appEnum'
import { BasicTable, useTable, TableAction } from '@/components/Table'
import { deleteTag, getTagPage, syncTag } from '@/api/mp/tag'
import { columns, searchFormSchema } from './tag.data'
const { t } = useI18n()
const { createConfirm, createMessage } = useMessage()
const [registerModal, { openModal }] = useModal()
const [registerTable, { getForm, reload }] = useTable({
title: '标签列表',
api: getTagPage,
columns,
formConfig: { labelWidth: 120, schemas: searchFormSchema },
useSearchForm: true,
showTableSetting: true,
actionColumn: {
width: 140,
title: t('common.action'),
dataIndex: 'action',
fixed: 'right'
}
})
function handleCreate() {
openModal(true, { isUpdate: false })
}
function handleEdit(record: Recordable) {
openModal(true, { record, isUpdate: true })
}
async function handleSync() {
createConfirm({
title: t('action.sync'),
iconType: 'warning',
content: t('common.exportMessage'),
async onOk() {
await syncTag(getForm().getFieldsValue().accountId)
createMessage.success(t('common.exportSuccessText'))
}
})
}
async function handleDelete(record: Recordable) {
await deleteTag(record.id)
createMessage.success(t('common.delSuccessText'))
reload()
}
</script>

View File

@ -0,0 +1,63 @@
import { getSimpleAccounts } from '@/api/mp/account'
import { BasicColumn, FormSchema, useRender } from '@/components/Table'
export const columns: BasicColumn[] = [
{
title: '编号',
dataIndex: 'id',
width: 100
},
{
title: '标签名称',
dataIndex: 'name',
width: 180
},
{
title: '粉丝数',
dataIndex: 'count',
width: 100
},
{
title: '创建时间',
dataIndex: 'createTime',
width: 180,
customRender: ({ text }) => {
return useRender.renderDate(text)
}
}
]
export const searchFormSchema: FormSchema[] = [
{
label: '公众号',
field: 'accountId',
component: 'ApiSelect',
componentProps: {
api: () => getSimpleAccounts(),
labelField: 'name',
valueField: 'id'
},
colProps: { span: 8 }
},
{
label: '标签名称',
field: 'name',
component: 'Input',
colProps: { span: 8 }
}
]
export const formSchema: FormSchema[] = [
{
label: '编号',
field: 'id',
show: false,
component: 'Input'
},
{
label: '标签名称',
field: 'name',
required: true,
component: 'Input'
}
]

View File

@ -20,7 +20,7 @@ const __APP_INFO__ = {
lastBuildTime: dayjs().format('YYYY-MM-DD HH:mm:ss')
}
export default ({ command, mode }: ConfigEnv): UserConfig => {
export default async ({ command, mode }: ConfigEnv): Promise<UserConfig> => {
const root = process.cwd()
const env = loadEnv(mode, root)
@ -92,7 +92,7 @@ export default ({ command, mode }: ConfigEnv): UserConfig => {
},
// The vite plugin used by the project. The quantity is large, so it is separately extracted and managed
plugins: createVitePlugins(viteEnv, isBuild),
plugins: await createVitePlugins(viteEnv, isBuild),
optimizeDeps: { include, exclude }
}