chore(config): 添加项目配置文件和隐私协议

- 新增 .editorconfig 文件统一代码风格配置
- 新增 .env 环境变量配置文件
- 添加开发和生产环境的环境变量配置
- 配置 ESLint 忽略规则文件
- 设置代码检查配置文件 .eslintrc.js
- 添加 Git 忽略文件规则
- 创建 Prettier 格式化忽略规则
- 添加隐私政策和服务协议HTML文件
- 实现访问密钥编辑组件基础结构
This commit is contained in:
2026-02-07 16:33:13 +08:00
commit 92a6a32868
1384 changed files with 224513 additions and 0 deletions

View File

@@ -0,0 +1,75 @@
<!-- 搜索表单 -->
<template>
<a-space style="flex-wrap: wrap" v-if="hasRole('superAdmin') || hasRole('admin')">
<a-button
type="text"
@click="openUrl('/project/renew')"
>即将到期
</a-button>
<a-button
type="text"
@click="openUrl('/project/renew-log/index')"
>续费明细
</a-button>
<a-button
type="text"
@click="openUrl('/project/no-renew')"
>未签续费
</a-button>
<a-button
type="text"
@click="openUrl('/oa/app/index')"
>(旧版项目管理)
</a-button>
</a-space>
</template>
<script lang="ts" setup>
import {watch,nextTick} from 'vue';
import {CmsWebsite} from '@/api/cms/cmsWebsite/model';
import {openUrl} from "@/utils/common";
import {message} from 'ant-design-vue';
import {removeSiteInfoCache} from "@/api/cms/cmsWebsite";
import {hasRole} from "@/utils/permission";
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: [];
website?: CmsWebsite;
count?: 0;
}>(),
{}
);
const emit = defineEmits<{
(e: 'add'): void;
}>();
const add = () => {
emit('add');
};
// 清除缓存
const clearSiteInfoCache = () => {
removeSiteInfoCache('SiteInfo:' + localStorage.getItem('TenantId') + "*").then(
(msg) => {
if (msg) {
message.success(msg);
}
}
);
};
nextTick(() => {
if(localStorage.getItem('NotActive')){
// IsActive.value = false
}
})
watch(
() => props.selection,
() => {
}
);
</script>

View File

@@ -0,0 +1,317 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="600"
:visible="visible"
:maskClosable="false"
:maxable="maxable"
:title="isUpdate ? '编辑项目' : '添加项目'"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
@ok="save"
>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="styleResponsive ? { md: 4, sm: 5, xs: 24 } : { flex: '90px' }"
:wrapper-col="
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
"
>
<a-form-item label="项目名称" name="appName">
<div class="flex w-full justify-between gap-2">
<a-input
allow-clear
placeholder="内容管理系统"
v-model:value="form.appName"
/>
</div>
</a-form-item>
<a-form-item label="项目编号" name="appCode">
<a-input
allow-clear
placeholder="demo"
:disabled="isUpdate"
v-model:value="form.appCode"
/>
</a-form-item>
<a-form-item label="备注" name="comments">
<a-textarea
:rows="4"
:maxlength="200"
placeholder="请输入描述"
v-model:value="form.comments"
/>
</a-form-item>
<a-form-item label="排序" name="sortNumber">
<a-input-number
:min="0"
:max="9999"
class="ele-fluid"
placeholder="请输入排序号"
v-model:value="form.sortNumber"
/>
</a-form-item>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { Form, message } from 'ant-design-vue';
import { assignObject } from 'ele-admin-pro';
import {addProject, pageProject, updateProject} from '@/api/project/project';
import { Project } from '@/api/project/project/model';
import { useThemeStore } from '@/store/modules/theme';
import { storeToRefs } from 'pinia';
import {debounce} from 'lodash-es';
import { ItemType } from 'ele-admin-pro/es/ele-image-upload/types';
import { FormInstance } from 'ant-design-vue/es/form';
import {DictData} from "@/api/system/dict-data/model";
import {CmsWebsite} from "@/api/cms/cmsWebsite/model";
import {pageUserAll} from "@/api/system/user";
import {User} from "@/api/system/user/model";
// 是否是修改
const isUpdate = ref(false);
const useForm = Form.useForm;
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: Project | null;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
// 提交状态
const loading = ref(false);
// 是否显示最大化切换按钮
const maxable = ref(true);
// 表格选中数据
const formRef = ref<FormInstance | null>(null);
const images = ref<ItemType[]>([]);
const state = reactive<any>({
data: [],
value: undefined,
fetching: false,
});
// 用户信息
const form = reactive<Project>({
appId: undefined,
appName: undefined,
appCode: undefined,
appSecret: undefined,
parentId: undefined,
appType: undefined,
appTypeMultiple: undefined,
menuType: undefined,
companyId: undefined,
companyName: undefined,
appIcon: undefined,
appQrcode: undefined,
appUrl: undefined,
adminUrl: undefined,
downUrl: undefined,
serverUrl: undefined,
fileUrl: undefined,
callbackUrl: undefined,
docsUrl: undefined,
gitUrl: undefined,
prototypeUrl: undefined,
ipAddress: undefined,
images: undefined,
packageName: undefined,
clicks: undefined,
installs: undefined,
comments: undefined,
content: undefined,
requirement: undefined,
developer: undefined,
director: undefined,
projectDirector: undefined,
salesman: undefined,
price: undefined,
linePrice: undefined,
score: undefined,
star: undefined,
path: undefined,
component: undefined,
authority: undefined,
target: undefined,
hide: undefined,
search: undefined,
active: undefined,
meta: undefined,
edition: undefined,
version: undefined,
isUse: undefined,
file1: undefined,
file2: undefined,
file3: undefined,
showExpiration: undefined,
showCase: undefined,
showIndex: undefined,
recommend: undefined,
expirationTime: undefined,
soon: undefined,
renewMoney: undefined,
appStatus: '开发中',
progress: undefined,
deleted: undefined,
userId: undefined,
websiteId: undefined,
tenantId: undefined,
createTime: undefined,
updateTime: undefined,
status: 0,
sortNumber: 100
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
companyName: [
{
required: true,
type: 'string',
message: '请填写客户名称',
trigger: 'blur'
}
],
appName: [
{
required: true,
type: 'string',
message: '请填写项目名称',
trigger: 'blur'
}
],
progress: [
{
required: true,
type: 'number',
message: '请填写开发进度',
trigger: 'blur'
}
],
appCode: [
{
required: true,
type: 'string',
message: '请填写项目标识',
trigger: 'blur'
}
],
appStatus: [
{
required: true,
type: 'string',
message: '请选择项目状态',
trigger: 'blur'
}
]
});
const onDone = (item: DictData) => {
form.appStatus = item.dictDataCode
}
const chooseCmsWebsite = (item: CmsWebsite) => {
form.websiteId = item.websiteId;
form.appName = item.websiteName;
form.appIcon = item.websiteLogo;
form.appUrl = item.domain;
form.appType = item.websiteType;
form.companyId = item.userId;
form.adminUrl = item.adminUrl;
}
let lastFetchId = 0;
const fetchUser = debounce(value => {
lastFetchId += 1;
const fetchId = lastFetchId;
state.data = [];
state.fetching = true;
pageUserAll({isAdmin: true,keywords: value})
.then(body => {
if (fetchId !== lastFetchId) {
return;
}
state.data = body?.map(d => {
d.label = d.realName;
d.value = d.userId;
return d
}) || [];
state.fetching = false;
});
}, 300);
const onUser = (item: User) => {
form.companyId = item.userId;
form.companyName = item.realName;
}
const { resetFields } = useForm(form, rules);
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const formData = {
...form
};
const saveOrUpdate = isUpdate.value ? updateProject : addProject;
saveOrUpdate(formData)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
watch(
() => props.visible,
(visible) => {
if (visible) {
images.value = [];
if (props.data) {
assignObject(form, props.data);
isUpdate.value = true;
} else {
isUpdate.value = false;
}
} else {
resetFields();
}
},
{ immediate: true }
);
</script>

View File

@@ -0,0 +1,154 @@
<!-- 搜索表单 -->
<template>
<a-space :size="10" style="flex-wrap: wrap; margin-bottom: 16px">
<a-button type="primary" class="ele-btn-icon" @click="add">
<template #icon>
<PlusOutlined />
</template>
<span>添加</span>
</a-button>
<!-- <a-radio-group v-if="hasRole('superAdmin') || hasRole('admin')" v-model:value="where.appStatus" @change="handleAppStatus">-->
<!-- <a-radio-button value="all">全部({{ conut.totalNum }})</a-radio-button>-->
<!-- <a-radio-button value="开发中"-->
<!-- >开发中({{ conut.totalNum2 }})-->
<!-- </a-radio-button-->
<!-- >-->
<!-- <a-radio-button value="已上架"-->
<!-- >运行中({{ conut.totalNum3 }})-->
<!-- </a-radio-button-->
<!-- >-->
<!-- <a-radio-button value="已下架"-->
<!-- >已下线({{ conut.totalNum5 }})-->
<!-- </a-radio-button-->
<!-- >-->
<!-- </a-radio-group>-->
<a-radio-group v-model:value="where.sceneType" @change="handleSceneType">
<a-radio-button value="myProject">我的项目</a-radio-button>
<a-radio-button value="involved">我的参与</a-radio-button>
<a-radio-button value="collection">我的收藏</a-radio-button>
</a-radio-group>
<!-- <a-select-->
<!-- v-model:value="where.appType"-->
<!-- style="width: 150px"-->
<!-- placeholder="产品类型"-->
<!-- @change="search"-->
<!-- >-->
<!-- <a-select-option value="云·企业官网">·企业官网</a-select-option>-->
<!-- <a-select-option value="小程序开发">小程序开发</a-select-option>-->
<!-- <a-select-option value="企业商城">企业商城</a-select-option>-->
<!-- <a-select-option value="办公协同(OA)">办公协同(OA)</a-select-option>-->
<!-- </a-select>-->
<!-- <a-select-->
<!-- v-model:value="where.appStatus"-->
<!-- style="width: 150px"-->
<!-- :placeholder="`开发进度(${conut.totalNum})`"-->
<!-- @change="handleAppStatus"-->
<!-- >-->
<!-- <a-select-option value="开发中">开发中({{conut.totalNum2}})</a-select-option>-->
<!-- <a-select-option value="已上架">已上架({{conut.totalNum3}})</a-select-option>-->
<!-- <a-select-option value="已下架">已下线({{conut.totalNum5}})</a-select-option>-->
<!-- </a-select>-->
<a-input-search
allow-clear
placeholder="请输入关键词"
v-model:value="where.keywords"
@pressEnter="search"
@search="search"
/>
<a-button @click="reset">重置</a-button>
</a-space>
</template>
<script lang="ts" setup>
import { PlusOutlined } from '@ant-design/icons-vue';
import {ProjectParam} from "@/api/project/project/model";
import {ref, watch} from 'vue';
import useSearch from "@/utils/use-search";
import {getData} from "@/api/project/project";
import {hasRole} from "@/utils/permission";
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: [];
}>(),
{}
);
const emit = defineEmits<{
(e: 'search', where?: ProjectParam): void;
(e: 'add'): void;
(e: 'remove'): void;
(e: 'advanced'): void;
}>();
// 日期范围选择
const dateRange = ref<[string, string]>(['', '']);
// 表单数据
const {where,resetFields} = useSearch<ProjectParam>({
appId: undefined,
userId: undefined,
appStatus: undefined,
companyId: undefined,
keywords: ''
});
// 统计数据
const conut = ref({
totalNum: 0,
totalNum2: 0,
totalNum3: 0,
totalNum4: 0,
totalNum5: 0,
totalNum6: 0,
totalNum7: 0
});
const handleSceneType = () => {
where.appStatus = undefined;
emit('search', where);
};
const handleAppStatus = () => {
where.sceneType = undefined;
if(where.appStatus === 'all'){
where.appStatus = undefined;
}
emit('search', where);
};
/* 搜索 */
const search = () => {
const [d1, d2] = dateRange.value ?? [];
emit('search', {
...where,
createTimeStart: d1 ? d1 + ' 00:00:00' : '',
createTimeEnd: d2 ? d2 + ' 23:59:59' : ''
});
};
// 新增
const add = () => {
emit('add');
};
// 批量删除
const removeBatch = () => {
emit('remove');
};
/* 重置 */
const reset = () => {
resetFields();
search();
};
getData().then((data: any) => {
conut.value = data;
});
watch(
() => props.selection,
() => {}
);
</script>

View File

