feat: collaboration and statistics

This commit is contained in:
gin
2026-05-15 09:19:09 +08:00
parent cdee21ee8e
commit 2757a4fb49
91 changed files with 4504 additions and 1301 deletions
-24
View File
@@ -1,24 +0,0 @@
node_modules
.DS_Store
dist
dist-ssr
*.local
.eslintcache
report.html
yarn.lock
npm-debug.log*
.pnpm-error.log*
.pnpm-debug.log
tests/**/coverage/
# 本机调试debug配置文件
.vscode/launch.json
# Editor directories and files
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
tsconfig.tsbuildinfo
-31
View File
@@ -1,31 +0,0 @@
{
"recommendations": [
"akamud.vscode-theme-onedark",
"antfu.iconify",
"bradlc.vscode-tailwindcss",
"christian-kohler.npm-intellisense",
"christian-kohler.path-intellisense",
"Codeium.codeium",
"csstools.postcss",
"DavidAnson.vscode-markdownlint",
"dbaeumer.vscode-eslint",
"donjayamanne.githistory",
"dsznajder.es7-react-js-snippets",
"eamodio.gitlens",
"ecmel.vscode-html-css",
"esbenp.prettier-vscode",
"genieai.chatgpt-vscode",
"hollowtree.vue-snippets",
"lokalise.i18n-ally",
"mhutchie.git-graph",
"mikestead.dotenv",
"pmneo.tsimporter",
"streetsidesoftware.code-spell-checker",
"stylelint.vscode-stylelint",
"syler.sass-indented",
"sysoev.language-stylus",
"vscode-icons-team.vscode-icons",
"Vue.volar",
"xabikos.JavaScriptSnippets"
]
}
-32
View File
@@ -1,32 +0,0 @@
{
"editor.formatOnType": true,
"editor.formatOnSave": true,
"[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"editor.tabSize": 2,
"editor.formatOnPaste": true,
"editor.guides.bracketPairs": "active",
"files.autoSave": "afterDelay",
"git.confirmSync": false,
"workbench.startupEditor": "newUntitledFile",
"editor.suggestSelection": "first",
"editor.acceptSuggestionOnCommitCharacter": false,
"css.lint.propertyIgnoredDueToDisplay": "ignore",
"editor.quickSuggestions": {
"other": true,
"comments": true,
"strings": true
},
"files.associations": {
"editor.snippetSuggestions": "top"
},
"[css]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
"iconify.excludes": ["el"],
"cSpell.words": ["iconify", "Qrcode"]
}
-22
View File
@@ -1,22 +0,0 @@
{
"Vue3.0快速生成模板": {
"scope": "vue",
"prefix": "Vue3.0",
"body": [
"<template>",
"\t<div>test</div>",
"</template>\n",
"<script lang='ts'>",
"export default {",
"\tsetup() {",
"\t\treturn {}",
"\t}",
"}",
"</script>\n",
"<style lang='scss' scoped>\n",
"</style>",
"$2"
],
"description": "Vue3.0"
}
}
-17
View File
@@ -1,17 +0,0 @@
{
"Vue3.2+快速生成模板": {
"scope": "vue",
"prefix": "Vue3.2+",
"body": [
"<script setup lang='ts'>",
"</script>\n",
"<template>",
"\t<div>test</div>",
"</template>\n",
"<style lang='scss' scoped>\n",
"</style>",
"$2"
],
"description": "Vue3.2+"
}
}
-20
View File
@@ -1,20 +0,0 @@
{
"Vue3.3+defineOptions快速生成模板": {
"scope": "vue",
"prefix": "Vue3.3+",
"body": [
"<script setup lang='ts'>",
"defineOptions({",
"\tname: ''",
"})",
"</script>\n",
"<template>",
"\t<div>test</div>",
"</template>\n",
"<style lang='scss' scoped>\n",
"</style>",
"$2"
],
"description": "Vue3.3+defineOptions快速生成模板"
}
}
+1
View File
@@ -41,6 +41,7 @@
"jsencrypt": "^3.3.2",
"mitt": "^3.0.0",
"nprogress": "^0.2.0",
"path": "^0.12.7",
"pinia": "^2.1.4",
"pinyin-pro": "^3.15.2",
"cropperjs": "^1.5.13",
@@ -0,0 +1,206 @@
import { http } from "@/utils/http";
export type SettlementStatusValue =
| "NONE"
| "SETTLED"
| "UNSETTLED"
| "PARTIAL";
export type CollaborationFileType = "GOODS_IMAGE" | "ATTACHMENT";
export interface SettlementStatusDTO {
status: SettlementStatusValue;
label: string;
}
export interface CollaborationRecordListCommand extends BasePageQuery {
brand?: string;
goods?: string;
cooperationPlatform?: string;
purchaseBeginTime?: string;
purchaseEndTime?: string;
}
export interface CollaborationTaskCommand {
releaseDate?: string;
}
export interface CollaborationExpenditureCommand {
spendDate?: string;
amount?: number;
purpose?: string;
}
export interface CollaborationSettlementCommand {
settleDate?: string;
method?: string;
income?: number;
purpose?: string;
}
export interface CollaborationFileCommand {
fileType: CollaborationFileType;
url: string;
fileName?: string;
newFileName?: string;
originalFilename?: string;
}
export interface AddCollaborationRecordCommand {
brand: string;
goods: string;
cooperationPlatform?: string;
imageReturnNum: number;
retainedMethod?: string;
cooperatedMethod?: string;
purchaseMethod?: string;
purchasePrice?: number;
purchaseDate?: string;
purchasePlatform?: string;
deadline?: string;
remuneration?: number;
completeDate?: string;
requirements?: string;
remark?: string;
tasks: CollaborationTaskCommand[];
expenditures: CollaborationExpenditureCommand[];
settlements: CollaborationSettlementCommand[];
files: CollaborationFileCommand[];
}
export interface UpdateCollaborationRecordCommand
extends AddCollaborationRecordCommand {
recordId: number;
}
export interface CollaborationRecordPageResponse {
recordId: number;
brand: string;
goods: string;
cooperationPlatform?: string;
imageReturnNum: number;
retainedMethod?: string;
cooperatedMethod?: string;
purchaseMethod?: string;
purchasePrice?: number;
purchaseDate?: string;
purchasePlatform?: string;
deadline?: string;
remuneration?: number;
completeDate?: string;
requirements?: string;
remark?: string;
tasksNum: number;
completedTasksNum: number;
purchaseSettlementStatus: SettlementStatusDTO;
deliverySettlementStatus: SettlementStatusDTO;
remunerationSettlementStatus: SettlementStatusDTO;
createTime: string;
}
export interface CollaborationRecordDetailResponse
extends CollaborationRecordPageResponse {
tasks: Array<
CollaborationTaskCommand & { taskId?: number; sortOrder?: number }
>;
expenditures: Array<
CollaborationExpenditureCommand & { expenditureId?: number }
>;
settlements: Array<
CollaborationSettlementCommand & { settlementId?: number }
>;
files: Array<
CollaborationFileCommand & { fileId?: number; sortOrder?: number }
>;
}
export interface CollaborationOptionResponse {
type: string;
label: string;
values: string[];
}
export interface CollaborationMonthlyStatisticsResponse {
month: number;
purchasePrice: number;
expenditureAmount: number;
settledRemuneration: number;
settledTotal: number;
}
export interface UploadResponse {
url: string;
fileName: string;
newFileName: string;
originalFilename: string;
}
export const getCollaborationRecordListApi = (
params: CollaborationRecordListCommand
) => {
return http.request<ResponseData<PageDTO<CollaborationRecordPageResponse>>>(
"get",
"/collaboration/record/list",
{ params }
);
};
export const getCollaborationRecordInfoApi = (recordId: number) => {
return http.request<ResponseData<CollaborationRecordDetailResponse>>(
"get",
`/collaboration/record/${recordId}`
);
};
export const addCollaborationRecordApi = (
data: AddCollaborationRecordCommand
) => {
return http.request<ResponseData<void>>("post", "/collaboration/record", {
data
});
};
export const updateCollaborationRecordApi = (
data: UpdateCollaborationRecordCommand
) => {
return http.request<ResponseData<void>>("put", "/collaboration/record", {
data
});
};
export const deleteCollaborationRecordApi = (data: Array<number>) => {
return http.request<ResponseData<void>>("delete", "/collaboration/record", {
params: {
ids: data.toString()
}
});
};
export const getCollaborationOptionsApi = () => {
return http.request<ResponseData<CollaborationOptionResponse[]>>(
"get",
"/collaboration/record/options"
);
};
export const getCollaborationMonthlyStatisticsApi = (year: number) => {
return http.request<ResponseData<CollaborationMonthlyStatisticsResponse[]>>(
"get",
"/collaboration/record/monthly-statistics",
{
params: { year }
}
);
};
export const uploadCollaborationFileApi = (data: FormData) => {
return http.request<ResponseData<UploadResponse>>(
"post",
"/file/upload",
{ data },
{
headers: {
"Content-Type": "multipart/form-data"
}
}
);
};
@@ -89,6 +89,7 @@ function handleClose(
:key="index"
v-bind="options"
v-model="options.visible"
:align-center="options.alignCenter ?? true"
:fullscreen="fullscreen ? true : options?.fullscreen ? true : false"
@close="handleClose(options, index)"
@opened="eventsCallBack('open', options, index)"
@@ -40,7 +40,7 @@
class="header-btn"
/>
<el-button
:icon="Close"
:icon="closeIcon"
link
@click="handleCloseClick"
class="header-btn"
@@ -81,7 +81,8 @@
import { computed, ref } from "vue";
import { ElDialog, ElButton, ElScrollbar } from "element-plus";
import { DialogEmits, DialogProps } from "./dialog";
import { Close } from "@element-plus/icons-vue";
import Close from "@iconify-icons/ep/close";
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
import FullScreenMaximize from "@/assets/svg/FullScreenMaximize.svg?component";
import FullScreenMinimize from "@/assets/svg/FullScreenMinimize.svg?component";
@@ -96,6 +97,7 @@ const props = withDefaults(defineProps<DialogProps>(), {
loading: false
});
const emits = defineEmits<DialogEmits>();
const closeIcon = useRenderIcon(Close);
const visible = computed<boolean>({
get: () => {
@@ -107,7 +109,6 @@ const visible = computed<boolean>({
const fullScreenState = ref(!!props.initFullScreen);
const fullScreen = computed<boolean>({
get: () => {
console.log("fullScreen getter", props.fullScreen, fullScreenState.value);
// 非受控模式,状态完全由组件内部控制
if (props.fullScreen === undefined) {
return fullScreenState.value;
@@ -117,7 +118,6 @@ const fullScreen = computed<boolean>({
},
set: v => {
fullScreenState.value = v;
console.log("fullScreen setter", v, props.fullScreen);
// 受控模式,将状态更新到父组件
if (props.fullScreen !== undefined) {
emits("update:fullScreen", v);
@@ -1,4 +1,5 @@
<script setup lang="ts">
import path from "path";
import { getConfig } from "@/config";
import { menuType } from "../../types";
import extraIcon from "./extraIcon.vue";
@@ -174,10 +175,7 @@ function resolvePath(routePath) {
return routePath || props.basePath;
} else {
// 使用path.posix.resolve替代path.resolve 避免windows环境下使用electron出现盘符问题
const segments = `${props.basePath}/${routePath}`
.split("/")
.filter(Boolean);
return `/${segments.join("/")}`;
return path.posix.resolve(props.basePath, routePath);
}
}
</script>
+27
View File
@@ -69,11 +69,38 @@
}
}
.el-overlay-dialog:has(.pure-dialog.el-dialog:not(.is-fullscreen)) {
box-sizing: border-box;
display: flex !important;
align-items: center;
justify-content: center;
padding: 24px 0;
overflow: hidden;
}
.pure-dialog {
&.el-dialog:not(.is-fullscreen) {
display: flex;
flex-direction: column;
max-height: min(88vh, calc(100vh - 48px));
margin: auto !important;
}
.pure-dialog-svg {
color: var(--el-color-info);
}
.el-dialog__header,
.el-dialog__footer {
flex: none;
}
.el-dialog__body {
flex: 1;
min-height: 0;
overflow-y: auto;
}
.el-dialog__headerbtn {
top: 20px;
right: 14px;
@@ -0,0 +1,238 @@
<script setup lang="ts">
import { h, ref } from "vue";
import { PureTableBar } from "@/components/RePureTableBar";
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
import { addDialog } from "@/components/ReDialog";
import { useCollaborationRecordHook } from "./utils/hook";
import { CollaborationRecordPageResponse } from "@/api/collaboration/record";
import RecordFormModal from "./record-form-modal.vue";
import AddFill from "@iconify-icons/ri/add-circle-line";
import Delete from "@iconify-icons/ep/delete";
import EditPen from "@iconify-icons/ep/edit-pen";
import Refresh from "@iconify-icons/ep/refresh";
import Search from "@iconify-icons/ep/search";
defineOptions({
name: "CollaborationRecord"
});
const tableRef = ref();
const searchFormRef = ref();
const {
searchFormParams,
pageLoading,
columns,
dataList,
pagination,
defaultSort,
deadlineRange,
purchaseRange,
optionMap,
multipleSelection,
onSearch,
onSortChanged,
getRecordList,
resetForm,
handleDelete,
handleBulkDelete
} = useCollaborationRecordHook();
const recordFormRef = ref();
function openDialog(
type: "add" | "update",
row?: CollaborationRecordPageResponse
) {
addDialog({
title: type === "add" ? "新增合作记录" : "编辑合作记录",
props: {
type,
row,
optionMap
},
width: "1180px",
top: "6vh",
draggable: true,
fullscreenIcon: true,
closeOnClickModal: false,
contentRenderer: () => h(RecordFormModal as any, { ref: recordFormRef }),
beforeSure: async done => {
const isSuccess = await recordFormRef.value.handleConfirm();
if (isSuccess) {
done();
onSearch(tableRef);
}
}
});
}
</script>
<template>
<div class="main">
<el-form
ref="searchFormRef"
:inline="true"
:model="searchFormParams"
class="search-form bg-bg_color w-[99/100] pl-8 pt-[12px]"
>
<el-form-item label="品牌" prop="brand">
<el-input
v-model="searchFormParams.brand"
placeholder="请输入品牌"
clearable
class="!w-[180px]"
/>
</el-form-item>
<el-form-item label="物品" prop="goods">
<el-input
v-model="searchFormParams.goods"
placeholder="请输入物品"
clearable
class="!w-[180px]"
/>
</el-form-item>
<el-form-item label="合作平台" prop="cooperationPlatform">
<el-select
v-model="searchFormParams.cooperationPlatform"
placeholder="请选择合作平台"
clearable
class="!w-[160px]"
>
<el-option
v-for="item in optionMap.cooperationPlatform"
:key="item"
:label="item"
:value="item"
/>
</el-select>
</el-form-item>
<el-form-item label="预完成日期">
<el-date-picker
v-model="deadlineRange"
value-format="YYYY-MM-DD"
type="daterange"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
class="!w-[240px]"
/>
</el-form-item>
<el-form-item label="购入日期">
<el-date-picker
v-model="purchaseRange"
value-format="YYYY-MM-DD"
type="daterange"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
class="!w-[240px]"
/>
</el-form-item>
<el-form-item>
<el-button
type="primary"
:icon="useRenderIcon(Search)"
:loading="pageLoading"
@click="onSearch(tableRef)"
>
搜索
</el-button>
<el-button
:icon="useRenderIcon(Refresh)"
@click="resetForm(searchFormRef, tableRef)"
>
重置
</el-button>
</el-form-item>
</el-form>
<PureTableBar
title="合作记录"
:columns="columns"
@refresh="onSearch(tableRef)"
>
<template #buttons>
<el-button
type="primary"
:icon="useRenderIcon(AddFill)"
@click="openDialog('add')"
>
新增合作
</el-button>
<el-button
type="danger"
:icon="useRenderIcon(Delete)"
@click="handleBulkDelete(tableRef)"
>
批量删除
</el-button>
</template>
<template v-slot="{ size, dynamicColumns }">
<pure-table
border
ref="tableRef"
align-whole="center"
showOverflowTooltip
table-layout="auto"
:loading="pageLoading"
:size="size"
adaptive
:data="dataList"
:columns="dynamicColumns"
:default-sort="defaultSort"
:pagination="pagination"
:paginationSmall="size === 'small'"
:header-cell-style="{
background: 'var(--el-table-row-hover-bg-color)',
color: 'var(--el-text-color-primary)'
}"
@page-size-change="getRecordList"
@page-current-change="getRecordList"
@sort-change="onSortChanged"
@selection-change="
rows => (multipleSelection = rows.map(item => item.recordId))
"
>
<template #operation="{ row }">
<el-button
class="reset-margin"
link
type="primary"
:size="size"
:icon="useRenderIcon(EditPen)"
@click="openDialog('update', row)"
>
编辑
</el-button>
<el-popconfirm
:title="`是否确认删除编号为${row.recordId}的合作记录`"
@confirm="handleDelete(row)"
>
<template #reference>
<el-button
class="reset-margin"
link
type="danger"
:size="size"
:icon="useRenderIcon(Delete)"
>
删除
</el-button>
</template>
</el-popconfirm>
</template>
</pure-table>
</template>
</PureTableBar>
</div>
</template>
<style scoped lang="scss">
.search-form {
:deep(.el-form-item) {
margin-bottom: 12px;
}
}
</style>
@@ -0,0 +1,558 @@
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from "vue";
import { ElMessage, FormInstance, FormRules } from "element-plus";
import type {
UploadFile,
UploadRequestOptions,
UploadUserFile
} from "element-plus";
import Plus from "@iconify-icons/ep/plus";
import {
AddCollaborationRecordCommand,
CollaborationFileCommand,
CollaborationFileType,
CollaborationRecordPageResponse,
UpdateCollaborationRecordCommand,
addCollaborationRecordApi,
getCollaborationRecordInfoApi,
updateCollaborationRecordApi,
uploadCollaborationFileApi
} from "@/api/collaboration/record";
interface Props {
type: "add" | "update";
row?: CollaborationRecordPageResponse;
optionMap: Record<string, string[]>;
}
const props = defineProps<Props>();
const defaultFormData = (): AddCollaborationRecordCommand &
Partial<UpdateCollaborationRecordCommand> => ({
recordId: 0,
brand: "",
goods: "",
cooperationPlatform: "小红书",
imageReturnNum: 1,
retainedMethod: "寄拍",
cooperatedMethod: "水下",
purchaseMethod: "拍单",
purchasePrice: undefined,
purchaseDate: "",
purchasePlatform: "",
deadline: "",
remuneration: undefined,
completeDate: "",
requirements: "",
remark: "",
tasks: [{ releaseDate: "" }],
expenditures: [],
settlements: [],
files: []
});
const formData = reactive(defaultFormData());
const formRef = ref<FormInstance>();
const previewImageUrl = ref("");
const isImagePreviewVisible = ref(false);
const rules: FormRules = {
brand: [{ required: true, message: "品牌不能为空" }],
goods: [{ required: true, message: "物品不能为空" }],
cooperationPlatform: [{ required: true, message: "合作平台不能为空" }],
imageReturnNum: [{ required: true, message: "返图数量不能为空" }],
deadline: [{ required: true, message: "预完成日期不能为空" }]
};
const goodsImages = computed(() =>
formData.files.filter(item => item.fileType === "GOODS_IMAGE")
);
const attachments = computed(() =>
formData.files.filter(item => item.fileType === "ATTACHMENT")
);
const goodsImageUploadFiles = computed<UploadUserFile[]>(() =>
goodsImages.value.map(file => ({
name: getFileName(file),
url: getFileUrl(file)
}))
);
const attachmentUploadFiles = computed<UploadUserFile[]>(() =>
attachments.value.map(file => ({
name: getFileName(file),
url: getFileUrl(file)
}))
);
async function handleOpened() {
resetFormData();
if (props.type === "update" && props.row?.recordId) {
await loadDetail(props.row.recordId);
}
}
function resetFormData() {
Object.assign(formData, defaultFormData());
formRef.value?.clearValidate();
}
async function loadDetail(recordId: number) {
const { data } = await getCollaborationRecordInfoApi(recordId);
Object.assign(formData, {
...defaultFormData(),
...data,
tasks: data.tasks.length ? data.tasks : [{ releaseDate: "" }],
expenditures: data.expenditures,
settlements: data.settlements,
files: data.files
});
}
function addTask() {
formData.tasks.push({ releaseDate: "" });
}
function removeTask(index: number) {
formData.tasks.splice(index, 1);
}
function addExpenditure() {
formData.expenditures.push({
spendDate: "",
amount: undefined,
purpose: ""
});
}
function removeExpenditure(index: number) {
formData.expenditures.splice(index, 1);
}
function addSettlement() {
formData.settlements.push({
settleDate: "",
method: "",
income: undefined,
purpose: ""
});
}
function removeSettlement(index: number) {
formData.settlements.splice(index, 1);
}
async function handleUpload(
option: UploadRequestOptions,
fileType: CollaborationFileType
) {
const data = new FormData();
data.append("file", option.file);
const response = await uploadCollaborationFileApi(data);
formData.files.push(toFileCommand(response.data, fileType));
option.onSuccess(response.data);
}
function toFileCommand(file, fileType: CollaborationFileType) {
return {
fileType,
url: file.url,
fileName: file.fileName,
newFileName: file.newFileName,
originalFilename: file.originalFilename
};
}
function removeFile(file: CollaborationFileCommand) {
const index = formData.files.indexOf(file);
if (index >= 0) {
formData.files.splice(index, 1);
}
}
function handlePreviewImage(file: UploadFile) {
if (!file.url) return;
previewImageUrl.value = file.url;
isImagePreviewVisible.value = true;
}
function handleRemoveGoodsImage(file: UploadFile) {
const target = goodsImages.value.find(item => getFileUrl(item) === file.url);
if (target) {
removeFile(target);
}
}
function handlePreviewAttachment(file: UploadFile) {
if (!file.url) {
ElMessage.warning("文件地址不存在");
return;
}
window.open(file.url, "_blank", "noopener,noreferrer");
}
function handleRemoveAttachment(file: UploadFile) {
const target = attachments.value.find(item => getFileUrl(item) === file.url);
if (target) {
removeFile(target);
}
}
function getFileName(file: CollaborationFileCommand) {
return file.originalFilename || file.newFileName || "未命名文件";
}
function getFileUrl(file: CollaborationFileCommand) {
if (file.url) return file.url;
if (!file.fileName) return "";
return `${import.meta.env.VITE_APP_BASE_API}${file.fileName}`;
}
async function handleConfirm() {
const isValid = await formRef.value?.validate().catch(() => false);
if (!isValid) return false;
return submitForm();
}
async function submitForm() {
try {
if (props.type === "add") {
await addCollaborationRecordApi(formData);
} else {
await updateCollaborationRecordApi(
formData as UpdateCollaborationRecordCommand
);
}
ElMessage.success("提交成功");
return true;
} catch (e) {
ElMessage.error((e as Error)?.message || "提交失败");
return false;
}
}
onMounted(handleOpened);
defineExpose({ handleConfirm });
</script>
<template>
<el-form
ref="formRef"
class="record-form"
:model="formData"
:rules="rules"
label-width="112px"
>
<el-tabs>
<el-tab-pane label="基本信息">
<el-row :gutter="16">
<el-col :span="12">
<el-form-item prop="brand" label="品牌" required>
<el-input v-model="formData.brand" placeholder="请输入品牌" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item prop="goods" label="物品" required>
<el-input v-model="formData.goods" placeholder="请输入物品" />
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item prop="cooperationPlatform" label="合作平台" required>
<el-select v-model="formData.cooperationPlatform" clearable>
<el-option
v-for="item in optionMap.cooperationPlatform"
:key="item"
:label="item"
:value="item"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item prop="imageReturnNum" label="返图数量" required>
<el-input-number
:min="1"
controls-position="right"
v-model="formData.imageReturnNum"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="留存方式">
<el-select v-model="formData.retainedMethod">
<el-option
v-for="item in optionMap.retainedMethod"
:key="item"
:label="item"
:value="item"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="合作方式">
<el-select v-model="formData.cooperatedMethod">
<el-option
v-for="item in optionMap.cooperatedMethod"
:key="item"
:label="item"
:value="item"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="购入方式">
<el-select v-model="formData.purchaseMethod">
<el-option
v-for="item in optionMap.purchaseMethod"
:key="item"
:label="item"
:value="item"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="购入金额">
<el-input-number
:min="0"
controls-position="right"
v-model="formData.purchasePrice"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="购入平台">
<el-select v-model="formData.purchasePlatform" clearable>
<el-option
v-for="item in optionMap.purchasePlatform"
:key="item"
:label="item"
:value="item"
/>
</el-select>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="购入日期">
<el-date-picker
v-model="formData.purchaseDate"
value-format="YYYY-MM-DD"
type="date"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item prop="deadline" label="预完成日期" required>
<el-date-picker
v-model="formData.deadline"
value-format="YYYY-MM-DD"
type="date"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="稿费">
<el-input-number
:min="0"
controls-position="right"
v-model="formData.remuneration"
/>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="物品图片">
<el-upload
multiple
accept="image/*"
class="goods-image-upload"
list-type="picture-card"
:file-list="goodsImageUploadFiles"
:http-request="option => handleUpload(option, 'GOODS_IMAGE')"
:on-preview="handlePreviewImage"
:on-remove="handleRemoveGoodsImage"
>
<IconifyIconOffline class="upload-plus" :icon="Plus" />
</el-upload>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="附件">
<el-upload
multiple
class="attachment-upload"
:file-list="attachmentUploadFiles"
:http-request="option => handleUpload(option, 'ATTACHMENT')"
:on-preview="handlePreviewAttachment"
:on-remove="handleRemoveAttachment"
>
<el-button>上传附件</el-button>
</el-upload>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="16">
<el-col :span="12">
<el-form-item label="拍摄要求">
<el-input
type="textarea"
:rows="3"
v-model="formData.requirements"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="备注">
<el-input type="textarea" :rows="3" v-model="formData.remark" />
</el-form-item>
</el-col>
</el-row>
</el-tab-pane>
<el-tab-pane label="笔记任务">
<el-button type="primary" plain @click="addTask">添加笔记</el-button>
<div
v-for="(item, index) in formData.tasks"
:key="index"
class="line-item"
>
<el-date-picker
v-model="item.releaseDate"
value-format="YYYY-MM-DD"
type="date"
placeholder="发布日期"
/>
<el-button type="danger" link @click="removeTask(index)"
>删除</el-button
>
</div>
</el-tab-pane>
<el-tab-pane label="支出信息">
<el-button type="primary" plain @click="addExpenditure"
>添加支出</el-button
>
<div
v-for="(item, index) in formData.expenditures"
:key="index"
class="line-item"
>
<el-date-picker
v-model="item.spendDate"
value-format="YYYY-MM-DD"
type="date"
placeholder="支出日期"
/>
<el-input-number
:min="0"
controls-position="right"
v-model="item.amount"
placeholder="金额"
/>
<el-select v-model="item.purpose" placeholder="用途" clearable>
<el-option
v-for="option in optionMap.expenditurePurpose"
:key="option"
:label="option"
:value="option"
/>
</el-select>
<el-button type="danger" link @click="removeExpenditure(index)"
>删除</el-button
>
</div>
</el-tab-pane>
<el-tab-pane label="结款信息">
<el-button type="primary" plain @click="addSettlement"
>添加结款</el-button
>
<div
v-for="(item, index) in formData.settlements"
:key="index"
class="line-item"
>
<el-date-picker
v-model="item.settleDate"
value-format="YYYY-MM-DD"
type="date"
placeholder="结款日期"
/>
<el-select v-model="item.method" placeholder="方式" clearable>
<el-option
v-for="option in optionMap.settlementMethod"
:key="option"
:label="option"
:value="option"
/>
</el-select>
<el-input-number
:min="0"
controls-position="right"
v-model="item.income"
placeholder="金额"
/>
<el-select v-model="item.purpose" placeholder="用途" clearable>
<el-option
v-for="option in optionMap.settlementPurpose"
:key="option"
:label="option"
:value="option"
/>
</el-select>
<el-button type="danger" link @click="removeSettlement(index)"
>删除</el-button
>
</div>
</el-tab-pane>
</el-tabs>
</el-form>
<el-dialog v-model="isImagePreviewVisible" append-to-body>
<img class="preview-image" :src="previewImageUrl" alt="" />
</el-dialog>
</template>
<style scoped lang="scss">
.line-item {
display: flex;
gap: 12px;
align-items: center;
margin-top: 12px;
}
.record-form {
:deep(.el-row .el-select),
:deep(.el-row .el-date-editor.el-input),
:deep(.el-row .el-input-number) {
width: 100%;
}
:deep(.el-input-number .el-input__inner) {
text-align: left;
}
}
.goods-image-upload {
:deep(.el-upload--picture-card),
:deep(.el-upload-list--picture-card .el-upload-list__item) {
width: 96px;
height: 96px;
line-height: 96px;
}
}
.upload-plus {
font-size: 24px;
color: var(--el-text-color-placeholder);
}
.attachment-upload {
width: 100%;
max-width: 100%;
}
.preview-image {
display: block;
max-width: 100%;
margin: 0 auto;
}
</style>
@@ -0,0 +1,241 @@
import dayjs from "dayjs";
import { message } from "@/utils/message";
import { ElMessageBox, Sort } from "element-plus";
import { computed, onMounted, reactive, ref, toRaw } from "vue";
import { CommonUtils } from "@/utils/common";
import { PaginationProps } from "@pureadmin/table";
import {
CollaborationOptionResponse,
CollaborationRecordListCommand,
CollaborationRecordPageResponse,
deleteCollaborationRecordApi,
getCollaborationOptionsApi,
getCollaborationRecordListApi
} from "@/api/collaboration/record";
const statusTypeMap = {
NONE: "info",
SETTLED: "success",
UNSETTLED: "danger",
PARTIAL: "warning"
};
export function useCollaborationRecordHook() {
const defaultSort: Sort = {
prop: "deadline",
order: "descending"
};
const pagination: PaginationProps = {
total: 0,
pageSize: 10,
currentPage: 1,
background: true
};
const searchFormParams = reactive<CollaborationRecordListCommand>({
brand: "",
goods: "",
cooperationPlatform: undefined
});
const deadlineRange = computed<[string, string] | null>({
get: () => getRange(searchFormParams.beginTime, searchFormParams.endTime),
set: v => fillRange(v, "beginTime", "endTime")
});
const purchaseRange = computed<[string, string] | null>({
get: () =>
getRange(
searchFormParams.purchaseBeginTime,
searchFormParams.purchaseEndTime
),
set: v => fillRange(v, "purchaseBeginTime", "purchaseEndTime")
});
const dataList = ref<CollaborationRecordPageResponse[]>([]);
const optionMap = ref<Record<string, string[]>>({});
const pageLoading = ref(true);
const multipleSelection = ref<number[]>([]);
const sortState = ref<Sort>(defaultSort);
const columns: TableColumnList = [
{ type: "selection", align: "left" },
{ label: "品牌", prop: "brand", minWidth: 120 },
{ label: "物品", prop: "goods", minWidth: 120 },
{ label: "合作平台", prop: "cooperationPlatform", minWidth: 110 },
{ label: "留存方式", prop: "retainedMethod", minWidth: 100 },
{ label: "购入方式", prop: "purchaseMethod", minWidth: 100 },
{
label: "预完成日期",
prop: "deadline",
minWidth: 130,
sortable: "custom",
formatter: ({ deadline }) => formatDate(deadline)
},
{
label: "任务进度",
minWidth: 130,
cellRenderer: ({ row }) => `${row.completedTasksNum}/${row.tasksNum}`
},
{
label: "拍单费用",
minWidth: 110,
cellRenderer: ({ row, props }) =>
renderStatus(row.purchaseSettlementStatus, props.size)
},
{
label: "快递费用",
minWidth: 110,
cellRenderer: ({ row, props }) =>
renderStatus(row.deliverySettlementStatus, props.size)
},
{
label: "稿费",
minWidth: 110,
cellRenderer: ({ row, props }) =>
renderStatus(row.remunerationSettlementStatus, props.size)
},
{
label: "创建时间",
prop: "createTime",
minWidth: 160,
sortable: "custom",
formatter: ({ createTime }) => formatDateTime(createTime)
},
{ label: "操作", fixed: "right", width: 140, slot: "operation" }
];
function getRange(start?: string, end?: string) {
if (!start || !end) return null;
return [start, end] as [string, string];
}
function fillRange(v, startKey: string, endKey: string) {
searchFormParams[startKey] = v?.length === 2 ? v[0] : undefined;
searchFormParams[endKey] = v?.length === 2 ? v[1] : undefined;
}
function formatDate(value?: string) {
return value ? dayjs(value).format("YYYY-MM-DD") : "";
}
function formatDateTime(value?: string) {
return value ? dayjs(value).format("YYYY-MM-DD HH:mm:ss") : "";
}
function renderStatus(status, size) {
if (!status) return "";
return (
<el-tag size={size} type={statusTypeMap[status.status]} effect="plain">
{status.label}
</el-tag>
);
}
function onSortChanged(sort: Sort) {
sortState.value = sort;
pagination.currentPage = 1;
getRecordList();
}
async function onSearch(tableRef) {
tableRef.getTableRef().sort("deadline", "descending");
}
function resetForm(formEl, tableRef) {
if (!formEl) return;
formEl.resetFields();
fillRange(null, "beginTime", "endTime");
fillRange(null, "purchaseBeginTime", "purchaseEndTime");
onSearch(tableRef);
}
async function getRecordList() {
pageLoading.value = true;
CommonUtils.fillSortParams(searchFormParams, sortState.value);
CommonUtils.fillPaginationParams(searchFormParams, pagination);
const { data } = await getCollaborationRecordListApi(
toRaw(searchFormParams)
).finally(() => {
pageLoading.value = false;
});
dataList.value = data.rows;
pagination.total = data.total;
}
async function getOptions() {
const { data } = await getCollaborationOptionsApi();
optionMap.value = data.reduce(
(result, item: CollaborationOptionResponse) => {
result[item.type] = item.values;
return result;
},
{}
);
}
async function handleDelete(row: CollaborationRecordPageResponse) {
await deleteCollaborationRecordApi([row.recordId]);
message(`您删除了编号为${row.recordId}的合作记录`, { type: "success" });
getRecordList();
}
async function handleBulkDelete(tableRef) {
if (multipleSelection.value.length === 0) {
message("请选择需要删除的数据", { type: "warning" });
return;
}
confirmBulkDelete(tableRef);
}
function confirmBulkDelete(tableRef) {
ElMessageBox.confirm(
`确认删除编号为[ ${multipleSelection.value} ]的合作记录吗?`,
"系统提示",
{
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
draggable: true
}
)
.then(deleteSelectedRecords)
.catch(() => {
message("取消删除", { type: "info" });
tableRef.getTableRef().clearSelection();
});
}
async function deleteSelectedRecords() {
await deleteCollaborationRecordApi(multipleSelection.value);
message(`您删除了编号为[ ${multipleSelection.value} ]的合作记录`, {
type: "success"
});
getRecordList();
}
onMounted(() => {
getOptions();
getRecordList();
});
return {
searchFormParams,
pageLoading,
columns,
dataList,
pagination,
defaultSort,
deadlineRange,
purchaseRange,
optionMap,
multipleSelection,
onSearch,
onSortChanged,
getRecordList,
resetForm,
handleDelete,
handleBulkDelete
};
}
@@ -0,0 +1,48 @@
<script setup lang="ts">
import { useCollaborationStatisticsHook } from "./utils/hook";
defineOptions({
name: "CollaborationStatistics"
});
const { chartRef, selectedYear, yearOptions, getStatistics } =
useCollaborationStatisticsHook();
</script>
<template>
<div class="main">
<el-card shadow="never">
<template #header>
<div class="card-header">
<span>月度统计</span>
<el-select
v-model="selectedYear"
class="!w-[140px]"
@change="getStatistics"
>
<el-option
v-for="year in yearOptions"
:key="year"
:label="`${year}年`"
:value="year"
/>
</el-select>
</div>
</template>
<div ref="chartRef" class="chart" />
</el-card>
</div>
</template>
<style scoped lang="scss">
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.chart {
width: 100%;
height: 600px;
}
</style>
@@ -0,0 +1,84 @@
import { onMounted, ref } from "vue";
import * as echarts from "echarts";
import { getCollaborationMonthlyStatisticsApi } from "@/api/collaboration/record";
export function useCollaborationStatisticsHook() {
const currentYear = new Date().getFullYear();
const selectedYear = ref(currentYear);
const yearOptions = Array.from(
{ length: currentYear - 2012 },
(_, index) => currentYear - index
);
const chartRef = ref<HTMLElement>();
let chart: echarts.ECharts | undefined;
async function getStatistics() {
const { data } = await getCollaborationMonthlyStatisticsApi(
selectedYear.value
);
renderChart(data);
}
function renderChart(data) {
chart?.dispose();
chart = echarts.init(chartRef.value);
chart.setOption({
tooltip: { trigger: "axis", axisPointer: { type: "shadow" } },
legend: {
data: ["拍单费用", "支出费用", "已结稿费", "已结总费用"],
top: "2%",
right: "0"
},
grid: { left: "0%", right: "0%", bottom: "0%", containLabel: true },
xAxis: { type: "category", data: data.map(item => `${item.month}`) },
yAxis: { type: "value" },
series: [
buildSeries(
"拍单费用",
data.map(item => item.purchasePrice),
"#ffbe00"
),
buildSeries(
"支出费用",
data.map(item => item.expenditureAmount),
"#f56c6c"
),
buildSeries(
"已结稿费",
data.map(item => item.settledRemuneration),
"#409eff"
),
buildSeries(
"已结总费用",
data.map(item => item.settledTotal),
"#67c23a"
)
]
});
}
function buildSeries(name: string, data: number[], color: string) {
return {
name,
data,
type: "bar",
color
};
}
function resizeChart() {
chart?.resize({ width: "auto" });
}
onMounted(() => {
getStatistics();
window.addEventListener("resize", resizeChart);
});
return {
chartRef,
selectedYear,
yearOptions,
getStatistics
};
}
+58 -15
View File
@@ -1,8 +1,9 @@
<script setup lang="ts">
import { ref } from "vue";
import { h, ref } from "vue";
import { usePostHook } from "./utils/hook";
import { PureTableBar } from "@/components/RePureTableBar";
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
import { addDialog } from "@/components/ReDialog";
import Delete from "@iconify-icons/ep/delete";
import Search from "@iconify-icons/ep/search";
@@ -12,8 +13,15 @@ import { useUserStoreHook } from "@/store/modules/user";
import { CommonUtils } from "@/utils/common";
import PostFormModal from "@/views/system/post/post-form-modal.vue";
import EditPen from "@iconify-icons/ep/edit-pen";
import { PostPageResponse } from "@/api/system/post";
import {
AddPostCommand,
PostPageResponse,
UpdatePostCommand,
addPostApi,
updatePostApi
} from "@/api/system/post";
import AddFill from "@iconify-icons/ri/add-circle-line";
import { ElMessage } from "element-plus";
/** 组件name最好和菜单表中的router_name一致 */
defineOptions({
@@ -43,13 +51,55 @@ const {
handleBulkDelete
} = usePostHook();
const opType = ref<"add" | "update">("add");
const modalVisible = ref(false);
const opRow = ref<PostPageResponse>();
const postFormRef = ref();
function getPostFormData(row?: PostPageResponse) {
return {
postId: row?.postId ?? 0,
postCode: row?.postCode ?? "",
postName: row?.postName ?? "",
postSort: row?.postSort ?? 1,
remark: row?.remark ?? "",
status: row?.status?.toString() ?? ""
};
}
async function submitPostForm(
type: "add" | "update",
formData: AddPostCommand & Partial<UpdatePostCommand>,
done: () => void
) {
if (type === "add") {
await addPostApi(formData);
} else {
await updatePostApi(formData as UpdatePostCommand);
}
ElMessage.success("提交成功");
done();
onSearch(tableRef);
}
function openDialog(type: "add" | "update", row?: PostPageResponse) {
opType.value = type;
opRow.value = row;
modalVisible.value = true;
const formInline = getPostFormData(row);
addDialog({
title: type === "add" ? "新增岗位" : "更新岗位",
props: { formInline },
width: "40%",
draggable: true,
fullscreenIcon: true,
closeOnClickModal: false,
contentRenderer: () => h(PostFormModal, { ref: postFormRef }),
beforeSure: (done, { options }) => {
const formRuleRef = postFormRef.value.getFormRuleRef();
const formData = options.props.formInline as AddPostCommand &
Partial<UpdatePostCommand>;
formRuleRef.validate(valid => {
if (valid) {
submitPostForm(type, formData, () => done());
}
});
}
});
}
</script>
@@ -205,13 +255,6 @@ function openDialog(type: "add" | "update", row?: PostPageResponse) {
</pure-table>
</template>
</PureTableBar>
<post-form-modal
v-model="modalVisible"
:type="opType"
:row="opRow"
@success="onSearch"
/>
</div>
</template>
@@ -1,43 +1,24 @@
<script setup lang="ts">
import VDialog from "@/components/VDialog/VDialog.vue";
import { computed, reactive, ref } from "vue";
import {
AddPostCommand,
PostPageResponse,
UpdatePostCommand,
addPostApi,
updatePostApi
} from "@/api/system/post";
import { ref } from "vue";
import { AddPostCommand, UpdatePostCommand } from "@/api/system/post";
import { useUserStoreHook } from "@/store/modules/user";
import { ElMessage, FormInstance, FormRules } from "element-plus";
import { FormInstance, FormRules } from "element-plus";
interface Props {
type: "add" | "update";
modelValue: boolean;
row?: PostPageResponse;
formInline: AddPostCommand & Partial<UpdatePostCommand>;
}
const props = defineProps<Props>();
const emits = defineEmits<{
(e: "update:modelValue", v: boolean): void;
(e: "success"): void;
}>();
const visible = computed({
get: () => props.modelValue,
set(v) {
emits("update:modelValue", v);
}
});
const formData = reactive<AddPostCommand | UpdatePostCommand>({
postId: 0,
postCode: "",
postName: "",
postSort: 1,
remark: "",
status: ""
const props = withDefaults(defineProps<Props>(), {
formInline: () => ({
postId: 0,
postCode: "",
postName: "",
postSort: 1,
remark: "",
status: ""
})
});
const formData = ref(props.formInline);
const statusList = useUserStoreHook().dictionaryMap["common.status"];
@@ -62,70 +43,37 @@ const rules: FormRules = {
]
};
const formRef = ref<FormInstance>();
function handleOpened() {
if (props.row) {
Object.assign(formData, props.row);
} else {
formRef.value?.resetFields();
}
function getFormRuleRef() {
return formRef.value;
}
const loading = ref(false);
async function handleConfirm() {
try {
loading.value = true;
if (props.type === "add") {
await addPostApi(formData);
} else if (props.type === "update") {
await updatePostApi(formData as UpdatePostCommand);
}
ElMessage.info("提交成功");
visible.value = false;
emits("success");
} catch (e) {
console.error(e);
ElMessage.error((e as Error)?.message || "提交失败");
} finally {
loading.value = false;
}
}
defineExpose({ getFormRuleRef });
</script>
<template>
<v-dialog
show-full-screen
:fixed-body-height="false"
use-body-scrolling
:title="type === 'add' ? '新增岗位' : '更新岗位'"
v-model="visible"
:loading="loading"
@confirm="handleConfirm"
@cancel="visible = false"
@opened="handleOpened"
>
<el-form :model="formData" label-width="120px" :rules="rules" ref="formRef">
<el-form-item prop="postName" label="岗位名称" required inline-message>
<el-input v-model="formData.postName" />
</el-form-item>
<el-form-item prop="postCode" label="岗位编码" required>
<el-input v-model="formData.postCode" />
</el-form-item>
<el-form-item prop="postSort" label="岗位顺序" required>
<el-input-number :min="1" v-model="formData.postSort" />
</el-form-item>
<el-form-item prop="status" label="岗位状态">
<el-radio-group v-model="formData.status">
<el-radio
v-for="item in Object.keys(statusList)"
:key="item"
:label="statusList[item].value"
>{{ statusList[item].label }}</el-radio
>
</el-radio-group>
</el-form-item>
<el-form-item prop="remark" label="备注" style="margin-bottom: 0">
<el-input type="textarea" v-model="formData.remark" />
</el-form-item>
</el-form>
</v-dialog>
<el-form :model="formData" label-width="120px" :rules="rules" ref="formRef">
<el-form-item prop="postName" label="岗位名称" required inline-message>
<el-input v-model="formData.postName" />
</el-form-item>
<el-form-item prop="postCode" label="岗位编码" required>
<el-input v-model="formData.postCode" />
</el-form-item>
<el-form-item prop="postSort" label="岗位顺序" required>
<el-input-number :min="1" v-model="formData.postSort" />
</el-form-item>
<el-form-item prop="status" label="岗位状态">
<el-radio-group v-model="formData.status">
<el-radio
v-for="item in Object.keys(statusList)"
:key="item"
:label="statusList[item].value"
>{{ statusList[item].label }}</el-radio
>
</el-radio-group>
</el-form-item>
<el-form-item prop="remark" label="备注" style="margin-bottom: 0">
<el-input type="textarea" v-model="formData.remark" />
</el-form-item>
</el-form>
</template>
+76 -16
View File
@@ -1,15 +1,23 @@
<script setup lang="ts">
import { ref } from "vue";
import { h, ref } from "vue";
import { useRole } from "./utils/hook";
import { PureTableBar } from "@/components/RePureTableBar";
import { useRenderIcon } from "@/components/ReIcon/src/hooks";
import { addDialog } from "@/components/ReDialog";
import Delete from "@iconify-icons/ep/delete";
import EditPen from "@iconify-icons/ep/edit-pen";
import Search from "@iconify-icons/ep/search";
import Refresh from "@iconify-icons/ep/refresh";
import AddFill from "@iconify-icons/ri/add-circle-line";
import { getRoleInfoApi, RoleDTO } from "@/api/system/role";
import {
AddRoleCommand,
RoleDTO,
UpdateRoleCommand,
addRoleApi,
getRoleInfoApi,
updateRoleApi
} from "@/api/system/role";
import RoleFormModal from "@/views/system/role/role-form-modal.vue";
import { ElMessage } from "element-plus";
@@ -31,11 +39,37 @@ const {
handleDelete
} = useRole();
const opType = ref<"add" | "update">("add");
const modalVisible = ref(false);
const opRow = ref<RoleDTO>();
const roleFormRef = ref();
function getRoleFormData(row?: RoleDTO) {
return {
roleId: row?.roleId ?? 0,
dataScope: row?.dataScope?.toString() ?? "",
menuIds: row?.selectedMenuList ?? [],
remark: row?.remark ?? "",
roleKey: row?.roleKey ?? "",
roleName: row?.roleName ?? "",
roleSort: row?.roleSort ?? 1,
status: row?.status?.toString() ?? ""
};
}
async function submitRoleForm(
type: "add" | "update",
formData: AddRoleCommand & Partial<UpdateRoleCommand>,
done: () => void
) {
if (type === "add") {
await addRoleApi(formData);
} else {
await updateRoleApi(formData as UpdateRoleCommand);
}
ElMessage.success("提交成功");
done();
onSearch();
}
async function openDialog(type: "add" | "update", row?: RoleDTO) {
debugger;
try {
await getMenuTree();
if (row) {
@@ -46,10 +80,43 @@ async function openDialog(type: "add" | "update", row?: RoleDTO) {
} catch (e) {
console.error(e);
ElMessage.error((e as Error)?.message || "加载菜单失败");
return;
}
opType.value = type;
opRow.value = row;
modalVisible.value = true;
const formInline = getRoleFormData(row);
addDialog({
title: type === "add" ? "新增角色" : "更新角色",
props: {
formInline,
menuOptions: menuTree.value
},
width: "40%",
draggable: true,
fullscreenIcon: true,
closeOnClickModal: false,
contentRenderer: () => h(RoleFormModal, { ref: roleFormRef }),
beforeSure: (done, { options }) => {
const formRuleRef = roleFormRef.value.getFormRuleRef();
const formData = options.props.formInline as AddRoleCommand &
Partial<UpdateRoleCommand>;
formRuleRef.validate(valid => {
if (valid) {
submitRoleForm(type, formData, () => done());
}
});
}
});
}
function handleSelectionChange(rows: RoleDTO[]) {
void rows;
}
function handleSizeChange() {
onSearch();
}
function handleCurrentChange() {
onSearch();
}
</script>
<template>
@@ -205,13 +272,6 @@ async function openDialog(type: "add" | "update", row?: RoleDTO) {
</pure-table>
</template>
</PureTableBar>
<role-form-modal
v-model="modalVisible"
:type="opType"
:row="opRow"
:menu-options="menuTree"
/>
</div>
</template>
@@ -1,47 +1,29 @@
<script setup lang="ts">
import VDialog from "@/components/VDialog/VDialog.vue";
import { computed, reactive, ref } from "vue";
import { ref } from "vue";
import { useUserStoreHook } from "@/store/modules/user";
import { ElMessage, FormInstance, FormRules } from "element-plus";
import {
AddRoleCommand,
RoleDTO,
UpdateRoleCommand,
addRoleApi,
updateRoleApi
} from "@/api/system/role";
import { ElTree, FormInstance, FormRules } from "element-plus";
import { AddRoleCommand, UpdateRoleCommand } from "@/api/system/role";
import { MenuDTO } from "@/api/system/menu";
interface Props {
type: "add" | "update";
modelValue: boolean;
row?: RoleDTO;
formInline: AddRoleCommand & Partial<UpdateRoleCommand>;
menuOptions: MenuDTO[];
}
const props = defineProps<Props>();
const emits = defineEmits<{
(e: "update:modelValue", v: boolean): void;
(e: "success"): void;
}>();
const visible = computed({
get: () => props.modelValue,
set(v) {
emits("update:modelValue", v);
}
});
const formData = reactive<AddRoleCommand | UpdateRoleCommand>({
roleId: 0,
dataScope: "",
menuIds: [],
remark: "",
roleKey: "",
roleName: "",
roleSort: 1,
status: ""
const props = withDefaults(defineProps<Props>(), {
formInline: () => ({
roleId: 0,
dataScope: "",
menuIds: [],
remark: "",
roleKey: "",
roleName: "",
roleSort: 1,
status: ""
}),
menuOptions: () => []
});
const formData = ref(props.formInline);
const statusList = useUserStoreHook().dictionaryMap["common.status"];
@@ -66,93 +48,58 @@ const rules: FormRules = {
]
};
const formRef = ref<FormInstance>();
function handleOpened() {
console.log("opened", props.row);
if (props.row) {
Object.assign(formData, props.row);
formData.menuIds = props.row.selectedMenuList;
} else {
formRef.value?.resetFields();
}
}
const treeRef = ref<InstanceType<typeof ElTree>>();
function handleCheckChange() {
formData.menuIds = treeRef.value.getCheckedKeys(false) as number[];
formData.value.menuIds = treeRef.value.getCheckedKeys(false) as number[];
}
const loading = ref(false);
async function handleConfirm() {
try {
loading.value = true;
if (props.type === "add") {
await addRoleApi(formData);
} else if (props.type === "update") {
await updateRoleApi(formData as UpdateRoleCommand);
}
ElMessage.info("提交成功");
visible.value = false;
emits("success");
} catch (e) {
console.error(e);
ElMessage.error((e as Error)?.message || "提交失败");
} finally {
loading.value = false;
}
function getFormRuleRef() {
return formRef.value;
}
defineExpose({ getFormRuleRef });
</script>
<template>
<v-dialog
show-full-screen
fixed-body-height
use-body-scrolling
:title="type === 'add' ? '新增角色' : '更新角色'"
v-model="visible"
:loading="loading"
@confirm="handleConfirm"
@cancel="visible = false"
@opened="handleOpened"
>
<el-form :model="formData" label-width="120px" :rules="rules" ref="formRef">
<el-form-item prop="roleName" label="角色名称" required inline-message>
<el-input v-model="formData.roleName" />
</el-form-item>
<el-form-item prop="roleKey" label="权限字符" required>
<el-input v-model="formData.roleKey" />
</el-form-item>
<el-form-item prop="roleSort" label="角色顺序" required>
<el-input-number :min="1" v-model="formData.roleSort" />
</el-form-item>
<el-form-item prop="status" label="角色状态">
<el-radio-group v-model="formData.status">
<el-radio
v-for="item in Object.keys(statusList)"
:key="item"
:label="statusList[item].value"
>{{ statusList[item].label }}</el-radio
>
</el-radio-group>
</el-form-item>
<el-form-item label="菜单权限" prop="menuIds">
<el-tree
ref="treeRef"
:props="{ label: 'menuName', children: 'children' }"
:data="props.menuOptions"
node-key="id"
check-strictly
show-checkbox
default-expand-all
check-on-click-node
:expand-on-click-node="false"
:default-checked-keys="formData.menuIds"
@check-change="handleCheckChange"
style="width: 100%"
/>
</el-form-item>
<el-form-item prop="remark" label="备注" style="margin-bottom: 0">
<el-input type="textarea" v-model="formData.remark" />
</el-form-item>
</el-form>
</v-dialog>
<el-form :model="formData" label-width="120px" :rules="rules" ref="formRef">
<el-form-item prop="roleName" label="角色名称" required inline-message>
<el-input v-model="formData.roleName" />
</el-form-item>
<el-form-item prop="roleKey" label="权限字符" required>
<el-input v-model="formData.roleKey" />
</el-form-item>
<el-form-item prop="roleSort" label="角色顺序" required>
<el-input-number :min="1" v-model="formData.roleSort" />
</el-form-item>
<el-form-item prop="status" label="角色状态">
<el-radio-group v-model="formData.status">
<el-radio
v-for="item in Object.keys(statusList)"
:key="item"
:label="statusList[item].value"
>{{ statusList[item].label }}</el-radio
>
</el-radio-group>
</el-form-item>
<el-form-item label="菜单权限" prop="menuIds">
<el-tree
ref="treeRef"
:props="{ label: 'menuName', children: 'children' }"
:data="props.menuOptions"
node-key="id"
check-strictly
show-checkbox
default-expand-all
check-on-click-node
:expand-on-click-node="false"
:default-checked-keys="formData.menuIds"
@check-change="handleCheckChange"
style="width: 100%"
/>
</el-form-item>
<el-form-item prop="remark" label="备注" style="margin-bottom: 0">
<el-input type="textarea" v-model="formData.remark" />
</el-form-item>
</el-form>
</template>