Initial commit

This commit is contained in:
南宁网宿科技
2024-04-24 16:36:46 +08:00
commit 121348e011
991 changed files with 158700 additions and 0 deletions
@@ -0,0 +1,623 @@
<!-- 编辑弹窗 -->
<template>
<ele-modal
width="70%"
:visible="visible"
:maskClosable="false"
:maxable="maxable"
:title="isUpdate ? '编辑服务器' : '新增服务器'"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
@ok="save"
>
<div style="background-color: #f3f3f3; padding: 8px">
<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-card title="基本信息" :bordered="false">
<!-- <template #extra>-->
<!-- <a-button type="link" @click="handleEditStatus">{{ editStatus ? "预览" : "编辑" }}</a-button>-->
<!-- </template>-->
<a-row :gutter="16">
<a-col
v-bind="styleResponsive ? { md: 12, sm: 24, xs: 24 } : { span: 8 }"
>
<a-form-item label="公网IP" name="code">
<a-input
allow-clear
:maxlength="20"
placeholder="请输入合法的IP地址"
v-if="editStatus"
v-model:value="form.code"
/>
<span v-else class="ele-text-primary">{{ form.code }}</span>
</a-form-item>
<a-form-item label="品牌厂商" name="brand">
<DictSelect
dict-code="serverBrand"
placeholder="请选择品牌厂商"
v-if="editStatus"
v-model:value="form.brand"
/>
<span v-else class="ele-text-primary">{{ form.brand }}</span>
</a-form-item>
<a-form-item label="所属企业" name="companyName">
<SelectCompany
:placeholder="`选择所属企业`"
v-model:value="form.companyName"
v-if="editStatus"
@done="chooseCompanyId"
/>
<span v-else class="ele-text-primary">{{ form.companyName }}</span>
</a-form-item>
<a-form-item label="描述" name="name">
<a-input
allow-clear
placeholder="请输入服务器描述"
v-if="editStatus"
v-model:value="form.name"
/>
<span v-else class="ele-text-primary">{{ form.name }}</span>
</a-form-item>
<a-form-item label="服务器配置" name="configuration">
<a-select
v-if="editStatus"
v-model:value="form.configuration"
mode="tags"
placeholder="请填写服务器配置"
></a-select>
<a-tag v-else v-for="item in form.configuration">
{{ item }}
</a-tag>
</a-form-item>
</a-col>
<a-col
v-bind="styleResponsive ? { md: 12, sm: 24, xs: 24 } : { span: 8 }"
>
<a-form-item label="宝塔面板" name="panel">
<a-input
allow-clear
placeholder="https://47.106.145.40:20780/603f8087"
v-model:value="form.panel"
v-if="editStatus"
/>
<a :href="form.panel" target="_blank" v-else class="ele-text-primary">{{ form.panel }}</a>
</a-form-item>
<a-form-item label="宝塔面板账号" name="panelAccount">
<a-input
allow-clear
:maxlength="20"
placeholder="宝塔面板账号"
v-if="editStatus"
v-model:value="form.panelAccount"
/>
<span v-else class="ele-text-primary">{{ form.panelAccount }}</span>
</a-form-item>
<a-form-item
label="宝塔面板密码"
name="panelPassword"
>
<a-input
allow-clear
:maxlength="20"
placeholder="宝塔面板密码"
v-if="editStatus"
v-model:value="form.panelPassword"
/>
<span v-else class="ele-text-primary">{{ form.panelPassword }}</span>
</a-form-item>
<a-form-item label="开放端口" name="openPort">
<a-select
v-if="editStatus"
v-model:value="form.openPort"
mode="tags"
placeholder="请选择开放端口"
></a-select>
<a-tag v-else v-for="item in form.openPort">
{{ item }}
</a-tag>
</a-form-item>
<a-form-item label="状态" name="status">
<DictSelect
dict-code="serverStatus"
placeholder="请选择状态"
v-if="editStatus"
v-model:value="form.status"
/>
<template v-else>
<a-tag :color="form.status == '正常' ? 'green' : ''">
{{ form.status }}
</a-tag>
</template>
</a-form-item>
</a-col>
<a-col
v-bind="styleResponsive ? { md: 12, sm: 24, xs: 24 } : { span: 8 }"
>
<a-form-item label="宝塔API接口秘钥" name="btSign">
<a-input
allow-clear
:maxlength="50"
placeholder="宝塔API接口秘钥"
v-if="editStatus"
v-model:value="form.btSign"
/>
<span v-else class="ele-text-primary">{{ form.btSign }}</span>
</a-form-item>
</a-col>
<a-col
v-bind="styleResponsive ? { md: 12, sm: 24, xs: 24 } : { span: 8 }"
>
<a-form-item label="排序" name="sortNumber">
<a-input
allow-clear
:maxlength="20"
placeholder="排序"
v-if="editStatus"
v-model:value="form.sortNumber"
/>
<span v-else class="ele-text-primary">{{ form.sortNumber }}</span>
</a-form-item>
</a-col>
</a-row>
</a-card>
</a-form>
</div>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch, createVNode, computed } from "vue";
import { Form, message } from "ant-design-vue";
import { assignObject, ipReg, toDateString } from "ele-admin-pro";
import { useUserStore } from "@/store/modules/user";
import { useThemeStore } from "@/store/modules/theme";
import { storeToRefs } from "pinia";
// 中文语言文件
import zh_Hans from "bytemd/locales/zh_Hans.json";
// 链接、删除线、复选框、表格等的插件
// 插件的中文语言文件
import zh_HansGfm from "@bytemd/plugin-gfm/locales/zh_Hans.json";
import "bytemd/dist/index.min.css";
import "github-markdown-css/github-markdown-light.css";
import { Modal } from "ant-design-vue";
// import TinymceEditor from '@/components/TinymceEditor/index.vue';
import highlight from "@bytemd/plugin-highlight";
// 中文语言文件
import gfm from "@bytemd/plugin-gfm";
// // 预览界面的样式,这里用的 github 的 markdown 主题
import "github-markdown-css/github-markdown-light.css";
import {
EyeOutlined,
EyeInvisibleOutlined,
EnvironmentOutlined
} from "@ant-design/icons-vue";
import { UploadOutlined } from "@/layout/menu-icons";
import { FormInstance, RuleObject } from "ant-design-vue/es/form";
import { Assets } from "@/api/oa/assets/model";
import { ItemType } from "ele-admin-pro/es/ele-image-upload/types";
import { User } from "@/api/system/user/model";
import { uploadFile } from "@/api/system/file";
import { FILE_SERVER, FILE_THUMBNAIL, TOKEN_STORE_NAME } from "@/config/setting";
import { listAssetsUser, addAssetsUser, removeAssetsUser } from "@/api/oa/assets/user";
import { AssetsUser } from "@/api/oa/assets/user/model";
import { addAssets, updateAssets } from "@/api/oa/assets";
import { isIP } from "ele-admin-pro";
import { Company } from "@/api/system/company/model";
import { Dayjs } from "dayjs";
// 是否是修改
const isUpdate = ref(false);
const useForm = Form.useForm;
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const { darkMode } = storeToRefs(themeStore);
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: Assets | null;
}>();
const emit = defineEmits<{
(e: "done"): void;
(e: "update:visible", visible: boolean): void;
}>();
// 提交状态
const loading = ref(false);
// 是否显示最大化切换按钮
const maxable = ref(true);
// 当期时间
const dayTime = toDateString(new Date());
// 已上传数据
const images = ref<ItemType[]>([]);
const assetsUsers = ref<AssetsUser[]>([]);
const selectUser = ref<string>();
const editStatus = ref(true);
const formRef = ref<FormInstance | null>(null);
// const type = ref<string>('single');
// 日期范围选择
const dateRange = ref<[string, string]>(["", ""]);
const content = ref("");
const requirement = ref("");
// 插件
const plugins = ref([
gfm({
locale: zh_HansGfm
}),
highlight()
]);
// 购买日期
const startTime = ref<Dayjs>();
// 结束日期
const endTime = ref<Dayjs>();
const showAddUserForm = ref(false);
// 文件模型
interface FileItem {
uid: string;
name?: string;
status?: string;
response?: Response;
thumbUrl?: string;
downloadUrl?: string;
url: string;
}
interface FileInfo {
file: FileItem;
fileList: FileItem[];
}
// 文件上传
const file1 = ref<FileItem[]>();
const file2 = ref<FileItem[]>();
const file3 = ref<FileItem[]>();
// 用户信息
const form = reactive<Assets>({
type: "服务器",
code: "",
createTime: "",
endTime: "",
name: "",
brand: undefined,
account: "",
password: "",
panelAccount: "",
panelPassword: "",
panel: "",
financeAmount: 0,
financeYears: 0,
financeRenew: 0,
customerId: undefined,
customerName: "",
companyName: "",
financeCustomerName: "",
financeCustomerContact: "",
financeCustomerPhone: "",
brandAccount: "",
brandPassword: "",
btSign: "",
openPort: "",
configuration: "",
comments: "",
root: "root",
sortNumber: 100,
status: "运行中",
assetsId: 0,
visibility: "private",
userList: [],
userId: undefined
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit("update:visible", value);
};
// 表单验证规则
const rules = reactive({
name: [
{
required: true,
type: "string",
message: "请输入服务器名称",
trigger: "blur"
}
],
code: [
{
required: true,
type: "string",
message: "请输入合法的IP地址",
trigger: "blur",
validator: async (_rule: RuleObject, value: string) => {
if (!isIP(value)) {
const msg = "请输入合法的IP地址";
try {
if (!isIP(value)) {
return Promise.reject(msg);
}
} catch (_e) {
return Promise.reject(msg);
}
}
return Promise.resolve();
}
}
],
brand: [
{
required: true,
type: "string",
message: "请选择跟进状态",
trigger: "blur"
}
],
companyName: [
{
required: true,
type: "string",
message: "请选择所属企业",
trigger: "blur"
}
]
// assetsName: [
// {
// required: true,
// type: 'string',
// message: '请选择租赁单位',
// trigger: 'blur'
// }
// ],
// appRegion: [
// {
// required: true,
// type: 'string',
// message: '请输入服务器地址',
// trigger: 'blur'
// }
// ]
});
const { resetFields, validate, validateInfos } = useForm(form, rules);
const addUser = () => {
showAddUserForm.value = true;
};
// 所属用户
const chooseCompanyId = (data: Company) => {
form.companyName = data.companyName;
form.companyId = data.companyId;
};
// 添加开发成员
const addAssetsDevUser = (data: User) => {
addAssetsUser({
userId: data.userId,
assetsId: props.data?.assetsId,
role: 20
}).then((msg) => {
message.success(msg);
reload();
})
.catch((e) => {
message.error(e.message);
});
}
// 移除成员
const removeUser = (data: AssetsUser) => {
removeAssetsUser(data.id)
.then((msg) => {
message.success(msg);
reload();
})
.catch((e) => {
message.error(e.message);
});
};
const handleEditStatus = () => {
editStatus.value = !editStatus.value;
};
// const chooseAssetsName = (data: Assets) => {
// form.assetsName = data.assetsName;
// };
const onFile1 = (info: FileInfo) => {
let resFileList = [...info.fileList];
file1.value = resFileList.map((file) => {
if (file.response) {
file.url = file.response.url;
}
return file;
});
form.file1 = JSON.stringify(file1.value);
};
const config = ref({
width: 1000,
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(FILE_SERVER + result.path);
} else {
error("上传失败");
}
})
.catch((e) => {
message.error(e.message);
});
}
});
// 上传文件
const onUpload = (item) => {
const { file } = item;
images.value = [];
uploadFile(file)
.then((data) => {
images.value.push({
uid: data.id,
url: FILE_THUMBNAIL + data.path,
status: "done"
});
message.success("上传成功");
})
.catch((e) => {
message.error(e.message);
});
};
/* 粘贴图片上传服务器并插入编辑器 */
const onPaste = (e) => {
console.log(e);
const items = (e.clipboardData || e.originalEvent.clipboardData).items;
console.log(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 + "](" + FILE_SERVER + result.path + ")\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,
openPort: form.openPort.toString(),
configuration: form.configuration.toString()
};
// 转字符串
const saveOrUpdate = isUpdate.value ? updateAssets : addAssets;
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 reload = () => {
loading.value = true;
// 加载项目成员
listAssetsUser({assetsId:props.data?.assetsId}).then(data => {
assetsUsers.value = data
})
// 加载企业信息
const companyId = props.data?.companyId;
// pageCompany({companyId}).then(res => {
// form.tenantId = undefined;
// const list = res?.list;
// if(list && list.length > 0){
// form.tenantId = list[0].tenantId;
// }
// })
};
watch(
() => props.visible,
(visible) => {
if (visible) {
content.value = "";
requirement.value = "";
if (props.data) {
assignObject(form, props.data);
images.value = [];
// if (props.data.file1) {
// file1.value = JSON.parse(props.data.file1);
// }
if (props.data.openPort) {
form.openPort = props.data.openPort.split(",");
}
if (props.data.configuration) {
form.configuration = props.data.configuration.split(",");
}
isUpdate.value = true;
reload();
} else {
isUpdate.value = false;
editStatus.value = true;
}
} else {
resetFields();
}
}
);
</script>
<style lang="less">
.tab-pane {
min-height: 300px;
}
.ml-10 {
margin-left: 5px;
}
.upload-text {
margin-right: 70px;
}
.upload-image {
margin-bottom: 30px;
display: flex;
justify-content: center;
}
</style>
@@ -0,0 +1,259 @@
<template>
<a-card
title="续费管理"
:bordered="false"
v-if="hasRole('superAdmin') || hasRole('admin')"
>
<template #extra>
<a-button @click="addRecord">添加</a-button>
</template>
<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>
<th width="360">描述</th>
<th style="text-align: center">操作</th>
</tr>
</thead>
<tr v-for="(item, index) in list" :key="index">
<td>
<template v-if="item.editStatus">
<a-select placeholder="请选择" v-model:value="item.status">
<a-select-option :value="0">待缴费</a-select-option>
<a-select-option :value="1">已缴费</a-select-option>
</a-select>
</template>
<template v-else>
<span v-if="item.status == 1">已缴费</span>
<span v-if="item.status == 0">待缴费</span>
</template>
</td>
<td>
<template v-if="item.editStatus">
<a-input-number
:min="0"
:max="999999"
class="ele-fluid"
placeholder="请输入续费金额"
v-model:value="item.money"
/>
</template>
<template v-else> ¥{{ item.money }} </template>
</td>
<td>
<template v-if="item.editStatus">
<a-date-picker
placeholder="开始日期"
value-format="YYYY-MM-DD"
v-model:value="item.startTime"
/>
</template>
<template v-else>
{{ toDateString(item.startTime, 'yyyy-MM-dd') }}
</template>
</td>
<td>
<template v-if="item.editStatus">
<a-date-picker
placeholder="结束日期"
value-format="YYYY-MM-DD"
v-model:value="item.endTime"
/>
</template>
<template v-else>
{{ toDateString(item.endTime, 'yyyy-MM-dd') }}
</template>
</td>
<td>
<template v-if="item.editStatus">
<a-input
class="ele-fluid"
placeholder="请输入描述内容"
v-model:value="item.comments"
/>
<a-upload
v-model:file-list="item.images"
class="upload-list-inline"
list-type="picture"
:before-upload="beforeUpload"
>
<a-button>
<UploadOutlined />
上传附件
</a-button>
</a-upload>
</template>
<template v-else>
<div class="comments">{{ item.comments }}</div>
<div class="files">
<a-upload
v-model:file-list="item.images"
class="upload-list-inline"
>
</a-upload>
</div>
</template>
</td>
<td style="text-align: center">
<template v-if="item.editStatus">
<a-space>
<a @click="save(item, index)">保存</a>
<a-divider type="vertical" />
<a-popconfirm title="确定要删除此记录吗?" @confirm="remove(index)">
<a class="ele-text-info">删除</a>
</a-popconfirm>
</a-space>
</template>
<template v-else>
<a-space>
<a @click="openEdit(index)">编辑</a>
<a-divider type="vertical" v-if="hasRole('superAdmin')" @confirm="removeRel(index)" />
<a-popconfirm title="确定要删除此记录吗?" v-if="hasRole('superAdmin')" @confirm="removeRel(index)">
<a class="ele-text-info">删除</a>
</a-popconfirm>
</a-space>
</template>
</td>
</tr>
</table>
</div>
</a-card>
</template>
<script setup lang="ts">
import { hasRole } from '@/utils/permission';
import { ref, watch } from 'vue';
import { addAppRenew, pageAppRenew, removeAppRenew } from "@/api/oa/app/renew";
import { AppRenew } from '@/api/oa/app/renew/model';
import { useUserStore } from '@/store/modules/user';
import { message } from 'ant-design-vue';
import { toDateString } from 'ele-admin-pro';
import { ItemType } from "ele-admin-pro/es/ele-image-upload/types";
import { uploadOss } from "@/api/system/file";
import { isImage } from "@/utils/common";
const props = defineProps<{
editStatus?: boolean;
appId?: number;
companyId?: number;
}>();
const userStore = useUserStore();
const list = ref<AppRenew[]>([]);
const files = ref<ItemType[]>([]);
const addRecord = () => {
list.value.unshift({
money: undefined,
comments: undefined,
startTime: undefined,
endTime: undefined,
nickname: userStore.info?.nickname,
status: 0,
images: undefined,
editStatus: true
});
};
// 文件上传事件
const beforeUpload = (file: File) => {
const item: ItemType = {
file,
uid: (file as any).uid,
name: file.name
}
if (!file.type.startsWith("image")) {
if (file.size / 1024 / 1024 > 100) {
message.error("大小不能超过 100MB");
return;
}
}
onUpload(item);
return false;
};
const onUpload = (d: ItemType) => {
uploadOss(<File>d.file)
.then((result) => {
files.value.push({
uid: result.id,
url: result.path,
name: isImage(result.path) ? 'image' : result.name,
status: "done"
});
message.success("上传成功");
})
.catch((e) => {
message.error(e.message);
});
};
const remove = (index: number) => {
list.value.splice(index, 1);
};
const openEdit = (index: number) => {
list.value[index].editStatus = true
}
const removeRel = (index: number) => {
removeAppRenew(list.value[index].appRenewId).then(() => {
list.value.splice(index, 1);
message.success('删除成功')
})
}
const save = (item: AppRenew, index: number) => {
item.appId = props.appId;
item.companyId = props.companyId;
item.startTime = item.startTime ? item.startTime + ' 00:00:00' : '';
item.endTime = item.endTime ? item.endTime + ' 23:59:59' : '';
item.nickname = userStore.info?.nickname;
item.userId = userStore.info?.userId;
if(files.value.length > 0){
item.images = JSON.stringify(files.value)
}
if (item.money == undefined || item.money == 0) {
message.error('请填写金额');
return false;
}
addAppRenew(item).then(() => {
list.value[index].editStatus = false;
message.success('保存成功');
});
};
const reload = () => {
pageAppRenew({ appId: props.appId }).then((res) => {
if (res?.list) {
list.value = res?.list.map((d) => {
d.editStatus = false;
if(d.images){
d.images = JSON.parse(d.images);
}
return d;
});
}
});
};
watch(
() => props.appId,
(options) => {
if (options) {
reload();
}
},
{
immediate: true
}
);
</script>
<style scoped lang="less"></style>
@@ -0,0 +1,160 @@
<!-- 搜索表单 -->
<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
danger
type="primary"
class="ele-btn-icon"
:disabled="selection?.length === 0"
@click="removeBatch"
>
<template #icon>
<DeleteOutlined />
</template>
<span>批量删除</span>
</a-button>
<a-radio-group v-model:value="type" @change="handleSearch">
<a-radio-button value="全部">全部({{ conut.totalNum }})</a-radio-button>
<a-radio-button value="运行中"
>运行中({{ conut.totalNum1 }})</a-radio-button
>
<a-radio-button value="已到期"
>已到期({{ conut.totalNum2 }})</a-radio-button
>
<a-radio-button value="已销毁"
>已销毁({{ conut.totalNum3 }})</a-radio-button
>
</a-radio-group>
<SelectCompany
:placeholder="`按企业筛选`"
v-model:value="where.companyName"
@done="chooseCompanyName"
/>
<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, DeleteOutlined } from '@ant-design/icons-vue';
import useSearch from '@/utils/use-search';
import { ref, watch } from 'vue';
import { AssetsParam } from '@/api/oa/assets/model';
import { Company } from '@/api/system/company/model';
import { getCount } from '@/api/oa/assets';
const props = withDefaults(
defineProps<{
// 选中的角色
selection?: [];
}>(),
{}
);
const emit = defineEmits<{
(e: 'search', where?: AssetsParam): void;
(e: 'add'): void;
(e: 'remove'): void;
(e: 'advanced'): void;
}>();
// 下来选项
// 日期范围选择
const dateRange = ref<[string, string]>(['', '']);
const type = ref<any>('开发中');
// 表单数据
const { where, resetFields } = useSearch<AssetsParam>({
assetsId: undefined,
userId: undefined,
status: undefined,
companyId: undefined,
showExpiration: '',
companyName: '',
keywords: ''
});
// 统计数据
const conut = ref({
totalNum: 0,
totalNum1: 0,
totalNum2: 0,
totalNum3: 0,
totalNum4: 0,
totalNum5: 0
});
/* 搜索 */
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 chooseCompanyName = (data: Company) => {
where.companyName = data.companyName;
where.companyId = data.companyId;
emit('search', where);
};
const handleSearch = () => {
where.showExpiration = undefined;
where.status = type.value;
if (type.value == '全部') {
where.status = undefined;
where.showExpiration = undefined;
} else {
where.status = type.value;
if (type.value == '未签续费') {
where.sort = 'expirationTime';
where.order = 'asc';
where.showExpiration = false;
where.status = '运行中';
} else {
where.sort = undefined;
where.order = undefined;
}
}
emit('search', where);
};
/* 重置 */
const reset = () => {
resetFields();
search();
};
getCount().then((data: any) => {
conut.value = data;
});
watch(
() => props.selection,
() => {}
);
</script>
@@ -0,0 +1,152 @@
<template>
<a-card
title="待处理工单"
:bordered="false"
v-if="hasRole('superAdmin') || hasRole('admin')"
>
<template #extra>
<a href="/oa/task">查看更多</a>
</template>
<div class="content">
<a-space style="margin-bottom: 15px" v-if="editStatus" />
<table class="ele-table ele-table-stripe ele-table-medium">
<thead>
<tr>
<th width="80">工单号</th>
<th width="100">工单类型</th>
<th>问题描述</th>
<th width="150">创建时间</th>
<th width="100">状态</th>
</tr>
</thead>
<tbody v-for="(item, index) in list" :key="index">
<tr>
<td>{{ item.taskId }}</td>
<td>{{ item.taskType }}</td>
<td>
<div class="user-box">
<div class="user-info">
<a-badge
:dot="!item.isRead && item.userId != userStore.info?.userId"
>
<a-avatar :src="item.lastAvatar" size="large" />
</a-badge>
</div>
<div
class="content"
style="display: flex; flex-direction: column"
>
<div class="nickname">
<span class="ele-text-secondary">{{
item.lastNickname ? item.lastNickname : '未知'
}}</span>
<span
class="ele-text-placeholder"
style="padding-left: 10px"
>{{ timeAgo(item.updateTime) }}</span
>
</div>
<div class="text">
<a
class="ele-text-heading"
@click="openUrl('/oa/task/detail/' + item.taskId)"
>{{ item.content }}</a
>
</div>
</div>
<div class="last-time ele-text-info"> </div>
</div>
</td>
<td>{{ toDateString(item.createTime, 'MM月dd日 HH:mm') }}</td>
<td>
<a-tag v-if="item.progress === TOBEARRANGED" color="red"
>待安排</a-tag
>
<a-tag v-if="item.progress === PENDING" color="orange"
>待处理</a-tag
>
<a-tag v-if="item.progress === PROCESSING" color="purple"
>处理中</a-tag
>
<a-tag v-if="item.progress === TOBECONFIRMED" color="cyan"
>待评价</a-tag
>
<a-tag v-if="item.progress === COMPLETED" color="green"
>已完成</a-tag
>
<a-tag v-if="item.progress === CLOSED">已关闭</a-tag>
</td>
</tr>
</tbody>
</table>
</div>
</a-card>
</template>
<script setup lang="ts">
import { hasRole } from '@/utils/permission';
import { ref, watch } from 'vue';
import { pageTask } from '@/api/oa/task';
import { Task } from '@/api/oa/task/model';
import {
CLOSED,
COMPLETED,
PENDING,
PROCESSING,
TOBEARRANGED,
TOBECONFIRMED
} from '@/api/oa/task/model/progress';
import { openUrl } from '@/utils/common';
import { timeAgo, toDateString } from 'ele-admin-pro';
import { useUserStore } from '@/store/modules/user';
const props = defineProps<{
editStatus?: boolean;
appId?: number;
companyId?: number;
}>();
const list = ref<Task[]>([]);
const userStore = useUserStore();
const reload = () => {
pageTask({ appId: props.appId }).then((res) => {
if (res?.list) {
list.value = res?.list;
}
});
};
watch(
() => props.appId,
(options) => {
if (options) {
reload();
}
},
{
immediate: true
}
);
</script>
<style scoped lang="less">
.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%;
}
}
}
</style>
@@ -0,0 +1,454 @@
<!-- 用户编辑弹窗 -->
<template>
<ele-modal
:width="1000"
:visible="visible"
:maskClosable="false"
:maxable="maxable"
:title="isUpdate ? '编辑服务器' : '新增服务器'"
:body-style="{ paddingBottom: '28px' }"
@update:visible="updateVisible"
@ok="save"
>
<div style="background-color: #f3f3f3; padding: 8px">
<a-card title="基本信息" :bordered="false">
<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-row :gutter="16">
<a-col
v-bind="
styleResponsive ? { md: 12, sm: 24, xs: 24 } : { span: 8 }
"
>
<a-form-item label="公网IP" name="code">
<a-input
allow-clear
:maxlength="20"
placeholder="请输入合法的IP地址"
v-if="editStatus"
v-model:value="form.code"
/>
<span v-else class="ele-text-primary">{{ form.code }}</span>
</a-form-item>
<a-form-item label="品牌厂商" name="brand">
<DictSelect
dict-code="serverBrand"
placeholder="请选择品牌厂商"
v-if="editStatus"
v-model:value="form.brand"
/>
<span v-else class="ele-text-primary">{{ form.brand }}</span>
</a-form-item>
<a-form-item label="所属企业" name="companyName">
<SelectCompany
:placeholder="`选择所属企业`"
v-model:value="form.companyName"
v-if="editStatus"
@done="chooseCompanyId"
/>
<span v-else class="ele-text-primary">{{
form.companyName
}}</span>
</a-form-item>
<a-form-item label="描述" name="name">
<a-input
allow-clear
placeholder="请输入服务器描述"
v-if="editStatus"
v-model:value="form.name"
/>
<span v-else class="ele-text-primary">{{ form.name }}</span>
</a-form-item>
<a-form-item label="服务器配置" name="configuration">
<a-select
v-if="editStatus"
v-model:value="form.configuration"
mode="tags"
placeholder="请填写服务器配置"
/>
<a-tag v-else v-for="item in form.configuration">
{{ item }}
</a-tag>
</a-form-item>
</a-col>
<a-col
v-bind="
styleResponsive ? { md: 12, sm: 24, xs: 24 } : { span: 8 }
"
>
<a-form-item label="宝塔面板" name="panel">
<a-input
allow-clear
placeholder="https://47.106.145.40:20780/603f8087"
v-model:value="form.panel"
v-if="editStatus"
/>
<a
:href="form.panel"
target="_blank"
v-else
class="ele-text-primary"
>{{ form.panel }}</a
>
</a-form-item>
<a-form-item label="宝塔面板账号" name="panelAccount">
<a-input
allow-clear
:maxlength="20"
placeholder="宝塔面板账号"
v-if="editStatus"
v-model:value="form.panelAccount"
/>
<span v-else class="ele-text-primary">{{
form.panelAccount
}}</span>
</a-form-item>
<a-form-item label="宝塔面板密码" name="panelPassword">
<a-input
allow-clear
:maxlength="20"
placeholder="宝塔面板密码"
v-if="editStatus"
v-model:value="form.panelPassword"
/>
<span v-else class="ele-text-primary">{{
form.panelPassword
}}</span>
</a-form-item>
<a-form-item label="开放端口" name="openPort">
<a-select
v-if="editStatus"
v-model:value="form.openPort"
mode="tags"
placeholder="请选择开放端口"
/>
<a-tag v-else v-for="item in form.openPort">
{{ item }}
</a-tag>
</a-form-item>
<a-form-item label="状态" name="status">
<DictSelect
dict-code="serverStatus"
placeholder="请选择状态"
v-if="editStatus"
v-model:value="form.status"
/>
<template v-else>
<a-tag :color="form.status == '正常' ? 'green' : ''">
{{ form.status }}
</a-tag>
</template>
</a-form-item>
</a-col>
<a-col
v-bind="
styleResponsive ? { md: 12, sm: 24, xs: 24 } : { span: 8 }
"
>
<a-form-item label="宝塔API接口秘钥" name="btSign">
<a-input
allow-clear
:maxlength="50"
placeholder="宝塔API接口秘钥"
v-if="editStatus"
v-model:value="form.btSign"
/>
<span v-else class="ele-text-primary">{{ form.btSign }}</span>
</a-form-item>
</a-col>
<a-col
v-bind="
styleResponsive ? { md: 12, sm: 24, xs: 24 } : { span: 8 }
"
>
<a-form-item label="排序" name="sortNumber">
<a-input
allow-clear
:maxlength="20"
placeholder="排序"
v-if="editStatus"
v-model:value="form.sortNumber"
/>
<span v-else class="ele-text-primary">{{
form.sortNumber
}}</span>
</a-form-item>
</a-col>
</a-row>
</a-form>
</a-card>
</div>
</ele-modal>
</template>
<script lang="ts" setup>
import { ref, reactive, watch } from 'vue';
import { Form, message } from 'ant-design-vue';
import { assignObject, ipReg, isChinese } from 'ele-admin-pro';
import { addAssets, updateAssets } from '@/api/oa/assets';
import type { Assets } from '@/api/oa/assets/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 { uploadFile } from '@/api/system/file';
import { TOKEN_STORE_NAME } from '@/config/setting';
import { useThemeStore } from '@/store/modules/theme';
import { storeToRefs } from 'pinia';
// 是否是修改
const isUpdate = ref(false);
const useForm = Form.useForm;
// 是否开启响应式布局
const themeStore = useThemeStore();
const { styleResponsive } = storeToRefs(themeStore);
const props = defineProps<{
// 弹窗是否打开
visible: boolean;
// 修改回显的数据
data?: Assets | 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 assetsQrcode = ref<ItemType[]>([]);
const images = ref<ItemType[]>([]);
const editStatus = ref(true);
// 日期范围选择
const content = ref('');
const token = localStorage.getItem(TOKEN_STORE_NAME);
const formRef = ref<FormInstance | null>(null);
const form = reactive<Assets>({
type: '服务器',
code: '',
createTime: '',
endTime: '',
name: '',
brand: undefined,
account: '',
password: '',
panelAccount: '',
panelPassword: '',
panel: '',
financeAmount: 0,
financeYears: 0,
financeRenew: 0,
customerId: undefined,
customerName: '',
companyName: '',
financeCustomerName: '',
financeCustomerContact: '',
financeCustomerPhone: '',
brandAccount: '',
brandPassword: '',
btSign: '',
openPort: '',
configuration: '',
comments: '',
root: 'root',
sortNumber: 100,
status: '运行中',
assetsId: 0,
visibility: 'private',
userList: [],
userId: undefined,
// 成员管理
users: []
});
/* 更新visible */
const updateVisible = (value: boolean) => {
emit('update:visible', value);
};
// 表单验证规则
const rules = reactive({
code: [
{
required: true,
type: 'string',
message: '请输入公网IP地址',
trigger: 'blur'
}
],
companyName: [
{
required: true,
type: 'string',
message: '请选择所属企业',
trigger: 'blur'
}
],
assetsCode: [
{
required: true,
type: 'string',
message: '请输入应用标识(英文字母)',
trigger: 'blur',
validator: async (_rule: RuleObject, value: string) => {
if (isChinese(value)) {
return Promise.reject('请输入正确的应用标识');
}
return Promise.resolve();
}
}
],
assetsType: [
{
required: true,
message: '请选择项目类型'
}
],
ipAddress: [
{
pattern: ipReg,
message: 'IP地址不合法',
type: 'string'
}
],
assetsStatus: [
{
required: true,
type: 'string',
message: '请选择应用状态',
trigger: 'blur'
}
]
});
const { resetFields, validate } = useForm(form, rules);
// 所属用户
const chooseCompanyId = (data: Company) => {
form.companyName = data.companyName;
form.companyId = data.companyId;
};
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const formData = {
...form,
content: content.value,
openPort: form.openPort.toString(),
configuration: form.configuration.toString()
};
const saveOrUpdate = isUpdate.value ? updateAssets : addAssets;
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 onUploadIcon = (item) => {
const { file } = item;
logo.value = [];
uploadFile(file)
.then((data) => {
logo.value.push({
uid: data.id,
url: data.url,
status: 'done'
});
message.success('上传成功');
})
.catch((e) => {
message.error(e.message);
});
};
const onUploadQrcode = (item) => {
const { file } = item;
assetsQrcode.value = [];
uploadFile(file)
.then((data) => {
assetsQrcode.value.push({
uid: data.id,
url: data.url,
status: 'done'
});
message.success('上传成功');
})
.catch((e) => {
message.error(e.message);
});
};
// 上传文件
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);
});
};
const reload = () => {
loading.value = true;
};
watch(
() => props.visible,
(visible) => {
if (visible) {
content.value = '';
logo.value = [];
images.value = [];
assetsQrcode.value = [];
if (props.data) {
assignObject(form, props.data);
isUpdate.value = true;
reload();
} else {
isUpdate.value = false;
}
} else {
resetFields();
}
}
);
</script>
@@ -0,0 +1,131 @@
<template>
<a-descriptions title="基本信息" :column="2" bordered>
<template #extra>
<a @click="openEdit">编辑</a>
</template>
<a-descriptions-item
label="公网IP"
:labelStyle="{ width: '200px', color: '#808080' }"
>
<div style="min-width: 200px">{{ data.code }}</div>
</a-descriptions-item
>
<a-descriptions-item
label="状态"
:labelStyle="{ width: '200px', color: '#808080' }"
>
<div style="min-width: 200px"><a-badge status="processing" :text="data.status" /></div>
</a-descriptions-item>
<a-descriptions-item
label="品牌厂商"
:labelStyle="{ width: '200px', color: '#808080' }"
>{{ data.brand }}</a-descriptions-item
>
<a-descriptions-item
label="描述"
:labelStyle="{ width: '200px', color: '#808080' }"
>
{{ data.name }}
</a-descriptions-item>
<a-descriptions-item
label="所属企业"
:labelStyle="{ width: '200px', color: '#808080' }"
>
{{ data.companyName }}
</a-descriptions-item>
<a-descriptions-item
label="服务器配置"
:labelStyle="{ width: '200px', color: '#808080' }"
>
{{ data.configuration }}
</a-descriptions-item>
<a-descriptions-item
label="宝塔面板"
:labelStyle="{ width: '200px', color: '#808080' }"
>
{{ data.panel }} <a style="margin-left: 12px" @click="openNew(`${data.panel}`)"><ExportOutlined /></a>
</a-descriptions-item>
<a-descriptions-item
label="开放端口"
:span="2"
layout="vertical"
:labelStyle="{ width: '200px', color: '#808080' }"
>
<a-tag v-for="item in data.openPort.split(',')">
{{ item }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item
label="宝塔面板账号"
layout="vertical"
:labelStyle="{ width: '200px', color: '#808080' }"
>
{{ data.panelAccount }}
</a-descriptions-item>
<a-descriptions-item
label="宝塔API接口秘钥"
layout="vertical"
:labelStyle="{ width: '200px', color: '#808080' }"
>
<span>****</span>
</a-descriptions-item>
<a-descriptions-item
label="宝塔面板密码"
layout="vertical"
:labelStyle="{ width: '200px', color: '#808080' }"
>
{{ data.panelPassword }}
</a-descriptions-item>
<a-descriptions-item
label="排序"
layout="vertical"
:labelStyle="{ width: '200px', color: '#808080' }"
>
{{ data.sortNumber }}
</a-descriptions-item>
<a-descriptions-item
label="备注"
layout="vertical"
:span="2"
:labelStyle="{ width: '200px', color: '#808080' }"
>
<byte-md-viewer :value="data.comments" />
</a-descriptions-item>
</a-descriptions>
<!-- 编辑弹窗 -->
<AssetsInfoEdit v-model:visible="showEdit" :data="data" @done="reload" />
</template>
<script lang="ts" setup>
import { Assets } from '@/api/oa/assets/model';
import { ref } from 'vue';
import AssetsInfoEdit from './assets-info-edit.vue';
import { ExportOutlined } from '@ant-design/icons-vue';
import {openNew} from "@/utils/common";
defineProps<{
data: Assets;
logo: [] | any;
assetsQrcode: any | undefined;
}>();
const emit = defineEmits<{
(e: 'done'): void;
}>();
// 是否显示编辑弹窗
const showEdit = ref(false);
const showAssetsSecretForm = ref<boolean>(false);
const openAssetsSecretForm = () => {
showAssetsSecretForm.value = true;
};
/* 打开编辑弹窗 */
const openEdit = () => {
showEdit.value = true;
};
const reload = () => {
emit('done');
};
</script>
@@ -0,0 +1,95 @@
<template>
<a-space style="margin-bottom: 20px">
<SelectStaff
v-if="hasRole('superAdmin') || hasRole('admin')"
:placeholder="`添加成员`"
@done="addAssetsDevUser"
/>
<a-button>邀请添加</a-button>
</a-space>
<div class="content">
<table class="ele-table">
<thead>
<tr>
<th>用户ID</th>
<th>角色</th>
<th>昵称</th>
<th>加入时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="(user, index) in data.users" :key="index">
<td>
<span style="padding-left: 5px">{{ user.userId }}</span>
</td>
<td>
<span style="padding-left: 5px">{{ user.nickname }}</span>
</td>
<td>
<span v-if="user.role === 20">项目成员</span>
<span v-if="user.role === 30">管理员</span>
</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 { Assets } from '@/api/oa/assets/model';
import { hasRole } from '@/utils/permission';
import { AssetsUser } from '@/api/oa/assets/user/model';
import { addAssetsUser, removeAssetsUser } from '@/api/oa/assets/user';
import { message } from 'ant-design-vue';
const props = defineProps<{
assetsId: any;
data: Assets;
}>();
const emit = defineEmits<{
(e: 'done'): void;
(e: 'update:visible', visible: boolean): void;
}>();
// 添加开发成员
const addAssetsDevUser = (data: User) => {
addAssetsUser({
userId: data.userId,
assetsId: props.data?.assetsId,
role: 20,
nickname: data.nickname
})
.then((msg) => {
message.success(msg);
emit('done');
})
.catch((e) => {
message.error(e.message);
});
};
// 移除成员
const removeUser = (data: AssetsUser) => {
removeAssetsUser(data.id)
.then((msg) => {
message.success(msg);
emit('done');
})
.catch((e) => {
message.error(e.message);
});
};
</script>
+174
View File
@@ -0,0 +1,174 @@
<template>
<a-page-header
:title="title"
:style="{ padding: screenWidth > 480 ? '16px 24px' : '0' }"
@back="() => $router.go(-1)"
>
<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="base">
<AssetsInfo
:data="form"
:logo="logo"
:assets-qrcode="assetsQrcode"
@done="reload"
/>
</a-tab-pane>
<a-tab-pane
tab="成员管理"
key="users"
v-if="hasRole('superAdmin') || hasRole('admin')"
>
<AssetsUsers :assets-id="assetsId" :data="form" @done="reload" />
</a-tab-pane>
</a-tabs>
</a-card>
<a-card v-else :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('/assets/server')"
>返回</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 AssetsInfo from './components/assets-info.vue';
import AssetsUsers from './components/assets-users.vue';
import { getAssets } from '@/api/oa/assets';
import useFormData from '@/utils/use-form-data';
import { Assets } from '@/api/oa/assets/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 { openUrl } from '@/utils/common';
import { pageAssetsUser } from '@/api/oa/assets/user';
const { currentRoute } = useRouter();
// 当前选项卡
const active = ref('base');
// 是否开启响应式布局
const themeStore = useThemeStore();
const { screenWidth } = storeToRefs(themeStore);
const title = ref('项目名称');
const spinning = ref(true);
const isShow = ref(true);
const logo = ref<any[]>([]);
const assetsId = ref<number>(0);
const assetsQrcode = ref<any[]>([]);
const images = ref<ItemType[]>([]);
// 服务器信息
const { form, assignFields, resetFields } = useFormData<Assets>({
type: "服务器",
code: "",
createTime: "",
endTime: "",
name: "",
brand: undefined,
account: "",
password: "",
panelAccount: "",
panelPassword: "",
panel: "",
financeAmount: 0,
financeYears: 0,
financeRenew: 0,
customerId: undefined,
customerName: "",
companyName: "",
financeCustomerName: "",
financeCustomerContact: "",
financeCustomerPhone: "",
brandAccount: "",
brandPassword: "",
btSign: "",
openPort: "",
configuration: "",
comments: "",
root: "root",
sortNumber: 100,
status: "0",
assetsId: 0,
visibility: "private",
userList: [],
userId: undefined,
// 成员管理
users: [],
});
const onChange = () => {
reload();
};
/**
* 加载数据
*/
const reload = () => {
resetFields();
// 加载项目详情
getAssets(assetsId.value)
.then((data) => {
console.log(data)
assignFields(data);
if (data.code) {
title.value = data.code;
// 修改页签标题
setPageTabTitle(data.code);
}
isShow.value = true;
spinning.value = false;
})
.catch((err) => {
console.log(err)
isShow.value = false;
spinning.value = false;
});
// 加载项目成员
pageAssetsUser({ assetsId: assetsId.value }).then((res) => {
if (res?.list) {
form.users = res.list;
}
});
};
watch(
currentRoute,
(route) => {
const { params } = unref(route);
const { id } = params;
if (id) {
assetsId.value = Number(id);
}
reload();
},
{ immediate: true }
);
</script>
<script lang="ts">
export default {
name: 'AssetsDetail'
};
</script>
@@ -0,0 +1,178 @@
<!-- 分类编辑弹窗 -->
<template>
<ele-modal
:width="460"
: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: 5, sm: 5, xs: 24 } : { flex: '90px' }"
:wrapper-col="
styleResponsive ? { md: 19, sm: 19, xs: 24 } : { flex: '1' }
"
>
<a-form-item label="分类标识" name="dictCode">
<a-input
allow-clear
:maxlength="20"
disabled
placeholder="请输入分类标识"
v-model:value="form.dictCode"
/>
</a-form-item>
<a-form-item label="分类名称" name="dictDataName">
<a-input
allow-clear
:maxlength="20"
placeholder="请输入分类名称"
v-model:value="form.dictDataName"
/>
</a-form-item>
<a-form-item label="排序" name="sortNumber">
<a-input-number
:min="0"
:max="99999"
class="ele-fluid"
placeholder="请输入排序号"
v-model:value="form.sortNumber"
/>
</a-form-item>
<a-form-item label="备注">
<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 { addDictData, updateDictData } from '@/api/system/dict-data';
import { DictData } from '@/api/system/dict-data/model';
import {removeSiteInfoCache} from "@/api/cms/website";
// 是否开启响应式布局
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?: DictData | null;
// 字典ID
dictId?: number | 0;
}>();
//
const formRef = ref<FormInstance | null>(null);
// 是否是修改
const isUpdate = ref(false);
// 提交状态
const loading = ref(false);
// 表单数据
const { form, resetFields, assignFields } = useFormData<DictData>({
dictId: undefined,
dictDataId: undefined,
dictDataName: '',
dictCode: 'appType',
dictDataCode: '',
sortNumber: 100,
comments: ''
});
// 表单验证规则
const rules = reactive<Record<string, Rule[]>>({
dictDataCode: [
{
required: true,
message: '请输入分类名称',
type: 'string',
trigger: 'blur'
}
],
dictCode: [
{
required: true,
message: '请输入分类标识',
type: 'string',
trigger: 'blur'
}
]
});
/* 保存编辑 */
const save = () => {
if (!formRef.value) {
return;
}
formRef.value
.validate()
.then(() => {
loading.value = true;
const saveOrUpdate = isUpdate.value ? updateDictData : addDictData;
form.dictDataCode = form.dictDataName;
form.dictId = props.dictId;
saveOrUpdate(form)
.then((msg) => {
loading.value = false;
message.success(msg);
// 清除字典缓存
removeSiteInfoCache(form.dictCode + ':' + form.tenantId);
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>
+222
View File
@@ -0,0 +1,222 @@
<template>
<div class="ele-body">
<a-card :bordered="false">
<!-- 表格 -->
<ele-pro-table
ref="tableRef"
row-key="dictDataId"
:columns="columns"
:datasource="datasource"
:customRow="customRow"
:scroll="{ x: 800 }"
cache-key="appDictTable"
>
<template #toolbar>
<a-space>
<a-button type="primary" class="ele-btn-icon" @click="openEdit()">
<template #icon>
<plus-outlined />
</template>
<span>新建</span>
</a-button>
<a-button
danger
type="primary"
class="ele-btn-icon"
@click="removeBatch"
>
<template #icon>
<delete-outlined />
</template>
<span>删除</span>
</a-button>
</a-space>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'action'">
<a-space>
<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>
</a-space>
</template>
</template>
</ele-pro-table>
</a-card>
<!-- 编辑弹窗 -->
<dict-edit
v-model:visible="showEdit"
:dictId="dictId"
:data="current"
@done="reload"
/>
</div>
</template>
<script lang="ts" setup>
import { createVNode, ref } from 'vue';
import { message, Modal } from 'ant-design-vue/es';
import {
PlusOutlined,
DeleteOutlined,
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 } from 'ele-admin-pro/es';
import DictEdit from './components/dict-edit.vue';
import {
pageDictData,
removeDictData,
removeDictDataBatch
} from '@/api/system/dict-data';
import { DictParam } from '@/api/system/dict/model';
import { DictData } from '@/api/system/dict-data/model';
import { addDict, listDictionaries } from '@/api/system/dict';
import { Dictionary } from '@/api/system/dictionary/model';
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
const dictId = ref(0);
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: 'ID',
dataIndex: 'dictDataId',
width: 80
},
{
title: '分类名称',
dataIndex: 'dictDataName',
showSorterTooltip: false
},
{
title: '备注',
dataIndex: 'comments',
sorter: true,
showSorterTooltip: false
},
{
title: '排序号',
width: 180,
align: 'center',
dataIndex: 'sortNumber'
},
{
title: '操作',
key: 'action',
width: 180,
align: 'center'
}
]);
// 表格选中数据
const selection = ref<DictData[]>([]);
// 当前编辑数据
const current = ref<Dictionary | null>(null);
// 是否显示编辑弹窗
const showEdit = ref(false);
// 表格数据源
const datasource: DatasourceFunction = ({ page, limit, where, orders }) => {
where.dictCode = 'appType';
return pageDictData({ ...where, ...orders, page, limit });
};
/* 搜索 */
const reload = (where?: DictParam) => {
selection.value = [];
tableRef?.value?.reload({ page: 1, where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: DictData) => {
current.value = row ?? null;
showEdit.value = true;
};
/* 删除单个 */
const remove = (row: DictData) => {
const hide = messageLoading('请求中..', 0);
removeDictData(row.dictDataId)
.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);
removeDictDataBatch(selection.value.map((d) => d.dictDataId))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
// 初始化字典
const loadDict = () => {
listDictionaries({ dictCode: 'appType' }).then(async (data) => {
if (data?.length == 0) {
await addDict({ dictCode: 'appType', dictName: '链接分类' });
}
await listDictionaries({ dictCode: 'appType' }).then((list) => {
list?.map((d) => {
dictId.value = Number(d.dictId);
});
});
});
};
loadDict();
/* 自定义行属性 */
const customRow = (record: DictData) => {
return {
onDblclick: () => {
openEdit(record);
}
};
};
</script>
<script lang="ts">
export default {
name: 'TaskDictData'
};
</script>
+371
View File
@@ -0,0 +1,371 @@
<template>
<div class="page">
<div class="ele-body">
<a-card :bordered="false">
<!-- 表格 -->
<ele-pro-table
ref="tableRef"
row-key="assetsId"
:columns="columns"
:datasource="datasource"
v-model:selection="selection"
:customRow="customRow"
tool-class="ele-toolbar-form"
:scroll="{ x: 1200 }"
class="sys-org-table"
:striped="true"
>
<template #toolbar>
<search
@search="reload"
:selection="selection"
@add="openEdit"
@remove="removeBatch"
@advanced="openAdvanced"
/>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'assetsName'">
<div class="assets-box">
<a-image
:height="45"
:width="45"
:preview="false"
:src="record.assetsIcon"
fallback="https://file.wsdns.cn/20230218/550e610d43334dd2a7f66d5b20bd58eb.svg"
/>
<div class="assets-info">
<a
class="ele-text-heading"
@click="openUrl('/assets/server/detail/' + record.assetsId)"
>
{{ record.assetsName }}
</a>
<span class="ele-text-placeholder">
{{ record.assetsCode }}
</span>
</div>
</div>
</template>
<template v-if="column.key === 'code'">
<span class="ele-text-heading">{{ record.code }}</span>
</template>
<template v-if="column.key === 'status'">
<a-badge
v-if="record.status == '运行中'"
status="processing"
:text="record.status"
/>
<a-badge
v-if="record.status == '已到期'"
status="error"
:text="record.status"
/>
</template>
<template v-if="column.key === 'endTime'">
<div class="ele-text-info">{{
toDateString(record.startTime, 'yyyy-MM-dd')
}}</div>
<div v-html="getEndTime(record.endTime)"></div>
</template>
<template v-if="column.key === 'action'">
<a-space>
<a @click="openUrl('/assets/server/detail/' + record.assetsId)"
>详情</a
>
</a-space>
</template>
</template>
</ele-pro-table>
</a-card>
</div>
<!-- 编辑弹窗 -->
<AssetsEdit v-model:visible="showEdit" :data="current" @done="reload" />
</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 { getEndTime, openUrl } from '@/utils/common';
import type {
DatasourceFunction,
ColumnItem
} from 'ele-admin-pro/es/ele-pro-table/types';
import Search from './components/search.vue';
import AssetsEdit from './components/assets-edit.vue';
import {
pageAssets,
removeBatchAssets,
removeAssets,
getCount
} from '@/api/oa/assets';
import { Assets, AssetsParam } from '@/api/oa/assets/model';
import { toDateString } from 'ele-admin-pro';
defineProps<{
activeKey?: boolean;
data?: any;
}>();
// 表格实例
const tableRef = ref<InstanceType<typeof EleProTable> | null>(null);
// 表格列配置
const columns = ref<ColumnItem[]>([
{
title: 'ID',
dataIndex: 'assetsId',
width: 80,
hideInTable: true
},
{
title: '所属企业',
dataIndex: 'companyName',
width: 280,
key: 'companyName'
},
{
title: '服务器IP',
dataIndex: 'code',
align: 'center',
width: 200,
key: 'code'
},
{
title: '状态',
dataIndex: 'status',
sorter: true,
width: 120,
align: 'center',
key: 'status'
},
{
title: '品牌',
width: 120,
align: 'center',
sorter: true,
dataIndex: 'brand'
},
{
title: '付费方式',
dataIndex: 'endTime',
key: 'endTime',
align: 'center',
sorter: true
},
{
title: '描述',
dataIndex: 'name',
key: 'name',
ellipsis: true
},
{
title: '操作',
key: 'action',
width: 120,
align: 'center',
fixed: 'right',
hideInSetting: true
}
]);
// 表格选中数据
const selection = ref<Assets[]>([]);
// 当前编辑数据
const current = ref<Assets | null>(null);
// 是否显示资产详情
// const showInfo = ref(false);
const active = ref<any>('开发中');
// 是否显示编辑弹窗
const showEdit = ref(false);
// 是否显示高级搜索
const showAdvancedSearch = ref(false);
// 表格数据源
const datasource: DatasourceFunction = ({ page, limit, where, orders }) => {
// 默认类型
if (where.keywords == undefined) {
where.assetsStatus = '开发中';
where.showExpiration = undefined;
}
if (where.assetsStatus == '未签续费') {
where.assetsStatus = '已上架';
where.showExpiration = false;
where.sort = 'expirationTime';
where.order = 'asc';
}
return pageAssets({
...where,
...orders,
page,
limit
});
};
/* 搜索 */
const reload = (where?: AssetsParam) => {
selection.value = [];
tableRef?.value?.reload({ where: where });
};
/* 打开编辑弹窗 */
const openEdit = (row?: Assets) => {
current.value = row ?? null;
showEdit.value = true;
};
const expirationTime = (dateTime) => {
const now = new Date().getTime();
const expiration = new Date(dateTime).getTime();
const mss = expiration - now;
let days = Math.floor(mss / (1000 * 60 * 60 * 24));
let hours = Math.floor((mss % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
let minutes = Math.floor((mss % (1000 * 60 * 60)) / (1000 * 60));
let seconds = Math.round((mss % (1000 * 60)) / 1000);
return days;
};
/* 打开高级搜索 */
const openAdvanced = () => {
showAdvancedSearch.value = !showAdvancedSearch.value;
};
/* 删除单个 */
const remove = (row: Assets) => {
const hide = message.loading('请求中..', 0);
removeAssets(row.assetsId)
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
};
/* 批量删除 */
const removeBatch = () => {
console.log(selection.value);
if (!selection.value.length) {
message.error('请至少选择一条数据');
return;
}
Modal.confirm({
title: '提示',
content: '确定要删除选中的记录吗?',
icon: createVNode(ExclamationCircleOutlined),
maskClosable: true,
onOk: () => {
const hide = message.loading('请求中..', 0);
removeBatchAssets(selection.value.map((d) => d.assetsId))
.then((msg) => {
hide();
message.success(msg);
reload();
})
.catch((e) => {
hide();
message.error(e.message);
});
}
});
};
/* 自定义行属性 */
const customRow = (record: Assets) => {
return {
// 行点击事件
onClick: () => {
// console.log(record);
},
// 行双击事件
onDblclick: () => {
openUrl('/assets/server/detail/' + record.assetsId);
}
};
};
// 统计数据
const conut = ref({
totalNum: 0,
totalNum2: 0,
totalNum3: 0,
totalNum4: 0,
totalNum5: 0
});
getCount().then((data: any) => {
conut.value = data;
});
reload();
</script>
<script lang="ts">
export default {
name: 'AssetsIndex'
};
</script>
<style lang="less" scoped>
p {
line-height: 0.8;
}
.sys-org-table :deep(.ant-table-body) {
overflow: auto !important;
overflow: overlay !important;
}
.sys-org-table :deep(.ant-table-pagination.ant-pagination) {
padding: 0 4px;
margin-bottom: 0;
}
.price-edit {
padding-right: 5px;
}
.comments {
max-width: 200px;
}
.assets-box {
display: flex;
.assets-info {
display: flex;
margin-left: 5px;
flex-direction: column;
}
}
.count {
display: flex;
.btn {
margin: 0 12px;
padding: 5px 16px;
background-color: #f3f3f3;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
font-size: 15px;
.divider {
width: 4px;
height: 14px;
border-radius: 8px;
margin-right: 8px;
background-color: #c9c9c9;
}
.divider-active {
width: 4px;
height: 14px;
border-radius: 8px;
margin-right: 8px;
background-color: #16a90b;
}
.active {
color: #16a90b;
}
}
}
</style>