@@ -0,0 +1,524 @@
<template>
<a-page-header :title="getPageTitle()" @back="() => $router.go(-1)">
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<ele-pro-table
ref="tableRef"
row-key="appId"
:columns="columns"
:datasource="datasource"
:customRow="customRow"
tool-class="ele-toolbar-form"
class="sys-org-table"
>
<template #toolbar>
<search
@search="reload"
:selection="selection"
@add="openEdit"
@remove="removeBatch"
@batchMove="openMove"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'appIcon'">
<a-avatar :src="record.appIcon" :size="40"/>
</template>
<template v-if="column.key === 'appName'">
<a-space :size="12">
<a-tooltip title="查看详情">
<span class="cursor-pointer" @click="push('/project/detail/' + record.appId)">{{
record.appName
}}</span>
</a-tooltip>
<StarOutlined :style="record.collection ? 'color: #f56a00' : 'color: #e5e7eb'"
@click="onCollection(record)"/>
</a-space>
</template>
<template v-if="column.key === 'appType'">
<span class="cursor-pointer"
@click="openUrl(`https://${record.adminUrl}/login?loginPhone=${record.superAdminPhone}`)">
<a-tooltip title="跳转后台登录">
{{ record.appType }}
</a-tooltip>
</span>
</template>
<template v-if="column.key === 'appUrl'">
<template v-if="record.appUrl">
<a-tooltip title="访问PC版">
<DesktopOutlined class="text-xl" @click="openUrl(`http://${record.appUrl}`)"/>
</a-tooltip>
<a-divider type="vertical"/>
<a-tooltip title="访问移动版">
<MobileOutlined class="text-xl" @click="onShare(record)"/>
</a-tooltip>
</template>
</template>
<template v-if="column.key === 'status'">
<a-tag v-if="record.status === 0" color="green">显示</a-tag>
<a-tag v-if="record.status === 1" color="red">隐藏</a-tag>
</template>
<template v-if="column.key === 'recommend'">
<a-space @click="onRecommend(record)">
<span v-if="record.recommend === 1" class="ele-text-success"
><CheckOutlined
/></span>
<span v-else class="ele-text-placeholder"><CloseOutlined/></span>
</a-space>
</template>
<template v-if="column.key === 'appStatus'">
<a-tag v-if="record.appStatus === '开发中'" color="red">开发中</a-tag>
<a-tag v-if="record.appStatus === '已上架'" color="success">运行中</a-tag>
<a-tag v-if="record.appStatus === '维护中'" color="orange"></a-tag>
<a-tag v-if="record.appStatus === '已下架'">已下线</a-tag>
<a-tag v-if="!record.appStatus">未分配</a-tag>
</template>
<template v-if="column.key === 'progress'">
<div class="w-full flex justify-center">
<div class="w-24">
<a-progress :percent="record.progress" size="small"/>
</div>
</div>
</template>
<template v-if="column.key === 'nickname'">
<span>{{ record.nickname || '未分配' }}</span>
</template>
<template v-if="column.key === 'projectUsers'">
<a-avatar-group :max-count="3" :max-style="{ color: '#f56a00', backgroundColor: '#fde3cf' }">
<template v-for="item in record.projectUsers">
<a-tooltip :title="item.nickname" placement="top">
<a-avatar :src="item.avatar"/>
</a-tooltip>
</template>
</a-avatar-group>
</template>
<template v-if="column.key === 'expirationTime'">
<div>{{ toDateString(record.createTime, 'yyyy-MM-dd') }}</div>
<div @click="openExpirationTimeEdit(record)">{{ toDateString(record.expirationTime, 'yyyy-MM-dd') }}</div>
<template v-if="sceneType == 'totalPrice30'">
<span class="text-purple-500">剩余 {{ record.expiredDays }} </span>
</template>
<template v-else-if="record.expired < 0 || sceneType == 'Expired'">
<span class="text-red-500">已过期 {{ record.expiredDays }} </span>
</template>
<template v-else>
<span class="text-gray-400">剩余 {{ record.expiredDays }} </span>
</template>
</template>
<template v-if="column.key === 'action'">
<a @click="push('/project/detail/' + record.appId)">详情</a>
<a-divider type="vertical"/>
<a @click="openEdit(record)">修改</a>
<a-divider type="vertical"/>
<a-popconfirm
title="确定要删除此记录吗?"
@confirm="remove(record)"
>
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</template>
</template>
</ele-pro-table>
</a-card>
<!-- 编辑弹窗 -->
<ProjectEdit v-model:visible="showEdit" :data="current" @done="reload"/>
<!-- 二维码 -->
<Qrcode v-model:visible="showQrcode" :data="`${qrcode}`" @done="hideShare"/>
<!-- 修复到期时间 -->
<ExpirationTimeEdit v-model:visible="showExpirationTimeEdit" :data="current" @done="reload"/>
</a-page-header>
</template>
<script lang="ts" setup>
import {createVNode, ref} from 'vue';
import {message, Modal} from 'ant-design-vue';
import {
ExclamationCircleOutlined,
CheckOutlined,
CloseOutlined,
StarOutlined,
MobileOutlined,
DesktopOutlined
} from '@ant-design/icons-vue';
import type {EleProTable} from 'ele-admin-pro';
import {toDateString} from 'ele-admin-pro';
import type {
DatasourceFunction,
ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types';
import Search from './components/search.vue';
import ProjectEdit from './components/projectEdit.vue';
import {pageProject, removeProject, removeBatchProject} from '@/api/project/project';
import type {Project, ProjectParam} from '@/api/project/project/model';
import {getPageTitle, openUrl, push} from "@/utils/common";
// import Extra from "./components/extra.vue";
import {updateProject} from "@/api/project/project";
import {addProjectCollection, getProjectCollection, removeProjectCollection} from "@/api/project/projectCollection";
import Qrcode from "@/components/QrCode/index.vue";
import {hasRole} from "@/utils/permission";
import ExpirationTimeEdit from "@/views/project/projectRenew/components/expirationTimeEdit.vue";
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<Project[]>([]);
// 当前编辑数据
const current = ref<Project | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 是否显示批量移动弹窗
const showMove = ref(false);
// 是否显示续费时间修复弹窗
const showExpirationTimeEdit = ref(false);
// 加载状态
const loading = ref(true);
// 是否显示二维码
const showQrcode = ref(false);
// 二维码内容
const qrcode = ref();
// 表格数据源
const datasource: DatasourceFunction = ({
page,
limit,
where,
orders,
filters
}) => {
if (filters) {
where.status = filters.status;
}
return pageProject({
...where,
...orders,
page,
limit
});
};
// 表格列配置
const columns = ref<ColumnItem[]>([
{
key: 'index',
width: 48,
align: 'center',
fixed: 'left',
hideInSetting: true,
customRender: ({ index }) => index + (tableRef.value?.tableIndex ?? 0)
},
// {
// title: 'LOGO',
// dataIndex: 'appIcon',
// key: 'appIcon',
// align: 'center',
// width: 120
// },
{
title: '项目名称',
dataIndex: 'appName',
key: 'appName'
},
// {
// title: '产品',
// dataIndex: 'appType',
// key: 'appType',
// align: 'center'
// },
// {
// title: '访问域名',
// dataIndex: 'appUrl',
// key: 'appUrl',
// align: 'center'
// },
{
title: '项目编号',
dataIndex: 'appCode',
key: 'appCode',
hideInTable: true,
align: 'center'
},
// {
// title: '包名',
// dataIndex: 'packageName',
// key: 'packageName',
// hideInTable: true,
// align: 'center',
// },
{
title: '备注',
dataIndex: 'comments',
key: 'comments',
hideInTable: true,
align: 'center',
},
// {
// title: '开发者',
// dataIndex: 'developer',
// key: 'developer',
// hideInTable: true,
// align: 'center',
// },
{
title: '项目负责人',
dataIndex: 'director',
key: 'director',
hideInTable: true,
align: 'center',
},
// {
// title: '定价',
// dataIndex: 'price',
// key: 'price',
// hideInTable: true,
// align: 'center',
// },
// {
// title: '评分',
// dataIndex: 'score',
// key: 'score',
// hideInTable: true,
// align: 'center',
// },
// {
// title: '星级',
// dataIndex: 'star',
// key: 'star',
// align: 'center',
// hideInTable: true,
// },
// {
// title: '版本',
// dataIndex: 'edition',
// key: 'edition',
// hideInTable: true,
// align: 'center',
// },
// {
// title: '续费提醒',
// dataIndex: 'showExpiration',
// key: 'showExpiration',
// hideInTable: true,
// align: 'center',
// },
// {
// title: '案例',
// dataIndex: 'showCase',
// key: 'showCase',
// hideInTable: true,
// align: 'center',
// },
// {
// title: '首页',
// dataIndex: 'showIndex',
// key: 'showIndex',
// hideInTable: true,
// align: 'center',
// },
// {
// title: '过期状态',
// dataIndex: 'soon',
// key: 'soon',
// hideInTable: true,
// align: 'center',
// },
// {
// title: '状态',
// dataIndex: 'status',
// key: 'status',
// hideInTable: true,
// align: 'center',
// },
// {
// title: '推荐',
// dataIndex: 'recommend',
// key: 'recommend',
// align: 'center',
// },
{
title: '项目成员',
dataIndex: 'projectUsers',
key: 'projectUsers',
align: 'center'
},
{
title: '项目进度',
dataIndex: 'progress',
key: 'progress',
align: 'center'
},
{
title: '状态',
dataIndex: 'appStatus',
key: 'appStatus',
align: 'center',
},
{
title: '排序',
dataIndex: 'sortNumber',
key: 'sortNumber',
align: 'center',
width: 90
},
// {
// title: '到期时间',
// dataIndex: 'expirationTime',
// key: 'expirationTime',
// align: 'center',
// sorter: true,
// ellipsis: true,
// width: 150,
// customRender: ({text}) => toDateString(text, 'yyyy-MM-dd')
// },
{
title: '操作',
key: 'action',
width: 200,
fixed: 'right',
align: 'center',
hideInSetting: true
}
]);
/* 搜索 */
const reload = (where?: ProjectParam) => {
selection.value = [];
tableRef?.value?.reload({where: where});
};
const onRecommend = (row: Project) => {
updateProject({
...row,
recommend: row.recommend == 1 ? 0 : 1
}).then((msg) => {
message.success(msg);
reload();
});
};
/* 打开续费修复弹窗 */
const openExpirationTimeEdit = (row: Project) => {
// 非财务人员不能修改
if (!hasRole('superAdmin')) {
return false;
}
current.value = row;
showExpirationTimeEdit.value = true;
};
// 加入我的收藏
const onCollection = (item: Project) => {
// 是否已收藏
getProjectCollection(item.appId).then((data) => {
if (data) {
item.collection = true;
removeProjectCollection(item.appId).then(msg => {
message.success(msg);
})
} else {
addProjectCollection({
appId: item.appId
}).then((msg) => {
message.success(msg);
})
}
reload()
}).catch(err => {
console.log(err)
});
}
const onShare = (row?: Project) => {
qrcode.value = `https://${row?.appUrl}`
showQrcode.value = true;
}
const hideShare = () => {
showQrcode.value = false;
}
/* 打开编辑弹窗 */
const openEdit = (row?: Project) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 删除单个 */
const remove = (row: Project) => {
const hide = message.loading('请求中..', 0);
removeProject(row.appId)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 批量删除 */
const removeBatch = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
Modal.confirm({
title: '提示',
content: '确定要删除选中的记录吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = message.loading('请求中..', 0);
removeBatchProject(selection.value.map((d) => d.appId))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
/* 查询 */
const query = () => {
loading.value = true;
};
/* 自定义行属性 */
const customRow = (record: Project) => {
return {
// 行点击事件
onClick: () => {
// push('/project/detail/' + record.appId);
},
// 行双击事件
onDblclick: () => {
// push('/project/detail/' + record.appId);
openEdit(record);
}
};
};
query();
</script>
<script lang="ts">
export default {
name: 'Project'
};
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,178 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="800"
:visible="visible"
:maskClosable="false"
:maxable="maxable"
:title="isUpdate ? '编辑我的收藏' : '添加我的收藏'"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
@ok="save"
>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="styleResponsive ? { md: 4, sm: 5, xs: 24 } : { flex: '90px' }"
:wrapper-col="
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
"
>
<a-form-item label="用户ID" name="userId">
<a-input
allow-clear
placeholder="请输入用户ID"
v-model:value="form.userId"
/>
</a-form-item>
<a-form-item label="应用ID" name="appId">
<a-input
allow-clear
placeholder="请输入应用ID"
v-model:value="form.appId"
/>
</a-form-item>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { Form, message } from 'ant-design-vue';
import { assignObject, uuid } from 'ele-admin-pro';
import { addProjectCollection, updateProjectCollection } from '@/api/project/projectCollection';
import { ProjectCollection } from '@/api/project/projectCollection/model';
import { useThemeStore } from '@/store/modules/theme';
import { storeToRefs } from 'pinia';
import { ItemType } from 'ele-admin-pro/es/ele-image-upload/types';
import { FormInstance } from 'ant-design-vue/es/form';
import { FileRecord } from '@/api/system/file/model';
// 是否是修改
const isUpdate = ref(false);
const useForm = Form.useForm;
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: ProjectCollection | null;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
// 提交状态
const loading = ref(false);
// 是否显示最大化切换按钮
const maxable = ref(true);
// 表格选中数据
const formRef = ref<FormInstance | null>(null);
const images = ref<ItemType[]>([]);
// 用户信息
const form = reactive<ProjectCollection>({
id: undefined,
userId: undefined,
appId: undefined,
tenantId: undefined,
createTime: undefined,
projectCollectionId: undefined,
projectCollectionName: '',
status: 0,
comments: '',
sortNumber: 100
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
projectCollectionName: [
{
required: true,
type: 'string',
message: '请填写我的收藏名称',
trigger: 'blur'
}
]
});
const chooseImage = (data: FileRecord) => {
images.value.push({
uid: data.id,
url: data.path,
status: 'done'
});
form.image = data.path;
};
const onDeleteItem = (index: number) => {
images.value.splice(index, 1);
form.image = '';
};
const { resetFields } = useForm(form, rules);
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const formData = {
...form
};
const saveOrUpdate = isUpdate.value ? updateProjectCollection : addProjectCollection;
saveOrUpdate(formData)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
watch(
() => props.visible,
(visible) => {
if (visible) {
images.value = [];
if (props.data) {
assignObject(form, props.data);
if(props.data.image){
images.value.push({
uid: uuid(),
url: props.data.image,
status: 'done'
})
}
isUpdate.value = true;
} else {
isUpdate.value = false;
}
} else {
resetFields();
}
},
{ immediate: true }
);
</script>

View File

@@ -0,0 +1,42 @@
<!-- 搜索表单 -->
<template>
<a-space :size="10" style="flex-wrap: wrap">
<a-button type="primary" class="ele-btn-icon" @click="add">
<template #icon>
<PlusOutlined />
</template>
<span>添加</span>
</a-button>
</a-space>
</template>
<script lang="ts" setup>
import { PlusOutlined } from '@ant-design/icons-vue';
import type { GradeParam } from '@/api/user/grade/model';
import { watch } from 'vue';
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: [];
}>(),
{}
);
const emit = defineEmits<{
(e: 'search', where?: GradeParam): void;
(e: 'add'): void;
(e: 'remove'): void;
(e: 'batchMove'): void;
}>();
// 新增
const add = () => {
emit('add');
};
watch(
() => props.selection,
() => {}
);
</script>

View File

@@ -0,0 +1,227 @@
<template>
<div class="page">
<div class="ele-body">
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<ele-pro-table
ref="tableRef"
row-key="projectCollectionId"
:columns="columns"
:datasource="datasource"
:customRow="customRow"
tool-class="ele-toolbar-form"
class="sys-org-table"
>
<template #toolbar>
<search
@search="reload"
:selection="selection"
@add="openEdit"
@remove="removeBatch"
@batchMove="openMove"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'image'">
<a-image :src="record.image" :width="50" />
</template>
<template v-if="column.key === 'status'">
<a-tag v-if="record.status === 0" color="green">显示</a-tag>
<a-tag v-if="record.status === 1" color="red">隐藏</a-tag>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a @click="openEdit(record)">修改</a>
<a-divider type="vertical" />
<a-popconfirm
title="确定要删除此记录吗?"
@confirm="remove(record)"
>
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</ele-pro-table>
</a-card>
<!-- 编辑弹窗 -->
<ProjectCollectionEdit v-model:visible="showEdit" :data="current" @done="reload" />
</div>
</div>
</template>
<script lang="ts" setup>
import { createVNode, ref } from 'vue';
import { message, Modal } from 'ant-design-vue';
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
import type { EleProTable } from 'ele-admin-pro';
import { toDateString } from 'ele-admin-pro';
import type {
DatasourceFunction,
ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types';
import Search from './components/search.vue';
import ProjectCollectionEdit from './components/projectCollectionEdit.vue';
import { pageProjectCollection, removeProjectCollection, removeBatchProjectCollection } from '@/api/project/projectCollection';
import type { ProjectCollection, ProjectCollectionParam } from '@/api/project/projectCollection/model';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<ProjectCollection[]>([]);
// 当前编辑数据
const current = ref<ProjectCollection | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 是否显示批量移动弹窗
const showMove = ref(false);
// 加载状态
const loading = ref(true);
// 表格数据源
const datasource: DatasourceFunction = ({
page,
limit,
where,
orders,
filters
}) => {
if (filters) {
where.status = filters.status;
}
return pageProjectCollection({
...where,
...orders,
page,
limit
});
};
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: '自增ID',
dataIndex: 'id',
key: 'id',
align: 'center',
width: 90,
},
{
title: '用户ID',
dataIndex: 'userId',
key: 'userId',
align: 'center',
},
{
title: '应用ID',
dataIndex: 'appId',
key: 'appId',
align: 'center',
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
align: 'center',
sorter: true,
ellipsis: true,
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd')
},
{
title: '操作',
key: 'action',
width: 180,
fixed: 'right',
align: 'center',
hideInSetting: true
}
]);
/* 搜索 */
const reload = (where?: ProjectCollectionParam) => {
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: ProjectCollection) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 删除单个 */
const remove = (row: ProjectCollection) => {
const hide = message.loading('请求中..', 0);
removeProjectCollection(row.projectCollectionId)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 批量删除 */
const removeBatch = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
Modal.confirm({
title: '提示',
content: '确定要删除选中的记录吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = message.loading('请求中..', 0);
removeBatchProjectCollection(selection.value.map((d) => d.projectCollectionId))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
/* 查询 */
const query = () => {
loading.value = true;
};
/* 自定义行属性 */
const customRow = (record: ProjectCollection) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
query();
</script>
<script lang="ts">
export default {
name: 'ProjectCollection'
};
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,213 @@
<!-- 用户编辑弹窗 -->
<template>
<ele-modal
width="80%"
:visible="visible"
:maskClosable="false"
:maxable="maxable"
:title="isUpdate ? '项目介绍' : '项目介绍'"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
@ok="save"
>
<a-form
ref="formRef"
:model="form"
:label-col="{ md: { span: 7 }, sm: { span: 4 }, xs: { span: 24 } }"
:wrapper-col="{ md: { span: 17 }, sm: { span: 20 }, xs: { span: 24 } }"
>
<!-- 编辑器 -->
<div class="content">
<!-- 编辑器 -->
<MdEditor v-model="content" @paste="onPaste" placeholder="用于显示插件市场上的介绍(机密信息勿填),图片请直接粘贴" />
</div>
<!-- 编辑器 -->
<!-- <byte-md-editor-->
<!-- v-model:value="content"-->
<!-- placeholder="请输入您的内容,图片请直接粘贴"-->
<!-- :locale="zh_Hans"-->
<!-- mode="split"-->
<!-- :plugins="plugins"-->
<!-- height="500px"-->
<!-- :editorConfig="{ lineNumbers: true }"-->
<!-- @paste="onPaste"-->
<!-- />-->
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { Form, message } from 'ant-design-vue';
import {assignObject} from "ele-admin-pro";
import { addProject, updateProject } from '@/api/project/project';
import type { Project } from '@/api/project/project/model';
import { useThemeStore } from '@/store/modules/theme';
import { storeToRefs } from 'pinia';
import {MdEditor} from 'md-editor-v3';
import 'md-editor-v3/lib/style.css';
import 'md-editor-v3/lib/preview.css';
import {FormInstance} from 'ant-design-vue/es/form';
import { ItemType } from 'ele-admin-pro/es/ele-image-upload/types';
import {uploadFile, uploadOss} from "@/api/system/file";
import { TOKEN_STORE_NAME } from "@/config/setting";
// 是否是修改
const isUpdate = ref(false);
const useForm = Form.useForm;
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: Project | null;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
// 提交状态
const loading = ref(false);
// 是否显示最大化切换按钮
const maxable = ref(true);
const images = ref<ItemType[]>([]);
const content = ref('');
const token = localStorage.getItem(TOKEN_STORE_NAME);
const formRef = ref<FormInstance | null>(null);
// 用户信息
const form = reactive<Project>({
// 应用id
appId: undefined,
// 项目介绍
content: ''
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
const { resetFields, validate, validateInfos } = useForm(form);
/* 粘贴图片上传服务器并插入编辑器 */
const onPaste = (e) => {
const items = (e.clipboardData || e.originalEvent.clipboardData).items;
let hasFile = false;
for (let i = 0; i < items.length; i++) {
if (items[i].type.indexOf('image') !== -1) {
let file = items[i].getAsFile();
const item: ItemType = {
file,
uid: (file as any).lastModified,
name: file.name
};
uploadOss(<File>item.file)
.then((result) => {
const addPath = `<p><img class="content-img" src="${result.url}"></p>`;
content.value = content.value + addPath
// const addPath = '!['+result.name+']('+ result.url+')\n\r';
// content.value = content.value + addPath
})
.catch((e) => {
message.error(e.message);
});
hasFile = true;
}
}
if (hasFile) {
e.preventDefault();
}
}
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const formData = {
...form,
content: content.value
};
const saveOrUpdate = isUpdate.value ? updateProject : addProject;
saveOrUpdate(formData)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
/* 上传事件 */
const uploadHandler = (file: File) => {
const item: ItemType = {
file,
uid: (file as any).uid,
name: file.name
};
if (!file.type.startsWith('image')) {
message.error('只能选择图片');
return;
}
if (file.size / 1024 / 1024 > 2) {
message.error('大小不能超过 2MB');
return;
}
onUpload(item);
};
// 上传文件
const onUpload = (item) => {
const { file } = item;
uploadFile(file)
.then((data) => {
images.value.push({
uid: data.id,
url: data.url,
status: 'done'
});
})
.catch((e) => {
message.error(e.message);
});
};
watch(
() => props.visible,
(visible) => {
if (visible) {
content.value = '';
if (props.data) {
assignObject(form, props.data);
if (props.data.content) {
content.value = props.data.content;
}
isUpdate.value = true;
} else {
isUpdate.value = false;
}
} else {
resetFields();
}
}
);
</script>

View File

@@ -0,0 +1,49 @@
<template>
<div class="relative">
<a-space style="margin-bottom: 20px">
<a-button type="primary" @click="openEdit">编辑</a-button>
</a-space>
<MdPreview v-if="data.content" class="max-w-5xl" :modelValue="data.content" />
<a-empty
v-else
image="https://gw.alipayobjects.com/mdn/miniapp_social/afts/img/A*pevERLJC9v0AAAAAAAAAAABjAQAAAQ/original"
:image-style="{
height: '60px'
}"
>
<template #description>
<span class="ele-text-placeholder">请填写项目介绍</span>
</template>
</a-empty>
</div>
<!-- 编辑弹窗 -->
<ProjectAboutEdit v-model:visible="showEdit" :data="data" @done="reload" />
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { MdPreview } from 'md-editor-v3';
import 'md-editor-v3/lib/preview.css';
import ProjectAboutEdit from './app-about-edit.vue';
defineProps<{
data: any;
}>();
const emit = defineEmits<{
(e: 'done'): void;
}>();
// 是否显示编辑弹窗
const showEdit = ref(false);
/* 打开编辑弹窗 */
const openEdit = () => {
showEdit.value = true;
};
const reload = () => {
emit('done');
};
</script>

View File

@@ -0,0 +1,221 @@
<!-- 角色编辑弹窗 -->
<template>
<ele-modal
:width="600"
:visible="visible"
:confirm-loading="loading"
:title="isUpdate ? '编辑内容' : '上传文件'"
:body-style="{ paddingBottom: '8px' }"
@update:visible="updateVisible"
@ok="save"
>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="styleResponsive ? { md: 4, sm: 5, xs: 24 } : { flex: '90px' }"
:wrapper-col="
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
"
>
<a-form-item label="上传文件" name="fileName" v-if="!isUpdate">
<span
class="ele-text-success"
v-if="fileName"
style="margin-right: 10px"
>
{{ fileName }}
</span>
<a-upload
:show-upload-list="false"
:accept="'video/*'"
v-if="!fileName"
:customRequest="onUpload"
>
<a-button type="primary" class="ele-btn-icon">
<template #icon>
<UploadOutlined />
</template>
<span>上传文件</span>
</a-button>
</a-upload>
</a-form-item>
<a-form-item label="设置分类" name="name">
<SelectDict
dict-code="groupId"
:placeholder="`选择分类`"
v-model:value="form.groupName"
@done="chooseGroupId"
/>
</a-form-item>
<a-form-item label="文件名称" name="name">
<a-input
allow-clear
:maxlength="20"
placeholder="请输入文件名称"
v-model:value="form.name"
/>
</a-form-item>
<a-form-item label="描述" name="comments">
<a-textarea
:rows="4"
:maxlength="200"
placeholder="这一刻的想法.."
v-model:value="form.comments"
/>
</a-form-item>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { message } from 'ant-design-vue/es';
import type { FormInstance, Rule } from 'ant-design-vue/es/form';
import { storeToRefs } from 'pinia';
import { useThemeStore } from '@/store/modules/theme';
import useFormData from '@/utils/use-form-data';
import type { FileRecord } from '@/api/system/file/model';
import { messageLoading } from 'ele-admin-pro';
import { addFiles, updateFiles, uploadFile } from '@/api/system/file';
import { UploadOutlined } from '@ant-design/icons-vue';
import { RuleObject } from 'ant-design-vue/es/form';
import { DictData } from '@/api/system/dict-data/model';
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: FileRecord | null;
}>();
//
const formRef = ref<FormInstance | null>(null);
const fileName = ref('');
// 是否是修改
const isUpdate = ref(false);
// 提交状态
const loading = ref(false);
// 表单数据
const { form, resetFields, assignFields } = useFormData<FileRecord>({
id: 0,
name: '',
comments: ''
});
// 表单验证规则
const rules = reactive<Record<string, Rule[]>>({
fileName: [
{
required: true,
message: '请上传文件',
type: 'string',
trigger: 'blur',
validator: async (_rule: RuleObject) => {
if (!isUpdate.value && fileName.value.length == 0) {
return Promise.reject('请上传文件');
}
return Promise.resolve();
}
}
],
name: [
{
required: true,
message: '请输入文件名称',
type: 'string',
trigger: 'blur'
}
]
});
const chooseGroupId = (item: DictData) => {
form.groupId = item.dictDataId;
form.groupName = item.dictDataName;
};
// 上传文件
const onUpload = (item) => {
const { file } = item;
if (!file.type.startsWith('video')) {
message.error('文件格式不正确!');
return;
}
if (file.size / 1024 / 1024 > 100) {
message.error('大小不能超过 100MB');
return;
}
const hide = messageLoading({
content: '上传中..',
duration: 0,
mask: true
});
uploadFile(file)
.then((data) => {
hide();
fileName.value = String(data.name);
message.success('上传成功');
})
.catch((e) => {
message.error(e.message);
hide();
});
};
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const saveOrUpdate = isUpdate.value ? updateFiles : addFiles;
saveOrUpdate(form)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
watch(
() => props.visible,
(visible) => {
if (visible) {
if (props.data) {
assignFields(props.data);
isUpdate.value = true;
} else {
isUpdate.value = false;
}
} else {
resetFields();
formRef.value?.clearValidate();
}
}
);
</script>

View File

@@ -0,0 +1,337 @@
<template>
<!-- 表格 -->
<ele-pro-table
ref="tableRef"
row-key="id"
:columns="columns"
:datasource="datasource"
:customRow="customRow"
:scroll="{ x: 800 }"
cache-key="proProjectAnnexTable"
>
<template #toolbar>
<a-space>
<a-upload :show-upload-list="false" :customRequest="onUpload">
<a-button class="ele-btn-icon">
<template #icon>
<UploadOutlined />
</template>
<span>上传文件</span>
</a-button>
</a-upload>
<a-button
danger
type="primary"
class="ele-btn-icon"
v-if="selection.length > 0"
@click="removeBatch"
>
<template #icon>
<delete-outlined />
</template>
<span>删除</span>
</a-button>
<a-input-search
allow-clear
v-model:value="searchText"
placeholder="请输入关键词"
@search="reload"
@pressEnter="reload"
/>
</a-space>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'path'">
<!-- 文件类型 -->
<template v-if="!isImage(record.path)">
<span class="ele-text-secondary">[文件]</span>
</template>
<!-- 含http -->
<template v-else-if="record.path.indexOf('http') == 0">
<a-image
:src="`${record.thumbnail}`"
:preview="{
src: `${record.downloadUrl}`
}"
:width="80"
/>
</template>
<!-- path -->
<template v-else>
<a-image
src="https://file.wsdns.cn/20230218/550e610d43334dd2a7f66d5b20bd58eb.svg"
:preview="{
src: `https://file.wsdns.cn${record.downloadUrl}`
}"
:width="80"
/>
</template>
</template>
<template v-if="column.key === 'name'">
<span>{{ record.name }}</span>
<a-tooltip :title="`复制链接地址`">
<copy-outlined
style="padding-left: 4px"
@click="copyText(record.downloadUrl)"
/>
</a-tooltip>
</template>
<template v-if="column.key === 'action'">
<a @click="openPreview(record.downloadUrl)">预览</a>
<a-divider type="vertical" />
<a :href="record.downloadUrl" target="_blank">下载</a>
<a-divider type="vertical" />
<a @click="openEdit(record)">修改</a>
<a-divider type="vertical" />
<a-popconfirm
placement="topRight"
title="确定要删除此文件吗?"
@confirm="remove(record)"
>
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</template>
</template>
</ele-pro-table>
<!-- 编辑弹窗 -->
<ProjectAnnexEdit v-model:visible="showEdit" :data="current" @done="reload" />
</template>
<script lang="ts" setup>
import { createVNode, ref, watch } from 'vue';
import { message, Modal } from 'ant-design-vue/es';
import {
UploadOutlined,
DeleteOutlined,
CopyOutlined,
ExclamationCircleOutlined
} from '@ant-design/icons-vue';
import type { EleProTable } from 'ele-admin-pro/es';
import type {
DatasourceFunction,
ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types';
import { messageLoading, toDateString } from 'ele-admin-pro/es';
import ProjectAnnexEdit from './app-annex-edit.vue';
import {
pageFiles,
removeFile,
removeFiles,
uploadOssByAppId
} from '@/api/system/file';
import type { FileRecord, FileRecordParam } from '@/api/system/file/model';
import {copyText, isImage, openPreview} from '@/utils/common';
import { FILE_SERVER } from '@/config/setting';
const props = defineProps<{
appId: any;
data: any;
}>();
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<FileRecord[]>([]);
// 当前编辑数据
const current = ref<FileRecord | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
const type = ref('name');
const groupId = ref<number>(0);
const searchText = ref('');
// 表格列配置
const columns = ref<ColumnItem[]>([
// {
// key: 'index',
// width: 48,
// align: 'center',
// fixed: 'left',
// hideInSetting: true,
// customRender: ({ index }) => index + (tableRef.value?.tableIndex ?? 0)
// },
{
title: '附件',
dataIndex: 'path',
key: 'path',
align: 'center',
width: 180,
ellipsis: true
},
{
title: '文件名称',
dataIndex: 'name',
key: 'name',
ellipsis: true
},
{
title: '描述',
dataIndex: 'comments',
ellipsis: true
},
{
title: '文件大小',
dataIndex: 'length',
sorter: true,
showSorterTooltip: false,
ellipsis: true,
customRender: ({ text }) => {
if (text < 1024) {
return text + 'B';
} else if (text < 1024 * 1024) {
return (text / 1024).toFixed(1) + 'KB';
} else if (text < 1024 * 1024 * 1024) {
return (text / 1024 / 1024).toFixed(1) + 'M';
} else {
return (text / 1024 / 1024 / 1024).toFixed(1) + 'G';
}
},
width: 120
},
{
title: '上传者',
width: 120,
dataIndex: 'createNickname'
},
{
title: '上传时间',
dataIndex: 'createTime',
sorter: true,
width: 180,
showSorterTooltip: false,
ellipsis: true,
customRender: ({ text }) => toDateString(text)
},
{
title: '操作',
key: 'action',
width: 260,
align: 'center'
}
]);
// 表格数据源
const datasource: DatasourceFunction = ({ page, limit, where, orders }) => {
where = {};
if (type.value == 'name') {
where.name = searchText.value;
}
if (type.value == 'createNickname') {
where.createNickname = searchText.value;
}
if (groupId.value > 0) {
where.groupId = groupId.value;
}
// where.contentType = 'application';
where.appId = props.appId;
return pageFiles({ ...where, ...orders, page, limit });
};
/* 搜索 */
const reload = (where?: FileRecordParam) => {
selection.value = [];
tableRef?.value?.reload({ page: 1, where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: FileRecord) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 删除单个 */
const remove = (row: FileRecord) => {
const hide = messageLoading('请求中..', 0);
removeFile(row.id)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 批量删除 */
const removeBatch = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
Modal.confirm({
title: '提示',
content: '确定要删除选中的文件吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = messageLoading('请求中..', 0);
removeFiles(selection.value.map((d) => d.id))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
// 上传文件
const onUpload = (item) => {
const { file } = item;
if (file.size / 1024 / 1024 > 100) {
message.error('大小不能超过 100MB');
return;
}
const hide = messageLoading({
content: '上传中..',
duration: 0,
mask: true
});
uploadOssByAppId(file, props.data.appId)
.then((data) => {
console.log(data);
hide();
message.success('上传成功');
reload();
})
.catch((e) => {
message.error(e.message);
hide();
});
};
/* 自定义行属性 */
const customRow = (record: FileRecord) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openPreview(FILE_SERVER + record.downloadUrl);
}
};
};
watch(
() => props.appId,
(appId) => {
if (appId) {
reload();
}
},
{ immediate: true }
);
</script>
<script lang="ts">
export default {
name: 'ProjectAnnexIndex'
};
</script>

View File

@@ -0,0 +1,221 @@
<!-- 用户编辑弹窗 -->
<template>
<ele-modal
width="80%"
:visible="visible"
:maskClosable="false"
:title="isUpdate ? '编辑' : '添加'"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
@ok="save"
>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="{ md: { span: 2 }, sm: { span: 3 }, xs: { span: 24 } }"
:wrapper-col="{ md: { span: 20 }, sm: { span: 20 }, xs: { span: 24 } }"
>
<a-form-item label="标题" name="name">
<div class="w-full flex justify-between">
<a-input
allow-clear
:maxlength="20"
placeholder="请输入标题"
v-model:value="form.name"
/>
</div>
</a-form-item>
<a-form-item label="内容" name="comments">
<MdEditor v-model="form.comments" placeholder="请填写内容(已加密处理),图片支持一键粘贴。" />
</a-form-item>
<a-form-item label="排序" name="sortNumber">
<a-input-number
:min="0"
:max="99999"
placeholder="请输入排序号"
v-model:value="form.sortNumber"
/>
</a-form-item>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { FormInstance } from 'ant-design-vue/es/form';
import useFormData from '@/utils/use-form-data';
import {
addProjectField,
updateProjectField
} from '@/api/project/projectField';
import { message } from 'ant-design-vue/es';
import { ItemType } from 'ele-admin-pro/es/ele-image-upload/types';
import { FileRecord } from '@/api/system/file/model';
import { uuid } from 'ele-admin-pro';
import {MdEditor} from 'md-editor-v3';
import 'md-editor-v3/lib/style.css';
import 'md-editor-v3/lib/preview.css';
import { ProjectField } from '@/api/project/projectField/model';
import {decrypt, encrypt} from "@/utils/common";
import DictSelect from "@/components/DictSelect/index.vue";
import {DictData} from "@/api/system/dict-data/model";
// 是否是修改
const isUpdate = ref(false);
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
appId: number;
// 修改回显的数据
data?: ProjectField | null;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
// 提交状态
const loading = ref(false);
const images = ref<ItemType[]>([]);
const id = 'preview-only';
const formRef = ref<FormInstance | null>(null);// 国际化
const { form, resetFields, assignFields } = useFormData<ProjectField>({
id: undefined,
type: undefined,
appId: undefined,
name: undefined,
status: undefined,
comments: '',
sortNumber: 100
});
// 表单验证规则
const rules = reactive({
type: [
{
required: true,
type: 'number',
message: '请选择字段类型'
}
],
// name: [
// {
// required: true,
// type: 'string',
// message: '请选择类型'
// }
// ],
value: [
{
required: true,
type: 'string',
message: '请填写字段值'
}
],
comments: [
{
required: true,
type: 'string',
message: '请输入内容'
}
],
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
const chooseType = (item: DictData) => {
form.name = item.dictDataCode;
form.comments = item.comments;
};
const chooseImage = (data: FileRecord) => {
images.value.push({
uid: data.id,
url: data.path,
status: 'done'
});
form.comments = data.downloadUrl;
form.type = 1;
};
const onDeleteItem = (index: number) => {
images.value.splice(index, 1);
form.type = 0;
};
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const data = {
...form,
appId: props.appId,
comments: encrypt(form.comments)
};
const saveOrUpdate = isUpdate.value
? updateProjectField
: addProjectField;
saveOrUpdate(data)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
// 清除缓存
if (form.name == 'i18n') {
localStorage.setItem('i18n','1');
window.location.reload();
}
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
watch(
() => props.visible,
(visible) => {
if (visible) {
images.value = [];
if (props.data) {
assignFields(props.data);
form.comments = decrypt(props.data.comments);
if (form.type == 1) {
images.value.push({
uid: uuid(),
url: props.data.comments,
status: 'done'
});
}
isUpdate.value = true;
} else {
isUpdate.value = false;
}
} else {
form.comments = undefined
// resetFields();
}
}
);
</script>
<style lang="less">
svg.md-editor-icon{
width: 24px !important;
height: 24px !important;
}
</style>

View File

@@ -0,0 +1,40 @@
<template>
<a-space>
<a-button type="primary" @click="add">添加</a-button>
<a-input-search
allow-clear
placeholder="请输入标题关键词"
v-model:value="where.keywords"
@pressEnter="search"
@search="search"
/>
<QuestionIcon message="添加项目相关信息和资料" />
</a-space>
</template>
<script lang="ts" setup>
import useSearch from "@/utils/use-search";
import QuestionIcon from '@/components/QuestionIcon/index.vue';
import {ProjectFieldParam} from "@/api/project/projectField/model";
const emit = defineEmits<{
(e: 'add'): void;
(e: 'search', where?: ProjectFieldParam): void;
}>();
// 表单数据
const {where} = useSearch<ProjectFieldParam>({
appId: undefined,
name: undefined,
keywords: ''
});
const add = () => {
emit('add');
};
/* 搜索 */
const search = () => {
emit('search', where);
};
</script>

View File

@@ -0,0 +1,246 @@
<template>
<div class="project-field">
<!-- 表格 -->
<ele-pro-table
ref="tableRef"
row-key="taskId"
:columns="columns"
:customRow="customRow"
:datasource="datasource"
>
<template #toolbar>
<ProjectFieldSearch @add="openEdit" @search="reload" @remove="removeBatch" />
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'comments'">
<MdPreview v-if="record.comments" :id="id" :modelValue="decrypt(record.comments)" />
</template>
<template v-if="column.key === 'status'">
<CheckCircleOutlined v-if="!record.status" :style="`color: #16a34a`" @click="updateForStatus(record)" />
<CloseCircleOutlined v-else :style="`color: #ff0000`" @click="updateForStatus(record)" />
</template>
<template v-if="column.key === 'name'">
<span class="text-gray-400">{{ record.name || '-' }}</span>
</template>
<template v-if="column.key === 'userId'">
<a-space direction="vertical">
<a-tooltip>
<template #title>
<div class="flex flex-col text-left">
<span>发布人{{ record.nickname }}</span>
<span>发布时间{{ record.createTime }}</span>
<span v-if="record.updateUserName">更新时间{{ record.updateTime }}</span>
</div>
</template>
<a-avatar :src="record.avatar" size="small" />
</a-tooltip>
</a-space>
</template>
<template v-if="column.key === 'action'">
<a @click="openEdit(record)">编辑</a>
<a-divider type="vertical" />
<a-popconfirm title="确定要删除此记录吗?" @confirm="remove(record)">
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</template>
</template>
</ele-pro-table>
<!-- 编辑弹窗 -->
<ProjectFieldEdit
v-model:visible="showEdit"
:app-id="data.appId"
:data="current"
@done="reload"
/>
</div>
</template>
<script lang="ts" setup>
import { createVNode, ref, watch } from 'vue';
import { message, Modal } from 'ant-design-vue';
import { ExclamationCircleOutlined, CheckCircleOutlined,CloseCircleOutlined } from '@ant-design/icons-vue';
import { EleProTable } from 'ele-admin-pro';
import type { DatasourceFunction } from 'ele-admin-pro/es/ele-pro-table/types';
import ProjectFieldSearch from './app-field-search.vue';
import { decrypt } from '@/utils/common';
import { MdPreview } from 'md-editor-v3';
import 'md-editor-v3/lib/preview.css';
import { Project } from '@/api/project/project/model';
import ProjectFieldEdit from './app-field-edit.vue';
import { ProjectField, ProjectFieldParam } from '@/api/project/projectField/model';
import {
pageProjectField,
removeProjectField,
removeBatchProjectField,
updateProjectField
} from '@/api/project/projectField';
import gfm from '@bytemd/plugin-gfm';
import zh_HansGfm from '@bytemd/plugin-gfm/locales/zh_Hans.json';
import highlight from '@bytemd/plugin-highlight';
const props = defineProps<{
appId: any;
data: Project;
}>();
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
const selection = ref<ProjectField[]>([]);
// 当前编辑数据
const current = ref<ProjectField | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
const id = 'preview-only';
// 表格数据源
const datasource: DatasourceFunction = ({ page, limit, where, orders }) => {
// 搜索条件
where.appId = props.appId;
return pageProjectField({
...where,
...orders,
page,
limit
});
};
// 表格列配置
const columns = ref<any[]>([
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 90,
align: 'center'
},
{
title: '标题',
dataIndex: 'name',
key: 'name',
width: 120,
align: 'center',
ellipsis: true
},
{
title: '内容',
dataIndex: 'comments',
key: 'comments',
ellipsis: true
},
{
title: '发布人',
dataIndex: 'userId',
key: 'userId',
align: 'center',
width: 90,
ellipsis: true
},
{
title: '操作',
key: 'action',
width: 180,
align: 'center',
hideInSetting: true
}
]);
const updateForStatus = (row?: ProjectField) => {
updateProjectField({
id: row?.id,
status: row?.status == 0 ? 1 : 0
}).then(() => {
message.success('操作成功');
reload();
});
};
/* 打开编辑弹窗 */
const openEdit = (row?: ProjectField) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 搜索 */
const reload = (where?: ProjectFieldParam) => {
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 删除单个 */
const remove = (row: ProjectField) => {
const hide = message.loading('请求中..', 0);
removeProjectField(row.id)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 批量删除 */
const removeBatch = () => {
if (!selection.value?.length) {
message.error('请至少选择一条数据');
return;
}
if (selection.value?.length) {
Modal.confirm({
title: '提示',
content: '确定要删除选中的记录吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = message.loading('请求中..', 0);
removeBatchProjectField(selection.value.map((d) => d.id))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
}
};
/* 自定义行属性 */
const customRow = (record: ProjectField) => {
return {
// 行双击事件
onDblclick: () => {
openEdit(record)
}
};
};
// 插件
const plugins = ref([
gfm({
locale: zh_HansGfm
}),
highlight()
]);
watch(
() => props.appId,
(appId) => {
if (appId) {
reload();
}
},
{ immediate: true }
);
</script>
<script lang="ts">
export default {
name: 'ProjectFieldIndex'
};
</script>

View File

@@ -0,0 +1,564 @@
<!-- 用户编辑弹窗 -->
<template>
<ele-modal
width="80%"
:visible="visible"
:maskClosable="false"
:maxable="maxable"
:title="isUpdate ? '编辑' : '新增'"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
@ok="save"
>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="{ md: { span: 7 }, sm: { span: 4 }, xs: { span: 24 } }"
:wrapper-col="{ md: { span: 17 }, sm: { span: 20 }, xs: { span: 24 } }"
>
<a-descriptions title="基本信息" :column="2" bordered>
<a-descriptions-item
label="Logo"
layout="vertical"
:labelStyle="{ width: '200px', color: '#808080' }"
>
<SelectFile
:placeholder="`请选择图片`"
:limit="1"
:data="logo"
@done="chooseFile"
@del="onDeleteItem"
/>
</a-descriptions-item>
<a-descriptions-item
label="二维码"
layout="vertical"
:labelStyle="{ width: '200px', color: '#808080' }"
>
<SelectFile
:placeholder="`请选择图片`"
:limit="4"
:data="appQrcode"
@done="chooseFile2"
@del="onDeleteItem2"
/>
</a-descriptions-item>
<a-descriptions-item
label="名称"
:labelStyle="{ width: '200px', color: '#808080' }"
>
<a-input
allow-clear
placeholder="请输入应用名称"
class="input-item"
v-model:value="form.appName"
/>
</a-descriptions-item>
<a-descriptions-item
label="状态"
:labelStyle="{ width: '200px', color: '#808080' }"
>
<DictSelect
dict-code="appstoreStatus"
placeholder="请选择应用状态"
class="input-item"
v-model:value="form.appStatus"
@done="onDone"
/>
</a-descriptions-item>
<a-descriptions-item
label="TenantId"
:labelStyle="{ width: '200px', color: '#808080' }"
><a-input-number
allow-clear
placeholder="请输入租户ID"
class="input-item"
:maxlength="5"
v-model:value="form.tenantId"
/></a-descriptions-item>
<a-descriptions-item
label="标识"
:labelStyle="{ width: '200px', color: '#808080' }"
><a-input
allow-clear
:maxlength="16"
class="input-item"
placeholder="请输入应用标识(英文字母)"
v-model:value="form.appCode"
@change="changeAppCode"
/></a-descriptions-item>
<a-descriptions-item
label="AppSecret"
:labelStyle="{ width: '200px', color: '#808080' }"
>
<span>****</span>
</a-descriptions-item>
<a-descriptions-item
label="所属企业"
:labelStyle="{ width: '200px', color: '#808080' }"
>
<SelectCompany
:placeholder="`所属企业`"
class="input-item"
v-model:value="form.companyName"
@done="chooseCompanyName"
/>
</a-descriptions-item>
<a-descriptions-item
label="项目域名"
:labelStyle="{ width: '200px', color: '#808080' }"
>
<a-input
allow-clear
class="input-item"
placeholder="请输入项目地址"
v-model:value="form.appUrl"
/>
</a-descriptions-item>
<a-descriptions-item
label="Git仓库地址"
:labelStyle="{ width: '200px', color: '#808080' }"
>
<a-input
allow-clear
class="input-item"
placeholder="请输入Git仓库地址"
v-model:value="form.gitUrl"
/>
</a-descriptions-item>
<a-descriptions-item
label="项目类型"
:labelStyle="{ width: '200px', color: '#808080' }"
>
<DictSelect
dict-code="appType"
class="input-item"
placeholder="请选择项目类型"
v-model:value="form.appType"
/>
</a-descriptions-item>
<a-descriptions-item
label="开发者"
:labelStyle="{ width: '200px', color: '#808080' }"
>
<SelectCompany
:placeholder="`开发者单位`"
class="input-item"
v-model:value="form.developer"
@done="chooseDeveloper"
/>
</a-descriptions-item>
<a-descriptions-item
label="是否作为案例展示"
:labelStyle="{ width: '200px', color: '#808080' }"
>
<a-switch
size="small"
:checked="form.showCase"
@change="updatShowCase"
/>
</a-descriptions-item>
<a-descriptions-item
label="是否推荐到首页"
:labelStyle="{ width: '200px', color: '#808080' }"
>
<a-switch
size="small"
:checked="form.showIndex"
@change="updatShowIndex"
/>
</a-descriptions-item>
<a-descriptions-item
label="描述"
layout="vertical"
:span="2"
:labelStyle="{ width: '200px', color: '#808080' }"
>
<a-textarea
:rows="4"
:maxlength="200"
class="ele-fluid"
placeholder="请输入应用描述"
v-model:value="form.comments"
/>
</a-descriptions-item>
</a-descriptions>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { Form, message } from 'ant-design-vue';
import { assignObject, ipReg, isChinese, uuid } from 'ele-admin-pro';
import { addApp, updateApp } from '@/api/oa/app';
import type { App } from '@/api/oa/app/model';
import { FormInstance, RuleObject } from 'ant-design-vue/es/form';
import { Company } from '@/api/system/company/model';
import { ItemType } from 'ele-admin-pro/es/ele-image-upload/types';
import { TOKEN_STORE_NAME } from '@/config/setting';
import { FileRecord } from '@/api/system/file/model';
import {DictData} from "@/api/system/dict-data/model";
// 是否是修改
const isUpdate = ref(false);
const useForm = Form.useForm;
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: App | null;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
// 提交状态
const loading = ref(false);
// 是否显示最大化切换按钮
const maxable = ref(true);
// 当期时间
// 已上传数据
const logo = ref<ItemType[]>([]);
const appQrcode = ref<ItemType[]>([]);
const images = ref<ItemType[]>([]);
// 日期范围选择
const content = ref('');
const token = localStorage.getItem(TOKEN_STORE_NAME);
const formRef = ref<FormInstance | null>(null);
// 用户信息
const form = reactive<App>({
// 应用id
appId: undefined,
// 应用秘钥
appSecret: '',
enName: '',
// 应用名称
appName: '',
// 上级id, 0是顶级
parentId: undefined,
// 应用编号
appCode: '',
// 应用图标
appIcon: '',
appQrcode: '',
// 应用截图
images: '',
appType: undefined,
appTypeMultiple: undefined,
// 菜单类型
menuType: undefined,
// 应用地址
appUrl: '',
gitUrl: '',
// 后台管理地址
adminUrl: undefined,
// 下载地址
downUrl: undefined,
serverUrl: undefined,
callbackUrl: undefined,
docsUrl: undefined,
prototypeUrl: undefined,
ipAddress: undefined,
fileUrl: undefined,
// 应用包名
packageName: '',
// 点击次数
clicks: '',
// 安装次数
installs: '',
// 项目介绍
content: '',
// 开发者(个人)
developer: '',
director: '',
projectDirector: '',
salesman: '',
// 软件定价
price: '',
// 评分
score: '',
// 星级
star: '',
// 菜单组件地址
component: '',
// 菜单路由地址
path: '',
// 权限标识
authority: '',
// 打开位置
target: '',
// 是否隐藏, 0否, 1是(仅注册路由不显示在左侧菜单)
hide: undefined,
// 菜单侧栏选中的path
active: '',
// 其它路由元信息
meta: '',
// 版本
edition: '',
// 版本号
version: '',
// 是否已安装
isUse: undefined,
// 排序
sortNumber: undefined,
// 备注
comments: undefined,
tenantName: '',
companyName: '',
// 租户编号
tenantCode: '',
// 租户id
tenantId: undefined,
// 创建时间
createTime: '',
appStatus: '开发中',
// 状态
status: undefined,
// 发布者
userId: '',
// 发布者昵称
nickname: '',
// 子菜单
children: [],
// 权限树回显选中状态, 0未选中, 1选中
checked: false,
//
key: undefined,
//
value: undefined,
//
parentIds: [],
//
openType: undefined,
//
search: undefined,
showCase: undefined,
showIndex: undefined,
recommend: undefined
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
appName: [
{
required: true,
type: 'string',
message: '请输入应用名称',
trigger: 'blur'
}
],
companyName: [
{
required: true,
type: 'string',
message: '请选择所属企业',
trigger: 'blur'
}
],
appCode: [
{
required: true,
type: 'string',
message: '请输入应用标识(英文字母)',
trigger: 'blur',
validator: async (_rule: RuleObject, value: string) => {
if (isChinese(value)) {
return Promise.reject('请输入正确的应用标识');
}
return Promise.resolve();
}
}
],
appType: [
{
required: true,
message: '请选择项目类型'
}
],
ipAddress: [
{
pattern: ipReg,
message: 'IP地址不合法',
type: 'string'
}
],
appStatus: [
{
required: true,
type: 'string',
message: '请选择应用状态',
trigger: 'blur'
}
]
});
const onDone = (item: DictData) => {
form.appStatus = item.dictDataCode
}
const { resetFields, validate } = useForm(form, rules);
const chooseCompanyName = (data: Company) => {
form.appUrl = data.domain;
form.companyName = data.companyName;
form.companyId = data.companyId;
form.tenantId = data.tenantId;
};
const chooseDeveloper = (data: Company) => {
form.developer = data.companyName;
};
const changeAppCode = () => {
form.packageName = `com.gxwebsoft.${form.appCode}`;
};
const updatShowCase = () => {
form.showCase = !form.showCase;
};
const updatShowIndex = () => {
form.showIndex = !form.showIndex;
};
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const formData = {
...form,
content: content.value,
search: form.search ? 1 : 0,
images: JSON.stringify(images.value)
// appTypeMultiple: JSON.stringify(form.appTypeMultiple)
};
const saveOrUpdate = isUpdate.value ? updateApp : addApp;
saveOrUpdate(formData)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
const chooseFile = (data: FileRecord) => {
logo.value.push({
uid: data.id,
url: data.path,
status: 'done'
});
form.appIcon = data.path;
};
const onDeleteItem = (index: number) => {
logo.value.splice(index, 1);
form.appIcon = '';
};
const chooseFile2 = (data: FileRecord) => {
appQrcode.value.push({
uid: data.id,
url: data.path,
status: 'done'
});
const arr = appQrcode.value.map((d) => d.url);
form.appQrcode = arr.join('|||');
};
const onDeleteItem2 = (index: number) => {
appQrcode.value.splice(index, 1);
form.appQrcode = '';
};
const reload = () => {
loading.value = true;
};
watch(
() => props.visible,
(visible) => {
if (visible) {
content.value = '';
logo.value = [];
images.value = [];
appQrcode.value = [];
if (props.data) {
assignObject(form, props.data);
if (props.data.appIcon) {
logo.value.push({
uid: 0,
url: props.data.appIcon,
status: 'done'
});
}
if (props.data.appQrcode) {
const split = props.data.appQrcode.split('|||');
split.map((url) => {
appQrcode.value.push({
uid: uuid(),
url: url,
status: 'done'
});
});
// appQrcode.value.push({
// uid: 0,
// url: props.data.appQrcode,
// status: 'done'
// });
}
if (props.data.companyId) {
console.log(props.data);
}
if (props.data.images) {
const arr = JSON.parse(props.data.images);
arr.map((d) => {
images.value.push({
uid: d.uid,
url: d.url,
status: 'done'
});
});
}
if (props.data.search) {
form.search = props.data.search == 1 ? true : 0;
}
if (props.data.content) {
content.value = props.data.content;
}
isUpdate.value = true;
reload();
} else {
isUpdate.value = false;
}
} else {
resetFields();
}
}
);
</script>
<style lang="less" scoped>
.input-item {
width: 300px;
}
</style>

View File

@@ -0,0 +1,171 @@
<template>
<a-space style="margin-bottom: 20px">
<a-button @click="openEdit">后台管理</a-button>
</a-space>
<template v-if="item">
<a-descriptions :column="2" bordered>
<a-descriptions-item
label="项目名称"
>{{ item.websiteName }}
</a-descriptions-item
>
<a-descriptions-item
label="状态"
:labelStyle="{ width: '200px', color: '#808080' }"
>
<a-tag v-if="data.appStatus === '开发中'" color="red">开发中</a-tag>
<a-tag v-if="data.appStatus === '已上架'" color="success">运行中</a-tag>
<a-tag v-if="data.appStatus === '维护中'" color="orange"></a-tag>
<a-tag v-if="data.appStatus === '已下架'">已下线</a-tag>
<a-tag v-if="!data.appStatus">未分配</a-tag>
</a-descriptions-item>
<a-descriptions-item
label="Logo"
layout="vertical"
:labelStyle="{ width: '200px', color: '#808080' }"
>
<a-avatar :src="item.websiteLogo" shape="square" :size="100"/>
</a-descriptions-item>
<a-descriptions-item
label="开发进度"
:labelStyle="{ width: '200px', color: '#808080' }"
>
<div class="w-24">
<a-progress :percent="data.progress" size="small" />
</div>
</a-descriptions-item>
<a-descriptions-item
label="域名"
:labelStyle="{ width: '200px', color: '#808080' }"
>{{ item.domain }}
</a-descriptions-item
>
<a-descriptions-item
label="TenantId"
:labelStyle="{ width: '200px', color: '#808080' }"
>{{ item.tenantId }}
</a-descriptions-item
>
<!-- <a-descriptions-item-->
<!-- label="标识"-->
<!-- :labelStyle="{ width: '200px', color: '#808080' }"-->
<!-- >{{ data.appCode }}</a-descriptions-item-->
<!-- >-->
<!-- <a-descriptions-item-->
<!-- label="AppSecret"-->
<!-- :labelStyle="{ width: '200px', color: '#808080' }"-->
<!-- >-->
<!-- <a @click="openAppSecretForm">重置</a>-->
<!-- &lt;!&ndash; 编辑弹窗 &ndash;&gt;-->
<!-- <AppSecretForm v-model:visible="showAppSecretForm" :app-id="data.appId" />-->
<!-- </a-descriptions-item>-->
<!-- <a-descriptions-item-->
<!-- label="项目域名"-->
<!-- :labelStyle="{ width: '200px', color: '#808080' }"-->
<!-- >-->
<!-- <a @click="openNew(`${data.appUrl}`)">{{ data.appUrl }}</a>-->
<!-- </a-descriptions-item>-->
<!-- <a-descriptions-item-->
<!-- label="Git仓库地址"-->
<!-- :labelStyle="{ width: '200px', color: '#808080' }"-->
<!-- >-->
<!-- <a @click="openNew(`${data.gitUrl}`)" v-if="data.gitUrl">-->
<!-- <GithubOutlined style="font-size: 20px" />-->
<!-- </a>-->
<!-- </a-descriptions-item>-->
<!-- <a-descriptions-item-->
<!-- label="项目类型"-->
<!-- :labelStyle="{ width: '200px', color: '#808080' }"-->
<!-- >-->
<!-- APP-->
<!-- </a-descriptions-item>-->
<!-- <a-descriptions-item-->
<!-- label="开发者"-->
<!-- :labelStyle="{ width: '200px', color: '#808080' }"-->
<!-- >-->
<!-- {{ data.developer }}-->
<!-- </a-descriptions-item>-->
<!-- <a-descriptions-item-->
<!-- label="是否作为案例展示"-->
<!-- layout="vertical"-->
<!-- :labelStyle="{ width: '200px', color: '#808080' }"-->
<!-- >-->
<!-- {{ data.showCase ? '是' : '否' }}-->
<!-- &lt;!&ndash; <a-switch size="small" :checked="data.showCase" />&ndash;&gt;-->
<!-- </a-descriptions-item>-->
<!-- <a-descriptions-item-->
<!-- label="是否推荐到首页"-->
<!-- layout="vertical"-->
<!-- :labelStyle="{ width: '200px', color: '#808080' }"-->
<!-- >-->
<!-- {{ data.showIndex ? '是' : '否' }}-->
<!-- &lt;!&ndash; <a-switch size="small" :checked="data.showIndex" />&ndash;&gt;-->
<!-- </a-descriptions-item>-->
<!-- <a-descriptions-item-->
<!-- label="描述"-->
<!-- :span="2"-->
<!-- layout="vertical"-->
<!-- :labelStyle="{ width: '200px', color: '#808080' }"-->
<!-- >-->
<!-- <span class="ele-text-secondary">{{ data.comments }}</span>-->
<!-- </a-descriptions-item>-->
</a-descriptions>
</template>
<!-- 编辑弹窗 -->
<AppInfoEdit v-model:visible="showEdit" :data="data" @done="reload"/>
</template>
<script lang="ts" setup>
import {ref, reactive, watch} from 'vue';
import AppSecretForm from './app-secret-form.vue';
import AppInfoEdit from './app-info-edit.vue';
import {AppField} from '@/api/oa/app/field/model';
import {openNew} from '@/utils/common';
import {GithubOutlined} from '@ant-design/icons-vue';
import {CmsWebsite} from "@/api/cms/cmsWebsite/model";
import {Project} from "@/api/project/project/model";
import {getCmsWebsiteAll} from "@/api/cms/cmsWebsite";
const props = defineProps<{
data: Project;
logo: [] | any;
appField: AppField[];
appQrcode: any | undefined;
}>();
const emit = defineEmits<{
(e: 'done'): void;
}>();
// 是否显示编辑弹窗
const showEdit = ref(false);
const showAppSecretForm = ref<boolean>(false);
const item = ref<CmsWebsite>();
const appQrcodeContent = ref<string>(props.appQrcode);
const openAppSecretForm = () => {
showAppSecretForm.value = true;
};
/* 打开编辑弹窗 */
const openEdit = () => {
showEdit.value = true;
};
const reload = () => {
emit('done');
};
watch(
() => props.data.websiteId,
(websiteId) => {
if (websiteId) {
getCmsWebsiteAll(websiteId).then(data => {
item.value = data;
console.log(data, 'data...')
})
}
},
{immediate: true}
);
</script>

View File

@@ -0,0 +1,164 @@
<!-- 用户编辑弹窗 -->
<template>
<ele-modal
:width="1000"
:visible="visible"
:maskClosable="false"
:maxable="maxable"
:title="isUpdate ? '创建订单' : '创建订单'"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
@ok="save"
>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="{ md: { span: 2 }, sm: { span: 4 }, xs: { span: 24 } }"
:wrapper-col="{ md: { span: 21 }, sm: { span: 22 }, xs: { span: 24 } }"
>
<a-form-item label="类型" name="type">
<a-radio-group v-model:value="form.type">
<a-radio :value="0">续费</a-radio>
<a-radio :value="1">新购</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="访问域名" name="domain">
<a-input
v-model:value="form.domain"
placeholder="https://site.websoft.top"
/>
</a-form-item>
<a-form-item label="账号" name="account">
<a-input allow-clear placeholder="demo" v-model:value="form.account" />
</a-form-item>
<a-form-item label="密码" name="password">
<a-input-password
allow-clear
placeholder="123456"
v-model:value="form.password"
/>
</a-form-item>
<a-form-item label="描述" name="comments">
<a-textarea
:rows="4"
:maxlength="200"
placeholder="备注信息"
v-model:value="form.comments"
/>
</a-form-item>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { FormInstance } from 'ant-design-vue/es/form';
import { ShopOrder } from '@/api/shop/shopOrder/model';
import useFormData from '@/utils/use-form-data';
import { addOrder, updateOrder } from '@/api/shop/shopOrder';
import { message } from 'ant-design-vue/es';
import {DictData} from "@/api/system/dict-data/model";
// 是否是修改
const isUpdate = ref(false);
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
appId: number | null;
// 修改回显的数据
data?: ShopOrder | null;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
// 提交状态
const loading = ref(false);
// 是否显示最大化切换按钮
const maxable = ref(true);
const formRef = ref<FormInstance | null>(null);
const { form, resetFields, assignFields } = useFormData<ShopOrder>({
orderNo: undefined,
type: 0,
price: undefined,
payType: undefined,
comments: undefined
});
// 表单验证规则
const rules = reactive({
name: [
{
required: true,
message: '请输入名称'
}
],
domain: [
{
required: true,
message: '请输入域名'
}
]
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
const chooseType = (item: DictData) => {
form.name = item.dictDataCode;
form.domain = item.comments;
};
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const data = {
...form,
appId: props.appId
};
const saveOrUpdate = isUpdate.value ? updateOrder : addOrder;
saveOrUpdate(data)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
watch(
() => props.visible,
(visible) => {
if (visible) {
if (props.data) {
assignFields(props.data);
isUpdate.value = true;
} else {
isUpdate.value = false;
}
} else {
resetFields();
}
}
);
</script>

View File

@@ -0,0 +1,13 @@
<template>
<a-button type="primary" @click="add">创建订单</a-button>
</template>
<script lang="ts" setup>
const emit = defineEmits<{
(e: 'add'): void;
}>();
const add = () => {
emit('add');
};
</script>

View File

@@ -0,0 +1,271 @@
<template>
<div class="app-task">
<!-- 表格 -->
<ele-pro-table
ref="tableRef"
row-key="orderId"
:columns="columns"
:datasource="datasource"
:customRow="customRow"
:scroll="{ x: 2400 }"
tool-class="ele-toolbar-form"
class="sys-org-table"
>
<template #toolbar>
<ShopOrderOrderSearch @add="openEdit" @remove="removeBatch"/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'type'">
<a-tag v-if="record.type === 0">续费</a-tag>
<a-tag v-if="record.type === 1" color="orange">新购</a-tag>
</template>
<template v-if="column.key === 'domain'">
<a :href="`http://${record.domain}`" target="_blank">{{ record.domain }}</a>
</template>
<template v-if="column.key === 'password'">
<a-tooltip title="点击查看密码" class="cursor-pointer" @click="showPassword">{{password ? decrypt(record.password) : '******'}}</a-tooltip>
</template>
<template v-if="column.key === 'orderStatus'">
<a-tag color="">处理中</a-tag>
<a-tag color="">已完成</a-tag>
</template>
<template v-if="column.key === 'action'">
<a @click="openEdit(record)">编辑</a>
<a-divider type="vertical"/>
<a-popconfirm title="确定要删除此记录吗?" @confirm="remove(record)">
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</template>
</template>
</ele-pro-table>
<!-- 编辑弹窗 -->
<ShopOrderOrderEdit
v-model:visible="showEdit"
:appId="appId"
:data="current"
@done="reload"
/>
</div>
</template>
<script lang="ts" setup>
import {createVNode, ref, watch} from 'vue';
import {message, Modal} from 'ant-design-vue';
import {ExclamationCircleOutlined} from '@ant-design/icons-vue';
import type {EleProTable} from 'ele-admin-pro';
import type {DatasourceFunction} from 'ele-admin-pro/es/ele-pro-table/types';
import ShopOrderOrderSearch from './app-order-search.vue';
import ShopOrderOrderEdit from './app-order-edit.vue';
import { formatNumber } from 'ele-admin-pro/es';
import {ShopOrder, OrderParam} from '@/api/shop/shopOrder/model';
import {
pageOrder,
removeOrder,
removeBatchOrder
} from '@/api/shop/shopOrder';
import {decrypt} from "@/utils/common";
const props = defineProps<{
appId: any;
data: ShopOrder;
}>();
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
const selection = ref<ShopOrder[]>([]);
// 当前编辑数据
const current = ref<ShopOrder | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 查看密码
const password = ref(false);
// 表格数据源
const datasource: DatasourceFunction = ({page, limit, where, orders}) => {
// 搜索条件
// where.appId = props.appId;
return pageOrder({
...where,
...orders,
page,
limit
});
};
// 表格列配置
const columns = ref<any[]>([
{
title: '订单号',
dataIndex: 'orderNo',
key: 'orderNo',
width: 220,
},
{
title: '类型',
dataIndex: 'type',
key: 'type',
align: 'center',
width: 90
},
{
title: '金额',
dataIndex: 'money',
key: 'money',
align: 'center',
width: 280,
customRender: ({text}) => `${text}`
},
{
title: '支付方式',
dataIndex: 'account',
align: 'center',
width: 280
},
{
title: '订单状态',
dataIndex: 'orderStatus',
key: 'orderStatus',
width: 120,
align: 'center'
},
{
title: '备注',
dataIndex: 'comments',
key: 'comments',
ellipsis: true
},
{
title: '减少金额',
dataIndex: 'orderNo',
key: 'orderNo',
align: 'center',
customRender: ({ text }) => `${formatNumber(text)}`
},
{
title: "实付金额",
dataIndex: "payPrice",
key: "payPrice",
align: "center",
sorter: true,
customRender: ({ text }) => `${formatNumber(text)}`
},
{
title: '支付方式',
dataIndex: 'payType',
key: 'payType',
align: 'center'
},
{
title: '付款状态',
dataIndex: 'payStatus',
key: 'payStatus',
align: 'center'
},
{
title: '操作人',
dataIndex: 'nickname',
key: 'nickname',
align: 'center',
width: 280
},
{
title: '操作',
key: 'action',
width: 180,
fixed: 'right',
align: 'center',
hideInSetting: true
}
]);
const showPassword = () => {
password.value = !password.value;
}
/* 打开编辑弹窗 */
const openEdit = (row?: ShopOrder) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 搜索 */
const reload = (where?: OrderParam) => {
selection.value = [];
tableRef?.value?.reload({where: where});
};
/* 删除单个 */
const remove = (row: ShopOrder) => {
const hide = message.loading('请求中..', 0);
removeOrder(row.orderId)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 批量删除 */
const removeBatch = () => {
if (!selection.value?.length) {
message.error('请至少选择一条数据');
return;
}
if (selection.value?.length) {
Modal.confirm({
title: '提示',
content: '确定要删除选中的记录吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = message.loading('请求中..', 0);
removeBatchOrder(selection.value.map((d) => d.orderId))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
}
};
/* 自定义行属性 */
const customRow = (record: ShopOrder) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record)
}
};
};
watch(
() => props.appId,
(appId) => {
if (appId) {
reload();
}
},
{immediate: true}
);
</script>
<script lang="ts">
export default {
name: 'ProjectOrderIndex'
};
</script>

View File

@@ -0,0 +1,240 @@
<!-- 用户编辑弹窗 -->
<template>
<ele-modal
width="80%"
:visible="visible"
:maskClosable="false"
:maxable="maxable"
:title="isUpdate ? '编辑' : '添加'"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
@ok="save"
>
<a-form
ref="formRef"
:model="form"
:label-col="{ md: { span: 7 }, sm: { span: 4 }, xs: { span: 24 } }"
:wrapper-col="{ md: { span: 17 }, sm: { span: 20 }, xs: { span: 24 } }"
>
<div class="content">
<SelectFile
:placeholder="`请选择图片`"
:limit="9"
:data="images"
@done="chooseFile"
@del="onDeleteItem"
/>
<!-- <ele-image-upload-->
<!-- v-model:value="images"-->
<!-- :limit="6"-->
<!-- :drag="true"-->
<!-- :item-style="{ width: '150px', height: '267px' }"-->
<!-- :upload-handler="uploadHandler"-->
<!-- @upload="onUpload"-->
<!-- />-->
<small class="ele-text-placeholder">
请上传应用截图(最多9张)建议宽度300*533像素小于2M/
</small>
</div>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { Form, message } from 'ant-design-vue';
import { assignObject } from 'ele-admin-pro';
import { addApp, updateApp } from '@/api/oa/app';
import type { App } from '@/api/oa/app/model';
import { FormInstance } from 'ant-design-vue/es/form';
import { ItemType } from 'ele-admin-pro/es/ele-image-upload/types';
import { uploadFileLocal } from '@/api/system/file';
import { TOKEN_STORE_NAME } from '@/config/setting';
import { FileRecord } from '@/api/system/file/model';
// 是否是修改
const isUpdate = ref(false);
const useForm = Form.useForm;
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: App | null;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
// 提交状态
const loading = ref(false);
// 是否显示最大化切换按钮
const maxable = ref(true);
// 当期时间
const logo = ref<ItemType[]>([]);
const appQrcode = ref<ItemType[]>([]);
const images = ref<ItemType[]>([]);
const content = ref('');
const requirement = ref('');
const token = localStorage.getItem(TOKEN_STORE_NAME);
const formRef = ref<FormInstance | null>(null);
// 用户信息
const form = reactive<App>({
// 应用id
appId: undefined,
// 应用截图
images: ''
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
const { resetFields } = useForm(form);
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const formData = {
...form,
content: content.value,
requirement: requirement.value,
search: form.search ? 1 : 0,
images: JSON.stringify(images.value)
// appTypeMultiple: JSON.stringify(form.appTypeMultiple)
};
const saveOrUpdate = isUpdate.value ? updateApp : addApp;
saveOrUpdate(formData)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
/* 上传事件 */
const uploadHandler = (file: File) => {
const item: ItemType = {
file,
uid: (file as any).uid,
name: file.name
};
if (!file.type.startsWith('image')) {
message.error('只能选择图片');
return;
}
if (file.size / 1024 / 1024 > 2) {
message.error('大小不能超过 2MB');
return;
}
onUpload(item);
};
// 上传文件
const onUpload = (item) => {
const { file } = item;
uploadFileLocal(file, props.data?.appId)
.then((data) => {
images.value.push({
uid: data.id,
url: data.url,
status: 'done'
});
})
.catch((e) => {
message.error(e.message);
});
};
const chooseFile = (data: FileRecord) => {
images.value.push({
uid: data.id,
url: data.url,
status: 'done'
});
};
const onDeleteItem = (index: number) => {
images.value.splice(index, 1);
};
const reload = () => {
loading.value = true;
};
watch(
() => props.visible,
(visible) => {
if (visible) {
content.value = '';
requirement.value = '';
logo.value = [];
images.value = [];
appQrcode.value = [];
if (props.data) {
assignObject(form, props.data);
if (props.data.appIcon) {
logo.value.push({
uid: 0,
url: props.data.appIcon,
status: 'done'
});
}
if (props.data.appQrcode) {
appQrcode.value.push({
uid: 0,
url: props.data.appQrcode,
status: 'done'
});
}
if (props.data.companyId) {
console.log(props.data);
}
if (props.data.images) {
const arr = JSON.parse(props.data.images);
arr.map((d, i) => {
images.value.push({
uid: d.uid,
url: d.url,
status: 'done'
});
});
}
if (props.data.search) {
form.search = props.data.search == 1 ? true : 0;
}
if (props.data.content) {
content.value = props.data.content;
}
if (props.data.requirement) {
requirement.value = props.data.requirement;
}
isUpdate.value = true;
reload();
} else {
isUpdate.value = false;
}
} else {
resetFields();
}
}
);
</script>

View File

@@ -0,0 +1,91 @@
<template>
<div class="content">
<SelectFile
:placeholder="`请选择图片`"
:data="images"
:limit="9"
:width="width"
:height="height"
@done="chooseFile"
@del="onDeleteItem"
/>
</div>
</template>
<script lang="ts" setup>
import {Project} from '@/api/project/project/model';
import {ref, watch} from 'vue';
import {message} from 'ant-design-vue';
import {FileRecord} from '@/api/system/file/model';
import {ItemType} from 'ele-admin-pro/es/ele-image-upload/types';
import {updateProject} from "@/api/project/project";
const props = defineProps<{
appId: any;
data: Project;
images: any[];
}>();
const emit = defineEmits<{
(e: 'done'): void;
}>();
const images = ref<ItemType[]>([]);
const width = ref<number>(100)
const height = ref<number>(100)
const chooseFile = (data: FileRecord) => {
console.log(data)
images.value.push({
uid: data.id,
url: data.url,
status: 'done'
});
message.success('保存成功');
update()
};
const onDeleteItem = (index: number) => {
images.value.splice(index, 1);
message.success('删除成功');
update()
};
const update = () => {
updateProject({appId: props.appId, images: JSON.stringify(images.value)}).then(() => {})
}
// 是否显示编辑弹窗
const showEdit = ref(false);
/* 打开编辑弹窗 */
const openEdit = () => {
showEdit.value = true;
};
watch(
() => props.appId,
(appId) => {
if (appId) {
images.value = [];
if (props.data.appType == 'all') {
width.value = 160
height.value = 284
}
console.log(props.appId,'222')
if (props.data.images) {
const arr = JSON.parse(props.data.images);
console.log(arr,'ssss')
arr.map((d, i) => {
images.value.push({
uid: d.uid,
url: d.url,
status: 'done'
});
});
}
}
},
{immediate: true}
);
</script>

View File

@@ -0,0 +1,228 @@
<!-- 用户编辑弹窗 -->
<template>
<ele-modal
width="80%"
:visible="visible"
:maskClosable="false"
:maxable="maxable"
:title="isUpdate ? '协同文档' : '协同文档'"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
@ok="save"
>
<a-form
ref="formRef"
:model="form"
:label-col="{ md: { span: 7 }, sm: { span: 4 }, xs: { span: 24 } }"
:wrapper-col="{ md: { span: 17 }, sm: { span: 20 }, xs: { span: 24 } }"
>
<!-- 编辑器 -->
<MdEditor v-model="requirement" @paste="onPaste" placeholder="类似腾讯文档的功能,支持多人编辑" />
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { Form, message } from 'ant-design-vue';
import {assignObject} from "ele-admin-pro";
import { addProject, updateProject } from '@/api/project/project';
import type { Project } from '@/api/project/project/model';
import { useThemeStore } from '@/store/modules/theme';
import { storeToRefs } from 'pinia';
import {MdEditor} from 'md-editor-v3';
import 'md-editor-v3/lib/style.css';
import 'md-editor-v3/lib/preview.css';
import {FormInstance} from 'ant-design-vue/es/form';
import { ItemType } from 'ele-admin-pro/es/ele-image-upload/types';
import { uploadFile } from "@/api/system/file";
import { TOKEN_STORE_NAME } from "@/config/setting";
// 是否是修改
const isUpdate = ref(false);
const useForm = Form.useForm;
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: Project | null;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
// 提交状态
const loading = ref(false);
// 是否显示最大化切换按钮
const maxable = ref(true);
const images = ref<ItemType[]>([]);
const content = ref('');
const requirement = ref('');
const token = localStorage.getItem(TOKEN_STORE_NAME);
const formRef = ref<FormInstance | null>(null);
// 用户信息
const form = reactive<Project>({
// 应用id
appId: undefined,
// 项目需求
requirement: ''
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
const { resetFields, validate, validateInfos } = useForm(form);
const config = ref({
height: 500,
images_upload_handler: (blobInfo, success, error) => {
const file = blobInfo.blob();
// 使用 axios 上传,实际开发这段建议写在 api 中再调用 api
const formData = new FormData();
formData.append('file', file, file.name);
uploadFile(<File>file)
.then((result) => {
if (result.length) {
if (file.size / 1024 / 1024 > 2) {
error('图片大小不能超过 2MB');
}
success(result.url);
} else {
error('上传失败');
}
})
.catch((e) => {
message.error(e.message);
});
},
});
/* 粘贴图片上传服务器并插入编辑器 */
const onPaste = (e) => {
const items = (e.clipboardData || e.originalEvent.clipboardData).items;
let hasFile = false;
for (let i = 0; i < items.length; i++) {
if (items[i].type.indexOf('image') !== -1) {
let file = items[i].getAsFile();
const item: ItemType = {
file,
uid: (file as any).lastModified,
name: file.name
};
uploadFile(<File>item.file)
.then((result) => {
const addPath = '!['+result.name+']('+ result.url+')\n\r';
requirement.value = requirement.value + addPath
})
.catch((e) => {
message.error(e.message);
});
hasFile = true;
}
}
if (hasFile) {
e.preventDefault();
}
}
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const formData = {
...form,
content: content.value,
requirement: requirement.value,
images: JSON.stringify(images.value)
};
const saveOrUpdate = isUpdate.value ? updateProject : addProject;
saveOrUpdate(formData)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
/* 上传事件 */
const uploadHandler = (file: File) => {
const item: ItemType = {
file,
uid: (file as any).uid,
name: file.name
};
if (!file.type.startsWith('image')) {
message.error('只能选择图片');
return;
}
if (file.size / 1024 / 1024 > 2) {
message.error('大小不能超过 2MB');
return;
}
onUpload(item);
};
// 上传文件
const onUpload = (item) => {
const { file } = item;
uploadFile(file)
.then((data) => {
images.value.push({
uid: data.id,
url: data.url,
status: 'done'
});
})
.catch((e) => {
message.error(e.message);
});
};
watch(
() => props.visible,
(visible) => {
if (visible) {
content.value = '';
requirement.value = '';
if (props.data) {
assignObject(form, props.data);
if (props.data.requirement){
requirement.value = props.data.requirement;
}
if (props.data.content) {
content.value = props.data.content;
}
isUpdate.value = true;
} else {
isUpdate.value = false;
}
} else {
resetFields();
}
}
);
</script>

View File

@@ -0,0 +1,67 @@
<template>
<div class="relative min-h-[300px]">
<a-space style="margin-bottom: 20px">
<a-button type="primary" @click="openEdit">编辑</a-button>
</a-space>
<MdPreview v-if="data.requirement" class="max-w-5xl" :modelValue="data.requirement" />
<a-empty
v-else
image="https://gw.alipayobjects.com/mdn/miniapp_social/afts/img/A*pevERLJC9v0AAAAAAAAAAABjAQAAAQ/original"
:image-style="{
height: '60px'
}"
>
<template #description>
<span class="ele-text-placeholder">类似腾讯文档的功能支持多人编辑</span>
</template>
</a-empty>
</div>
<!-- 编辑弹窗 -->
<AppProfileEdit v-model:visible="showEdit" :data="data" @done="reload" />
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import { MdPreview } from 'md-editor-v3';
import 'md-editor-v3/lib/preview.css';
import gfm from '@bytemd/plugin-gfm';
import zh_HansGfm from '@bytemd/plugin-gfm/locales/zh_Hans.json';
import highlight from '@bytemd/plugin-highlight';
import 'bytemd/dist/index.min.css';
import 'github-markdown-css/github-markdown-light.css';
import 'github-markdown-css/github-markdown-light.css';
import AppProfileEdit from './app-profile-edit.vue';
defineProps<{
appId: number;
data: any;
}>();
const emit = defineEmits<{
(e: 'done'): void;
}>();
// 是否显示编辑弹窗
const showEdit = ref(false);
/* 打开编辑弹窗 */
const openEdit = () => {
showEdit.value = true;
};
const reload = () => {
emit('done');
};
// 插件
const plugins = ref([
gfm({
locale: zh_HansGfm
}),
highlight()
]);
</script>
<style lang="less">
.app-profile * {
max-width: 1000px;
}
</style>

View File

@@ -0,0 +1,210 @@
<!-- 角色编辑弹窗 -->
<template>
<ele-modal
:width="550"
:visible="visible"
:confirm-loading="loading"
title="重置AppSecret"
:body-style="{ paddingBottom: '8px' }"
@update:visible="updateVisible"
ok-text="重置"
cancel-text="关闭"
@ok="save"
>
<a-form
ref="formRef"
:model="form"
:label-col="styleResponsive ? { md: 5, sm: 5, xs: 24 } : { flex: '90px' }"
:wrapper-col="
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
"
>
<template v-if="!appSecret">
<a-form-item label="手机号码" name="phone">
<a-input
:maxlength="20"
:disabled="true"
placeholder="请输入短信验证码"
v-model:value="form.phone"
/>
</a-form-item>
<a-form-item label="短信验证码" name="code">
<div class="login-input-group">
<a-input
placeholder="请输入验证码"
v-model:value="form.code"
:maxlength="6"
allow-clear
/>
<a-button
class="login-captcha"
:disabled="!!countdownTime"
@click="openImgCodeModal"
>
<span v-if="!countdownTime" @click="sendCode">发送验证码</span>
<span v-else>已发送 {{ countdownTime }} s</span>
</a-button>
</div>
</a-form-item>
</template>
<template v-else>
<a-form-item label="AppID" name="appId">
<a-typography-text copyable code class="ele-text-secondary">{{
appId
}}</a-typography-text>
</a-form-item>
<a-form-item label="AppSecret" name="appSecret">
<a-typography-text copyable code class="ele-text-secondary">{{
appSecret
}}</a-typography-text>
</a-form-item>
</template>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue';
import { message } from 'ant-design-vue/es';
import type { FormInstance } from 'ant-design-vue/es/form';
import { storeToRefs } from 'pinia';
import { useThemeStore } from '@/store/modules/theme';
import useFormData from '@/utils/use-form-data';
import { User } from '@/api/system/user/model';
import { sendSmsCaptcha } from '@/api/passport/login';
import { updateAppSecret } from '@/api/oa/app';
import { createCode, encrypt } from '@/utils/common';
import { pageAppUser } from '@/api/oa/app/user';
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
appId?: number;
}>();
const formRef = ref<FormInstance | null>(null);
// 验证码倒计时时间
const countdownTime = ref(0);
// 验证码倒计时定时器
let countdownTimer: number | null = null;
const loading = ref(false);
const appSecret = ref('');
// 表单数据
const { form, resetFields } = useFormData<User>({
phone: '',
userId: undefined
});
/* 显示发送短信验证码弹窗 */
const openImgCodeModal = () => {
if (!form.phone) {
message.error('请输入手机号码');
return;
}
};
/* 发送短信验证码 */
const sendCode = () => {
sendSmsCaptcha({ phone: form.phone }).then(() => {
message.success('短信验证码发送成功, 请注意查收!');
countdownTime.value = 60;
// 开始对按钮进行倒计时
countdownTimer = window.setInterval(() => {
if (countdownTime.value <= 1) {
countdownTimer && clearInterval(countdownTimer);
countdownTimer = null;
}
countdownTime.value--;
}, 1000);
});
};
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
if (appSecret.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
updateAppSecret({
phone: form.phone,
appCode: form.code,
appId: props.appId,
appSecret: encrypt(createCode())
})
.then((res) => {
loading.value = false;
message.success(res.message);
appSecret.value = String(res.data);
// updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
watch(
() => props.visible,
(visible) => {
if (visible) {
pageAppUser({ appId: props.appId, role: 30 }).then((res) => {
if (res?.list) {
form.phone = res.list[0].phone;
}
});
console.log(props.appId);
} else {
resetFields();
formRef.value?.clearValidate();
}
}
);
</script>
<style lang="less" scoped>
/* 验证码 */
.login-input-group {
display: flex;
align-items: center;
:deep(.ant-input-affix-wrapper) {
flex: 1;
}
.login-captcha {
width: 102px;
height: 33px;
margin-left: 10px;
padding: 0;
& > img {
width: 100%;
height: 100%;
}
}
}
</style>

View File

@@ -0,0 +1,34 @@
<template>
<a-button
@click="openNew(`/oa/task/add?appid=${data.appId}&appName=${data.appName}`)"
>提交工单</a-button
>
</template>
<script lang="ts" setup>
import { TaskParam } from '@/api/oa/task/model';
import useSearch from '@/utils/use-search';
import { openNew } from '@/utils/common';
import { App } from '@/api/oa/app/model';
defineProps<{
data: App;
}>();
const emit = defineEmits<{
(e: 'search', where: TaskParam): void;
(e: 'remove'): void;
}>();
// 表单数据
const { where } = useSearch<TaskParam>({
keywords: '',
status: undefined,
commander: undefined
});
/* 搜索 */
const search = () => {
emit('search', where);
};
</script>

View File

@@ -0,0 +1,299 @@
<template>
<div class="app-task">
<!-- 表格 -->
<ele-pro-table
ref="tableRef"
row-key="taskId"
:columns="columns"
:datasource="datasource"
:customRow="customRow"
tool-class="ele-toolbar-form"
class="sys-org-table"
>
<template #toolbar>
<AppTaskSearch :data="data" @search="reload" @remove="removeBatch" />
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'content'">
<div class="user-box">
<div class="user-info">
<a-badge
:dot="!record.isRead && record.userId != userStore.info?.userId"
>
<a-avatar :src="record.avatar" size="large" />
</a-badge>
</div>
<div class="content" style="display: flex; flex-direction: column">
<div class="nickname">
<a-typography-text strong>
{{ `${record.nickname}` }}
</a-typography-text>
<span class="ele-text-placeholder" style="padding-left: 10px">{{
timeAgo(record.createTime)
}}</span>
</div>
<a
class="ele-text-heading"
@click="openNew('/oa/task/detail/' + record.taskId)"
>
{{ `工单标题:${record.name}` }}
</a>
<div class="ele-text-placeholder">{{
record.appId > 0 ? `项目名称:【${record.appName}` : ''
}}</div>
<div
class="ele-text-secondary"
style="display: flex; align-items: center"
>
<a-avatar :size="18" :src="record.lastAvatar" />
<div class="content" style="padding-left: 8px">
{{ record.content }}
</div>
<span class="ele-text-placeholder" style="padding-left: 10px">{{
timeAgo(record.updateTime)
}}</span>
</div>
</div>
<div class="last-time ele-text-info"> </div>
</div>
</template>
<template v-else-if="column.key === 'status'">
<a-tag v-if="record.progress === TOBEARRANGED" color="red"
>待安排</a-tag
>
<a-tag v-if="record.progress === PENDING" color="orange"
>待处理</a-tag
>
<a-tag v-if="record.progress === PROCESSING" color="purple"
>处理中</a-tag
>
<a-tag v-if="record.progress === TOBECONFIRMED" color="cyan"
>待评价</a-tag
>
<a-tag v-if="record.progress === COMPLETED" color="green"
>已完成</a-tag
>
<a-tag v-if="record.progress === CLOSED">已关闭</a-tag>
<div class="ele-text-danger" v-if="record.overdueDays">
已逾期{{ record.overdueDays }}
</div>
</template>
<template v-else-if="column.key === 'progress'">
<a-progress :percent="record.progress * 2" :steps="5" />
</template>
<template v-if="column.key === 'action'">
<a-space>
<a @click="onDetail(record)">查看</a>
<a-divider type="vertical" />
<a-popconfirm
title="确定要删除此记录吗?"
@confirm="remove(record)"
>
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</ele-pro-table>
</div>
</template>
<script lang="ts" setup>
import { createVNode, ref, watch } from 'vue';
import { message, Modal } from 'ant-design-vue';
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
import type { EleProTable } from 'ele-admin-pro';
import type { DatasourceFunction } from 'ele-admin-pro/es/ele-pro-table/types';
import { timeAgo } from 'ele-admin-pro';
import { pageTask, removeTask, removeBatchTask } from '@/api/oa/task';
import type { Task, TaskParam } from '@/api/oa/task/model';
import { useUserStore } from '@/store/modules/user';
import AppTaskSearch from './app-task-search.vue';
import {
CLOSED,
COMPLETED,
PENDING,
PROCESSING,
TOBEARRANGED,
TOBECONFIRMED
} from '@/api/oa/task/model/progress';
import { hasRole } from '@/utils/permission';
import { openNew } from '@/utils/common';
import { App } from '@/api/oa/app/model';
const props = defineProps<{
appId: number | undefined;
data: App;
}>();
const userStore = useUserStore();
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
const selection = ref<any[]>();
const status = ref<number>();
// 表格数据源
const datasource: DatasourceFunction = ({ page, limit, where, orders }) => {
// 搜索条件
// 工单发起人
if (hasRole('promoter') || hasRole('user')) {
where.commander = undefined;
where.userId = userStore.info?.userId;
}
// 管理人员
if (hasRole('superAdmin') || hasRole('admin')) {
where.commander = undefined;
}
// 工单受理人员
if (hasRole('commander')) {
where.commander = userStore.info?.userId;
}
where.appId = props.appId;
return pageTask({
...where,
...orders,
page,
limit
});
};
// 表格列配置
const columns = ref<any[]>([
{
title: '工单号',
dataIndex: 'taskId',
align: 'center',
width: 100
},
{
title: '工单类型',
dataIndex: 'taskType',
width: 100
},
{
title: '工单信息',
dataIndex: 'content',
key: 'content',
ellipsis: true
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
align: 'center',
width: 120
}
]);
/* 搜索 */
const reload = (where?: TaskParam) => {
status.value = where?.status;
selection.value = [];
tableRef?.value?.reload({ where: where });
};
const onDetail = (record?: Task) => {
window.location.href = 'detail?id=' + record?.taskId;
};
/* 删除单个 */
const remove = (row: Task) => {
const hide = message.loading('请求中..', 0);
removeTask(row.taskId)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 批量删除 */
const removeBatch = () => {
if (!selection.value?.length) {
message.error('请至少选择一条数据');
return;
}
if (selection.value?.length) {
Modal.confirm({
title: '提示',
content: '确定要删除选中的记录吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = message.loading('请求中..', 0);
removeBatchTask(selection.value.map((d) => d.taskId))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
}
};
// 是否展现多选按钮及批量删除按钮
if (hasRole('superAdmin') || hasRole('admin')) {
selection.value = [];
}
/* 自定义行属性 */
const customRow = (record: Task) => {
return {
// 行双击事件
onDblclick: () => {
window.open('/oa/task/detail/' + record.taskId);
}
};
};
watch(
() => props.appId,
(appId) => {
if (appId) {
reload({ appId });
}
},
{ immediate: true }
);
</script>
<script lang="ts">
export default {
name: 'AppTaskIndex'
};
</script>
<style lang="less" scoped>
.user-box {
display: flex;
align-items: center;
.user-info {
display: flex;
flex-direction: column;
align-items: start;
margin-right: 7px;
}
.last-time {
margin-left: 12px;
}
.content {
.text {
max-width: 90%;
}
}
}
.nickname {
.ele-text-heading {
font-weight: bold;
}
}
</style>

View File

@@ -0,0 +1,193 @@
<!-- 用户编辑弹窗 -->
<template>
<ele-modal
:width="1000"
:visible="visible"
:maskClosable="false"
:maxable="maxable"
:title="isUpdate ? '编辑域名' : '添加域名'"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
@ok="save"
>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="{ md: { span: 2 }, sm: { span: 4 }, xs: { span: 24 } }"
:wrapper-col="{ md: { span: 21 }, sm: { span: 22 }, xs: { span: 24 } }"
>
<!-- <a-form-item label="客户端" name="name">-->
<!-- <a-input allow-clear placeholder="PC端" v-model:value="form.name" />-->
<!-- </a-form-item>-->
<!-- <a-form-item label="类型" name="name">-->
<!-- <div class="w-full flex justify-between">-->
<!-- <DictSelect-->
<!-- dict-code="ProjectUrlType"-->
<!-- :width="200"-->
<!-- :show-search="true"-->
<!-- placeholder="请选择类型"-->
<!-- v-model:value="form.name"-->
<!-- @done="chooseType"-->
<!-- />-->
<!-- </div>-->
<!-- </a-form-item>-->
<a-form-item label="类型" name="name">
<div class="w-full flex justify-between">
<a-input
allow-clear
:maxlength="20"
placeholder="请输入链接名称"
v-model:value="form.name"
/>
</div>
</a-form-item>
<a-form-item label="访问域名" name="domain">
<a-input
v-model:value="form.domain"
placeholder="https://site.websoft.top"
/>
</a-form-item>
<a-form-item label="账号" name="account">
<a-input allow-clear placeholder="demo" v-model:value="form.account" />
</a-form-item>
<a-form-item label="密码" name="password">
<a-input-password
allow-clear
placeholder="123456"
v-model:value="form.password"
/>
</a-form-item>
<a-form-item label="描述" name="comments">
<a-textarea
:rows="4"
:maxlength="200"
placeholder="备注信息"
v-model:value="form.comments"
/>
</a-form-item>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { FormInstance } from 'ant-design-vue/es/form';
import { ProjectUrl } from '@/api/project/projectUrl/model';
import useFormData from '@/utils/use-form-data';
import { addProjectUrl, updateProjectUrl } from '@/api/project/projectUrl';
import { message } from 'ant-design-vue/es';
import {decrypt, encrypt} from "@/utils/common";
import DictSelect from "@/components/DictSelect/index.vue";
import {DictData} from "@/api/system/dict-data/model";
// 是否是修改
const isUpdate = ref(false);
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
appId: number | null;
// 修改回显的数据
data?: ProjectUrl | null;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
// 提交状态
const loading = ref(false);
// 是否显示最大化切换按钮
const maxable = ref(true);
const formRef = ref<FormInstance | null>(null);
const { form, resetFields, assignFields } = useFormData<ProjectUrl>({
appUrlId: undefined,
appId: undefined,
name: undefined,
domain: '',
account: '',
password: '',
comments: '',
status: 0,
sortNumber: 0
});
// 表单验证规则
const rules = reactive({
name: [
{
required: true,
message: '请输入名称'
}
],
domain: [
{
required: true,
message: '请输入域名'
}
]
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
const chooseType = (item: DictData) => {
form.name = item.dictDataCode;
form.domain = item.comments;
};
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const data = {
...form,
appId: props.appId,
password: encrypt(form.password)
};
const saveOrUpdate = isUpdate.value ? updateProjectUrl : addProjectUrl;
saveOrUpdate(data)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
watch(
() => props.visible,
(visible) => {
if (visible) {
if (props.data) {
assignFields(props.data);
if(props.data.password){
form.password = decrypt(props.data.password)
}
isUpdate.value = true;
} else {
isUpdate.value = false;
}
} else {
resetFields();
}
}
);
</script>

View File

@@ -0,0 +1,13 @@
<template>
<a-button type="primary" @click="add">添加</a-button>
</template>
<script lang="ts" setup>
const emit = defineEmits<{
(e: 'add'): void;
}>();
const add = () => {
emit('add');
};
</script>

View File

@@ -0,0 +1,227 @@
<template>
<div class="app-task">
<!-- 表格 -->
<ele-pro-table
ref="tableRef"
row-key="appUrlId"
:columns="columns"
:datasource="datasource"
:customRow="customRow"
tool-class="ele-toolbar-form"
class="sys-org-table"
>
<template #toolbar>
<ProjectUrlSearch @add="openEdit" @remove="removeBatch"/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'domain'">
<a :href="`http://${record.domain}`" target="_blank">{{ record.domain }}</a>
</template>
<template v-if="column.key === 'password'">
<a-tooltip title="点击查看密码" class="cursor-pointer" v-if="record.openPassword">{{ decrypt(record.password) }}</a-tooltip>
<a-tooltip title="点击查看密码" class="cursor-pointer" v-else @click="showPassword(record)">******</a-tooltip>
</template>
<template v-if="column.key === 'action'">
<a @click="openEdit(record)">编辑</a>
<a-divider type="vertical"/>
<a-popconfirm title="确定要删除此记录吗?" @confirm="remove(record)">
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</template>
</template>
</ele-pro-table>
<!-- 编辑弹窗 -->
<ProjectUrlEdit
v-model:visible="showEdit"
:app-id="data.appId"
:data="current"
@done="reload"
/>
</div>
</template>
<script lang="ts" setup>
import {createVNode, ref, watch} from 'vue';
import {message, Modal} from 'ant-design-vue';
import {ExclamationCircleOutlined} from '@ant-design/icons-vue';
import type {EleProTable} from 'ele-admin-pro';
import type {DatasourceFunction} from 'ele-admin-pro/es/ele-pro-table/types';
import ProjectUrlSearch from './app-url-search.vue';
import {Project} from '@/api/project/project/model';
import ProjectUrlEdit from './app-url-edit.vue';
import {ProjectUrl, ProjectUrlParam} from '@/api/project/projectUrl/model';
import {
pageProjectUrl,
removeProjectUrl,
removeBatchProjectUrl,
updateProjectUrl
} from '@/api/project/projectUrl';
import { decrypt } from '@/utils/common';
const props = defineProps<{
appId: any;
data: Project;
}>();
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
const selection = ref<ProjectUrl[]>([]);
// 当前编辑数据
const current = ref<ProjectUrl | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 查看密码
const appUrlId = ref();
// 表格数据源
const datasource: DatasourceFunction = ({page, limit, where, orders}) => {
// 搜索条件
where.appId = props.appId;
return pageProjectUrl({
...where,
...orders,
page,
limit
});
};
// 表格列配置
const columns = ref<any[]>([
{
title: '类型',
dataIndex: 'name',
width: 180
},
{
title: '访问域名',
dataIndex: 'domain',
key: 'domain',
width: 280
},
{
title: '账号',
dataIndex: 'account',
width: 280
},
{
title: '密码',
dataIndex: 'password',
key: 'password',
width: 280
},
{
title: '备注',
dataIndex: 'comments',
key: 'comments',
ellipsis: true
},
{
title: '操作',
key: 'action',
width: 180,
align: 'center',
hideInSetting: true
}
]);
const moveUp = (row?: ProjectUrl) => {
updateProjectUrl({
appUrlId: row?.appUrlId,
sortNumber: Number(row?.sortNumber) - 1
}).then((msg) => {
message.success(msg);
reload();
});
};
const showPassword = (item: ProjectUrl) => {
item.openPassword = true;
}
/* 打开编辑弹窗 */
const openEdit = (row?: ProjectUrl) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 搜索 */
const reload = (where?: ProjectUrlParam) => {
selection.value = [];
tableRef?.value?.reload({where: where});
};
/* 删除单个 */
const remove = (row: ProjectUrl) => {
const hide = message.loading('请求中..', 0);
removeProjectUrl(row.appUrlId)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 批量删除 */
const removeBatch = () => {
if (!selection.value?.length) {
message.error('请至少选择一条数据');
return;
}
if (selection.value?.length) {
Modal.confirm({
title: '提示',
content: '确定要删除选中的记录吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = message.loading('请求中..', 0);
removeBatchProjectUrl(selection.value.map((d) => d.appUrlId))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
}
};
/* 自定义行属性 */
const customRow = (record: Project) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record)
}
};
};
watch(
() => props.appId,
(appId) => {
if (appId) {
reload();
}
},
{immediate: true}
);
</script>
<script lang="ts">
export default {
name: 'ProjectUrlIndex'
};
</script>

View File

@@ -0,0 +1,139 @@
<template>
<a-space style="margin-bottom: 20px">
<SelectStaff
v-if="hasRole('superAdmin') || hasRole('admin')"
:placeholder="`添加成员`"
@done="addProjectDevUser"
/>
<a-button type="primary">邀请添加</a-button>
</a-space>
<div class="content">
<table class="ele-table ele-table-border ele-table-stripe ele-table-medium">
<thead>
<tr>
<th>用户</th>
<th>角色</th>
<th>加入时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="(user, index) in userList" :key="index">
<td>
<a-space>
<a-avatar :src="user.avatar" :size="30" />
<span>{{ user.nickname || user.userId }}</span>
</a-space>
</td>
<td>
<a-tag v-if="user.role === 10">项目成员</a-tag>
<a-tag v-if="user.role === 20" color="orange">项目成员</a-tag>
<a-tag v-if="user.role === 30" color="red" class="ele-text-danger">所有者</a-tag>
</td>
<td>{{ user.createTime }}</td>
<td>
<div v-if="user.role !== 30">
<a
@click="removeUser(user)"
v-if="hasRole('superAdmin') || hasRole('admin')"
>
移除
</a>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</template>
<script lang="ts" setup>
import { User } from '@/api/system/user/model';
import { Project } from '@/api/project/project/model';
import { hasRole } from '@/utils/permission';
import { ProjectUser } from '@/api/project/projectUser/model';
import { addProjectUser, pageProjectUser, removeProjectUser } from '@/api/project/projectUser';
import { message } from 'ant-design-vue';
import { ref, watch } from 'vue';
const props = defineProps<{
appId: any;
data: Project;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
const userList = ref<ProjectUser[]>();
// 添加开发成员
const addProjectDevUser = (data: User) => {
addProjectUser({
userId: data.userId,
appId: props.data?.appId,
nickname: data.nickname,
avatar: data.avatar,
role: 20
})
.then((msg) => {
reload();
message.success(msg);
emit('done');
})
.catch((e) => {
message.error(e.message);
});
};
// 添加体验成员
const addProjectExpUser = (data: User) => {
addProjectUser({
userId: data.userId,
appId: props.data?.appId,
nickname: data.nickname,
avatar: data.avatar,
role: 10
})
.then((msg) => {
reload();
message.success(msg);
emit('done');
})
.catch((e) => {
message.error(e.message);
});
};
// 移除成员
const removeUser = (data: ProjectUser) => {
removeProjectUser(data.appUserId)
.then((msg) => {
reload();
message.success(msg);
emit('done');
})
.catch((e) => {
message.error(e.message);
});
};
const reload = () => {
// 加载项目成员
pageProjectUser({ appId: props.appId }).then((res) => {
if (res?.list) {
userList.value = res.list;
}
});
};
watch(
() => props.appId,
(appId) => {
if (appId) {
reload();
}
},
{ immediate: true }
);
</script>

View File

@@ -0,0 +1,381 @@
<template>
<a-page-header
:title="title"
:style="{ padding: screenWidth > 480 ? '16px 24px' : '0' }"
@back="() => $router.go(-1)"
>
<!-- <template #extra>-->
<!-- <a-button v-if="collection" :icon="h(StarFilled)" @click="onCollection(collection)">已收藏</a-button>-->
<!-- <a-button v-else :icon="h(StarOutlined)" @click="onCollection(collection)">加入收藏</a-button>-->
<!-- </template>-->
<a-spin :spinning="spinning">
<a-card
:bordered="false"
:body-style="{ paddingTop: '5px' }"
v-if="isShow"
>
<a-tabs v-model:active-key="active" @change="onChange">
<a-tab-pane tab="项目动态" key="param">
<AppFieldForm :app-id="appId" :data="form" @done="reload"/>
</a-tab-pane>
<a-tab-pane tab="项目附件" key="annex">
<AppAnnex :app-id="appId" :data="form" @done="reload"/>
</a-tab-pane>
<!-- <a-tab-pane tab="项目图片" key="photo">-->
<!-- <AppPhoto-->
<!-- :appId="form.appId"-->
<!-- :data="form"-->
<!-- :images="images"-->
<!-- @done="reload"-->
<!-- />-->
<!-- </a-tab-pane>-->
<a-tab-pane tab="链接管理" key="domain">
<AppRulForm :app-id="appId" :data="form" @done="reload"/>
</a-tab-pane>
<a-tab-pane tab="协同文档" key="profile">
<AppProfile :app-id="appId" :data="form" @done="reload"/>
</a-tab-pane>
<a-tab-pane
tab="成员管理"
key="users"
v-if="hasRole('superAdmin') || hasRole('admin')"
>
<AppUsers :app-id="appId" :data="form" @done="reload"/>
</a-tab-pane>
<!-- <a-tab-pane tab="项目详情" key="base">-->
<!-- <AppInfo-->
<!-- :data="form"-->
<!-- :appField="appField"-->
<!-- :logo="logo"-->
<!-- :app-qrcode="appQrcode"-->
<!-- @done="reload"-->
<!-- />-->
<!-- </a-tab-pane>-->
<!-- <a-tab-pane tab="项目介绍" key="about">-->
<!-- <AppAbout :appId="form.appId" :data="form" @done="reload"/>-->
<!-- </a-tab-pane>-->
<!-- <a-tab-pane-->
<!-- tab="订单管理"-->
<!-- key="order"-->
<!-- v-if="hasRole('superAdmin') || hasRole('admin')"-->
<!-- >-->
<!-- <AppOrder-->
<!-- :appId="form.appId"-->
<!-- :editStatus="false"-->
<!-- @done="reload"-->
<!-- />-->
<!-- </a-tab-pane>-->
</a-tabs>
</a-card>
<!-- <a-card v-if="!isShow" :bordered="false">-->
<!-- <div style="max-width: 960px; margin: 0 auto">-->
<!-- <a-result-->
<!-- status="error"-->
<!-- title="无查看权限"-->
<!-- sub-title="请先添加为项目成员"-->
<!-- >-->
<!-- <template #extra>-->
<!-- <a-space size="middle">-->
<!-- <a-button type="primary" @click="openUrl('/oa/app/index')"-->
<!-- >返回-->
<!-- </a-button-->
<!-- >-->
<!-- </a-space>-->
<!-- </template>-->
<!-- </a-result>-->
<!-- </div>-->
<!-- </a-card>-->
</a-spin>
</a-page-header>
</template>
<script lang="ts" setup>
import {ref, unref, watch} from 'vue';
import {useRouter} from 'vue-router';
import {useThemeStore} from '@/store/modules/theme';
import {storeToRefs} from 'pinia';
import {message} from 'ant-design-vue';
import AppUsers from './components/app-users.vue';
import AppProfile from './components/app-profile.vue';
// import AppPhoto from './components/app-photo.vue';
import AppAnnex from './components/app-annex.vue';
// import AppOrder from './components/app-order.vue';
import AppFieldForm from './components/app-field.vue';
import AppRulForm from './components/app-url.vue';
import {getProject} from '@/api/project/project';
import useFormData from '@/utils/use-form-data';
import {Project} from '@/api/project/project/model';
import {ItemType} from 'ele-admin-pro/es/ele-image-upload/types';
import {hasRole} from '@/utils/permission';
import {setPageTabTitle} from '@/utils/page-tab-util';
import {pageProjectField} from '@/api/project/projectField';
import {ProjectField} from '@/api/project/projectField/model';
import {pageProjectUser} from '@/api/project/projectUser';
import {uuid} from 'ele-admin-pro';
import {addProjectCollection, getProjectCollection, removeProjectCollection} from "@/api/project/projectCollection";
// import AppInfo from "./components/app-info.vue";
import {getCmsWebsiteAll} from "@/api/cms/cmsWebsite";
import {CmsWebsite} from "@/api/cms/cmsWebsite/model";
// import AppInfo from "@/views/project/projectDetail/components/app-info.vue";
const {currentRoute} = useRouter();
// 当前选项卡
const active = ref('param');
// 是否开启响应式布局
const themeStore = useThemeStore();
const {screenWidth} = storeToRefs(themeStore);
const title = ref('项目名称');
const spinning = ref(true);
const isShow = ref(false);
const logo = ref<any[]>([]);
const appId = ref<number>(0);
const appQrcode = ref<any[]>([]);
const images = ref<ItemType[]>([]);
const appField = ref<ProjectField[]>();
const collection = ref<boolean>(false);
const website = ref<CmsWebsite>();
// 应用信息
const {form, assignFields, resetFields} = useFormData<Project>({
// 应用id
appId: undefined,
// 应用秘钥
appSecret: '',
enName: '',
// 应用名称
appName: '',
// 上级id, 0是顶级
parentId: undefined,
// 应用编号
appCode: '',
// 应用图标
appIcon: '',
appQrcode: '',
// 应用截图
images: '',
// 应用类型
appType: '',
appTypeMultiple: undefined,
// 菜单类型
menuType: undefined,
// 应用地址
appUrl: '',
// 后台管理地址
adminUrl: undefined,
// 下载地址
downUrl: undefined,
serverUrl: undefined,
callbackUrl: undefined,
gitUrl: undefined,
docsUrl: undefined,
prototypeUrl: undefined,
ipAddress: undefined,
fileUrl: undefined,
// 应用包名
packageName: '',
// 点击次数
clicks: '',
// 安装次数
installs: '',
// 项目介绍
content: '',
// 开发者(个人)
developer: '',
director: '',
projectDirector: '',
salesman: '',
// 软件定价
price: '',
// 评分
score: '',
// 星级
star: '',
// 菜单组件地址
component: '',
// 菜单路由地址
path: '',
// 权限标识
authority: '',
// 打开位置
target: '',
// 是否隐藏, 0否, 1是(仅注册路由不显示在左侧菜单)
hide: undefined,
// 菜单侧栏选中的path
active: '',
// 其它路由元信息
meta: '',
// 版本
edition: '',
// 版本号
version: '',
// 是否已安装
isUse: undefined,
// 排序
sortNumber: undefined,
// 备注
comments: '',
tenantName: '',
companyName: '',
// 租户编号
tenantCode: '',
// 租户id
tenantId: undefined,
// 创建时间
createTime: '',
appStatus: '开发中',
// 状态
status: undefined,
// 发布者
userId: undefined,
websiteId: undefined,
progress: undefined,
// 发布者昵称
nickname: '',
// 子菜单
children: [],
// 权限树回显选中状态, 0未选中, 1选中
checked: false,
//
key: undefined,
//
value: undefined,
//
parentIds: [],
//
openType: undefined,
//
search: undefined,
// 成员管理
users: [],
// 项目需求
requirement: '',
file1: '[]',
file2: '[]',
file3: '[]',
showCase: undefined,
showIndex: undefined,
recommend: undefined
});
const onChange = () => {
// reload();
};
// 加入我的收藏
const onCollection = () => {
if (collection.value) {
collection.value = false;
removeProjectCollection(appId.value).then(msg => {
message.success(msg);
})
} else {
collection.value = true;
addProjectCollection({
appId: appId.value
}).then((msg) => {
message.success(msg);
})
}
}
/**
* 加载数据
*/
const reload = () => {
resetFields();
logo.value = [];
appQrcode.value = [];
images.value = [];
appField.value = [];
assignFields({});
// 加载项目详情
getProject(appId.value)
.then((data) => {
spinning.value = false;
if (data.appName) {
title.value = data.appName;
// 修改页签标题
setPageTabTitle(data.appName);
}
if (data.appIcon) {
logo.value.push({
uid: data.appId,
url: data.appIcon,
status: 'done'
});
}
if (data.appQrcode) {
const split = data.appQrcode.split('|||');
split.map((url) => {
appQrcode.value.push({
uid: uuid(),
url: url,
status: 'done'
});
});
}
if (data.images) {
const arr = JSON.parse(data.images);
arr.map((d, i) => {
images.value.push({
uid: d.uid,
url: d.url,
status: 'done'
});
});
}
if (data.websiteId) {
getCmsWebsiteAll(data.websiteId).then(response => {
website.value = response;
})
}
isShow.value = true;
spinning.value = false;
assignFields(data);
})
.catch((err) => {
console.log(err, 'ssss')
isShow.value = false;
spinning.value = false;
});
// 加载项目参数
pageProjectField({appId: appId.value, limit: 50}).then((res) => {
appField.value = res?.list;
form.fields = res?.list;
});
// 加载项目成员
pageProjectUser({appId: appId.value}).then((res) => {
if (res?.list) {
form.users = res.list;
}
});
// 是否已收藏
getProjectCollection(appId.value).then((data) => {
if (data) {
collection.value = true;
}
}).catch(err => {
console.log(err)
});
};
watch(
currentRoute,
(route) => {
const {params} = unref(route);
const {id} = params;
if (id) {
appId.value = Number(id);
}
reload();
},
{immediate: true}
);
</script>
<script lang="ts">
export default {
name: 'ProjectDetail'
};
</script>

View File

@@ -0,0 +1,212 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="800"
:visible="visible"
:maskClosable="false"
:maxable="maxable"
:title="isUpdate ? '编辑应用参数' : '添加应用参数'"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
@ok="save"
>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="styleResponsive ? { md: 4, sm: 5, xs: 24 } : { flex: '90px' }"
:wrapper-col="
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
"
>
<a-form-item label="应用ID" name="appId">
<a-input
allow-clear
placeholder="请输入应用ID"
v-model:value="form.appId"
/>
</a-form-item>
<a-form-item label="名称" name="name">
<a-input
allow-clear
placeholder="请输入名称"
v-model:value="form.name"
/>
</a-form-item>
<a-form-item label="备注" name="comments">
<a-textarea
:rows="4"
:maxlength="200"
placeholder="请输入描述"
v-model:value="form.comments"
/>
</a-form-item>
<a-form-item label="用户ID" name="userId">
<a-input
allow-clear
placeholder="请输入用户ID"
v-model:value="form.userId"
/>
</a-form-item>
<a-form-item label="状态, 0正常, 1删除" name="status">
<a-radio-group v-model:value="form.status">
<a-radio :value="0">显示</a-radio>
<a-radio :value="1">隐藏</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="排序(数字越小越靠前)" name="sortNumber">
<a-input-number
:min="0"
:max="9999"
class="ele-fluid"
placeholder="请输入排序号"
v-model:value="form.sortNumber"
/>
</a-form-item>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { Form, message } from 'ant-design-vue';
import { assignObject, uuid } from 'ele-admin-pro';
import { addProjectField, updateProjectField } from '@/api/project/projectField';
import { ProjectField } from '@/api/project/projectField/model';
import { useThemeStore } from '@/store/modules/theme';
import { storeToRefs } from 'pinia';
import { ItemType } from 'ele-admin-pro/es/ele-image-upload/types';
import { FormInstance } from 'ant-design-vue/es/form';
import { FileRecord } from '@/api/system/file/model';
// 是否是修改
const isUpdate = ref(false);
const useForm = Form.useForm;
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: ProjectField | null;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
// 提交状态
const loading = ref(false);
// 是否显示最大化切换按钮
const maxable = ref(true);
// 表格选中数据
const formRef = ref<FormInstance | null>(null);
const images = ref<ItemType[]>([]);
// 用户信息
const form = reactive<ProjectField>({
id: undefined,
appId: undefined,
name: undefined,
comments: undefined,
userId: undefined,
status: undefined,
sortNumber: undefined,
tenantId: undefined,
createTime: undefined,
projectFieldId: undefined,
projectFieldName: '',
status: 0,
comments: '',
sortNumber: 100
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
projectFieldName: [
{
required: true,
type: 'string',
message: '请填写应用参数名称',
trigger: 'blur'
}
]
});
const chooseImage = (data: FileRecord) => {
images.value.push({
uid: data.id,
url: data.path,
status: 'done'
});
form.image = data.path;
};
const onDeleteItem = (index: number) => {
images.value.splice(index, 1);
form.image = '';
};
const { resetFields } = useForm(form, rules);
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const formData = {
...form
};
const saveOrUpdate = isUpdate.value ? updateProjectField : addProjectField;
saveOrUpdate(formData)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
watch(
() => props.visible,
(visible) => {
if (visible) {
images.value = [];
if (props.data) {
assignObject(form, props.data);
if(props.data.image){
images.value.push({
uid: uuid(),
url: props.data.image,
status: 'done'
})
}
isUpdate.value = true;
} else {
isUpdate.value = false;
}
} else {
resetFields();
}
},
{ immediate: true }
);
</script>

View File

@@ -0,0 +1,42 @@
<!-- 搜索表单 -->
<template>
<a-space :size="10" style="flex-wrap: wrap">
<a-button type="primary" class="ele-btn-icon" @click="add">
<template #icon>
<PlusOutlined />
</template>
<span>添加</span>
</a-button>
</a-space>
</template>
<script lang="ts" setup>
import { PlusOutlined } from '@ant-design/icons-vue';
import type { GradeParam } from '@/api/user/grade/model';
import { watch } from 'vue';
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: [];
}>(),
{}
);
const emit = defineEmits<{
(e: 'search', where?: GradeParam): void;
(e: 'add'): void;
(e: 'remove'): void;
(e: 'batchMove'): void;
}>();
// 新增
const add = () => {
emit('add');
};
watch(
() => props.selection,
() => {}
);
</script>

View File

@@ -0,0 +1,251 @@
<template>
<div class="page">
<div class="ele-body">
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<ele-pro-table
ref="tableRef"
row-key="projectFieldId"
:columns="columns"
:datasource="datasource"
:customRow="customRow"
tool-class="ele-toolbar-form"
class="sys-org-table"
>
<template #toolbar>
<search
@search="reload"
:selection="selection"
@add="openEdit"
@remove="removeBatch"
@batchMove="openMove"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'image'">
<a-image :src="record.image" :width="50" />
</template>
<template v-if="column.key === 'status'">
<a-tag v-if="record.status === 0" color="green">显示</a-tag>
<a-tag v-if="record.status === 1" color="red">隐藏</a-tag>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a @click="openEdit(record)">修改</a>
<a-divider type="vertical" />
<a-popconfirm
title="确定要删除此记录吗?"
@confirm="remove(record)"
>
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</ele-pro-table>
</a-card>
<!-- 编辑弹窗 -->
<ProjectFieldEdit v-model:visible="showEdit" :data="current" @done="reload" />
</div>
</div>
</template>
<script lang="ts" setup>
import { createVNode, ref } from 'vue';
import { message, Modal } from 'ant-design-vue';
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
import type { EleProTable } from 'ele-admin-pro';
import { toDateString } from 'ele-admin-pro';
import type {
DatasourceFunction,
ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types';
import Search from './components/search.vue';
import ProjectFieldEdit from './components/projectFieldEdit.vue';
import { pageProjectField, removeProjectField, removeBatchProjectField } from '@/api/project/projectField';
import type { ProjectField, ProjectFieldParam } from '@/api/project/projectField/model';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<ProjectField[]>([]);
// 当前编辑数据
const current = ref<ProjectField | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 是否显示批量移动弹窗
const showMove = ref(false);
// 加载状态
const loading = ref(true);
// 表格数据源
const datasource: DatasourceFunction = ({
page,
limit,
where,
orders,
filters
}) => {
if (filters) {
where.status = filters.status;
}
return pageProjectField({
...where,
...orders,
page,
limit
});
};
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: '自增ID',
dataIndex: 'id',
key: 'id',
align: 'center',
width: 90,
},
{
title: '应用ID',
dataIndex: 'appId',
key: 'appId',
align: 'center',
},
{
title: '名称',
dataIndex: 'name',
key: 'name',
align: 'center',
},
{
title: '备注',
dataIndex: 'comments',
key: 'comments',
align: 'center',
},
{
title: '用户ID',
dataIndex: 'userId',
key: 'userId',
align: 'center',
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
align: 'center',
},
{
title: '排序(数字越小越靠前)',
dataIndex: 'sortNumber',
key: 'sortNumber',
align: 'center',
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
align: 'center',
sorter: true,
ellipsis: true,
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd')
},
{
title: '操作',
key: 'action',
width: 180,
fixed: 'right',
align: 'center',
hideInSetting: true
}
]);
/* 搜索 */
const reload = (where?: ProjectFieldParam) => {
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: ProjectField) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 删除单个 */
const remove = (row: ProjectField) => {
const hide = message.loading('请求中..', 0);
removeProjectField(row.projectFieldId)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 批量删除 */
const removeBatch = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
Modal.confirm({
title: '提示',
content: '确定要删除选中的记录吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = message.loading('请求中..', 0);
removeBatchProjectField(selection.value.map((d) => d.projectFieldId))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
/* 查询 */
const query = () => {
loading.value = true;
};
/* 自定义行属性 */
const customRow = (record: ProjectField) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
query();
</script>
<script lang="ts">
export default {
name: 'ProjectField'
};
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,249 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="800"
:visible="visible"
:maskClosable="false"
:maxable="maxable"
:title="isUpdate ? '修复到期时间' : '修复到期时间'"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
@ok="save"
>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="styleResponsive ? { md: 4, sm: 5, xs: 24 } : { flex: '90px' }"
:wrapper-col="
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
"
>
<a-form-item label="选择项目" name="appName">
<a-select
v-model:value="form.appName"
show-search
:disabled="isUpdate"
:filter-option="false"
style="width: 350px"
placeholder="选择项目"
:options="state.data"
@search="fetchProject"
>
<template v-if="state.fetching" #notFoundContent>
<a-spin size="small"/>
</template>
</a-select>
</a-form-item>
<a-form-item label="到期时间" name="expirationTime">
<a-date-picker
v-model:value="form.expirationTime"
placeholder="修复到期时间"
value-format="YYYY-MM-DD HH:mm:ss"
/>
</a-form-item>
<a-form-item label="操作员" name="nickname">
{{ loginUser.nickname }}
</a-form-item>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import {ref,computed, reactive, watch} from 'vue';
import {Form, message} from 'ant-design-vue';
import {assignObject} from 'ele-admin-pro';
import {useThemeStore} from '@/store/modules/theme';
import {storeToRefs} from 'pinia';
import {debounce} from 'lodash-es';
import {ItemType} from 'ele-admin-pro/es/ele-image-upload/types';
import {FormInstance} from 'ant-design-vue/es/form';
import {pageProject, updateProject} from "@/api/project/project";
import {Project} from "@/api/project/project/model";
import {listDictData} from "@/api/system/dict-data";
import {DictData} from "@/api/system/dict-data/model";
import {useUserStore} from "@/store/modules/user";
// 是否是修改
const isUpdate = ref(false);
const useForm = Form.useForm;
// 是否开启响应式布局
const themeStore = useThemeStore();
const {styleResponsive} = storeToRefs(themeStore);
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: Project | null;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
// 提交状态
const loading = ref(false);
// 是否显示最大化切换按钮
const maxable = ref(true);
// 表格选中数据
const formRef = ref<FormInstance | null>(null);
const images = ref<ItemType[]>([]);
const durationDict = ref<DictData[]>([]);
const userStore = useUserStore();
const loginUser = computed(() => userStore.info ?? {});
// 用户信息
const form = reactive<Project>({
appId: undefined,
appName: undefined,
expirationTime: undefined
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
appName: [
{
required: true,
type: 'string',
message: '请选项项目',
trigger: 'blur'
}
],
duration: [
{
required: true,
type: 'number',
message: '请选择续费时长',
trigger: 'blur'
}
],
days: [
{
required: true,
type: 'number',
message: '请输入续费天数',
trigger: 'blur'
}
],
payPrice: [
{
required: true,
type: 'number',
message: '请输入续费金额',
trigger: 'blur'
}
],
payType: [
{
required: true,
type: 'number',
message: '请选择支付方式',
trigger: 'blur'
}
]
});
const {resetFields} = useForm(form, rules);
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const formData = {
...form
};
updateProject(formData)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {
});
};
let lastFetchId = 0;
const state = reactive<any>({
data: [],
value: undefined,
fetching: false,
});
const fetchProject = debounce(value => {
lastFetchId += 1;
const fetchId = lastFetchId;
state.data = [];
state.fetching = true;
pageProject({keywords: value})
.then(body => {
if (fetchId !== lastFetchId) {
return;
}
state.data = body?.list.map(d => {
d.label = d.appName;
d.value = d.appId;
return d
}) || [];
state.fetching = false;
});
}, 300);
watch(state.value, () => {
state.data = [];
state.fetching = false;
});
listDictData({dictCode:'ProjectDuration'}).then(res => {
durationDict.value = res.map(d => {
d.label = d.dictDataName;
d.value = Number(d.dictDataCode);
return d;
});
})
watch(
() => props.visible,
(visible) => {
if (visible) {
fetchProject(undefined)
images.value = [];
if (props.data) {
assignObject(form, props.data);
form.money = props.data.renewMoney;
form.payPrice = props.data.renewMoney;
form.totalPrice = props.data.renewMoney;
form.expirationTime = props.data.expirationTime;
if(!props.data.payType){
form.payType = 402;
}
isUpdate.value = true;
} else {
isUpdate.value = false;
}
} else {
resetFields();
}
},
{immediate: true}
);
</script>

View File

@@ -0,0 +1,361 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="800"
:visible="visible"
:maskClosable="false"
:maxable="maxable"
:title="isUpdate ? '创建订单' : '创建续费订单'"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
@ok="save"
>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="styleResponsive ? { md: 4, sm: 5, xs: 24 } : { flex: '90px' }"
:wrapper-col="
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
"
>
<a-form-item label="选择项目" name="appName">
<a-select
v-model:value="form.appName"
show-search
:disabled="isUpdate"
:filter-option="false"
style="width: 350px"
placeholder="选择项目"
:options="state.data"
@search="fetchProject"
@change="onDone"
>
<template v-if="state.fetching" #notFoundContent>
<a-spin size="small"/>
</template>
</a-select>
</a-form-item>
<a-form-item label="续费时长" name="duration">
<a-radio-group v-model:value="form.duration" button-style="solid" @change="handleDuration" @blur="handleDuration">
<template v-for="item in durationDict">
<a-radio-button :value="item.value">{{ item.dictDataName }}</a-radio-button>
</template>
</a-radio-group>
</a-form-item>
<a-form-item label="续费天数" name="days" v-if="form.duration == 0">
<a-input-number
allow-clear
placeholder="请输入天数"
style="width: 120px"
v-model:value="form.days"
@change="handleDays"
/>
</a-form-item>
<a-form-item label="续费金额" name="payPrice">
<a-input-number
allow-clear
placeholder="请输入续费金额"
prefix="¥"
suffix="元"
:precision="2"
style="width: 350px"
v-model:value="form.payPrice"
/>
</a-form-item>
<a-form-item label="支付方式" name="payType">
<a-radio-group v-model:value="form.payType" button-style="solid" @change="handleDuration" @blur="handleDuration">
<a-radio-button :value="0">余额支付</a-radio-button>
<a-radio-button :value="102">微信</a-radio-button>
<a-radio-button :value="3">支付宝</a-radio-button>
<a-radio-button :value="4">现金</a-radio-button>
<a-radio-button :value="402">银行转账</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item label="付款凭证" name="images">
<SelectFile
:placeholder="`请上传付款凭证`"
:limit="1"
:data="images"
@done="chooseImage"
@del="onDeleteItem"
/>
</a-form-item>
<a-form-item label="备注" name="comments">
<a-textarea
:rows="4"
:maxlength="200"
placeholder="请输入描述"
v-model:value="form.comments"
/>
</a-form-item>
<a-form-item label="操作员" name="nickname">
{{ loginUser.nickname }}
</a-form-item>
<a-form-item label="财务审核" name="status" v-if="hasRole('finance')">
<a-radio-group v-model:value="form.status">
<a-radio :value="1">已入账</a-radio>
<a-radio :value="0">待审核</a-radio>
</a-radio-group>
</a-form-item>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import {ref,computed, reactive, watch} from 'vue';
import {Form, message} from 'ant-design-vue';
import {assignObject, uuid} from 'ele-admin-pro';
import {addProjectRenew, updateProjectRenew} from '@/api/project/projectRenew';
import {ProjectRenew} from '@/api/project/projectRenew/model';
import {useThemeStore} from '@/store/modules/theme';
import {storeToRefs} from 'pinia';
import {debounce} from 'lodash-es';
import {ItemType} from 'ele-admin-pro/es/ele-image-upload/types';
import {FormInstance} from 'ant-design-vue/es/form';
import {pageProject} from "@/api/project/project";
import {Project} from "@/api/project/project/model";
import {listDictData} from "@/api/system/dict-data";
import {DictData} from "@/api/system/dict-data/model";
import {FileRecord} from "@/api/system/file/model";
import {hasRole} from "@/utils/permission";
import {useUserStore} from "@/store/modules/user";
// 是否是修改
const isUpdate = ref(false);
const useForm = Form.useForm;
// 是否开启响应式布局
const themeStore = useThemeStore();
const {styleResponsive} = storeToRefs(themeStore);
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: Project | null;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
// 提交状态
const loading = ref(false);
// 是否显示最大化切换按钮
const maxable = ref(true);
// 表格选中数据
const formRef = ref<FormInstance | null>(null);
const images = ref<ItemType[]>([]);
const durationDict = ref<DictData[]>([]);
const userStore = useUserStore();
const loginUser = computed(() => userStore.info ?? {});
// 用户信息
const form = reactive<ProjectRenew>({
appRenewId: undefined,
appId: undefined,
appName: undefined,
money: undefined,
totalPrice: undefined,
payPrice: undefined,
reducePrice: undefined,
duration: undefined,
days: undefined,
payType: 402,
comments: undefined,
startTime: undefined,
endTime: undefined,
soon: undefined,
customerId: undefined,
userId: undefined,
images: undefined,
nickname: undefined,
tenantId: undefined,
createTime: undefined,
status: 0,
fetching: false
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
appName: [
{
required: true,
type: 'string',
message: '请选项项目',
trigger: 'blur'
}
],
duration: [
{
required: true,
type: 'number',
message: '请选择续费时长',
trigger: 'blur'
}
],
days: [
{
required: true,
type: 'number',
message: '请输入续费天数',
trigger: 'blur'
}
],
payPrice: [
{
required: true,
type: 'number',
message: '请输入续费金额',
trigger: 'blur'
}
],
payType: [
{
required: true,
type: 'number',
message: '请选择支付方式',
trigger: 'blur'
}
]
});
const onDone = (appId: number, item: Project) => {
form.appId = appId;
form.appName = item.appName;
form.money = item.renewMoney;
form.payPrice = item.renewMoney;
}
const chooseImage = (data: FileRecord) => {
images.value.push({
uid: data.id,
url: data.path,
status: 'done'
});
form.images = data.path;
};
const onDeleteItem = (index: number) => {
images.value.splice(index, 1);
form.images = '';
};
const handleDuration = () => {
if(form.duration && form.money){
form.totalPrice = Number(form.money) * Number(form.duration);
form.payPrice = Number(form.money) * Number(form.duration);
}
}
const handleDays = () => {
// 计算每天的价格 form.money 价格
const priceDay = Number(form.money) / 365;
form.totalPrice = priceDay * Number(form.days);
form.payPrice = priceDay * Number(form.days);
}
const {resetFields} = useForm(form, rules);
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const formData = {
...form,
images: JSON.stringify(images.value)
};
addProjectRenew(formData)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {
});
};
let lastFetchId = 0;
const state = reactive<any>({
data: [],
value: undefined,
fetching: false,
});
const fetchProject = debounce(value => {
lastFetchId += 1;
const fetchId = lastFetchId;
state.data = [];
state.fetching = true;
pageProject({keywords: value})
.then(body => {
if (fetchId !== lastFetchId) {
return;
}
state.data = body?.list.map(d => {
d.label = d.appName;
d.value = d.appId;
return d
}) || [];
state.fetching = false;
});
}, 300);
watch(state.value, () => {
state.data = [];
state.fetching = false;
});
listDictData({dictCode:'ProjectDuration'}).then(res => {
durationDict.value = res.map(d => {
d.label = d.dictDataName;
d.value = Number(d.dictDataCode);
return d;
});
})
watch(
() => props.visible,
(visible) => {
if (visible) {
fetchProject(undefined)
images.value = [];
if (props.data) {
assignObject(form, props.data);
form.money = props.data.renewMoney;
form.payPrice = props.data.renewMoney;
form.totalPrice = props.data.renewMoney;
if(!props.data.payType){
form.payType = 402;
}
isUpdate.value = true;
} else {
isUpdate.value = false;
}
} else {
resetFields();
}
},
{immediate: true}
);
</script>

View File

@@ -0,0 +1,53 @@
<!-- 搜索表单 -->
<template>
<a-space :size="10" style="flex-wrap: wrap">
<a-input-search
allow-clear
placeholder="请输入项目名称"
v-model:value="where.keywords"
@pressEnter="search"
@search="search"
/>
</a-space>
</template>
<script lang="ts" setup>
import {watch} from 'vue';
import useSearch from "@/utils/use-search";
import {ProjectParam} from "@/api/project/project/model";
import {ProjectRenewParam} from "@/api/project/projectRenew/model";
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: [];
}>(),
{}
);
const emit = defineEmits<{
(e: 'search', where?: ProjectRenewParam): void;
(e: 'add'): void;
(e: 'remove'): void;
(e: 'batchMove'): void;
}>();
// 表单数据
const {where} = useSearch<ProjectParam>({
appId: undefined,
userId: undefined,
appStatus: undefined,
keywords: ''
});
/* 搜索 */
const search = () => {
emit('search', where);
};
watch(
() => props.selection,
() => {
}
);
</script>

View File

@@ -0,0 +1,289 @@
<template>
<a-page-header :title="getPageTitle()" @back="() => $router.go(-1)">
<template #extra>
<Extra/>
</template>
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<ele-pro-table
ref="tableRef"
row-key="appId"
:columns="columns"
:datasource="datasource"
tool-class="ele-toolbar-form"
class="sys-org-table"
>
<template #toolbar>
<search
@search="reload"
:selection="selection"
@add="openEdit"
@remove="removeBatch"
@batchMove="openMove"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'appName'">
<span>{{ record.appName }}</span>
</template>
<template v-if="column.key === 'appIcon'">
<a-avatar :src="record.appIcon" :size="40"/>
</template>
<template v-if="column.key === 'renewCount'">
<span class="cursor-pointer"
@click="openUrl(`/project/renew-log/${record.appId}`)">{{ record.renewCount }}</span>
</template>
<template v-if="column.key === 'customerName'">
<a-tooltip :title="`客户ID(${record.customerId})`">
{{ record.customerName }}
</a-tooltip>
</template>
<template v-if="column.key === 'expirationTime'">
<!-- <div><span class="text-gray-300">下单时间</span>{{ toDateString(record.createTime, 'yyyy-MM-dd') }}</div>-->
<div @click="openExpirationTimeEdit(record)">{{ toDateString(record.expirationTime, 'yyyy-MM-dd') }}</div>
<template v-if="sceneType == 'totalPrice30'">
<span class="text-purple-500">剩余 {{ record.expiredDays }} </span>
</template>
<template v-else-if="record.soon < 0 || sceneType == 'Expired'">
<span class="text-red-500">已过期 {{ record.expiredDays }} </span>
</template>
<template v-else>
<span class="text-gray-400">剩余 {{ record.expiredDays }} </span>
</template>
</template>
<template v-if="column.key === 'status'">
<a-tag color="green" v-if="record.status === 1">已入账</a-tag>
<a-tag color="orange" v-if="record.status === 0">待审核</a-tag>
</template>
<template v-if="column.key === 'action'">
<a @click="openEdit(record)">创建订单</a>
</template>
</template>
</ele-pro-table>
</a-card>
<!-- 编辑弹窗 -->
<ProjectRenewEdit v-model:visible="showEdit" :data="current" @done="reload"/>
<ExpirationTimeEdit v-model:visible="showExpirationTimeEdit" :data="current" @done="reload"/>
</a-page-header>
</template>
<script lang="ts" setup>
import {createVNode, ref, watch, unref} from 'vue';
import {message, Modal} from 'ant-design-vue';
import {ExclamationCircleOutlined} from '@ant-design/icons-vue';
import type {EleProTable} from 'ele-admin-pro';
import {useRouter} from 'vue-router';
import {toDateString} from 'ele-admin-pro';
import type {
DatasourceFunction,
ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types';
import Search from './components/search.vue';
import ProjectRenewEdit from './components/projectRenewEdit.vue';
import ExpirationTimeEdit from './components/expirationTimeEdit.vue';
import {pageProject, removeProject, removeBatchProject} from '@/api/project/project';
import type {Project, ProjectParam} from '@/api/project/project/model';
import {getPageTitle, openUrl, push} from "@/utils/common";
import Extra from "@/views/project/project/components/extra.vue";
import {hasRole} from "@/utils/permission";
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
const {currentRoute} = useRouter();
// 表格选中数据
const selection = ref<Project[]>([]);
// 当前编辑数据
const current = ref<Project | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 是否显示续费时间修复弹窗
const showExpirationTimeEdit = ref(false);
// 是否显示批量移动弹窗
const showMove = ref(false);
// 加载状态
const loading = ref(true);
// 场景类型
const sceneType = ref();
const isExpiration = ref(false)
// 表格数据源
const datasource: DatasourceFunction = ({
page,
limit,
where,
orders,
filters
}) => {
if (filters) {
where.status = filters.status;
}
where.showExpiration = false;
return pageProject({
...where,
...orders,
page,
limit
});
};
// 表格列配置
const columns = ref<ColumnItem[]>([
// {
// title: '项目ID',
// dataIndex: 'appId',
// key: 'appId'
// },
{
title: '项目名称',
dataIndex: 'appName',
key: 'appName'
},
{
title: '客户名称',
dataIndex: 'companyName',
key: 'companyName',
align: 'center',
},
{
title: '项目负责人',
dataIndex: 'nickname',
key: 'nickname',
align: 'center',
},
{
title: '备注',
dataIndex: 'comments',
key: 'comments',
align: 'center',
ellipsis: true,
},
// {
// title: '创建时间',
// dataIndex: 'createTime',
// key: 'createTime',
// sorter: true,
// align: 'center',
// customRender: ({text}) => toDateString(text, 'yyyy-MM-dd HH:mm')
// },
{
title: '操作',
key: 'action',
width: 180,
fixed: 'right',
align: 'center'
}
]);
/* 搜索 */
const reload = (where?: ProjectParam) => {
sceneType.value = where?.sceneType;
selection.value = [];
tableRef?.value?.reload({where: where});
};
/* 打开编辑弹窗 */
const openEdit = (row?: Project) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开续费修复弹窗 */
const openExpirationTimeEdit = (row: Project) => {
// 非财务人员不能修改
if (!hasRole('superAdmin')) {
return false;
}
current.value = row;
showExpirationTimeEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 删除单个 */
const remove = (row: Project) => {
const hide = message.loading('请求中..', 0);
removeProject(row.appId)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 批量删除 */
const removeBatch = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
Modal.confirm({
title: '提示',
content: '确定要删除选中的记录吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = message.loading('请求中..', 0);
removeBatchProject(selection.value.map((d) => d.appId))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
/* 查询 */
const query = () => {
loading.value = true;
};
/* 自定义行属性 */
const customRow = (record: Project) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
query();
watch(
currentRoute,
(route) => {
const {query} = unref(route);
const {expiration} = query;
if (expiration) {
isExpiration.value = true
}
reload();
},
{immediate: true}
);
</script>
<script lang="ts">
export default {
name: 'ProjectByRenew'
};
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,249 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="800"
:visible="visible"
:maskClosable="false"
:maxable="maxable"
:title="isUpdate ? '修复到期时间' : '修复到期时间'"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
@ok="save"
>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="styleResponsive ? { md: 4, sm: 5, xs: 24 } : { flex: '90px' }"
:wrapper-col="
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
"
>
<a-form-item label="选择项目" name="appName">
<a-select
v-model:value="form.appName"
show-search
:disabled="isUpdate"
:filter-option="false"
style="width: 350px"
placeholder="选择项目"
:options="state.data"
@search="fetchProject"
>
<template v-if="state.fetching" #notFoundContent>
<a-spin size="small"/>
</template>
</a-select>
</a-form-item>
<a-form-item label="到期时间" name="expirationTime">
<a-date-picker
v-model:value="form.expirationTime"
placeholder="修复到期时间"
value-format="YYYY-MM-DD HH:mm:ss"
/>
</a-form-item>
<a-form-item label="操作员" name="nickname">
{{ loginUser.nickname }}
</a-form-item>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import {ref,computed, reactive, watch} from 'vue';
import {Form, message} from 'ant-design-vue';
import {assignObject} from 'ele-admin-pro';
import {useThemeStore} from '@/store/modules/theme';
import {storeToRefs} from 'pinia';
import {debounce} from 'lodash-es';
import {ItemType} from 'ele-admin-pro/es/ele-image-upload/types';
import {FormInstance} from 'ant-design-vue/es/form';
import {pageProject, updateProject} from "@/api/project/project";
import {Project} from "@/api/project/project/model";
import {listDictData} from "@/api/system/dict-data";
import {DictData} from "@/api/system/dict-data/model";
import {useUserStore} from "@/store/modules/user";
// 是否是修改
const isUpdate = ref(false);
const useForm = Form.useForm;
// 是否开启响应式布局
const themeStore = useThemeStore();
const {styleResponsive} = storeToRefs(themeStore);
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: Project | null;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
// 提交状态
const loading = ref(false);
// 是否显示最大化切换按钮
const maxable = ref(true);
// 表格选中数据
const formRef = ref<FormInstance | null>(null);
const images = ref<ItemType[]>([]);
const durationDict = ref<DictData[]>([]);
const userStore = useUserStore();
const loginUser = computed(() => userStore.info ?? {});
// 用户信息
const form = reactive<Project>({
appId: undefined,
appName: undefined,
expirationTime: undefined
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
appName: [
{
required: true,
type: 'string',
message: '请选项项目',
trigger: 'blur'
}
],
duration: [
{
required: true,
type: 'number',
message: '请选择续费时长',
trigger: 'blur'
}
],
days: [
{
required: true,
type: 'number',
message: '请输入续费天数',
trigger: 'blur'
}
],
payPrice: [
{
required: true,
type: 'number',
message: '请输入续费金额',
trigger: 'blur'
}
],
payType: [
{
required: true,
type: 'number',
message: '请选择支付方式',
trigger: 'blur'
}
]
});
const {resetFields} = useForm(form, rules);
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const formData = {
...form
};
updateProject(formData)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {
});
};
let lastFetchId = 0;
const state = reactive<any>({
data: [],
value: undefined,
fetching: false,
});
const fetchProject = debounce(value => {
lastFetchId += 1;
const fetchId = lastFetchId;
state.data = [];
state.fetching = true;
pageProject({keywords: value})
.then(body => {
if (fetchId !== lastFetchId) {
return;
}
state.data = body?.list.map(d => {
d.label = d.appName;
d.value = d.appId;
return d
}) || [];
state.fetching = false;
});
}, 300);
watch(state.value, () => {
state.data = [];
state.fetching = false;
});
listDictData({dictCode:'ProjectDuration'}).then(res => {
durationDict.value = res.map(d => {
d.label = d.dictDataName;
d.value = Number(d.dictDataCode);
return d;
});
})
watch(
() => props.visible,
(visible) => {
if (visible) {
fetchProject(undefined)
images.value = [];
if (props.data) {
assignObject(form, props.data);
form.money = props.data.renewMoney;
form.payPrice = props.data.renewMoney;
form.totalPrice = props.data.renewMoney;
form.expirationTime = props.data.expirationTime;
if(!props.data.payType){
form.payType = 402;
}
isUpdate.value = true;
} else {
isUpdate.value = false;
}
} else {
resetFields();
}
},
{immediate: true}
);
</script>

View File

@@ -0,0 +1,361 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="800"
:visible="visible"
:maskClosable="false"
:maxable="maxable"
:title="isUpdate ? '立即续费' : '创建续费订单'"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
@ok="save"
>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="styleResponsive ? { md: 4, sm: 5, xs: 24 } : { flex: '90px' }"
:wrapper-col="
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
"
>
<a-form-item label="选择项目" name="appName">
<a-select
v-model:value="form.appName"
show-search
:disabled="isUpdate"
:filter-option="false"
style="width: 350px"
placeholder="选择项目"
:options="state.data"
@search="fetchProject"
@change="onDone"
>
<template v-if="state.fetching" #notFoundContent>
<a-spin size="small"/>
</template>
</a-select>
</a-form-item>
<a-form-item label="续费时长" name="duration">
<a-radio-group v-model:value="form.duration" button-style="solid" @change="handleDuration" @blur="handleDuration">
<template v-for="item in durationDict">
<a-radio-button :value="item.value">{{ item.dictDataName }}</a-radio-button>
</template>
</a-radio-group>
</a-form-item>
<a-form-item label="续费天数" name="days" v-if="form.duration == 0">
<a-input-number
allow-clear
placeholder="请输入天数"
style="width: 120px"
v-model:value="form.days"
@change="handleDays"
/>
</a-form-item>
<a-form-item label="续费金额" name="payPrice">
<a-input-number
allow-clear
placeholder="请输入续费金额"
prefix="¥"
suffix="元"
:precision="2"
style="width: 350px"
v-model:value="form.payPrice"
/>
</a-form-item>
<a-form-item label="支付方式" name="payType">
<a-radio-group v-model:value="form.payType" button-style="solid" @change="handleDuration" @blur="handleDuration">
<a-radio-button :value="0">余额支付</a-radio-button>
<a-radio-button :value="102">微信</a-radio-button>
<a-radio-button :value="3">支付宝</a-radio-button>
<a-radio-button :value="4">现金</a-radio-button>
<a-radio-button :value="402">银行转账</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item label="付款凭证" name="images">
<SelectFile
:placeholder="`请上传付款凭证`"
:limit="1"
:data="images"
@done="chooseImage"
@del="onDeleteItem"
/>
</a-form-item>
<a-form-item label="备注" name="comments">
<a-textarea
:rows="4"
:maxlength="200"
placeholder="请输入描述"
v-model:value="form.comments"
/>
</a-form-item>
<a-form-item label="操作员" name="nickname">
{{ loginUser.nickname }}
</a-form-item>
<a-form-item label="财务审核" name="status" v-if="hasRole('finance')">
<a-radio-group v-model:value="form.status">
<a-radio :value="1">已入账</a-radio>
<a-radio :value="0">待审核</a-radio>
</a-radio-group>
</a-form-item>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import {ref,computed, reactive, watch} from 'vue';
import {Form, message} from 'ant-design-vue';
import {assignObject, uuid} from 'ele-admin-pro';
import {addProjectRenew, updateProjectRenew} from '@/api/project/projectRenew';
import {ProjectRenew} from '@/api/project/projectRenew/model';
import {useThemeStore} from '@/store/modules/theme';
import {storeToRefs} from 'pinia';
import {debounce} from 'lodash-es';
import {ItemType} from 'ele-admin-pro/es/ele-image-upload/types';
import {FormInstance} from 'ant-design-vue/es/form';
import {pageProject} from "@/api/project/project";
import {Project} from "@/api/project/project/model";
import {listDictData} from "@/api/system/dict-data";
import {DictData} from "@/api/system/dict-data/model";
import {FileRecord} from "@/api/system/file/model";
import {hasRole} from "@/utils/permission";
import {useUserStore} from "@/store/modules/user";
// 是否是修改
const isUpdate = ref(false);
const useForm = Form.useForm;
// 是否开启响应式布局
const themeStore = useThemeStore();
const {styleResponsive} = storeToRefs(themeStore);
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: Project | null;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
// 提交状态
const loading = ref(false);
// 是否显示最大化切换按钮
const maxable = ref(true);
// 表格选中数据
const formRef = ref<FormInstance | null>(null);
const images = ref<ItemType[]>([]);
const durationDict = ref<DictData[]>([]);
const userStore = useUserStore();
const loginUser = computed(() => userStore.info ?? {});
// 用户信息
const form = reactive<ProjectRenew>({
appRenewId: undefined,
appId: undefined,
appName: undefined,
money: undefined,
totalPrice: undefined,
payPrice: undefined,
reducePrice: undefined,
duration: undefined,
days: undefined,
payType: 402,
comments: undefined,
startTime: undefined,
endTime: undefined,
soon: undefined,
customerId: undefined,
userId: undefined,
images: undefined,
nickname: undefined,
tenantId: undefined,
createTime: undefined,
status: 0,
fetching: false
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
appName: [
{
required: true,
type: 'string',
message: '请选项项目',
trigger: 'blur'
}
],
duration: [
{
required: true,
type: 'number',
message: '请选择续费时长',
trigger: 'blur'
}
],
days: [
{
required: true,
type: 'number',
message: '请输入续费天数',
trigger: 'blur'
}
],
payPrice: [
{
required: true,
type: 'number',
message: '请输入续费金额',
trigger: 'blur'
}
],
payType: [
{
required: true,
type: 'number',
message: '请选择支付方式',
trigger: 'blur'
}
]
});
const onDone = (appId: number, item: Project) => {
form.appId = appId;
form.appName = item.appName;
form.money = item.renewMoney;
form.payPrice = item.renewMoney;
}
const chooseImage = (data: FileRecord) => {
images.value.push({
uid: data.id,
url: data.path,
status: 'done'
});
form.images = data.path;
};
const onDeleteItem = (index: number) => {
images.value.splice(index, 1);
form.images = '';
};
const handleDuration = () => {
if(form.duration && form.money){
form.totalPrice = Number(form.money) * Number(form.duration);
form.payPrice = Number(form.money) * Number(form.duration);
}
}
const handleDays = () => {
// 计算每天的价格 form.money 价格
const priceDay = Number(form.money) / 365;
form.totalPrice = priceDay * Number(form.days);
form.payPrice = priceDay * Number(form.days);
}
const {resetFields} = useForm(form, rules);
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const formData = {
...form,
images: JSON.stringify(images.value)
};
addProjectRenew(formData)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {
});
};
let lastFetchId = 0;
const state = reactive<any>({
data: [],
value: undefined,
fetching: false,
});
const fetchProject = debounce(value => {
lastFetchId += 1;
const fetchId = lastFetchId;
state.data = [];
state.fetching = true;
pageProject({keywords: value})
.then(body => {
if (fetchId !== lastFetchId) {
return;
}
state.data = body?.list.map(d => {
d.label = d.appName;
d.value = d.appId;
return d
}) || [];
state.fetching = false;
});
}, 300);
watch(state.value, () => {
state.data = [];
state.fetching = false;
});
listDictData({dictCode:'ProjectDuration'}).then(res => {
durationDict.value = res.map(d => {
d.label = d.dictDataName;
d.value = Number(d.dictDataCode);
return d;
});
})
watch(
() => props.visible,
(visible) => {
if (visible) {
fetchProject(undefined)
images.value = [];
if (props.data) {
assignObject(form, props.data);
form.money = props.data.renewMoney;
form.payPrice = props.data.renewMoney;
form.totalPrice = props.data.renewMoney;
if(!props.data.payType){
form.payType = 402;
}
isUpdate.value = true;
} else {
isUpdate.value = false;
}
} else {
resetFields();
}
},
{immediate: true}
);
</script>

View File

@@ -0,0 +1,166 @@
<!-- 搜索表单 -->
<template>
<a-space :size="10" style="flex-wrap: wrap">
<!-- <a-button type="primary" class="ele-btn-icon" @click="add">-->
<!-- <template #icon>-->
<!-- <PlusOutlined/>-->
<!-- </template>-->
<!-- <span>创建订单</span>-->
<!-- </a-button>-->
<a-button type="text" @click="onSearch('Expired')">
<a-tooltip title="已超过催收日期的续费总金额" class="flex flex-col">
<span class="text-gray-400">超期续费</span>
<span class="text-red-600 font-bold">{{ formatNumber(count.expiredPrice) }}</span>
</a-tooltip>
</a-button>
<a-button type="text" @click="onSearch('effectiveTotalPrice')">
<a-tooltip title="未超催收日期的续费总金额" class="flex flex-col">
<span class="text-gray-400">正常续费</span>
<span class="text-green-500 font-bold">{{ formatNumber(count.effectiveTotalPrice) }}</span>
</a-tooltip>
</a-button>
<a-button type="text" @click="onSearch('AllRenewPrice')">
<a-tooltip title="每年可催收续费总额" class="flex flex-col">
<span class="text-gray-400">续费总额</span>
<span class="text-gray-600 font-bold">{{ formatNumber(count.totalRenewPrice) }}</span>
</a-tooltip>
</a-button>
<a-dropdown>
<a-button v-if="where?.month" type="text">
<a-tooltip :title="`${where.month}月份可催收的续费总额`" class="flex flex-col">
<span class="text-gray-400">{{where.month}}月份</span>
<span class="text-gray-600 font-bold">{{ formatNumber(count.currentQueryTotalPrice) }}</span>
</a-tooltip>
</a-button>
<a-button v-else type="text" @click="onSearch('totalPrice30')">
<a-tooltip title="近30天可催收的续费总额" class="flex flex-col">
<span class="text-gray-400">本月续费</span>
<span class="text-gray-600 font-bold">{{ formatNumber(count.totalPrice30) }}</span>
</a-tooltip>
</a-button>
<template #overlay>
<a-menu>
<a-menu-item v-for="(item,index) in monthRenews" :key="index" @click="onMonthSearch(index + 1)">
<div class="item flex w-full justify-between">
<span class="text-gray-400">{{ index + 1 }}</span>
<span>{{ formatNumber(item) }}</span>
</div>
</a-menu-item>
</a-menu>
</template>
</a-dropdown>
<a-range-picker
v-model:value="dateRange"
@change="search"
value-format="YYYY-MM-DD"
/>
<a-input-search
allow-clear
placeholder="请输入项目名称"
v-model:value="where.keywords"
@pressEnter="search"
@search="search"
/>
<a-button @click="reset">重置</a-button>
</a-space>
</template>
<script lang="ts" setup>
import {watch, ref} from 'vue';
import useSearch from "@/utils/use-search";
import {formatNumber} from 'ele-admin-pro/es';
import {ProjectCount, ProjectParam} from "@/api/project/project/model";
import {ProjectRenewParam} from "@/api/project/projectRenew/model";
import {countProjectRenew, listMonthRenewPrice} from "@/api/project/projectRenew";
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: [];
}>(),
{}
);
const emit = defineEmits<{
(e: 'search', where?: ProjectRenewParam): void;
(e: 'add'): void;
(e: 'remove'): void;
(e: 'batchMove'): void;
}>();
// 日期范围选择
const dateRange = ref<[string, string]>(['', '']);
const monthRenews = ref<[]>([]);
const count = ref<ProjectCount>({
// 今年已收续费总总额
yearTotalPrice: undefined,
// 去年已收续费总额
lastTotalPrice: undefined,
// 近30天可催收的续费总额
totalPrice30: undefined,
// 每年可催收续费总额
totalRenewPrice: undefined,
// 已经超过催收时间的续费总额
expiredPrice: undefined,
// 有效续费总金额
effectiveTotalPrice: undefined,
// 当前查询的总金额
currentQueryTotalPrice: undefined
});
// 表单数据
const {where, resetFields} = useSearch<ProjectParam>({
appId: undefined,
userId: undefined,
month: undefined,
appStatus: undefined,
keywords: ''
});
// 新增
const add = () => {
emit('add');
};
/* 重置 */
const reset = () => {
resetFields();
search();
};
/* 搜索 */
const search = () => {
const [d1, d2] = dateRange.value ?? [];
where.expirationTimeStart = d1 ? d1 + ' 00:00:00' : undefined;
where.expirationTimeEnd = d2 ? d2 + ' 23:59:59' : undefined;
emit('search', where);
};
const onSearch = (text: string) => {
where.sceneType = text
search();
}
const onMonthSearch = (text: number) => {
resetFields()
where.month = text
countProjectRenew({month: where.month}).then(res => {
count.value = res;
search();
})
}
countProjectRenew().then(data => {
count.value = data;
})
listMonthRenewPrice().then(data => {
monthRenews.value = data;
})
watch(
() => props.selection,
() => {
}
);
</script>

View File

@@ -0,0 +1,332 @@
<template>
<a-page-header :title="getPageTitle()" @back="() => $router.go(-1)">
<template #extra>
<Extra/>
</template>
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<ele-pro-table
ref="tableRef"
row-key="appId"
:columns="columns"
:datasource="datasource"
tool-class="ele-toolbar-form"
class="sys-org-table"
>
<template #toolbar>
<search
@search="reload"
:selection="selection"
@add="openEdit"
@remove="removeBatch"
@batchMove="openMove"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'appName'">
<a-tooltip :title="`查看续费明细`">
<span class="cursor-pointer" @click="push(`/project/renew-log/${record.appId}`)">{{
record.appName
}}</span>
</a-tooltip>
</template>
<template v-if="column.key === 'appIcon'">
<a-avatar :src="record.appIcon" :size="40"/>
</template>
<template v-if="column.key === 'renewCount'">
<span class="cursor-pointer"
@click="openUrl(`/project/renew-log/${record.appId}`)">{{ record.renewCount }}</span>
</template>
<template v-if="column.key === 'customerName'">
<a-tooltip :title="`客户ID(${record.customerId})`">
{{ record.customerName }}
</a-tooltip>
</template>
<template v-if="column.key === 'expirationTime'">
<div>{{ toDateString(record.createTime, 'yyyy-MM-dd') }}</div>
<div @click="openExpirationTimeEdit(record)">{{ toDateString(record.expirationTime, 'yyyy-MM-dd') }}</div>
<template v-if="sceneType == 'totalPrice30'">
<span class="text-purple-500">剩余 {{ record.expiredDays }} </span>
</template>
<template v-else-if="record.expired < 0 || sceneType == 'Expired'">
<span class="text-red-500">已过期 {{ record.expiredDays }} </span>
</template>
<template v-else>
<span class="text-gray-400">剩余 {{ record.expiredDays }} </span>
</template>
</template>
<template v-if="column.key === 'status'">
<a-tag color="green" v-if="record.status === 1">已入账</a-tag>
<a-tag color="orange" v-if="record.status === 0">待审核</a-tag>
</template>
<template v-if="column.key === 'action'">
<a @click="openEdit(record)">立即续费</a>
<a-divider type="vertical"/>
<a @click="openUrl(`/project/renew-log/${record.appId}`)">查看明细</a>
</template>
</template>
</ele-pro-table>
</a-card>
<!-- 编辑弹窗 -->
<ProjectRenewEdit v-model:visible="showEdit" :data="current" @done="reload"/>
<ExpirationTimeEdit v-model:visible="showExpirationTimeEdit" :data="current" @done="reload"/>
</a-page-header>
</template>
<script lang="ts" setup>
import {createVNode, ref, watch, unref} from 'vue';
import {message, Modal} from 'ant-design-vue';
import {ExclamationCircleOutlined} from '@ant-design/icons-vue';
import type {EleProTable} from 'ele-admin-pro';
import {useRouter} from 'vue-router';
import {toDateString} from 'ele-admin-pro';
import type {
DatasourceFunction,
ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types';
import Search from './components/search.vue';
import ProjectRenewEdit from './components/projectRenewEdit.vue';
import ExpirationTimeEdit from './components/expirationTimeEdit.vue';
import {pageProject, removeProject, removeBatchProject} from '@/api/project/project';
import type {Project, ProjectParam} from '@/api/project/project/model';
import {getPageTitle, openUrl, push} from "@/utils/common";
import Extra from "@/views/project/project/components/extra.vue";
import {hasRole} from "@/utils/permission";
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
const {currentRoute} = useRouter();
// 表格选中数据
const selection = ref<Project[]>([]);
// 当前编辑数据
const current = ref<Project | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 是否显示续费时间修复弹窗
const showExpirationTimeEdit = ref(false);
// 是否显示批量移动弹窗
const showMove = ref(false);
// 加载状态
const loading = ref(true);
// 场景类型
const sceneType = ref();
// 是否显示续费时间修复弹窗
const isExpiration = ref(false);
// 表格数据源
const datasource: DatasourceFunction = ({
page,
limit,
where,
orders,
filters
}) => {
if (filters) {
where.status = filters.status;
}
where.appStatus = '已上架';
where.showExpiration = true;
where.sort = 'expirationTime';
where.order = 'asc';
if (isExpiration.value) {
where.showExpiration = false;
}
return pageProject({
...where,
...orders,
page,
limit
});
};
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: '项目ID',
dataIndex: 'appId',
key: 'appId'
},
{
title: '项目名称',
dataIndex: 'appName',
key: 'appName'
},
{
title: '续费金额',
dataIndex: 'renewMoney',
key: 'renewMoney',
sorter: true,
align: 'center',
customRender: ({text}) => `${text}`
},
{
title: '续费次数',
dataIndex: 'renewCount',
key: 'renewCount',
align: 'center',
sorter: true,
},
// {
// title: '状态',
// dataIndex: 'status',
// key: 'status',
// align: 'center'
// },
{
title: '项目负责人',
dataIndex: 'nickname',
key: 'nickname',
align: 'center',
},
{
title: '客户名称',
dataIndex: 'customerName',
key: 'customerName',
align: 'center',
},
{
title: '到期时间',
dataIndex: 'expirationTime',
key: 'expirationTime',
sorter: true,
align: 'center',
customRender: ({text}) => toDateString(text, 'yyyy-MM-dd HH:mm')
},
{
title: '备注',
dataIndex: 'comments',
key: 'comments',
align: 'center',
ellipsis: true,
},
// {
// title: '创建时间',
// dataIndex: 'createTime',
// key: 'createTime',
// sorter: true,
// align: 'center',
// customRender: ({text}) => toDateString(text, 'yyyy-MM-dd HH:mm')
// },
{
title: '操作',
key: 'action',
width: 180,
fixed: 'right',
align: 'center',
hideInSetting: true
}
]);
/* 搜索 */
const reload = (where?: ProjectParam) => {
sceneType.value = where?.sceneType;
selection.value = [];
tableRef?.value?.reload({where: where});
};
/* 打开编辑弹窗 */
const openEdit = (row?: Project) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开续费修复弹窗 */
const openExpirationTimeEdit = (row: Project) => {
// 非财务人员不能修改
if (!hasRole('superAdmin')) {
return false;
}
current.value = row;
showExpirationTimeEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 删除单个 */
const remove = (row: Project) => {
const hide = message.loading('请求中..', 0);
removeProject(row.appId)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 批量删除 */
const removeBatch = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
Modal.confirm({
title: '提示',
content: '确定要删除选中的记录吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = message.loading('请求中..', 0);
removeBatchProject(selection.value.map((d) => d.appId))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
/* 查询 */
const query = () => {
loading.value = true;
};
/* 自定义行属性 */
const customRow = (record: Project) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
query();
watch(
currentRoute,
(route) => {
const {query} = unref(route);
const {expiration} = query;
if (expiration) {
isExpiration.value = true
}
reload();
},
{immediate: true}
);
</script>
<script lang="ts">
export default {
name: 'ProjectByRenew'
};
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,349 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="800"
:visible="visible"
:maskClosable="false"
:maxable="maxable"
:title="isUpdate ? '编辑续费订单' : '创建续费订单'"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
@ok="save"
>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="styleResponsive ? { md: 4, sm: 5, xs: 24 } : { flex: '90px' }"
:wrapper-col="
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
"
>
<a-form-item label="选择项目" name="appId">
<a-select
v-model:value="form.appId"
show-search
:filter-option="false"
style="width: 350px"
placeholder="选择项目"
:options="state.data"
@search="fetchProject"
@change="onDone"
>
<template v-if="state.fetching" #notFoundContent>
<a-spin size="small"/>
</template>
</a-select>
</a-form-item>
<a-form-item label="续费时长" name="duration">
<a-radio-group v-model:value="form.duration" button-style="solid" @change="handleDuration" @blur="handleDuration">
<template v-for="item in durationDict">
<a-radio-button :value="item.value">{{ item.dictDataName }}</a-radio-button>
</template>
</a-radio-group>
</a-form-item>
<a-form-item label="续费天数" name="days" v-if="form.duration == 0">
<a-input-number
allow-clear
placeholder="请输入天数"
style="width: 120px"
v-model:value="form.days"
@change="handleDays"
/>
</a-form-item>
<a-form-item label="续费金额" name="payPrice">
<a-input-number
allow-clear
placeholder="请输入续费金额"
prefix="¥"
suffix="元"
:precision="2"
style="width: 350px"
v-model:value="form.payPrice"
/>
</a-form-item>
<a-form-item label="支付方式" name="payType">
<a-radio-group v-model:value="form.payType" disabled button-style="solid" @change="handleDuration" @blur="handleDuration">
<a-radio-button :value="0">余额支付</a-radio-button>
<a-radio-button :value="102">微信</a-radio-button>
<a-radio-button :value="3">支付宝</a-radio-button>
<a-radio-button :value="4">现金</a-radio-button>
<a-radio-button :value="402">银行转账</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item label="付款凭证" name="images">
<SelectFile
:placeholder="`请上传付款凭证`"
:limit="1"
:data="images"
@done="chooseImage"
@del="onDeleteItem"
/>
</a-form-item>
<a-form-item label="备注" name="comments">
<a-textarea
:rows="4"
:maxlength="200"
placeholder="请输入描述"
v-model:value="form.comments"
/>
</a-form-item>
<a-form-item label="财务审核" name="status" v-if="hasRole('finance')">
<a-radio-group v-model:value="form.status">
<a-radio :value="1">已入账</a-radio>
<a-radio :value="0">待审核</a-radio>
</a-radio-group>
</a-form-item>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import {ref, reactive, watch} from 'vue';
import {Form, message} from 'ant-design-vue';
import {assignObject, uuid} from 'ele-admin-pro';
import {addProjectRenew, updateProjectRenew} from '@/api/project/projectRenew';
import {ProjectRenew} from '@/api/project/projectRenew/model';
import {useThemeStore} from '@/store/modules/theme';
import {storeToRefs} from 'pinia';
import {debounce} from 'lodash-es';
import {formatNumber} from 'ele-admin-pro/es';
import {ItemType} from 'ele-admin-pro/es/ele-image-upload/types';
import {FormInstance} from 'ant-design-vue/es/form';
import {pageProject} from "@/api/project/project";
import {Project} from "@/api/project/project/model";
import {listDictData} from "@/api/system/dict-data";
import {DictData} from "@/api/system/dict-data/model";
import {FileRecord} from "@/api/system/file/model";
import {hasRole} from "@/utils/permission";
// 是否是修改
const isUpdate = ref(false);
const useForm = Form.useForm;
// 是否开启响应式布局
const themeStore = useThemeStore();
const {styleResponsive} = storeToRefs(themeStore);
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: ProjectRenew | null;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
// 提交状态
const loading = ref(false);
// 是否显示最大化切换按钮
const maxable = ref(true);
// 表格选中数据
const formRef = ref<FormInstance | null>(null);
const images = ref<ItemType[]>([]);
const durationDict = ref<DictData[]>([]);
// 用户信息
const form = reactive<ProjectRenew>({
appRenewId: undefined,
appId: undefined,
appName: undefined,
money: undefined,
totalPrice: undefined,
payPrice: undefined,
reducePrice: undefined,
duration: undefined,
days: undefined,
payType: 402,
comments: undefined,
startTime: undefined,
endTime: undefined,
soon: undefined,
customerId: undefined,
userId: undefined,
images: undefined,
nickname: undefined,
tenantId: undefined,
createTime: undefined,
status: 0,
fetching: false
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
appId: [
{
required: true,
type: 'number',
message: '请选项项目',
trigger: 'blur'
}
],
duration: [
{
required: true,
type: 'number',
message: '请选择续费时长',
trigger: 'blur'
}
],
days: [
{
required: true,
type: 'number',
message: '请输入续费天数',
trigger: 'blur'
}
],
payPrice: [
{
required: true,
type: 'number',
message: '请输入续费金额',
trigger: 'blur'
}
],
payType: [
{
required: true,
type: 'number',
message: '请选择支付方式',
trigger: 'blur'
}
]
});
const onDone = (appId: number, item: Project) => {
form.appId = appId;
form.appName = item.appName;
form.money = item.renewMoney;
form.payPrice = item.renewMoney;
}
const chooseImage = (data: FileRecord) => {
images.value.push({
uid: data.id,
url: data.path,
status: 'done'
});
form.images = data.path;
};
const onDeleteItem = (index: number) => {
images.value.splice(index, 1);
form.images = '';
};
const handleDuration = () => {
if(form.duration && form.money){
form.totalPrice = Number(form.money) * Number(form.duration);
form.payPrice = Number(form.money) * Number(form.duration);
}
}
const handleDays = () => {
// 计算每天的价格 form.money 价格
const priceDay = Number(form.money) / 365;
form.totalPrice = priceDay * Number(form.days);
form.payPrice = priceDay * Number(form.days);
}
const {resetFields} = useForm(form, rules);
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const formData = {
...form,
images: JSON.stringify(images.value)
};
const saveOrUpdate = isUpdate.value ? updateProjectRenew : addProjectRenew;
saveOrUpdate(formData)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {
});
};
let lastFetchId = 0;
const state = reactive<any>({
data: [],
value: undefined,
fetching: false,
});
const fetchProject = debounce(value => {
lastFetchId += 1;
const fetchId = lastFetchId;
state.data = [];
state.fetching = true;
pageProject({keywords: value})
.then(body => {
if (fetchId !== lastFetchId) {
return;
}
state.data = body?.list.map(d => {
d.label = d.appName;
d.value = d.appId;
return d
}) || [];
state.fetching = false;
});
}, 300);
watch(state.value, () => {
state.data = [];
state.fetching = false;
});
listDictData({dictCode:'ProjectDuration'}).then(res => {
durationDict.value = res.map(d => {
d.label = d.dictDataName;
d.value = Number(d.dictDataCode);
return d;
});
})
watch(
() => props.visible,
(visible) => {
if (visible) {
fetchProject(undefined)
images.value = [];
if (props.data) {
assignObject(form, props.data);
isUpdate.value = true;
} else {
isUpdate.value = false;
}
} else {
resetFields();
}
},
{immediate: true}
);
</script>

View File

@@ -0,0 +1,119 @@
<!-- 搜索表单 -->
<template>
<a-space :size="10" style="flex-wrap: wrap">
<!-- <a-button type="primary" class="ele-btn-icon" @click="add">-->
<!-- <template #icon>-->
<!-- <PlusOutlined/>-->
<!-- </template>-->
<!-- <span>创建订单</span>-->
<!-- </a-button>-->
<a-input-search
allow-clear
placeholder="请输入项目名称"
v-model:value="where.keywords"
@pressEnter="search"
@search="search"
/>
<a-button @click="reset">重置</a-button>
<a-range-picker
v-model:value="dateRange"
@change="search"
value-format="YYYY-MM-DD"
/>
<a-button type="text" @click="onSearch('monthTotalPrice')">
<a-tooltip title="今年已收续费总额" class="flex flex-col">
<span class="text-gray-400">本月已收</span>
<span class="text-orange-600 font-bold">{{ formatNumber(count.monthTotalPrice) }}</span>
</a-tooltip>
</a-button>
<a-button type="text" @click="onSearch('yearTotalPrice')">
<a-tooltip title="今年已收续费总额" class="flex flex-col">
<span class="text-gray-400">今年已收</span>
<span class="text-green-600 font-bold">{{ formatNumber(count.yearTotalPrice) }}</span>
</a-tooltip>
</a-button>
<a-button type="text" @click="onSearch('lastTotalPrice')">
<a-tooltip title="去年已收续费总额" class="flex flex-col">
<span class="text-gray-400">去年总额</span>
<span class="text-gray-700 font-bold">{{ formatNumber(count.lastTotalPrice) }}</span>
</a-tooltip>
</a-button>
</a-space>
</template>
<script lang="ts" setup>
import {PlusOutlined} from '@ant-design/icons-vue';
import {watch, ref} from 'vue';
import useSearch from "@/utils/use-search";
import {formatNumber} from 'ele-admin-pro/es';
import {ProjectCount, ProjectParam} from "@/api/project/project/model";
import {ProjectRenewParam} from "@/api/project/projectRenew/model";
import {countProjectRenew} from "@/api/project/projectRenew";
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: [];
}>(),
{}
);
const emit = defineEmits<{
(e: 'search', where?: ProjectRenewParam): void;
(e: 'add'): void;
(e: 'remove'): void;
(e: 'batchMove'): void;
}>();
// 日期范围选择
const dateRange = ref<[string, string]>(['', '']);
const count = ref<ProjectCount>({
// 今年已收续费总总额
yearTotalPrice: undefined,
// 去年已收续费总额
lastTotalPrice: undefined,
// 近30天可催收的续费总额
totalPrice30: undefined,
// 本月已收续费总额
monthTotalPrice: undefined
});
// 表单数据
const {where,resetFields} = useSearch<ProjectParam>({
appId: undefined,
userId: undefined,
appStatus: undefined,
keywords: ''
});
// 新增
const add = () => {
emit('add');
};
/* 重置 */
const reset = () => {
resetFields();
search();
};
/* 搜索 */
const search = () => {
emit('search', where);
};
const onSearch = (text: string) => {
where.sceneType = text
search();
}
countProjectRenew().then(data => {
count.value = data;
})
watch(
() => props.selection,
() => {
}
);
</script>

View File

@@ -0,0 +1,314 @@
<template>
<a-page-header :title="getPageTitle()" @back="() => $router.go(-1)">
<template #extra>
<Extra/>
</template>
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<ele-pro-table
ref="tableRef"
row-key="appRenewId"
:columns="columns"
:datasource="datasource"
:customRow="customRow"
tool-class="ele-toolbar-form"
class="sys-org-table"
>
<template #toolbar>
<search
@search="reload"
:selection="selection"
@add="openEdit"
@remove="removeBatch"
@batchMove="openMove"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'appName'">
<span class="cursor-pointer" @click="onSearch(record)">{{ record.appName }}</span>
</template>
<template v-if="column.key === 'appIcon'">
<a-avatar :src="record.appIcon" :size="40"/>
</template>
<template v-if="column.key === 'images'">
<a-image v-if="record.images && JSON.parse(record.images).length > 0" style="width: 50px;" :src="JSON.parse(record.images)[0].url"/>
</template>
<template v-if="column.key === 'duration'">
<span v-if="record.days">{{ record.days }}</span>
<span v-else>{{ record.duration }}</span>
</template>
<template v-if="column.key === 'customerName'">
<a-tooltip :title="`客户ID(${record.customerId})`">
{{ record.customerName }}
</a-tooltip>
</template>
<template v-if="column.key === 'endTime'">
<div>续费前{{ toDateString(record.startTime, 'yyyy-MM-dd') }}</div>
<div>续费后{{ toDateString(record.endTime, 'yyyy-MM-dd') }}</div>
</template>
<template v-if="column.key === 'status'">
<a-tag color="green" v-if="record.status === 1">已入账</a-tag>
<a-tag color="orange" v-if="record.status === 0">待审核</a-tag>
</template>
<template v-if="column.key === 'action'">
<a-space v-if="hasRole('superAdmin') || hasRole('admin')">
<a-popconfirm
title="确定要撤销续费操作吗?"
@confirm="remove(record)"
>
<a-button size="small">取消订单</a-button>
</a-popconfirm>
</a-space>
</template>
</template>
</ele-pro-table>
</a-card>
<!-- 编辑弹窗 -->
<ProjectRenewEdit v-model:visible="showEdit" :data="current" @done="reload"/>
</a-page-header>
</template>
<script lang="ts" setup>
import { ref, unref, watch, createVNode } from 'vue';
import {message, Modal} from 'ant-design-vue';
import { useRouter } from 'vue-router';
import {ExclamationCircleOutlined} from '@ant-design/icons-vue';
import type {EleProTable} from 'ele-admin-pro';
import {toDateString} from 'ele-admin-pro';
import type {
DatasourceFunction,
ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types';
import Search from './components/search.vue';
import ProjectRenewEdit from './components/projectRenewEdit.vue';
import {pageProjectRenew, removeProjectRenew, removeBatchProjectRenew} from '@/api/project/projectRenew';
import type {ProjectRenew, ProjectRenewParam} from '@/api/project/projectRenew/model';
import {getPageTitle} from "@/utils/common";
import Extra from "@/views/project/project/components/extra.vue";
import {hasRole} from "@/utils/permission";
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<ProjectRenew[]>([]);
// 当前编辑数据
const current = ref<ProjectRenew | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 是否显示批量移动弹窗
const showMove = ref(false);
// 加载状态
const loading = ref(true);
const appId = ref<number>();
const { currentRoute } = useRouter();
// 表格数据源
const datasource: DatasourceFunction = ({
page,
limit,
where,
orders
}) => {
where.sort = 'appRenewId';
where.order = 'desc';
if(appId.value){
where.appId = appId.value;
}
return pageProjectRenew({
...where,
...orders,
page,
limit
});
};
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: '订单编号',
dataIndex: 'appRenewId',
key: 'appRenewId'
},
{
title: '项目名称',
dataIndex: 'appName',
key: 'appName'
},
{
title: '续费金额',
dataIndex: 'payPrice',
key: 'payPrice',
sorter: true,
customRender: ({text}) => `${text}`
},
{
title: '续费时长',
dataIndex: 'duration',
key: 'duration',
sorter: true,
align: 'center'
},
{
title: '到期时间',
dataIndex: 'endTime',
key: 'endTime',
sorter: true,
align: 'center',
customRender: ({text}) => toDateString(text, 'yyyy-MM-dd HH:mm')
},
// {
// title: '客户名称',
// dataIndex: 'customerName',
// key: 'customerName',
// align: 'center',
// },
// {
// title: '状态',
// dataIndex: 'status',
// key: 'status',
// align: 'center'
// },
{
title: '操作员',
dataIndex: 'nickname',
key: 'nickname',
align: 'center',
},
{
title: '备注',
dataIndex: 'comments',
key: 'comments',
align: 'center',
},
{
title: '付款凭证',
dataIndex: 'images',
key: 'images',
align: 'center',
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
sorter: true,
align: 'center',
customRender: ({text}) => toDateString(text, 'yyyy-MM-dd HH:mm:ss')
},
{
title: '操作',
key: 'action',
width: 180,
fixed: 'right',
align: 'center',
hideInSetting: true
}
]);
/* 搜索 */
const reload = (where?: ProjectRenewParam) => {
selection.value = [];
tableRef?.value?.reload({where: where});
};
/* 打开编辑弹窗 */
const openEdit = (row?: ProjectRenew) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
const onSearch = (row: ProjectRenew) => {
reload({appId: row?.appId})
}
/* 删除单个 */
const remove = (row: ProjectRenew) => {
const hide = message.loading('请求中..', 0);
removeProjectRenew(row.appRenewId)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 批量删除 */
const removeBatch = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
Modal.confirm({
title: '提示',
content: '确定要删除选中的记录吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = message.loading('请求中..', 0);
removeBatchProjectRenew(selection.value.map((d) => d.appRenewId))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
/* 查询 */
const query = () => {
loading.value = true;
};
/* 自定义行属性 */
const customRow = (record: ProjectRenew) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
if (hasRole('superAdmin')) {
openEdit(record);
}
}
};
};
query();
watch(
currentRoute,
(route) => {
const { params } = unref(route);
const { id } = params;
if (id) {
appId.value = Number(id);
reload()
}
reload();
},
{ immediate: true }
);
</script>
<script lang="ts">
export default {
name: 'ProjectRenew'
};
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,228 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="800"
:visible="visible"
:maskClosable="false"
:maxable="maxable"
:title="isUpdate ? '编辑项目域名' : '添加项目域名'"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
@ok="save"
>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="styleResponsive ? { md: 4, sm: 5, xs: 24 } : { flex: '90px' }"
:wrapper-col="
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
"
>
<a-form-item label="应用ID" name="appId">
<a-input
allow-clear
placeholder="请输入应用ID"
v-model:value="form.appId"
/>
</a-form-item>
<a-form-item label="域名类型" name="name">
<a-input
allow-clear
placeholder="请输入域名类型"
v-model:value="form.name"
/>
</a-form-item>
<a-form-item label="域名" name="domain">
<a-input
allow-clear
placeholder="请输入域名"
v-model:value="form.domain"
/>
</a-form-item>
<a-form-item label="账号" name="account">
<a-input
allow-clear
placeholder="请输入账号"
v-model:value="form.account"
/>
</a-form-item>
<a-form-item label="密码" name="password">
<a-input
allow-clear
placeholder="请输入密码"
v-model:value="form.password"
/>
</a-form-item>
<a-form-item label="备注" name="comments">
<a-textarea
:rows="4"
:maxlength="200"
placeholder="请输入描述"
v-model:value="form.comments"
/>
</a-form-item>
<a-form-item label="排序(数字越小越靠前)" name="sortNumber">
<a-input-number
:min="0"
:max="9999"
class="ele-fluid"
placeholder="请输入排序号"
v-model:value="form.sortNumber"
/>
</a-form-item>
<a-form-item label="状态, 0正常, 1待确认" name="status">
<a-radio-group v-model:value="form.status">
<a-radio :value="0">显示</a-radio>
<a-radio :value="1">隐藏</a-radio>
</a-radio-group>
</a-form-item>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { Form, message } from 'ant-design-vue';
import { assignObject, uuid } from 'ele-admin-pro';
import { addProjectUrl, updateProjectUrl } from '@/api/project/projectUrl';
import { ProjectUrl } from '@/api/project/projectUrl/model';
import { useThemeStore } from '@/store/modules/theme';
import { storeToRefs } from 'pinia';
import { ItemType } from 'ele-admin-pro/es/ele-image-upload/types';
import { FormInstance } from 'ant-design-vue/es/form';
import { FileRecord } from '@/api/system/file/model';
// 是否是修改
const isUpdate = ref(false);
const useForm = Form.useForm;
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: ProjectUrl | null;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
// 提交状态
const loading = ref(false);
// 是否显示最大化切换按钮
const maxable = ref(true);
// 表格选中数据
const formRef = ref<FormInstance | null>(null);
const images = ref<ItemType[]>([]);
// 用户信息
const form = reactive<ProjectUrl>({
appUrlId: undefined,
appId: undefined,
name: undefined,
domain: undefined,
account: undefined,
password: undefined,
comments: undefined,
sortNumber: undefined,
status: undefined,
createTime: undefined,
tenantId: undefined,
projectUrlId: undefined,
projectUrlName: '',
status: 0,
comments: '',
sortNumber: 100
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
projectUrlName: [
{
required: true,
type: 'string',
message: '请填写项目域名名称',
trigger: 'blur'
}
]
});
const chooseImage = (data: FileRecord) => {
images.value.push({
uid: data.id,
url: data.path,
status: 'done'
});
form.image = data.path;
};
const onDeleteItem = (index: number) => {
images.value.splice(index, 1);
form.image = '';
};
const { resetFields } = useForm(form, rules);
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const formData = {
...form
};
const saveOrUpdate = isUpdate.value ? updateProjectUrl : addProjectUrl;
saveOrUpdate(formData)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
watch(
() => props.visible,
(visible) => {
if (visible) {
images.value = [];
if (props.data) {
assignObject(form, props.data);
if(props.data.image){
images.value.push({
uid: uuid(),
url: props.data.image,
status: 'done'
})
}
isUpdate.value = true;
} else {
isUpdate.value = false;
}
} else {
resetFields();
}
},
{ immediate: true }
);
</script>

View File

@@ -0,0 +1,42 @@
<!-- 搜索表单 -->
<template>
<a-space :size="10" style="flex-wrap: wrap">
<a-button type="primary" class="ele-btn-icon" @click="add">
<template #icon>
<PlusOutlined />
</template>
<span>添加</span>
</a-button>
</a-space>
</template>
<script lang="ts" setup>
import { PlusOutlined } from '@ant-design/icons-vue';
import type { GradeParam } from '@/api/user/grade/model';
import { watch } from 'vue';
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: [];
}>(),
{}
);
const emit = defineEmits<{
(e: 'search', where?: GradeParam): void;
(e: 'add'): void;
(e: 'remove'): void;
(e: 'batchMove'): void;
}>();
// 新增
const add = () => {
emit('add');
};
watch(
() => props.selection,
() => {}
);
</script>

View File

@@ -0,0 +1,263 @@
<template>
<div class="page">
<div class="ele-body">
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<ele-pro-table
ref="tableRef"
row-key="projectUrlId"
:columns="columns"
:datasource="datasource"
:customRow="customRow"
tool-class="ele-toolbar-form"
class="sys-org-table"
>
<template #toolbar>
<search
@search="reload"
:selection="selection"
@add="openEdit"
@remove="removeBatch"
@batchMove="openMove"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'image'">
<a-image :src="record.image" :width="50" />
</template>
<template v-if="column.key === 'status'">
<a-tag v-if="record.status === 0" color="green">显示</a-tag>
<a-tag v-if="record.status === 1" color="red">隐藏</a-tag>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a @click="openEdit(record)">修改</a>
<a-divider type="vertical" />
<a-popconfirm
title="确定要删除此记录吗?"
@confirm="remove(record)"
>
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</ele-pro-table>
</a-card>
<!-- 编辑弹窗 -->
<ProjectUrlEdit v-model:visible="showEdit" :data="current" @done="reload" />
</div>
</div>
</template>
<script lang="ts" setup>
import { createVNode, ref } from 'vue';
import { message, Modal } from 'ant-design-vue';
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
import type { EleProTable } from 'ele-admin-pro';
import { toDateString } from 'ele-admin-pro';
import type {
DatasourceFunction,
ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types';
import Search from './components/search.vue';
import ProjectUrlEdit from './components/projectUrlEdit.vue';
import { pageProjectUrl, removeProjectUrl, removeBatchProjectUrl } from '@/api/project/projectUrl';
import type { ProjectUrl, ProjectUrlParam } from '@/api/project/projectUrl/model';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<ProjectUrl[]>([]);
// 当前编辑数据
const current = ref<ProjectUrl | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 是否显示批量移动弹窗
const showMove = ref(false);
// 加载状态
const loading = ref(true);
// 表格数据源
const datasource: DatasourceFunction = ({
page,
limit,
where,
orders,
filters
}) => {
if (filters) {
where.status = filters.status;
}
return pageProjectUrl({
...where,
...orders,
page,
limit
});
};
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: '自增ID',
dataIndex: 'appUrlId',
key: 'appUrlId',
align: 'center',
width: 90,
},
{
title: '应用ID',
dataIndex: 'appId',
key: 'appId',
align: 'center',
},
{
title: '域名类型',
dataIndex: 'name',
key: 'name',
align: 'center',
},
{
title: '域名',
dataIndex: 'domain',
key: 'domain',
align: 'center',
},
{
title: '账号',
dataIndex: 'account',
key: 'account',
align: 'center',
},
{
title: '密码',
dataIndex: 'password',
key: 'password',
align: 'center',
},
{
title: '备注',
dataIndex: 'comments',
key: 'comments',
align: 'center',
},
{
title: '排序(数字越小越靠前)',
dataIndex: 'sortNumber',
key: 'sortNumber',
align: 'center',
},
{
title: '状态, 0正常, 1待确认',
dataIndex: 'status',
key: 'status',
align: 'center',
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
align: 'center',
sorter: true,
ellipsis: true,
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd')
},
{
title: '操作',
key: 'action',
width: 180,
fixed: 'right',
align: 'center',
hideInSetting: true
}
]);
/* 搜索 */
const reload = (where?: ProjectUrlParam) => {
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: ProjectUrl) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 删除单个 */
const remove = (row: ProjectUrl) => {
const hide = message.loading('请求中..', 0);
removeProjectUrl(row.projectUrlId)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 批量删除 */
const removeBatch = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
Modal.confirm({
title: '提示',
content: '确定要删除选中的记录吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = message.loading('请求中..', 0);
removeBatchProjectUrl(selection.value.map((d) => d.projectUrlId))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
/* 查询 */
const query = () => {
loading.value = true;
};
/* 自定义行属性 */
const customRow = (record: ProjectUrl) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
query();
</script>
<script lang="ts">
export default {
name: 'ProjectUrl'
};
</script>
<style lang="less" scoped></style>

View File

@@ -0,0 +1,176 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
:width="800"
:visible="visible"
:maskClosable="false"
:maxable="maxable"
:title="isUpdate ? '编辑应用成员' : '添加应用成员'"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
@ok="save"
>
<a-form
ref="formRef"
:model="form"
:rules="rules"
:label-col="styleResponsive ? { md: 4, sm: 5, xs: 24 } : { flex: '90px' }"
:wrapper-col="
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
"
>
<a-form-item label="角色10体验成员 20开发者成员 30管理员 " name="role">
<a-input
allow-clear
placeholder="请输入角色10体验成员 20开发者成员 30管理员 "
v-model:value="form.role"
/>
</a-form-item>
<a-form-item label="用户ID" name="userId">
<a-input
allow-clear
placeholder="请输入用户ID"
v-model:value="form.userId"
/>
</a-form-item>
<a-form-item label="应用ID" name="appId">
<a-input
allow-clear
placeholder="请输入应用ID"
v-model:value="form.appId"
/>
</a-form-item>
<a-form-item label="昵称" name="nickname">
<a-input
allow-clear
placeholder="请输入昵称"
v-model:value="form.nickname"
/>
</a-form-item>
<a-form-item label="状态, 0正常, 1待确认" name="status">
<a-radio-group v-model:value="form.status">
<a-radio :value="0">显示</a-radio>
<a-radio :value="1">隐藏</a-radio>
</a-radio-group>
</a-form-item>
</a-form>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { Form, message } from 'ant-design-vue';
import { assignObject, uuid } from 'ele-admin-pro';
import { addProjectUser, updateProjectUser } from '@/api/project/projectUser';
import { ProjectUser } from '@/api/project/projectUser/model';
import { useThemeStore } from '@/store/modules/theme';
import { storeToRefs } from 'pinia';
import { ItemType } from 'ele-admin-pro/es/ele-image-upload/types';
import { FormInstance } from 'ant-design-vue/es/form';
import { FileRecord } from '@/api/system/file/model';
// 是否是修改
const isUpdate = ref(false);
const useForm = Form.useForm;
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: ProjectUser | null;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
// 提交状态
const loading = ref(false);
// 是否显示最大化切换按钮
const maxable = ref(true);
// 表格选中数据
const formRef = ref<FormInstance | null>(null);
const images = ref<ItemType[]>([]);
// 用户信息
const form = reactive<ProjectUser>({
appUserId: undefined,
role: undefined,
userId: undefined,
appId: undefined,
nickname: undefined,
status: undefined,
tenantId: undefined,
createTime: undefined,
sortNumber: 100
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
projectUserName: [
{
required: true,
type: 'string',
message: '请填写应用成员名称',
trigger: 'blur'
}
]
});
const { resetFields } = useForm(form, rules);
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const formData = {
...form
};
const saveOrUpdate = isUpdate.value ? updateProjectUser : addProjectUser;
saveOrUpdate(formData)
.then((msg) => {
loading.value = false;
message.success(msg);
updateVisible(false);
emit('done');
})
.catch((e) => {
loading.value = false;
message.error(e.message);
});
})
.catch(() => {});
};
watch(
() => props.visible,
(visible) => {
if (visible) {
images.value = [];
if (props.data) {
assignObject(form, props.data);
isUpdate.value = true;
} else {
isUpdate.value = false;
}
} else {
resetFields();
}
},
{ immediate: true }
);
</script>

View File

@@ -0,0 +1,42 @@
<!-- 搜索表单 -->
<template>
<a-space :size="10" style="flex-wrap: wrap">
<a-button type="primary" class="ele-btn-icon" @click="add">
<template #icon>
<PlusOutlined />
</template>
<span>添加</span>
</a-button>
</a-space>
</template>
<script lang="ts" setup>
import { PlusOutlined } from '@ant-design/icons-vue';
import type { GradeParam } from '@/api/user/grade/model';
import { watch } from 'vue';
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: [];
}>(),
{}
);
const emit = defineEmits<{
(e: 'search', where?: GradeParam): void;
(e: 'add'): void;
(e: 'remove'): void;
(e: 'batchMove'): void;
}>();
// 新增
const add = () => {
emit('add');
};
watch(
() => props.selection,
() => {}
);
</script>

View File

@@ -0,0 +1,245 @@
<template>
<div class="page">
<div class="ele-body">
<a-card :bordered="false" :body-style="{ padding: '16px' }">
<ele-pro-table
ref="tableRef"
row-key="projectUserId"
:columns="columns"
:datasource="datasource"
:customRow="customRow"
tool-class="ele-toolbar-form"
class="sys-org-table"
>
<template #toolbar>
<search
@search="reload"
:selection="selection"
@add="openEdit"
@remove="removeBatch"
@batchMove="openMove"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'image'">
<a-image :src="record.image" :width="50" />
</template>
<template v-if="column.key === 'status'">
<a-tag v-if="record.status === 0" color="green">显示</a-tag>
<a-tag v-if="record.status === 1" color="red">隐藏</a-tag>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a @click="openEdit(record)">修改</a>
<a-divider type="vertical" />
<a-popconfirm
title="确定要删除此记录吗?"
@confirm="remove(record)"
>
<a class="ele-text-danger">删除</a>
</a-popconfirm>
</a-space>
</template>
</template>
</ele-pro-table>
</a-card>
<!-- 编辑弹窗 -->
<ProjectUserEdit v-model:visible="showEdit" :data="current" @done="reload" />
</div>
</div>
</template>
<script lang="ts" setup>
import { createVNode, ref } from 'vue';
import { message, Modal } from 'ant-design-vue';
import { ExclamationCircleOutlined } from '@ant-design/icons-vue';
import type { EleProTable } from 'ele-admin-pro';
import { toDateString } from 'ele-admin-pro';
import type {
DatasourceFunction,
ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types';
import Search from './components/search.vue';
import ProjectUserEdit from './components/projectUserEdit.vue';
import { pageProjectUser, removeProjectUser, removeBatchProjectUser } from '@/api/project/projectUser';
import type { ProjectUser, ProjectUserParam } from '@/api/project/projectUser/model';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格选中数据
const selection = ref<ProjectUser[]>([]);
// 当前编辑数据
const current = ref<ProjectUser | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 是否显示批量移动弹窗
const showMove = ref(false);
// 加载状态
const loading = ref(true);
// 表格数据源
const datasource: DatasourceFunction = ({
page,
limit,
where,
orders,
filters
}) => {
if (filters) {
where.status = filters.status;
}
return pageProjectUser({
...where,
...orders,
page,
limit
});
};
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: '自增ID',
dataIndex: 'appUserId',
key: 'appUserId',
align: 'center',
width: 90,
},
{
title: '角色10体验成员 20开发者成员 30管理员 ',
dataIndex: 'role',
key: 'role',
align: 'center',
},
{
title: '用户ID',
dataIndex: 'userId',
key: 'userId',
align: 'center',
},
{
title: '应用ID',
dataIndex: 'appId',
key: 'appId',
align: 'center',
},
{
title: '昵称',
dataIndex: 'nickname',
key: 'nickname',
align: 'center',
},
{
title: '状态, 0正常, 1待确认',
dataIndex: 'status',
key: 'status',
align: 'center',
},
{
title: '创建时间',
dataIndex: 'createTime',
key: 'createTime',
align: 'center',
sorter: true,
ellipsis: true,
customRender: ({ text }) => toDateString(text, 'yyyy-MM-dd')
},
{
title: '操作',
key: 'action',
width: 180,
fixed: 'right',
align: 'center',
hideInSetting: true
}
]);
/* 搜索 */
const reload = (where?: ProjectUserParam) => {
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: ProjectUser) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 打开批量移动弹窗 */
const openMove = () => {
showMove.value = true;
};
/* 删除单个 */
const remove = (row: ProjectUser) => {
const hide = message.loading('请求中..', 0);
removeProjectUser(row.projectUserId)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 批量删除 */
const removeBatch = () => {
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
Modal.confirm({
title: '提示',
content: '确定要删除选中的记录吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = message.loading('请求中..', 0);
removeBatchProjectUser(selection.value.map((d) => d.projectUserId))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
/* 查询 */
const query = () => {
loading.value = true;
};
/* 自定义行属性 */
const customRow = (record: ProjectUser) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openEdit(record);
}
};
};
query();
</script>
<script lang="ts">
export default {
name: 'ProjectUser'
};
</script>
<style lang="less" scoped></style>