u
This commit is contained in:
1 parent
62550b8182
commit
3e172a1d53
26 files changed
+4820
-413
No files matched your search
+6
@@ -3,6 +3,7 @@ package cn.g3soft.controller;
|
||||
import cn.g3soft.utils.DbUtils;
|
||||
import cn.g3soft.utils.Result;
|
||||
import cn.g3soft.utils.ValidationUtils;
|
||||
import cn.g3soft.utils.uniqueId.idgen.UniqueId;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
@@ -164,4 +165,9 @@ public class DataListController {
|
||||
|
||||
return Result.ok(list.get(0));
|
||||
}
|
||||
|
||||
@PostMapping("/getUniqueId")
|
||||
public Result<Long> getUniqueId(){
|
||||
return Result.ok(UniqueId.nextId());
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -192,13 +192,13 @@ public class DataSaveController {
|
||||
return Result.ok();
|
||||
}
|
||||
|
||||
@PostMapping("/deleteModule")
|
||||
@PostMapping("/deleteObject")
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Result<Map<String, Integer>> deleteModule(@RequestBody Map<String, Object> params) {
|
||||
ValidationUtils.checkRequiredFields(params, "moduleId", "Id");
|
||||
ValidationUtils.checkRequiredFields(params, "moduleId", "id");
|
||||
|
||||
String moduleId = params.get("moduleId").toString();
|
||||
String id = params.get("Id").toString();
|
||||
String id = params.get("id").toString();
|
||||
|
||||
Map<String, Integer> operationCounts = new LinkedHashMap<>();
|
||||
operationCounts.put("total", 0);
|
||||
|
||||
+9
-6
@@ -21,13 +21,13 @@ public class FileController {
|
||||
|
||||
@PostMapping("/getAliyunKey")
|
||||
public Result<Map<String,Object>> getAliyunKey(@RequestBody Map<String, Object> params) {
|
||||
ValidationUtils.checkRequiredFields(params, "bucket");
|
||||
ValidationUtils.checkRequiredFields(params, "bucketName");
|
||||
|
||||
String bucket = (String) params.get("bucket");
|
||||
String bucketName = (String) params.get("bucketName");
|
||||
|
||||
String orgId = UserContext.get().getOrgId();
|
||||
|
||||
String requestUrl = "http://g3oss.g3soft.cn:8082/upload?orgid=" + orgId + "&bucket=" + bucket;
|
||||
String requestUrl = "http://g3oss.g3soft.cn:8082/upload?orgid=" + orgId + "&bucket=" + bucketName;
|
||||
String reqResult = HttpUtils.sendGet(requestUrl);
|
||||
|
||||
Map<String, Object> objectMap = JsonUtils.toMap(reqResult);
|
||||
@@ -37,19 +37,22 @@ public class FileController {
|
||||
|
||||
@PostMapping("/insert")
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Result<Void> insertFile(@RequestBody Map<String,Object> params) {
|
||||
public Result<Long> insertFile(@RequestBody Map<String,Object> params) {
|
||||
ValidationUtils.checkRequiredFields(params, "father", "mx_xh", "mx_filename", "mx_filesize", "mx_mapfilename", "mx_moduleid");
|
||||
|
||||
String userId = UserContext.get().getUserId();
|
||||
|
||||
params.put("subid", UniqueId.nextId());
|
||||
long nextId = UniqueId.nextId();
|
||||
|
||||
params.put("subid", nextId);
|
||||
params.put("mx_inputuser_id",userId);
|
||||
params.put("mx_inputdatetime", TimeUtils.getDateTime());
|
||||
|
||||
dbUtils.insert("bf_file","subid",params);
|
||||
|
||||
return Result.ok();
|
||||
return Result.ok(nextId);
|
||||
}
|
||||
|
||||
@PostMapping("/delete")
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Result<Void> deleteFile(@RequestBody Map<String, Object> params) {
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script lang="ts" setup>
|
||||
import { toast } from 'sard-uniapp'
|
||||
import { deleteAliyunFileApi, recognizeOcrBoxApi } from '@/request/api'
|
||||
import { imageCompress, uploadFileToAliyun } from '@/utils/file'
|
||||
import type { UploadFile, UploadFileItem } from 'sard-uniapp'
|
||||
import { dialog, isImageUrl, isVideoUrl, toast } from 'sard-uniapp'
|
||||
import { aliyunpluploadApi, deleteAliyunFileApi, deleteFileApi, recognizeOcrBoxApi } from '@/request/api'
|
||||
import { downloadFile, formatFileSize, getFileExtension, imageCompress, isImage, uploadFileToAliyun } from '@/utils/file'
|
||||
|
||||
const boxImage = ref('')
|
||||
|
||||
@@ -10,10 +11,13 @@ const isError = ref(false)
|
||||
const ocrScore = ref('0.00')
|
||||
|
||||
const form = ref({
|
||||
b_id: 0,
|
||||
boxNumber: '',
|
||||
boxImageUrl: '',
|
||||
})
|
||||
|
||||
const subFileList = ref([])
|
||||
|
||||
const otherImages = ref([])
|
||||
|
||||
function handleChooseBox() {
|
||||
@@ -76,8 +80,6 @@ function handleDeleteBoxImage() {
|
||||
transparent: false,
|
||||
})
|
||||
|
||||
console.log(form.value.boxImageUrl)
|
||||
|
||||
deleteAliyunFileApi(form.value.boxImageUrl).then((res) => {
|
||||
if (res.success) {
|
||||
boxImage.value = ''
|
||||
@@ -88,6 +90,94 @@ function handleDeleteBoxImage() {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function onItemClick(fileItem: UploadFileItem) {
|
||||
console.log(fileItem)
|
||||
if (!isImageUrl(fileItem.mx_mapfilename) && !isVideoUrl(fileItem.mx_mapfilename)) {
|
||||
downloadFile(fileItem.mx_mapfilename, fileItem.name)
|
||||
}
|
||||
}
|
||||
|
||||
function beforeRead(file: UploadFile): boolean | Promise<UploadFile> {
|
||||
// 非图片直接允许上传
|
||||
if (!isImage(file.name))
|
||||
return true
|
||||
|
||||
// 图片异步压缩,返回修改后的 UploadFile 对象
|
||||
return imageCompress(file.path).then((res) => {
|
||||
file.path = res.path
|
||||
file.size = res.size
|
||||
return file
|
||||
}).catch((err) => {
|
||||
console.error('图片压缩失败:', err)
|
||||
return file
|
||||
})
|
||||
}
|
||||
|
||||
async function afterRead(fileItem: UploadFileItem) {
|
||||
fileItem.isUpload = false
|
||||
|
||||
subFileList.value = [...subFileList.value]
|
||||
}
|
||||
|
||||
function beforeRemove(index: number, fileItem: UploadFileItem): boolean | Promise<void> {
|
||||
console.log(fileItem)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
dialog.confirm({
|
||||
title: '提示',
|
||||
message: `确定删除该张图片吗?`,
|
||||
onConfirm: () => resolve(),
|
||||
onCancel: () => reject(),
|
||||
onClose: () => reject(),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function onRemove(index: number, item: UploadFileItem) {
|
||||
console.log(item)
|
||||
|
||||
if (!item.isUpload) {
|
||||
return
|
||||
}
|
||||
|
||||
deleteFileApi(item.b_id).then((res) => {
|
||||
if (res.success) {
|
||||
toast.success('删除成功')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function handleConfirm() {
|
||||
subFileList.value.forEach(async (fileItem, index) => {
|
||||
// 上传到阿里云
|
||||
const res = await uploadFileToAliyun(
|
||||
fileItem.file.path,
|
||||
fileItem.file.name,
|
||||
fileItem.file.type,
|
||||
)
|
||||
|
||||
if (!res.url)
|
||||
throw new Error('上传失败:未获取到文件 URL')
|
||||
|
||||
// 调用后端接口保存文件信息
|
||||
const saveRes = await aliyunpluploadApi({
|
||||
father: form.value.b_id,
|
||||
mx_xh: index,
|
||||
mx_filename: fileItem.file.name,
|
||||
mx_filesize: fileItem.file.size,
|
||||
mx_fileext: getFileExtension(fileItem.file.name),
|
||||
mx_mapfilename: res.url,
|
||||
mx_moduleid: 'b_container',
|
||||
mx_cate_id: '1',
|
||||
})
|
||||
|
||||
if (saveRes.success) {
|
||||
fileItem.isUpload = true
|
||||
fileItem.b_id = saveRes.data
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -174,8 +264,13 @@ function handleDeleteBoxImage() {
|
||||
</view>
|
||||
<view class="step-content">
|
||||
<sar-upload
|
||||
v-model="otherImages"
|
||||
v-model="subFileList"
|
||||
multiple
|
||||
:after-read="afterRead"
|
||||
:before-read="beforeRead"
|
||||
:before-remove="beforeRemove"
|
||||
@item-click="onItemClick"
|
||||
@remove="onRemove"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
@@ -183,7 +278,7 @@ function handleDeleteBoxImage() {
|
||||
|
||||
<view class="g3-footer">
|
||||
<view class="footer-main">
|
||||
<view class="submit-btn">
|
||||
<view class="submit-btn" @click="handleConfirm">
|
||||
确认上传
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -13,7 +13,7 @@ export function loginApi(params: LoginParams) {
|
||||
}
|
||||
|
||||
interface GetaliyunkeyParams {
|
||||
bucket: string
|
||||
bucketName: string
|
||||
}
|
||||
|
||||
export function getaliyunkeyApi(params: GetaliyunkeyParams) {
|
||||
@@ -21,11 +21,12 @@ export function getaliyunkeyApi(params: GetaliyunkeyParams) {
|
||||
}
|
||||
|
||||
interface AliyunpluploadParams {
|
||||
despathfile: string
|
||||
myfilename: string
|
||||
size: string
|
||||
tablename: string
|
||||
sid: string
|
||||
father: number
|
||||
mx_xh: number
|
||||
mx_filename: string
|
||||
mx_filesize: string
|
||||
mx_fileext: string
|
||||
mx_mapfilename: string
|
||||
mx_moduleid: string
|
||||
mx_cate_id: string
|
||||
}
|
||||
@@ -41,6 +42,14 @@ export function recognizeOcrBoxApi(params: OcrBoxRequest) {
|
||||
return httpPost(`/ocr/box`, params)
|
||||
}
|
||||
|
||||
export function deleteFileApi(id) {
|
||||
return httpPost(`file/delete`, {
|
||||
id,
|
||||
tableName: 'bf_file',
|
||||
bucketName: aliyunBucket,
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteAliyunFileApi(fileUrl) {
|
||||
return httpPost(`/file/deleteAliyunFile`, {
|
||||
bucketName: aliyunBucket,
|
||||
|
||||
@@ -133,7 +133,7 @@ export async function uploadFileToAliyun(
|
||||
): Promise<AliyunUploadResult> {
|
||||
// 获取 OSS 临时上传凭证
|
||||
const keyRes = await getaliyunkeyApi({
|
||||
bucket: aliyunBucket,
|
||||
bucketName: aliyunBucket,
|
||||
})
|
||||
const { accessid, callback, dir, host, policy, signature } = keyRes.data
|
||||
|
||||
|
||||
@@ -10,4 +10,6 @@ PUBLIC_SERVER_PORT=3000
|
||||
|
||||
# API
|
||||
# 后端 API 基础地址
|
||||
PUBLIC_API_BASE_URL=http://127.0.0.1:8085/api
|
||||
PUBLIC_API_BASE_URL=http://127.0.0.1:8085/api
|
||||
|
||||
PUBLIC_ALIYUN_BUCKET = g3hy
|
||||
+8
@@ -11,7 +11,10 @@ export {}
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
Directory: typeof import('./src/components/wang-editor/modules/directory.vue')['default']
|
||||
ElAlert: typeof import('element-plus/es')['ElAlert']
|
||||
ElButton: typeof import('element-plus/es')['ElButton']
|
||||
ElCard: typeof import('element-plus/es')['ElCard']
|
||||
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
||||
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
||||
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||
@@ -20,6 +23,8 @@ declare module 'vue' {
|
||||
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
|
||||
ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu']
|
||||
ElIcon: typeof import('element-plus/es')['ElIcon']
|
||||
ElImage: typeof import('element-plus/es')['ElImage']
|
||||
ElImageViewer: typeof import('element-plus/es')['ElImageViewer']
|
||||
ElInput: typeof import('element-plus/es')['ElInput']
|
||||
ElMenu: typeof import('element-plus/es')['ElMenu']
|
||||
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
||||
@@ -41,6 +46,7 @@ declare module 'vue' {
|
||||
ElTree: typeof import('element-plus/es')['ElTree']
|
||||
G3Column: typeof import('./src/components/g3-table/g3-column.vue')['default']
|
||||
G3Detail: typeof import('./src/components/g3-detail/index.vue')['default']
|
||||
G3File: typeof import('./src/components/g3-file/index.vue')['default']
|
||||
G3Grid: typeof import('./src/components/g3-grid/g3-grid.vue')['default']
|
||||
G3GridGroup: typeof import('./src/components/g3-grid/g3-grid-group.vue')['default']
|
||||
G3GridItem: typeof import('./src/components/g3-grid/g3-grid-item.vue')['default']
|
||||
@@ -48,8 +54,10 @@ declare module 'vue' {
|
||||
G3RecursiveColumn: typeof import('./src/components/g3-table/g3-recursive-column.vue')['default']
|
||||
G3Select: typeof import('./src/components/g3-select/index.vue')['default']
|
||||
G3TableView: typeof import('./src/components/g3-table/g3-table-view.vue')['default']
|
||||
Preview: typeof import('./src/components/wang-editor/modules/preview.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
WangEditor: typeof import('./src/components/wang-editor/index.vue')['default']
|
||||
}
|
||||
export interface GlobalDirectives {
|
||||
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
|
||||
|
||||
@@ -9,8 +9,12 @@
|
||||
"preview": "rsbuild preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.2",
|
||||
"@iconify/vue": "^5.0.0",
|
||||
"@wangeditor/editor": "^5.1.23",
|
||||
"@wangeditor/editor-for-vue": "^5.1.12",
|
||||
"axios": "^1.13.6",
|
||||
"dayjs": "^1.11.20",
|
||||
"element-plus": "^2.13.3",
|
||||
"lodash": "^4.17.23",
|
||||
"mitt": "^3.0.1",
|
||||
@@ -35,8 +39,8 @@
|
||||
"@rsbuild/plugin-vue-jsx": "^1.1.2",
|
||||
"oxfmt": "^0.40.0",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"unplugin-icons": "^23.0.1",
|
||||
"unplugin-auto-import": "^21.0.0",
|
||||
"unplugin-icons": "^23.0.1",
|
||||
"unplugin-vue-components": "^31.0.0"
|
||||
}
|
||||
}
|
||||
+526
-6
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,63 @@
|
||||
import { Boot, IDomEditor, IButtonMenu } from "@wangeditor/editor";
|
||||
|
||||
// 当前的点击处理器,始终指向最新传入的函数
|
||||
let currentClickHandler: (() => void) | null = null;
|
||||
|
||||
// 是否已经注册过菜单
|
||||
let hasRegistered = false;
|
||||
|
||||
class PreviewButtonMenu implements IButtonMenu {
|
||||
title = "预览";
|
||||
iconSvg = `
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 16 16">
|
||||
<path fill="currentColor" fill-rule="evenodd" d="M12 4.57a.5.5 0 0 0-.024-.235l-.013-.063a1.5 1.5 0 0 0-.18-.434c-.092-.15-.222-.28-.482-.54L8.711.707c-.259-.26-.389-.39-.54-.483a1.5 1.5 0 0 0-.496-.193a.5.5 0 0 0-.235-.024C7.329.004 7.194.004 7.015.004h-2.21c-1.68 0-2.52 0-3.16.327a3.02 3.02 0 0 0-1.31 1.31C.008 2.283.008 3.12.008 4.8v6.4c0 1.68 0 2.52.327 3.16a3.02 3.02 0 0 0 1.31 1.31c.642.327 1.48.327 3.16.327h2.423c.401 0 .602-.523.347-.832a.45.45 0 0 0-.345-.168H4.8c-.857 0-1.44-.001-1.89-.038c-.438-.036-.663-.1-.819-.18a2 2 0 0 1-.874-.874c-.08-.156-.145-.38-.18-.819c-.036-.45-.037-1.03-.037-1.89v-6.4c0-.857 0-1.44.037-1.89c.036-.438.101-.663.18-.819c.192-.376.498-.682.874-.874c.156-.08.381-.145.82-.18C3.36.997 3.94.997 4.8.997H7v3.5a.5.5 0 0 0 .5.5H11v.547c0 .25.207.45.456.473c.285.025.543-.188.543-.474V4.99c0-.178 0-.313-.005-.425zM8 1.41L10.59 4H8z" clip-rule="evenodd"/>
|
||||
<path fill="currentColor" fill-rule="evenodd" d="M11 15c.834 0 1.61-.255 2.25-.691l1.47 1.47a.749.749 0 1 0 1.06-1.06l-1.47-1.47c.436-.641.691-1.41.691-2.25c0-2.21-1.79-4-4-4s-4 1.79-4 4s1.79 4 4 4zm0-1c1.66 0 3-1.34 3-3s-1.34-3-3-3s-3 1.34-3 3s1.34 3 3 3" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
`;
|
||||
tag = "button";
|
||||
|
||||
getValue(): string | boolean {
|
||||
return "";
|
||||
}
|
||||
|
||||
isActive(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
isDisabled(): boolean {
|
||||
return false;
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
exec(editor: IDomEditor): void {
|
||||
if (this.isDisabled()) return;
|
||||
currentClickHandler?.(); // 始终执行最新的回调
|
||||
}
|
||||
}
|
||||
|
||||
// 提供注册函数,允许多次调用,但只注册一次菜单
|
||||
export function registerPreviewMenu(handleClick: () => void) {
|
||||
currentClickHandler = handleClick;
|
||||
|
||||
if (hasRegistered) return;
|
||||
|
||||
try {
|
||||
Boot.registerMenu({
|
||||
key: "preview",
|
||||
factory: () => new PreviewButtonMenu()
|
||||
});
|
||||
hasRegistered = true;
|
||||
} catch (e) {
|
||||
if (!/duplicate/i.test(String(e))) {
|
||||
console.warn("注册预览菜单失败:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 菜单插入配置
|
||||
export const toolbarConfig = {
|
||||
excludeKeys: ["codeBlock", "fullScreen", "code"],
|
||||
insertKeys: {
|
||||
index: 32,
|
||||
keys: ["preview"]
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,236 @@
|
||||
<script setup>
|
||||
import { Editor, Toolbar } from "@wangeditor/editor-for-vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import {
|
||||
computed,
|
||||
defineExpose,
|
||||
nextTick,
|
||||
onBeforeUnmount,
|
||||
onMounted,
|
||||
ref,
|
||||
shallowRef,
|
||||
watch,
|
||||
} from "vue";
|
||||
|
||||
import { uploadFile, uploadImage, uploadVideo } from "@/utils/file";
|
||||
import { registerPreviewMenu, toolbarConfig } from "./config";
|
||||
import Direcotry from "./modules/directory.vue";
|
||||
import Preview from "./modules/preview.vue";
|
||||
import "@wangeditor/editor/dist/css/style.css";
|
||||
|
||||
const props = defineProps({
|
||||
displayMode: {
|
||||
type: String,
|
||||
default: "edit", // edit / view
|
||||
},
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
showDirecotry: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
moduleId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
father: {
|
||||
type: [String, Number],
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
|
||||
const previewHtml = ref("");
|
||||
const showPreview = ref(false);
|
||||
|
||||
// 内容
|
||||
const valueHtml = ref("");
|
||||
|
||||
onMounted(() => {
|
||||
registerPreviewMenu(() => {
|
||||
previewHtml.value = valueHtml.value;
|
||||
showPreview.value = true;
|
||||
});
|
||||
});
|
||||
|
||||
// 编辑器实例
|
||||
const editorRef = shallowRef();
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(val) => {
|
||||
valueHtml.value = val;
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
const toolbarClass = computed(() =>
|
||||
props.displayMode === "view" ? "toolbar-view" : "toolbar-edit",
|
||||
);
|
||||
|
||||
const editorClass = computed(() => (props.displayMode === "view" ? "editor-view" : "editor-edit"));
|
||||
|
||||
defineExpose({
|
||||
getText: () => {
|
||||
return editorRef.value ? editorRef.value.getText() : "";
|
||||
},
|
||||
});
|
||||
|
||||
function handleChange() {
|
||||
emit("update:modelValue", valueHtml.value);
|
||||
}
|
||||
|
||||
// ================= 编辑器配置 =================
|
||||
const editorConfig = {
|
||||
placeholder: props.displayMode === "edit" ? "请输入内容..." : "",
|
||||
readOnly: props.displayMode === "view",
|
||||
MENU_CONF: {},
|
||||
};
|
||||
|
||||
// 图片上传
|
||||
editorConfig.MENU_CONF.uploadImage = {
|
||||
async customBrowseAndUpload(insertFn) {
|
||||
try {
|
||||
const data = await uploadImage();
|
||||
if (!data) return;
|
||||
|
||||
const res = await uploadFile(
|
||||
data.blob,
|
||||
data.fileName,
|
||||
data.fileExt,
|
||||
data.fileSize,
|
||||
0,
|
||||
props.moduleId,
|
||||
props.father,
|
||||
"",
|
||||
);
|
||||
insertFn(res.mx_mapfilename, res.mx_filename, "");
|
||||
} catch (e) {
|
||||
ElMessage.error("图片上传失败");
|
||||
console.log(e);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// 视频上传
|
||||
editorConfig.MENU_CONF.uploadVideo = {
|
||||
async customBrowseAndUpload(insertFn) {
|
||||
try {
|
||||
const data = await uploadVideo();
|
||||
if (!data) return;
|
||||
|
||||
const res = await uploadFile(
|
||||
data.blob,
|
||||
data.fileName,
|
||||
data.fileExt,
|
||||
data.fileSize,
|
||||
0,
|
||||
props.moduleId,
|
||||
props.father,
|
||||
"",
|
||||
);
|
||||
insertFn(res.mx_mapfilename, res.mx_filename, "");
|
||||
} catch (e) {
|
||||
ElMessage.error("视频上传失败");
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// 销毁编辑器
|
||||
onBeforeUnmount(() => {
|
||||
const editor = editorRef.value;
|
||||
if (editor) editor.destroy();
|
||||
});
|
||||
|
||||
function handleCreated(editor) {
|
||||
editorRef.value = editor;
|
||||
nextTick(() => {});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-loading="false" class="w-full h-full editor-container">
|
||||
<div class="main">
|
||||
<Toolbar
|
||||
v-if="props.displayMode === 'edit'"
|
||||
class="toolbar"
|
||||
:class="toolbarClass"
|
||||
:editor="editorRef"
|
||||
:default-config="toolbarConfig"
|
||||
/>
|
||||
<Editor
|
||||
key="main"
|
||||
v-model="valueHtml"
|
||||
class="editor"
|
||||
:class="editorClass"
|
||||
:default-config="editorConfig"
|
||||
@on-change="handleChange"
|
||||
@on-created="handleCreated"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Direcotry v-if="showDirecotry" v-model:content="valueHtml" class="directory" />
|
||||
</div>
|
||||
|
||||
<Preview v-model:visible="showPreview" :content="previewHtml" />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.toolbar {
|
||||
}
|
||||
|
||||
.toolbar-view {
|
||||
}
|
||||
|
||||
.toolbar-edit {
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.editor {
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
/* 原来 .editor 内嵌的 :deep */
|
||||
.editor :deep(.w-e-text-container video) {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.editor-view {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* 原来 .editor-view 内嵌的 :deep */
|
||||
.editor-view :deep(.w-e-text-container) {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.editor-view :deep(.w-e-text-container .w-e-scroll [data-slate-editor]) {
|
||||
overflow-y: auto;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.editor-edit {
|
||||
height: 500px !important;
|
||||
border: 1px solid #ccc;
|
||||
border-top: transparent;
|
||||
}
|
||||
|
||||
.editor-container {
|
||||
display: flex;
|
||||
gap: 44px;
|
||||
}
|
||||
|
||||
/* 原来 .editor-container 内嵌的 .directory */
|
||||
.editor-container .directory {
|
||||
width: 200px;
|
||||
height: 450px;
|
||||
}
|
||||
|
||||
/* 原来 .editor-container 内嵌的 .main */
|
||||
.editor-container .main {
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,169 @@
|
||||
<template>
|
||||
<el-card shadow="never" class="directory-card" :class="{ collapsed }">
|
||||
<!-- 头部 -->
|
||||
<div class="directory-header" @click="toggleCollapse">
|
||||
<div class="directory-header-left">
|
||||
<el-icon>
|
||||
<Folder />
|
||||
</el-icon>
|
||||
<span>目录</span>
|
||||
</div>
|
||||
<el-icon>
|
||||
<ArrowDown v-if="!collapsed" />
|
||||
<ArrowRight v-else />
|
||||
</el-icon>
|
||||
</div>
|
||||
|
||||
<!-- 内容 -->
|
||||
<el-scrollbar v-show="!collapsed" class="directory-content">
|
||||
<div v-for="item in directory" :key="item.id" class="directory-item" :class="`level-${item.level}`"
|
||||
@click="scrollTo(item.id)">
|
||||
{{ item.text }}
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, nextTick } from 'vue'
|
||||
import { Folder, ArrowDown, ArrowRight } from '@element-plus/icons-vue'
|
||||
|
||||
defineOptions({
|
||||
name: 'WangEditorDirectory'
|
||||
})
|
||||
|
||||
const props = defineProps({
|
||||
content: String,
|
||||
top: Number
|
||||
})
|
||||
|
||||
const directory = ref([])
|
||||
const collapsed = ref(false)
|
||||
|
||||
const toggleCollapse = () => {
|
||||
collapsed.value = !collapsed.value
|
||||
}
|
||||
|
||||
const scrollTo = (id) => {
|
||||
const el = document.getElementById(id)
|
||||
if (!el) return
|
||||
|
||||
if (props.top) {
|
||||
const rect = el.getBoundingClientRect()
|
||||
const scrollTop =
|
||||
window.pageYOffset || document.documentElement.scrollTop
|
||||
const top = rect.top + scrollTop - props.top
|
||||
|
||||
window.scrollTo({
|
||||
top,
|
||||
behavior: 'smooth'
|
||||
})
|
||||
} else {
|
||||
el.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function throttle(fn) {
|
||||
let running = false
|
||||
return (...args) => {
|
||||
if (running) return
|
||||
running = true
|
||||
requestAnimationFrame(() => {
|
||||
fn(...args)
|
||||
running = false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function generateDirectory() {
|
||||
const root = document.querySelector('.editor')
|
||||
if (!root) return
|
||||
|
||||
const headings = root.querySelectorAll('h1, h2, h3')
|
||||
directory.value = Array.from(headings).map((h, i) => {
|
||||
if (!h.id) h.id = `section-${i + 1}`
|
||||
return {
|
||||
id: h.id,
|
||||
text: h.textContent || '',
|
||||
level: Number(h.tagName[1]),
|
||||
index: i
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const updateDirectory = throttle(generateDirectory)
|
||||
|
||||
watch(
|
||||
() => props.content,
|
||||
() => {
|
||||
nextTick(updateDirectory)
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
.directory-card {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* 折叠态 */
|
||||
.directory-card.collapsed {
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
.directory-header {
|
||||
height: 44px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
padding: 0 12px;
|
||||
border-bottom: 1px solid var(--el-border-color);
|
||||
}
|
||||
|
||||
.directory-header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.directory-content {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.directory-item {
|
||||
padding: 6px 8px;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.directory-item:hover {
|
||||
background: var(--el-color-primary-light-9);
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.directory-item.level-1 {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.directory-item.level-2 {
|
||||
padding-left: 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.directory-item.level-3 {
|
||||
padding-left: 40px;
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,133 @@
|
||||
<script setup>
|
||||
import { Close, FullScreen } from '@element-plus/icons-vue'
|
||||
import { Editor } from '@wangeditor/editor-for-vue'
|
||||
import { ref, shallowRef, watch } from 'vue'
|
||||
import '@wangeditor/editor/dist/css/style.css'
|
||||
// ================= Props =================
|
||||
const props = defineProps({
|
||||
content: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
visible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:visible'])
|
||||
|
||||
// ================= 编辑器配置 =================
|
||||
const editorConfig = {
|
||||
placeholder: '暂无内容',
|
||||
readOnly: true,
|
||||
MENU_CONF: {},
|
||||
}
|
||||
|
||||
const fullscreen = ref(false)
|
||||
const localVisible = ref(props.visible)
|
||||
const editorRef = shallowRef()
|
||||
|
||||
// 同步 visible
|
||||
watch(
|
||||
() => props.visible,
|
||||
(val) => {
|
||||
localVisible.value = val
|
||||
},
|
||||
)
|
||||
|
||||
// 编辑器创建
|
||||
function handleCreated(editor) {
|
||||
editorRef.value = editor
|
||||
if (props.content) {
|
||||
editor.setHtml(props.content)
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
function handleCloseDialog() {
|
||||
fullscreen.value = false
|
||||
localVisible.value = false
|
||||
|
||||
emit('update:visible', false)
|
||||
}
|
||||
|
||||
// 打开弹窗
|
||||
function handleOpenDialog() {
|
||||
if (editorRef.value) {
|
||||
editorRef.value.setHtml(props.content || '')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-dialog
|
||||
v-bind="$attrs"
|
||||
v-model="localVisible"
|
||||
:fullscreen="fullscreen"
|
||||
:show-close="false"
|
||||
@close="handleCloseDialog"
|
||||
@open="handleOpenDialog"
|
||||
>
|
||||
<template #header>
|
||||
<div class="header">
|
||||
<div />
|
||||
<div class="title">
|
||||
预览
|
||||
</div>
|
||||
<div class="actions">
|
||||
<div class="action" @click="fullscreen = !fullscreen">
|
||||
<el-icon>
|
||||
<FullScreen />
|
||||
</el-icon>
|
||||
</div>
|
||||
<div class="action" @click="handleCloseDialog">
|
||||
<el-icon>
|
||||
<Close />
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="editor-container" :class="[{ 'editor-fullscreen': fullscreen }]">
|
||||
<Editor
|
||||
key="preview"
|
||||
:default-config="editorConfig"
|
||||
style="height: 100%"
|
||||
@on-created="handleCreated"
|
||||
/>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.header {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.actions .action {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.editor-container {
|
||||
height: 500px;
|
||||
}
|
||||
|
||||
.editor-container.editor-fullscreen {
|
||||
height: calc(100vh - 100px);
|
||||
}
|
||||
</style>
|
||||
@@ -9,13 +9,11 @@ const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
onMounted(() => {
|
||||
loadDataApi("s_keyvalue", `b_key='bodycaption'`, ``, ["b_value"]).then(
|
||||
(res) => {
|
||||
if (res.success) {
|
||||
title.value = res.data[0].b_value;
|
||||
}
|
||||
},
|
||||
);
|
||||
loadDataApi("s_keyvalue", `b_key='bodycaption'`, ``, ["b_value"]).then((res) => {
|
||||
if (res.success) {
|
||||
title.value = res.data[0].b_value;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const menuList = ref([
|
||||
@@ -27,16 +25,7 @@ const menuList = ref([
|
||||
{
|
||||
label: "业务管理",
|
||||
icon: "lucide:briefcase-business",
|
||||
children: [
|
||||
{
|
||||
label: "海运管理",
|
||||
path: "/business",
|
||||
},
|
||||
{
|
||||
label: "装箱统计",
|
||||
path: "/business/field",
|
||||
},
|
||||
],
|
||||
children: [],
|
||||
},
|
||||
{
|
||||
label: "系统管理",
|
||||
@@ -178,9 +167,9 @@ function handleMenuChange(val) {
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
// .el-sub-menu__title {
|
||||
// color: var(--el-color-primary);
|
||||
// }
|
||||
> .el-sub-menu__title {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,12 +95,16 @@ export function saveObjectKeysApi(data) {
|
||||
return httpPost("/data/saveObjectKeys", data);
|
||||
}
|
||||
|
||||
export function deleteModuleApi({ moduleId, Id }) {
|
||||
return httpPost("/data/deleteModule", {
|
||||
export function deleteObjectApi({ moduleId, id }) {
|
||||
return httpPost("/data/deleteObject", {
|
||||
moduleId,
|
||||
Id,
|
||||
id,
|
||||
});
|
||||
}
|
||||
|
||||
export function getUniqueIdApi() {
|
||||
return httpPost("/data/getUniqueId");
|
||||
}
|
||||
// ============================= data =============================
|
||||
|
||||
// ============================= column =============================
|
||||
@@ -138,3 +142,23 @@ export function updateColumnsApi(columns) {
|
||||
return httpPost("/column/updateColumns", columns);
|
||||
}
|
||||
// ============================= column =============================
|
||||
|
||||
// ============================= file =============================
|
||||
export function getAliyunKeyApi() {
|
||||
return httpPost("/file/getAliyunKey", {
|
||||
bucketName: import.meta.env.PUBLIC_ALIYUN_BUCKET,
|
||||
});
|
||||
}
|
||||
|
||||
export function insertFileApi(params) {
|
||||
return httpPost("/file/insert", params);
|
||||
}
|
||||
|
||||
export function deleteFileApi({ id, tableName }) {
|
||||
return httpPost("/file/delete", {
|
||||
id,
|
||||
tableName,
|
||||
bucketName: import.meta.env.PUBLIC_ALIYUN_BUCKET,
|
||||
});
|
||||
}
|
||||
// ============================= file =============================
|
||||
@@ -33,3 +33,77 @@ export function diffObject(newData, oldData, primaryKeys = [], excludeKeys = [])
|
||||
|
||||
return { isChanged, result };
|
||||
}
|
||||
|
||||
/* 比对并分类数据变更
|
||||
* @param {Array} currentData 变更后的数据数组
|
||||
* @param {Array} originalData 原始数据数组
|
||||
* @param {String} primaryKey 主键字段名
|
||||
* @param {Array} ignoreFields 过滤字段(可传可不传)
|
||||
* @returns {Object} 包含分类结果的对象 { insertdata: [], updatedata: [], deletedata: [], hasChanges: Boolean }
|
||||
*/
|
||||
export function compareAndClassifyData(currentData, originalData, primaryKey) {
|
||||
const result = {
|
||||
insertdata: [],
|
||||
updatedata: [],
|
||||
deletedata: [],
|
||||
hasChanges: false, // 默认无变动
|
||||
};
|
||||
|
||||
// 1. 找出新增的数据
|
||||
result.insertdata = currentData.filter(
|
||||
(row) =>
|
||||
!row[primaryKey] ||
|
||||
!originalData.some((originalRow) => originalRow[primaryKey] === row[primaryKey]),
|
||||
);
|
||||
|
||||
// 2. 找出修改的数据
|
||||
result.updatedata = currentData.reduce((acc, row) => {
|
||||
const rowId = row[primaryKey];
|
||||
if (!rowId) return acc;
|
||||
|
||||
const originalRow = originalData.find((r) => r[primaryKey] === rowId);
|
||||
if (!originalRow) return acc;
|
||||
|
||||
const changedFields = {};
|
||||
Object.keys(row).forEach((key) => {
|
||||
if (key !== primaryKey && !key.startsWith("v_")) {
|
||||
const oldVal = originalRow[key];
|
||||
const newVal = row[key];
|
||||
|
||||
const normalize = (v) => (v === null || v === undefined ? "" : v);
|
||||
|
||||
if (normalize(newVal) !== normalize(oldVal)) {
|
||||
// ✅ 避免 undefined 被丢弃
|
||||
changedFields[key] = newVal ?? "";
|
||||
}
|
||||
}
|
||||
});
|
||||
// Object.keys(row).forEach((key) => {
|
||||
// if (key !== primaryKey && !isEqual(row[key], originalRow[key])) {
|
||||
// if (!key.startsWith('v_')) {
|
||||
// changedFields[key] = row[key]
|
||||
// }
|
||||
// }
|
||||
// })
|
||||
|
||||
if (Object.keys(changedFields).length > 0) {
|
||||
acc.push({
|
||||
[primaryKey]: rowId,
|
||||
...changedFields,
|
||||
});
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
// 3. 找出删除的数据
|
||||
result.deletedata = originalData.filter(
|
||||
(originalRow) => !currentData.some((row) => row[primaryKey] === originalRow[primaryKey]),
|
||||
);
|
||||
|
||||
// 4. 是否有任何变动
|
||||
result.hasChanges =
|
||||
result.insertdata.length > 0 || result.updatedata.length > 0 || result.deletedata.length > 0;
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,366 @@
|
||||
import { getAliyunKeyApi } from "@/request/api";
|
||||
|
||||
/**
|
||||
* 选择并压缩图片
|
||||
*/
|
||||
export function uploadImage(options = {}) {
|
||||
const { quality = 0.9, maxSize = 512000 } = options
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (typeof document === 'undefined') {
|
||||
reject(new Error('Require browser environment'))
|
||||
return
|
||||
}
|
||||
|
||||
const input = document.createElement('input')
|
||||
input.type = 'file'
|
||||
input.accept = 'image/*'
|
||||
input.style.display = 'none'
|
||||
|
||||
input.onchange = async (e) => {
|
||||
const file = e.target.files?.[0]
|
||||
if (!file) {
|
||||
reject(new Error('No file selected'))
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
if (!file.type.startsWith('image/')) {
|
||||
throw new Error('Invalid image file')
|
||||
}
|
||||
|
||||
// 创建一个临时URL来加载图片
|
||||
const url = URL.createObjectURL(file)
|
||||
|
||||
// 调用 compressImage 进行压缩
|
||||
const compressResult = await compressImage(url, quality, maxSize)
|
||||
|
||||
// 释放临时URL
|
||||
URL.revokeObjectURL(url)
|
||||
|
||||
resolve({
|
||||
blob: compressResult.blob,
|
||||
url: compressResult.url,
|
||||
fileName: file.name,
|
||||
fileExt: getFileExtension(file.name),
|
||||
fileSize: file.size,
|
||||
})
|
||||
}
|
||||
catch (err) {
|
||||
reject(err)
|
||||
}
|
||||
finally {
|
||||
document.body.removeChild(input)
|
||||
}
|
||||
}
|
||||
|
||||
document.body.appendChild(input)
|
||||
input.click()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 选择视频
|
||||
*/
|
||||
export function uploadVideo() {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (typeof document === 'undefined') {
|
||||
reject(new Error('Require browser environment'))
|
||||
return
|
||||
}
|
||||
|
||||
const input = document.createElement('input')
|
||||
input.type = 'file'
|
||||
input.accept = 'video/*'
|
||||
input.style.display = 'none'
|
||||
|
||||
input.onchange = (e) => {
|
||||
const file = e.target.files?.[0]
|
||||
if (!file) {
|
||||
reject(new Error('No file selected'))
|
||||
return
|
||||
}
|
||||
|
||||
if (!file.type.startsWith('video/')) {
|
||||
reject(new Error('Invalid video file'))
|
||||
return
|
||||
}
|
||||
|
||||
resolve({
|
||||
blob: file,
|
||||
url: URL.createObjectURL(file),
|
||||
fileName: file.name,
|
||||
fileExt: getFileExtension(file.name),
|
||||
fileSize: file.size,
|
||||
})
|
||||
|
||||
document.body.removeChild(input)
|
||||
}
|
||||
|
||||
document.body.appendChild(input)
|
||||
input.click()
|
||||
})
|
||||
}
|
||||
|
||||
// 压缩图片
|
||||
export async function compressImage(url, quality = 0.9, maxSize = 512000) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image()
|
||||
img.src = url
|
||||
|
||||
img.onload = async () => {
|
||||
try {
|
||||
// 获取原图大小
|
||||
const imgSize = await fetch(url)
|
||||
.then(res => res.blob())
|
||||
.then(blob => blob.size)
|
||||
|
||||
if (imgSize <= maxSize) {
|
||||
return resolve({ url, size: imgSize, blob: await fetch(url).then(res => res.blob()) })
|
||||
}
|
||||
const canvas = document.createElement('canvas')
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx)
|
||||
return reject(new Error('无法获取canvas上下文'))
|
||||
|
||||
let targetWidth = img.width
|
||||
let targetHeight = img.height
|
||||
|
||||
// 最大边长限制
|
||||
const maxEdge = 2000
|
||||
if (targetWidth > maxEdge || targetHeight > maxEdge) {
|
||||
if (targetWidth > targetHeight) {
|
||||
targetHeight = (maxEdge * targetHeight) / targetWidth
|
||||
targetWidth = maxEdge
|
||||
}
|
||||
else {
|
||||
targetWidth = (maxEdge * targetWidth) / targetHeight
|
||||
targetHeight = maxEdge
|
||||
}
|
||||
}
|
||||
|
||||
canvas.width = targetWidth
|
||||
canvas.height = targetHeight
|
||||
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||
ctx.drawImage(img, 0, 0, targetWidth, targetHeight)
|
||||
|
||||
const getBlob = q =>
|
||||
new Promise((resolve, reject) => {
|
||||
canvas.toBlob(
|
||||
blob => (blob ? resolve(blob) : reject(new Error('Blob转换失败'))),
|
||||
'image/jpeg',
|
||||
q,
|
||||
)
|
||||
})
|
||||
|
||||
let blob = await getBlob(quality)
|
||||
|
||||
// 调整质量直到接近最大文件大小
|
||||
let minQ = 0.5
|
||||
let maxQ = quality
|
||||
let attempts = 0
|
||||
|
||||
while (blob.size > maxSize && maxQ - minQ > 0.01 && attempts < 5) {
|
||||
const midQ = (minQ + maxQ) / 2
|
||||
blob = await getBlob(midQ)
|
||||
if (blob.size > maxSize) {
|
||||
maxQ = midQ
|
||||
}
|
||||
else {
|
||||
minQ = midQ
|
||||
}
|
||||
attempts++
|
||||
}
|
||||
resolve({ blob, size: blob.size, url: URL.createObjectURL(blob) })
|
||||
}
|
||||
catch (err) {
|
||||
reject(err)
|
||||
}
|
||||
}
|
||||
|
||||
img.onerror = () => reject(new Error('图片加载失败'))
|
||||
})
|
||||
}
|
||||
|
||||
export function formatFileSize(size) {
|
||||
if (size < 1024)
|
||||
return `${size} B`
|
||||
else if (size < 1024 * 1024)
|
||||
return `${(size / 1024).toFixed(2)} KB`
|
||||
else if (size < 1024 * 1024 * 1024)
|
||||
return `${(size / (1024 * 1024)).toFixed(2)} MB`
|
||||
else return `${(size / (1024 * 1024 * 1024)).toFixed(2)} GB`
|
||||
}
|
||||
|
||||
// 获取文件扩展名并进行分类
|
||||
export function getFileExtension(fileName) {
|
||||
// 获取文件扩展名
|
||||
const fileExt = fileName.includes('.')
|
||||
? fileName.slice(fileName.lastIndexOf('.') + 1).toLowerCase() // 获取扩展名并转为小写
|
||||
: ''
|
||||
|
||||
// 如果是office类文件,将其归类到 "office"
|
||||
if (['docx', 'docm', 'xls', 'xlsx'].includes(fileExt)) {
|
||||
return 'office'
|
||||
}
|
||||
|
||||
// 返回扩展名
|
||||
return fileExt
|
||||
}
|
||||
|
||||
// 上传到阿里云
|
||||
// blob fileName fileSize xh moduleid father cateid
|
||||
export function uploadFile(blob, fileName, fileExt, fileSize, xh, moduleid, father, cateid) {
|
||||
return new Promise((resolve, reject) => {
|
||||
getAliyunKeyApi()
|
||||
.then(async (res) => {
|
||||
try {
|
||||
const aliyunKey = res.data
|
||||
const formData = new FormData()
|
||||
|
||||
let uniqueName = `${Date.now()}_${Math.random()
|
||||
.toString(36)
|
||||
.slice(2)}`
|
||||
|
||||
if (fileExt) {
|
||||
uniqueName += `.${fileExt}`
|
||||
}
|
||||
|
||||
formData.append('key', `${aliyunKey.dir}${uniqueName}`)
|
||||
formData.append('OSSAccessKeyId', aliyunKey.accessid)
|
||||
formData.append('policy', aliyunKey.policy)
|
||||
formData.append('Signature', aliyunKey.signature)
|
||||
formData.append('file', blob, uniqueName)
|
||||
|
||||
const xhr = new XMLHttpRequest()
|
||||
xhr.open('POST', aliyunKey.host, true)
|
||||
|
||||
xhr.onload = async () => {
|
||||
if (xhr.status === 200 || xhr.status === 204) {
|
||||
const allFilePath = `${aliyunKey.host}/${aliyunKey.dir}${uniqueName}`
|
||||
|
||||
const insertRes = await insertFileApi({
|
||||
father,
|
||||
mx_moduleid: moduleid,
|
||||
mx_xh: xh,
|
||||
mx_filename: fileName,
|
||||
mx_mapfilename: allFilePath,
|
||||
mx_filesize: fileSize,
|
||||
mx_fileext: fileExt,
|
||||
mx_cate_id: cateid || '',
|
||||
})
|
||||
|
||||
if (insertRes.success) {
|
||||
resolve({
|
||||
father,
|
||||
mx_moduleid: moduleid,
|
||||
mx_xh: xh,
|
||||
mx_filename: fileName,
|
||||
mx_mapfilename: allFilePath,
|
||||
mx_filesize: fileSize,
|
||||
mx_fileext: fileExt,
|
||||
mx_cate_id: cateid || '',
|
||||
})
|
||||
}
|
||||
else {
|
||||
reject(new Error(insertRes.data.message))
|
||||
}
|
||||
}
|
||||
else {
|
||||
ElMessage.error('上传失败,请稍后再试')
|
||||
reject(new Error(`OSS 上传失败: ${xhr.responseText}`))
|
||||
}
|
||||
}
|
||||
|
||||
xhr.onerror = () => {
|
||||
ElMessage.error('上传失败,请稍后再试')
|
||||
reject(new Error('OSS 上传异常'))
|
||||
}
|
||||
|
||||
xhr.send(formData)
|
||||
}
|
||||
catch (err) {
|
||||
ElMessage.error('上传失败,请稍后再试')
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
.catch(reject)
|
||||
})
|
||||
}
|
||||
|
||||
export function getAliyunFileBlob(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 创建一个独立的异步函数来处理 fetch 请求
|
||||
async function fetchFile() {
|
||||
try {
|
||||
const response = await fetch(url, { mode: 'cors' })
|
||||
|
||||
// 检查响应状态
|
||||
if (!response.ok) {
|
||||
ElMessage.error(`下载失败:${response.status}`)
|
||||
reject(new Error(`下载失败:${response.status}`))
|
||||
return
|
||||
}
|
||||
|
||||
// 获取文件内容
|
||||
const blob = await response.blob()
|
||||
|
||||
// 返回blob
|
||||
resolve(blob)
|
||||
}
|
||||
catch (error) {
|
||||
// 捕获网络请求失败或其他错误
|
||||
ElMessage.error('下载文件时发生错误')
|
||||
reject(error)
|
||||
}
|
||||
}
|
||||
|
||||
// 调用异步函数
|
||||
fetchFile()
|
||||
})
|
||||
}
|
||||
|
||||
export function downloadZip(
|
||||
files,
|
||||
fileName = '文件合集',
|
||||
callback,
|
||||
) {
|
||||
const zip = new JSZip()
|
||||
const addedFiles = new Set() // 用来存储已添加的文件名
|
||||
|
||||
let result = files.map((item) => {
|
||||
let promise = getAliyunFileBlob(item.mx_mapfilename).then((res) => {
|
||||
// 处理文件名重复的情况
|
||||
let uniqueFileName = getUniqueFileName(item.mx_filename, addedFiles)
|
||||
zip.file(uniqueFileName, res, { binary: true })
|
||||
addedFiles.add(uniqueFileName) // 添加文件名到已添加的集合中
|
||||
})
|
||||
return promise
|
||||
})
|
||||
|
||||
Promise.all(result).then(() => {
|
||||
zip.generateAsync({ type: 'blob' }).then((res) => {
|
||||
callback && callback()
|
||||
saveAs(res, `${fileName}.zip`)
|
||||
})
|
||||
}).catch(() => {
|
||||
callback && callback()
|
||||
})
|
||||
}
|
||||
|
||||
// 处理文件名重复的函数
|
||||
function getUniqueFileName(fileName, addedFiles) {
|
||||
let uniqueFileName = fileName
|
||||
let counter = 1
|
||||
|
||||
// 如果文件名已经存在,则在文件名后加上 (1), (2), ...
|
||||
while (addedFiles.has(uniqueFileName)) {
|
||||
const fileNameWithoutExtension = uniqueFileName.replace(/\.[^/.]+$/, '')
|
||||
const extension = uniqueFileName.slice(uniqueFileName.lastIndexOf('.'))
|
||||
uniqueFileName = `${fileNameWithoutExtension} (${counter})${extension}`
|
||||
counter++
|
||||
}
|
||||
|
||||
return uniqueFileName
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
loadPageDataApi,
|
||||
loadViewColumnsApi,
|
||||
saveObjectApi,
|
||||
deleteModuleApi,
|
||||
deleteObjectApi,
|
||||
} from "@/request/api";
|
||||
import { updateViewTableColumns } from "@/utils/columns";
|
||||
|
||||
@@ -36,7 +36,7 @@ const tree_id = ref("");
|
||||
// 分页参数
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
size: 20,
|
||||
size: 50,
|
||||
total: 0,
|
||||
});
|
||||
|
||||
@@ -70,16 +70,14 @@ function buildSqlWhere() {
|
||||
}
|
||||
// 加载数据
|
||||
async function getdata() {
|
||||
pagination.currentPage = 1; // 重置到第一页
|
||||
pagination.page = 1; // 重置到第一页
|
||||
await gettabledata("s_module", buildSqlWhere());
|
||||
}
|
||||
|
||||
// 查看所有模块
|
||||
async function showallmod() {
|
||||
try {
|
||||
const response = await loadDataBySqlApi(
|
||||
`SELECT * FROM s_module ORDER BY b_Order`,
|
||||
);
|
||||
const response = await loadDataBySqlApi(`SELECT * FROM s_module ORDER BY b_Order`);
|
||||
|
||||
if (response.success) {
|
||||
tableData.value = response.data || [];
|
||||
@@ -162,9 +160,9 @@ function handleDelClick(row) {
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
}).then(async () => {
|
||||
deleteModuleApi({
|
||||
deleteObjectApi({
|
||||
moduleId: "s_module",
|
||||
Id: row.b_id,
|
||||
id: row.b_id,
|
||||
}).then((res) => {
|
||||
if (res.success) {
|
||||
ElMessage.success("删除成功");
|
||||
@@ -245,9 +243,7 @@ const checkedIds = [];
|
||||
async function loadNode(node, resolve) {
|
||||
if (node.level === 0) {
|
||||
// 根节点加载第1级
|
||||
const res = await loadDataBySqlApi(
|
||||
"select * from s_module_bmfl where js='1' order by b_xh",
|
||||
);
|
||||
const res = await loadDataBySqlApi("select * from s_module_bmfl where js='1' order by b_xh");
|
||||
|
||||
if (res.success) {
|
||||
const records = res.data.map((r) => {
|
||||
@@ -360,7 +356,7 @@ async function handleTreeClick(nodeData) {
|
||||
where = `b_Cate_ID='${nodeData.id}'`;
|
||||
}
|
||||
|
||||
pagination.currentPage = 1; // 重置到第一页
|
||||
pagination.page = 1; // 重置到第一页
|
||||
await gettabledata("s_module", where);
|
||||
tree_id.value = nodeData.id;
|
||||
}
|
||||
@@ -384,10 +380,7 @@ function onRightClick(event, data, node, comp) {
|
||||
contextMenu.x = event.clientX + 4;
|
||||
contextMenu.y = event.clientY + 4;
|
||||
contextMenu.node = node;
|
||||
contextMenu.parent = findAncestorNodeByLevel(
|
||||
node.data,
|
||||
Number(node.data.js) - 1,
|
||||
);
|
||||
contextMenu.parent = findAncestorNodeByLevel(node.data, Number(node.data.js) - 1);
|
||||
}
|
||||
|
||||
// 🌟 隐藏菜单
|
||||
@@ -468,15 +461,7 @@ async function deleteNode() {
|
||||
}
|
||||
});
|
||||
}
|
||||
async function handleDialogSubmit({
|
||||
p_id,
|
||||
p_name,
|
||||
bm_b,
|
||||
mc_b,
|
||||
b_xh,
|
||||
b_canuse,
|
||||
b_canmenu,
|
||||
}) {
|
||||
async function handleDialogSubmit({ p_id, p_name, bm_b, mc_b, b_xh, b_canuse, b_canmenu }) {
|
||||
if (dialogMode.value == "add") {
|
||||
const Fnode = contextMenu.node.data;
|
||||
|
||||
@@ -492,7 +477,7 @@ async function handleDialogSubmit({
|
||||
((obj.b_canuse = b_canuse),
|
||||
(obj.b_canmenu = b_canmenu),
|
||||
(obj.b_xh = b_xh),
|
||||
(obj.js = Number(Fnode.js) + 1));
|
||||
(obj.js = (Number(Fnode.js) || 0) + 1));
|
||||
obj.bm_1_b = GetTreeViewPath(Fnode, 1, "id");
|
||||
obj.bm_2_b = GetTreeViewPath(Fnode, 2, "id");
|
||||
obj.bm_3_b = GetTreeViewPath(Fnode, 3, "id");
|
||||
@@ -583,6 +568,11 @@ async function handleDialogSubmit({
|
||||
|
||||
// 获取节点路径
|
||||
function GetTreeViewPath(node, Ilevel, flag) {
|
||||
console.log(node);
|
||||
if (node.id === "all") {
|
||||
return "";
|
||||
}
|
||||
|
||||
let level = node.js;
|
||||
if (level < Ilevel) {
|
||||
return "";
|
||||
@@ -694,237 +684,202 @@ onMounted(async () => {
|
||||
<template>
|
||||
<div class="g3-flex-container">
|
||||
<div class="g3-header">
|
||||
<el-button type="primary" @click="setpowervalue = !setpowervalue">
|
||||
权限设置
|
||||
</el-button>
|
||||
<el-button type="primary" @click="setkeyvalue = !setkeyvalue">
|
||||
键值设置
|
||||
</el-button>
|
||||
<el-button type="primary" @click="setotherbmfl = !setotherbmfl">
|
||||
基础数据目录
|
||||
</el-button>
|
||||
<el-button type="primary" @click="setpowervalue = !setpowervalue"> 权限设置 </el-button>
|
||||
<el-button type="primary" @click="setkeyvalue = !setkeyvalue"> 键值设置 </el-button>
|
||||
<el-button type="primary" @click="setotherbmfl = !setotherbmfl"> 基础数据目录 </el-button>
|
||||
</div>
|
||||
|
||||
<div class="g3-container" @click="hideContextMenu">
|
||||
<div class="g3-container-left">
|
||||
<div class="g3-title">
|
||||
<G3Icon icon="lucide:blocks" />
|
||||
<div style="margin-left: 3px">模块设置</div>
|
||||
</div>
|
||||
<el-tree
|
||||
ref="treeRef"
|
||||
:data="treeData"
|
||||
:props="defaultProps"
|
||||
node-key="id"
|
||||
lazy
|
||||
:load="loadNode"
|
||||
:default-expanded-keys="['all']"
|
||||
:default-checked-keys="tree_checkedKeys"
|
||||
show-checkbox
|
||||
check-strictly
|
||||
:check-on-click-node="false"
|
||||
@node-contextmenu="onRightClick"
|
||||
@node-click="handleTreeClick"
|
||||
/>
|
||||
<!-- 右键菜单 -->
|
||||
<div
|
||||
v-if="contextMenu.visible"
|
||||
class="context-menu"
|
||||
:style="{ top: `${contextMenu.y}px`, left: `${contextMenu.x}px` }"
|
||||
>
|
||||
<div class="menu-item" @click.stop="addNode">
|
||||
<el-icon> <G3Icon icon="lucide:plus" /></el-icon>新增分类
|
||||
</div>
|
||||
<div class="menu-item" @click.stop="editNode">
|
||||
<el-icon> <G3Icon icon="lucide:pen-line" /></el-icon>修改分类
|
||||
</div>
|
||||
<div class="menu-item delete" @click.stop="deleteNode">
|
||||
<el-icon> <G3Icon icon="lucide:trash-2" /></el-icon>删除分类
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style="
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
"
|
||||
>
|
||||
<div class="g3-row-main">
|
||||
<div style="width: 100%">
|
||||
<div
|
||||
style="
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
height: 32px;
|
||||
margin-bottom: 8px;
|
||||
"
|
||||
>
|
||||
<div style="display: flex">
|
||||
<el-button type="primary"> 关联 </el-button>
|
||||
<el-button type="primary" @click="handleAdd"> 新建 </el-button>
|
||||
<el-button type="primary" @click="showallmod">
|
||||
查看所有模块
|
||||
</el-button>
|
||||
<el-button type="primary" @click="showmodSql">
|
||||
SQL语句
|
||||
</el-button>
|
||||
</div>
|
||||
<div style="display: flex">
|
||||
<el-dropdown>
|
||||
<div class="g3-svg-query">
|
||||
<span
|
||||
class="iconfont icon-gengduo1"
|
||||
style="font-size: 12px"
|
||||
/>
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item
|
||||
@click="updateViewTableColumns('s_module')"
|
||||
>
|
||||
栏位保存
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
<el-splitter>
|
||||
<el-splitter-panel size="220">
|
||||
<div class="g3-container-left">
|
||||
<div class="g3-title">
|
||||
<G3Icon icon="lucide:blocks" />
|
||||
<div style="margin-left: 3px">模块设置</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style="
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
"
|
||||
>
|
||||
<vxe-table
|
||||
ref="tableRef"
|
||||
border
|
||||
stripe
|
||||
header-align="center"
|
||||
keep-source
|
||||
:data="tableData"
|
||||
:loading="loading.table"
|
||||
:sort-config="{
|
||||
// showIcon: false,
|
||||
trigger: 'cell',
|
||||
}"
|
||||
:filter-config="{
|
||||
showIcon: false,
|
||||
}"
|
||||
:column-drag-config="{
|
||||
showIcon: false,
|
||||
trigger: 'cell',
|
||||
}"
|
||||
:row-config="{
|
||||
useKey: true,
|
||||
// keyField: 'b_id',
|
||||
isCurrent: true,
|
||||
isHover: true,
|
||||
}"
|
||||
:column-config="{
|
||||
resizable: true,
|
||||
drag: true,
|
||||
}"
|
||||
:edit-config="{
|
||||
trigger: 'click',
|
||||
mode: 'row',
|
||||
autoClear: false,
|
||||
showStatus: true,
|
||||
showIcon: false,
|
||||
}"
|
||||
height="100%"
|
||||
show-overflow
|
||||
show-header-overflow
|
||||
>
|
||||
<vxe-column type="checkbox" width="50" align="center" />
|
||||
<vxe-column
|
||||
v-for="column in columns"
|
||||
:key="column.col_FieldName"
|
||||
:field="column.col_FieldName"
|
||||
:title="column.col_Caption"
|
||||
:width="column.col_Width"
|
||||
sortable
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<template
|
||||
v-if="
|
||||
[
|
||||
'b_ISVirtualModule',
|
||||
'b_isMainModule',
|
||||
'b_SH',
|
||||
'b_autoid',
|
||||
].includes(column.col_FieldName)
|
||||
"
|
||||
>
|
||||
<el-checkbox
|
||||
v-model="row[column.col_FieldName]"
|
||||
disabled
|
||||
true-value="1"
|
||||
false-value="0"
|
||||
@change="handleCheckChange(row, column.col_FieldName)"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ row[column.col_FieldName] }}
|
||||
</template>
|
||||
</template>
|
||||
</vxe-column>
|
||||
|
||||
<vxe-column width="180" align="center" fixed="right" title="操作">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
size="small"
|
||||
style="font-size: 14px"
|
||||
@click="handleEditClick(row)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
size="small"
|
||||
style="font-size: 14px"
|
||||
@click="handleDelClick(row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</vxe-column>
|
||||
</vxe-table>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style="
|
||||
padding-top: 8px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
"
|
||||
>
|
||||
<el-pagination
|
||||
:current-page="pagination.currentPage"
|
||||
:page-sizes="[50, 100, 200, 500, 1000]"
|
||||
:page-size="pagination.pageSize"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="pagination.total"
|
||||
class="pagination"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
<el-tree
|
||||
ref="treeRef"
|
||||
:data="treeData"
|
||||
:props="defaultProps"
|
||||
node-key="id"
|
||||
lazy
|
||||
:load="loadNode"
|
||||
:default-expanded-keys="['all']"
|
||||
:default-checked-keys="tree_checkedKeys"
|
||||
show-checkbox
|
||||
check-strictly
|
||||
:check-on-click-node="false"
|
||||
@node-contextmenu="onRightClick"
|
||||
@node-click="handleTreeClick"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右键菜单 -->
|
||||
<div
|
||||
v-if="contextMenu.visible"
|
||||
class="context-menu"
|
||||
:style="{ top: `${contextMenu.y}px`, left: `${contextMenu.x}px` }"
|
||||
>
|
||||
<div class="menu-item" @click.stop="addNode">
|
||||
<el-icon> <G3Icon icon="lucide:plus" /></el-icon>新增分类
|
||||
</div>
|
||||
<div class="menu-item" @click.stop="editNode">
|
||||
<el-icon> <G3Icon icon="lucide:pen-line" /></el-icon>修改分类
|
||||
</div>
|
||||
<div class="menu-item delete" @click.stop="deleteNode">
|
||||
<el-icon> <G3Icon icon="lucide:trash-2" /></el-icon>删除分类
|
||||
</div>
|
||||
</div>
|
||||
</el-splitter-panel>
|
||||
|
||||
<el-splitter-panel>
|
||||
<div class="g3-container-right">
|
||||
<div style="width: 100%">
|
||||
<div
|
||||
style="
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
height: 32px;
|
||||
margin-bottom: 8px;
|
||||
"
|
||||
>
|
||||
<div style="display: flex">
|
||||
<el-button type="primary"> 关联 </el-button>
|
||||
<el-button type="primary" @click="handleAdd"> 新建 </el-button>
|
||||
<el-button type="primary" @click="showallmod"> 查看所有模块 </el-button>
|
||||
<el-button type="primary" @click="showmodSql"> SQL语句 </el-button>
|
||||
</div>
|
||||
<div style="display: flex">
|
||||
<el-dropdown>
|
||||
<div class="g3-svg">
|
||||
<G3Icon icon="lucide:ellipsis" />
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="updateViewTableColumns('s_module')">
|
||||
栏位保存
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="g3-table">
|
||||
<vxe-table
|
||||
ref="tableRef"
|
||||
border
|
||||
stripe
|
||||
header-align="center"
|
||||
keep-source
|
||||
:data="tableData"
|
||||
:loading="loading.table"
|
||||
:sort-config="{
|
||||
// showIcon: false,
|
||||
trigger: 'cell',
|
||||
}"
|
||||
:filter-config="{
|
||||
showIcon: false,
|
||||
}"
|
||||
:column-drag-config="{
|
||||
showIcon: false,
|
||||
trigger: 'cell',
|
||||
}"
|
||||
:row-config="{
|
||||
useKey: true,
|
||||
// keyField: 'b_id',
|
||||
isCurrent: true,
|
||||
isHover: true,
|
||||
}"
|
||||
:column-config="{
|
||||
resizable: true,
|
||||
drag: true,
|
||||
}"
|
||||
:edit-config="{
|
||||
trigger: 'click',
|
||||
mode: 'row',
|
||||
autoClear: false,
|
||||
showStatus: true,
|
||||
showIcon: false,
|
||||
}"
|
||||
height="100%"
|
||||
show-overflow
|
||||
show-header-overflow
|
||||
>
|
||||
<vxe-column type="checkbox" width="50" align="center" />
|
||||
<vxe-column
|
||||
v-for="column in columns"
|
||||
:key="column.col_FieldName"
|
||||
:field="column.col_FieldName"
|
||||
:title="column.col_Caption"
|
||||
:width="column.col_Width"
|
||||
sortable
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<template
|
||||
v-if="
|
||||
['b_ISVirtualModule', 'b_isMainModule', 'b_SH', 'b_autoid'].includes(
|
||||
column.col_FieldName,
|
||||
)
|
||||
"
|
||||
>
|
||||
<el-checkbox
|
||||
v-model="row[column.col_FieldName]"
|
||||
disabled
|
||||
true-value="1"
|
||||
false-value="0"
|
||||
@change="handleCheckChange(row, column.col_FieldName)"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ row[column.col_FieldName] }}
|
||||
</template>
|
||||
</template>
|
||||
</vxe-column>
|
||||
|
||||
<vxe-column width="180" align="center" fixed="right" title="操作">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
size="small"
|
||||
style="font-size: 14px"
|
||||
@click="handleEditClick(row)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
size="small"
|
||||
style="font-size: 14px"
|
||||
@click="handleDelClick(row)"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</vxe-column>
|
||||
</vxe-table>
|
||||
</div>
|
||||
|
||||
<div class="g3-footer">
|
||||
<el-pagination
|
||||
:current-page="pagination.page"
|
||||
:page-sizes="[50, 100, 200, 500, 1000]"
|
||||
:page-size="pagination.size"
|
||||
layout="total, sizes, prev, pager, next, jumper"
|
||||
:total="pagination.total"
|
||||
class="pagination"
|
||||
@size-change="handleSizeChange"
|
||||
@current-change="handleCurrentChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-splitter-panel>
|
||||
</el-splitter>
|
||||
</div>
|
||||
|
||||
<!-- 对话框内容 -->
|
||||
<el-dialog
|
||||
v-model="dialogSQLVisible"
|
||||
:title="dialogTitle"
|
||||
@@ -969,164 +924,93 @@ onMounted(async () => {
|
||||
<Modulekeyvalue v-model="setkeyvalue" :data="tableData" :b_id="tree_id" />
|
||||
<Moduleotherbmfl v-model="setotherbmfl" :data="tableData" :b_id="tree_id" />
|
||||
|
||||
<ModulePowervalue
|
||||
v-model="setpowervalue"
|
||||
:data="tableData"
|
||||
:b_id="tree_id"
|
||||
/>
|
||||
<ModulePowervalue v-model="setpowervalue" :data="tableData" :b_id="tree_id" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
<style lang="scss" scoped>
|
||||
@use "@/styles/mixins.scss" as *;
|
||||
|
||||
.g3-flex-container {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #ffffff;
|
||||
background-color: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.g3-header {
|
||||
padding: 10px 10px 8px 10px;
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #f2f6fc;
|
||||
}
|
||||
|
||||
.g3-container {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
background-color: #ffffff;
|
||||
width: 100%;
|
||||
padding: 4px 0px;
|
||||
/* padding: 10px 12px; */
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.g3-container-left {
|
||||
width: 190px;
|
||||
min-width: 170px;
|
||||
background-color: #ffffff;
|
||||
height: 100%;
|
||||
border-right: 1px solid #f2f6fc;
|
||||
padding: 10px;
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
width: 300px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.g3-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@include flexGapRow(2px);
|
||||
}
|
||||
|
||||
.pagination {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.fixed-row-table :deep(.el-table__row) {
|
||||
height: 40px !important;
|
||||
/* 设置固定行高 */
|
||||
}
|
||||
|
||||
/* 单元格内容样式 */
|
||||
.fixed-row-table :deep(.el-table__cell) {
|
||||
padding: 0 !important;
|
||||
/* 移除默认padding */
|
||||
}
|
||||
|
||||
/* 内容容器样式 */
|
||||
.cell-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
/* 垂直居中 */
|
||||
padding: 0 10px;
|
||||
/* 左右内边距 */
|
||||
box-sizing: border-box;
|
||||
text-align: left;
|
||||
/* 靠左显示 */
|
||||
overflow: hidden;
|
||||
/* 隐藏溢出 */
|
||||
text-overflow: ellipsis;
|
||||
/* 文本溢出显示省略号 */
|
||||
white-space: nowrap;
|
||||
/* 不换行 */
|
||||
}
|
||||
|
||||
/* 表头样式 */
|
||||
.fixed-row-table :deep(.el-table__header-wrapper th) {
|
||||
height: 40px !important;
|
||||
/* 表头高度与行高一致 */
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
/* 表头内容样式 */
|
||||
.fixed-row-table :deep(.el-table__header-wrapper .cell) {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 10px;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.my-table ::v-deep(.el-table__header-wrapper th) {
|
||||
background-color: #f5f7fa !important;
|
||||
color: #303133 !important;
|
||||
}
|
||||
|
||||
.g3-row-query {
|
||||
padding: 10px 10px 8px 10px;
|
||||
margin-bottom: 10px;
|
||||
background-color: #ffffff;
|
||||
border: 1px solid #e4e7ed;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 0 12px #0000000d;
|
||||
}
|
||||
|
||||
.g3-row-main {
|
||||
.g3-container-right {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
padding: 10px;
|
||||
height: 100%;
|
||||
background-color: #ffffff;
|
||||
box-sizing: border-box;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.g3-svg-query {
|
||||
.g3-table {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.g3-footer {
|
||||
padding-top: 8px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.g3-svg {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 50%;
|
||||
padding: 8px;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
border: 1px solid #dcdfe6;
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
}
|
||||
|
||||
.g3-svg-query:hover {
|
||||
.g3-svg:hover {
|
||||
border-color: #badeff;
|
||||
background-color: #e8f4ff;
|
||||
}
|
||||
|
||||
/* 给最后一列加右边框 */
|
||||
.my-table ::v-deep(.el-table-fixed-column--right) {
|
||||
border-left: 1px solid #ebeef5;
|
||||
}
|
||||
|
||||
/* 查询日期图标 */
|
||||
:deep(.g3-prefix .el-input__prefix) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.el-dropdown .g3-svg-query:focus,
|
||||
.el-dropdown-link:focus {
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
/* 自定义右键菜单样式 */
|
||||
.context-menu {
|
||||
position: fixed;
|
||||
|
||||
@@ -0,0 +1,546 @@
|
||||
<script setup>
|
||||
import { ElMessage } from "element-plus";
|
||||
import { ref } from "vue";
|
||||
import { loadDataApi, loadDataBySqlApi, saveObjectApi } from "@/request/api";
|
||||
import { compareAndClassifyData } from "@/utils/diff";
|
||||
|
||||
const tableRef = ref();
|
||||
const subTableRef = ref();
|
||||
|
||||
const loading = ref({
|
||||
main: true,
|
||||
sub: false,
|
||||
});
|
||||
|
||||
const tableData = ref([]);
|
||||
const originTableData = ref([]);
|
||||
|
||||
const subTableData = ref([]);
|
||||
const originSubTableData = ref([]);
|
||||
|
||||
async function loadTableData() {
|
||||
loading.value.main = true;
|
||||
|
||||
try {
|
||||
const res = await loadDataApi("oa_notices_dl");
|
||||
|
||||
if (res.success) {
|
||||
tableData.value = res.data;
|
||||
originTableData.value = JSON.parse(JSON.stringify(res.data));
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error(`加载失败:${error.message}`);
|
||||
} finally {
|
||||
// 无论成功或失败,都要关闭加载状态
|
||||
loading.value.main = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSubTableData(code) {
|
||||
if (!code) {
|
||||
loading.value.sub = false;
|
||||
subTableData.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value.sub = true;
|
||||
|
||||
try {
|
||||
const res = await loadDataApi("oa_notices_xl", `b_father_id = '${code}'`);
|
||||
|
||||
if (res.success) {
|
||||
subTableData.value = res.data;
|
||||
originSubTableData.value = JSON.parse(JSON.stringify(res.data));
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error(`加载失败:${error.message}`);
|
||||
} finally {
|
||||
// 无论成功或失败,都要关闭加载状态
|
||||
loading.value.sub = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleTableCurrentRowChange({ newValue }) {
|
||||
loadSubTableData(newValue.b_id);
|
||||
}
|
||||
|
||||
async function handleAddRow() {
|
||||
const $table = tableRef.value;
|
||||
if ($table) {
|
||||
const record = {
|
||||
b_xh: $table.getFullData().length + 1,
|
||||
};
|
||||
|
||||
const { row: newRow } = await $table.insertAt(record, -1);
|
||||
$table.setEditRow(newRow, true);
|
||||
$table.setCurrentRow(record, true);
|
||||
|
||||
loading.value.sub = false;
|
||||
subTableData.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
function handleDeleteRow() {
|
||||
const $table = tableRef.value;
|
||||
if ($table) {
|
||||
const currentRow = $table.getCurrentRecord();
|
||||
|
||||
if (!currentRow) {
|
||||
ElMessage.warning("请选择需要删除的行");
|
||||
return;
|
||||
}
|
||||
|
||||
$table.remove(currentRow);
|
||||
loading.value.sub = false;
|
||||
subTableData.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAddSubRow() {
|
||||
const currentRow = tableRef.value.getCurrentRecord();
|
||||
|
||||
if (!currentRow) {
|
||||
ElMessage.warning("请先选择大类");
|
||||
return;
|
||||
}
|
||||
|
||||
if (tableRef.value.isInsertByRow(currentRow)) {
|
||||
ElMessage.warning("请先保存大类");
|
||||
return;
|
||||
}
|
||||
|
||||
const $table = subTableRef.value;
|
||||
|
||||
if ($table) {
|
||||
const record = {
|
||||
b_father_id: currentRow.b_id,
|
||||
mx_xh: $table.getFullData().length + 1,
|
||||
};
|
||||
|
||||
const { row: newRow } = await $table.insertAt(record, -1);
|
||||
$table.setEditRow(newRow, true);
|
||||
$table.setCurrentRow(newRow, true);
|
||||
}
|
||||
}
|
||||
|
||||
function handleDeleteSubRow() {
|
||||
const $table = subTableRef.value;
|
||||
if ($table) {
|
||||
const currentRow = $table.getCurrentRecord();
|
||||
console.log(currentRow);
|
||||
if (!currentRow) {
|
||||
ElMessage.warning("请选择需要删除的行");
|
||||
return;
|
||||
}
|
||||
$table.remove(currentRow);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
const $table = tableRef.value;
|
||||
const fullData = $table.getFullData();
|
||||
|
||||
const seenBids = new Set();
|
||||
const duplicateBids = new Set();
|
||||
const newBids = [];
|
||||
let warningMessage = "";
|
||||
|
||||
try {
|
||||
const isValid = fullData.every((item, index) => {
|
||||
if (!item.b_id) {
|
||||
warningMessage = "大类代码不能为空";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (seenBids.has(item.b_id)) {
|
||||
duplicateBids.add(item.b_id);
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($table.isInsertByRow(item)) {
|
||||
newBids.push(item.b_id);
|
||||
}
|
||||
|
||||
seenBids.add(item.b_id);
|
||||
item.b_xh = index + 1;
|
||||
return true;
|
||||
});
|
||||
|
||||
if (!isValid) {
|
||||
if (duplicateBids.size > 0) {
|
||||
const duplicateCodes = Array.from(duplicateBids).join(", ");
|
||||
ElMessage.warning(`大类代码重复: ${duplicateCodes}`);
|
||||
} else {
|
||||
ElMessage.warning(warningMessage);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (newBids.length > 0) {
|
||||
const bIds = newBids.join("','");
|
||||
const searchsql = `select * from oa_notices_dl where b_id in ('${bIds}')`;
|
||||
|
||||
const response_data = await loadDataBySqlApi(searchsql);
|
||||
|
||||
if (response_data.success) {
|
||||
const records = response_data.data || [];
|
||||
if (records.length > 0) {
|
||||
const duplicateBidsFromDb = records.map((record) => record.b_id).join(", ");
|
||||
ElMessage.warning(`以下大类代码在数据库中重复: ${duplicateBidsFromDb}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { insertdata, updatedata, deletedata, hasChanges } = compareAndClassifyData(
|
||||
fullData,
|
||||
originTableData.value,
|
||||
"b_id",
|
||||
);
|
||||
|
||||
if (!hasChanges) {
|
||||
ElMessage.info("没有数据变更");
|
||||
return;
|
||||
}
|
||||
|
||||
const requestData = [
|
||||
{
|
||||
tableName: "oa_notices_dl",
|
||||
keyField: "b_id",
|
||||
inserts: insertdata,
|
||||
updates: updatedata,
|
||||
deletes: deletedata,
|
||||
},
|
||||
];
|
||||
|
||||
if (deletedata.length > 0) {
|
||||
const deletedBids = deletedata.map((item) => item.b_id);
|
||||
|
||||
const bIds = deletedBids.join("','");
|
||||
|
||||
const searchsql = `select b_id from oa_notices_xl where b_father_id in ('${bIds}')`;
|
||||
|
||||
const response_data = await loadDataBySqlApi(searchsql);
|
||||
|
||||
if (response_data.success) {
|
||||
const records = response_data.data || [];
|
||||
if (records.length > 0) {
|
||||
requestData.push({
|
||||
tableName: "oa_notices_xl",
|
||||
keyField: "b_id",
|
||||
inserts: [],
|
||||
updates: [],
|
||||
deletes: records,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
throw new Error(response.data?.message || "保存失败");
|
||||
}
|
||||
}
|
||||
|
||||
const response = await saveObjectApi(requestData);
|
||||
|
||||
if (response.success) {
|
||||
loadTableData();
|
||||
ElMessage.success("保存成功");
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error(`操作失败: ${error.message || error}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSaveSub() {
|
||||
const $table = subTableRef.value;
|
||||
const seenBids = new Set();
|
||||
const newBids = [];
|
||||
|
||||
const fullData = $table.getFullData();
|
||||
|
||||
try {
|
||||
const isValid = fullData.every((item, index) => {
|
||||
if (!item.b_id) {
|
||||
ElMessage.warning("小类代码不能为空");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (seenBids.has(item.b_id)) {
|
||||
ElMessage.warning("小类代码不能重复");
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($table.isInsertByRow(item)) {
|
||||
newBids.push(item.b_id);
|
||||
}
|
||||
|
||||
seenBids.add(item.b_id);
|
||||
item.b_xh = index + 1;
|
||||
return true;
|
||||
});
|
||||
|
||||
if (!isValid) return;
|
||||
|
||||
if (newBids.length > 0) {
|
||||
const bIds = newBids.join("','");
|
||||
const searchsql = `select * from oa_notices_xl where b_id in ('${bIds}')`;
|
||||
|
||||
const response_data = await loadDataBySqlApi(searchsql);
|
||||
|
||||
if (response_data.success) {
|
||||
const records = response_data.data || [];
|
||||
if (records.length > 0) {
|
||||
ElMessage.warning("小类代码重复!");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { insertdata, updatedata, deletedata, hasChanges } = compareAndClassifyData(
|
||||
fullData,
|
||||
originSubTableData.value,
|
||||
"b_id",
|
||||
);
|
||||
|
||||
if (!hasChanges) {
|
||||
ElMessage.info("没有数据变更");
|
||||
return;
|
||||
}
|
||||
|
||||
const requestData = [
|
||||
{
|
||||
tableName: "oa_notices_xl",
|
||||
keyField: "b_id",
|
||||
inserts: insertdata,
|
||||
updates: updatedata,
|
||||
deletes: deletedata,
|
||||
},
|
||||
];
|
||||
|
||||
const response = await saveObjectApi(requestData);
|
||||
|
||||
if (response.success) {
|
||||
loadSubTableData(tableRef.value.getCurrentRecord().b_id);
|
||||
ElMessage.success("保存成功");
|
||||
} else {
|
||||
throw new Error(response.message || "保存失败");
|
||||
}
|
||||
} catch (error) {
|
||||
ElMessage.error(`操作失败: ${error.message || error}`);
|
||||
}
|
||||
}
|
||||
|
||||
function onOpened() {
|
||||
loadTableData();
|
||||
}
|
||||
|
||||
function onClosed() {
|
||||
loading.value.main = true;
|
||||
loading.value.sub = false;
|
||||
|
||||
tableData.value = [];
|
||||
subTableData.value = [];
|
||||
|
||||
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-dialog title="类别设定" width="900" v-bind="$attrs" @opened="onOpened" @closed="onClosed">
|
||||
<div class="g3-container">
|
||||
<div class="g3-category">
|
||||
<div class="g3-toolbar">
|
||||
<div class="g3-title">大类</div>
|
||||
<div class="g3-actions">
|
||||
<el-button size="small" type="primary" @click="handleAddRow"> 新增 </el-button>
|
||||
<el-button size="small" type="danger" @click="handleDeleteRow"> 删除 </el-button>
|
||||
<el-button size="small" type="success" @click="handleSave"> 保存 </el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="g3-table">
|
||||
<vxe-table
|
||||
ref="tableRef"
|
||||
:loading="loading.main"
|
||||
:data="tableData"
|
||||
border
|
||||
stripe
|
||||
size="small"
|
||||
header-align="center"
|
||||
keep-source
|
||||
:edit-config="{
|
||||
trigger: 'click',
|
||||
mode: 'row',
|
||||
autoClear: false,
|
||||
showStatus: true,
|
||||
showIcon: false,
|
||||
}"
|
||||
:row-config="{
|
||||
keyField: 'b_xh',
|
||||
useKey: true,
|
||||
isCurrent: true,
|
||||
isHover: true,
|
||||
}"
|
||||
show-overflow
|
||||
show-header-overflow
|
||||
height="100%"
|
||||
@current-row-change="handleTableCurrentRowChange"
|
||||
>
|
||||
<vxe-column
|
||||
title="大类代码"
|
||||
field="b_id"
|
||||
width="80"
|
||||
align="center"
|
||||
:edit-render="{
|
||||
autoFocus: true,
|
||||
enabled: true,
|
||||
}"
|
||||
>
|
||||
<template #edit="{ row }">
|
||||
<el-input v-model="row.b_id" />
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column
|
||||
title="大类名称"
|
||||
field="b_name"
|
||||
align="center"
|
||||
:edit-render="{
|
||||
autoFocus: true,
|
||||
enabled: true,
|
||||
}"
|
||||
>
|
||||
<template #edit="{ row }">
|
||||
<el-input v-model="row.b_name" />
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column
|
||||
title="备注"
|
||||
field="b_bz"
|
||||
align="center"
|
||||
:edit-render="{
|
||||
autoFocus: true,
|
||||
enabled: true,
|
||||
}"
|
||||
>
|
||||
<template #edit="{ row }">
|
||||
<el-input v-model="row.b_bz" />
|
||||
</template>
|
||||
</vxe-column>
|
||||
</vxe-table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="g3-divider" />
|
||||
|
||||
<div class="g3-category">
|
||||
<div class="g3-toolbar">
|
||||
<div class="g3-title">小类</div>
|
||||
<div class="g3-actions">
|
||||
<el-button size="small" type="primary" @click="handleAddSubRow"> 新增 </el-button>
|
||||
<el-button size="small" type="danger" @click="handleDeleteSubRow"> 删除 </el-button>
|
||||
<el-button size="small" type="success" @click="handleSaveSub"> 保存 </el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="g3-table">
|
||||
<vxe-table
|
||||
ref="subTableRef"
|
||||
:loading="loading.sub"
|
||||
:data="subTableData"
|
||||
border
|
||||
stripe
|
||||
header-align="center"
|
||||
keep-source
|
||||
:edit-config="{
|
||||
trigger: 'click',
|
||||
mode: 'row',
|
||||
autoClear: false,
|
||||
showStatus: true,
|
||||
showIcon: false,
|
||||
}"
|
||||
:row-config="{
|
||||
keyField: 'b_xh',
|
||||
useKey: true,
|
||||
isCurrent: true,
|
||||
isHover: true,
|
||||
}"
|
||||
show-overflow
|
||||
show-header-overflow
|
||||
height="100%"
|
||||
>
|
||||
<vxe-column
|
||||
title="小类代码"
|
||||
field="b_id"
|
||||
width="80"
|
||||
align="center"
|
||||
:edit-render="{
|
||||
autoFocus: true,
|
||||
enabled: true,
|
||||
}"
|
||||
>
|
||||
<template #edit="{ row }">
|
||||
<el-input v-model="row.b_id" />
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column
|
||||
title="小类名称"
|
||||
field="b_name"
|
||||
align="center"
|
||||
:edit-render="{
|
||||
autoFocus: true,
|
||||
enabled: true,
|
||||
}"
|
||||
>
|
||||
<template #edit="{ row }">
|
||||
<el-input v-model="row.b_name" />
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column
|
||||
title="备注"
|
||||
field="b_bz"
|
||||
align="center"
|
||||
:edit-render="{
|
||||
autoFocus: true,
|
||||
enabled: true,
|
||||
}"
|
||||
>
|
||||
<template #edit="{ row }">
|
||||
<el-input v-model="row.b_bz" />
|
||||
</template>
|
||||
</vxe-column>
|
||||
</vxe-table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.g3-container {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 450px;
|
||||
border-top: 1px solid #ddd;
|
||||
}
|
||||
.g3-divider {
|
||||
height: 100%;
|
||||
width: 1px;
|
||||
background-color: #ddd;
|
||||
}
|
||||
.g3-category {
|
||||
width: 50%;
|
||||
margin-top: 8px;
|
||||
padding: 0px 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.g3-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.g3-title {
|
||||
margin-right: 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
.g3-table {
|
||||
margin-top: 8px;
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,174 @@
|
||||
<script lang="js" setup>
|
||||
import { getUniqueIdApi, loadDataBySqlApi, saveObjectApi } from "@/request/api";
|
||||
import { ElMessage } from "element-plus";
|
||||
const props = defineProps({
|
||||
editId: {
|
||||
type: null,
|
||||
},
|
||||
treeId: {
|
||||
type: null,
|
||||
},
|
||||
});
|
||||
|
||||
const loading = reactive({
|
||||
global: false,
|
||||
save: false,
|
||||
});
|
||||
|
||||
const emits = defineEmits(["change", "closed"]);
|
||||
|
||||
const showVisible = defineModel(false);
|
||||
|
||||
const form = ref({
|
||||
b_id: 0,
|
||||
b_subject: "",
|
||||
b_visible: "0",
|
||||
b_info: "",
|
||||
});
|
||||
|
||||
async function onOpened() {
|
||||
loading.global = true;
|
||||
if (props.editId) {
|
||||
const data_main = await loadDataBySqlApi(
|
||||
`select * from oa_notices where 2>1 and b_id='${props.editId}'`,
|
||||
);
|
||||
if (data_main.data.length > 0) {
|
||||
form.value = data_main.data[0];
|
||||
}
|
||||
} else {
|
||||
const response = await getUniqueIdApi();
|
||||
form.value.b_id = response.data;
|
||||
}
|
||||
loading.global = false;
|
||||
}
|
||||
|
||||
async function handleSave() {
|
||||
try {
|
||||
loading.save = true;
|
||||
|
||||
let updatemaintable = [];
|
||||
let insertmaintable = [];
|
||||
|
||||
if (props.editId) {
|
||||
updatemaintable.push({
|
||||
b_id: form.value.b_id,
|
||||
b_subject: form.value.b_subject,
|
||||
b_visible: form.value.b_visible,
|
||||
b_info: form.value.b_info,
|
||||
});
|
||||
} else {
|
||||
let dl_id = "";
|
||||
let xl_id = "";
|
||||
const data_dl = await loadDataBySqlApi(
|
||||
`select * from oa_notices_dl where 2>1 and b_id='${props.treeId}'`,
|
||||
);
|
||||
if (data_dl.data.length > 0) {
|
||||
dl_id = props.treeId;
|
||||
}
|
||||
const data_xl = await loadDataBySqlApi(
|
||||
`select * from oa_notices_xl where 2>1 and b_id='${props.treeId}'`,
|
||||
);
|
||||
if (data_xl.data.length > 0) {
|
||||
dl_id = data_xl.data[0].b_father_id;
|
||||
xl_id = props.treeId;
|
||||
}
|
||||
|
||||
insertmaintable.push({
|
||||
b_id: form.value.b_id,
|
||||
b_dl_id: dl_id,
|
||||
b_xl_id: xl_id,
|
||||
b_subject: form.value.b_subject,
|
||||
b_visible: form.value.b_visible,
|
||||
b_info: form.value.b_info,
|
||||
});
|
||||
}
|
||||
|
||||
const requestData = [
|
||||
{
|
||||
tableName: "oa_notices",
|
||||
keyField: "b_id",
|
||||
inserts: insertmaintable,
|
||||
updates: updatemaintable,
|
||||
deletes: [],
|
||||
},
|
||||
];
|
||||
|
||||
const response = await saveObjectApi(requestData);
|
||||
|
||||
if (response.success) {
|
||||
ElMessage.success("保存成功");
|
||||
emits("change");
|
||||
}
|
||||
} finally {
|
||||
loading.save = false;
|
||||
}
|
||||
}
|
||||
|
||||
function onClosed() {
|
||||
loading.global = true;
|
||||
loading.save = false;
|
||||
|
||||
form.value = {
|
||||
b_id: 0,
|
||||
b_subject: "",
|
||||
b_visible: "0",
|
||||
b_info: "",
|
||||
};
|
||||
|
||||
emits("closed");
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-dialog
|
||||
v-bind="$attrs"
|
||||
v-model="showVisible"
|
||||
title="公告编辑"
|
||||
width="800"
|
||||
@opened="onOpened"
|
||||
@closed="onClosed"
|
||||
>
|
||||
<div class="dialog-content" v-loading="loading.global">
|
||||
<div class="form-item">
|
||||
<div class="label">主题</div>
|
||||
<el-input v-model="form.b_subject" placeholder="请输入主题" x />
|
||||
</div>
|
||||
|
||||
<div class="form-item">
|
||||
<div class="label">发布</div>
|
||||
<el-switch v-model="form.b_visible" active-value="1" inactive-value="0" />
|
||||
</div>
|
||||
|
||||
<div class="form-item">
|
||||
<div class="label">公告内容</div>
|
||||
<WangEditor v-model="form.b_info" :father="form.b_id" module-id="oa_notices" />
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button @click="showVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="loading.save" @click="handleSave">确认</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.dialog-container {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.dialog-content {
|
||||
}
|
||||
|
||||
.form-item {
|
||||
margin-bottom: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
min-width: 70px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
@@ -1,7 +1,375 @@
|
||||
<script lang="js" setup></script>
|
||||
<script setup>
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import EditDrawer from "./edit-drawer.vue";
|
||||
import CategoryDialog from "./category-dialog.vue";
|
||||
import { loadDataBySqlApi, saveObjectApi } from "@/request/api";
|
||||
|
||||
const tableRef = ref(null);
|
||||
|
||||
const showCategory = ref(false);
|
||||
|
||||
const menuData = ref([]);
|
||||
const columnData = ref([
|
||||
{ prop: "b_subject", label: "主题", width: 200 },
|
||||
{ prop: "b_visible", label: "发布", width: 100 },
|
||||
{ prop: "v_inputuser_name", label: "发布人", width: 120 },
|
||||
{ prop: "b_inputdate", label: "发布日期", width: 140 },
|
||||
]);
|
||||
const treeId = ref("");
|
||||
const tableData = ref([]);
|
||||
const loading = reactive({
|
||||
table: false,
|
||||
save: false,
|
||||
delete: false,
|
||||
error: "",
|
||||
});
|
||||
|
||||
const defaultProps = {
|
||||
children: "children",
|
||||
label: "label",
|
||||
isLeaf: "leaf",
|
||||
};
|
||||
|
||||
async function handleNodeClick(node) {
|
||||
if (!node.id) {
|
||||
return;
|
||||
}
|
||||
treeId.value = node.id;
|
||||
loadNodeData(node.id, node.level);
|
||||
}
|
||||
|
||||
async function loadNode(node, resolve) {
|
||||
if (node.level === 0) {
|
||||
try {
|
||||
let sql = `select * from oa_notices_dl where 2>1 order by b_xh`;
|
||||
const response = await loadDataBySqlApi(sql);
|
||||
|
||||
if (response.success) {
|
||||
const records = response.data || [];
|
||||
|
||||
const children = records.map((r) => ({
|
||||
id: r.b_id,
|
||||
label: r.b_name,
|
||||
leaf: false,
|
||||
level: "0",
|
||||
}));
|
||||
resolve(children);
|
||||
} else {
|
||||
resolve([]);
|
||||
}
|
||||
} catch (err) {
|
||||
resolve([]);
|
||||
}
|
||||
} else {
|
||||
const parentId = node.data.id;
|
||||
let sql = `select * from oa_notices_xl where b_father_id='${parentId}' order by b_xh`;
|
||||
const res = await loadDataBySqlApi(sql);
|
||||
|
||||
resolve(
|
||||
res.data.map((item) => ({
|
||||
id: item.b_id,
|
||||
label: item.b_name,
|
||||
leaf: false,
|
||||
level: "1",
|
||||
})),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取表格数据
|
||||
async function loadNodeData(id, level) {
|
||||
try {
|
||||
loading.table = true;
|
||||
|
||||
let condition = `b_xl_id='${id}'`;
|
||||
if (level == "0") {
|
||||
condition = `b_dl_id='${id}'`;
|
||||
}
|
||||
if (level == "10") {
|
||||
condition = `(b_dl_id='${id}' or b_xl_id='${id}')`;
|
||||
}
|
||||
|
||||
let sql = `select oa_notices.*,b_user.b_name as v_inputuser_name from oa_notices LEFT OUTER JOIN b_user ON oa_notices.b_inputuser_id = dbo.b_user.b_id where 2>1 and ${condition} `;
|
||||
const response = await loadDataBySqlApi(sql);
|
||||
|
||||
if (response.success) {
|
||||
const records = response.data || [];
|
||||
tableData.value = records;
|
||||
}
|
||||
} catch (err) {
|
||||
ElMessage.error(err.message || "加载数据失败");
|
||||
} finally {
|
||||
loading.table = false;
|
||||
}
|
||||
}
|
||||
|
||||
// -------------------------------------
|
||||
const drawerVisible = ref(false);
|
||||
const editId = ref(null);
|
||||
|
||||
async function handleAdd() {
|
||||
if (!treeId.value) {
|
||||
ElMessage.warning("请选择类别");
|
||||
return;
|
||||
}
|
||||
drawerVisible.value = true;
|
||||
}
|
||||
|
||||
async function handleEditRow(row) {
|
||||
editId.value = row.b_id;
|
||||
drawerVisible.value = true;
|
||||
}
|
||||
|
||||
function handleEditChange() {
|
||||
drawerVisible.value = false;
|
||||
loadNodeData(treeId.value, "10");
|
||||
}
|
||||
|
||||
function handleEditClosed() {
|
||||
editId.value = null;
|
||||
}
|
||||
// -----------------------------------
|
||||
|
||||
// 删除单行
|
||||
async function handleDeleteRow(row) {
|
||||
if (!row) return;
|
||||
const $table = tableRef.value;
|
||||
|
||||
if ($table) {
|
||||
deleteRows([row]);
|
||||
}
|
||||
}
|
||||
|
||||
// 删除多选行
|
||||
async function handleDeleteRows() {
|
||||
const $table = tableRef.value;
|
||||
if ($table) {
|
||||
deleteRows($table.getCheckboxRecords());
|
||||
}
|
||||
}
|
||||
|
||||
// 删除(传入行数组)
|
||||
async function deleteRows(rows = []) {
|
||||
const $table = tableRef.value;
|
||||
if (!$table) {
|
||||
return;
|
||||
}
|
||||
if (!rows.length) {
|
||||
ElMessage.warning("请先选择要删除的行");
|
||||
return;
|
||||
}
|
||||
|
||||
const tipMessage = `确定要删除选中的数据么?`;
|
||||
|
||||
await ElMessageBox.confirm(tipMessage, "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
dangerouslyUseHTMLString: true,
|
||||
});
|
||||
|
||||
const requestData = [
|
||||
{
|
||||
tableName: "oa_notices",
|
||||
keyField: "b_id",
|
||||
inserts: [],
|
||||
updates: [],
|
||||
deletes: rows.map((row) => ({ b_id: row.b_id })),
|
||||
},
|
||||
];
|
||||
|
||||
await saveObjectApi(requestData).then((res) => {
|
||||
if (res.success) {
|
||||
ElMessage.success("删除成功");
|
||||
|
||||
loadNodeData(treeId.value, "10");
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class=""></div>
|
||||
<div class="g3-flex-container">
|
||||
<div class="g3-header">
|
||||
<el-button type="primary" @click="showCategory = true"> 分类 </el-button>
|
||||
</div>
|
||||
<div class="g3-container">
|
||||
<el-splitter>
|
||||
<el-splitter-panel size="220">
|
||||
<div class="g3-container-left">
|
||||
<div class="g3-title">
|
||||
<G3Icon icon="lucide:blocks" />
|
||||
<div style="margin-left: 3px">所有类别</div>
|
||||
</div>
|
||||
<el-tree
|
||||
ref="treeRef"
|
||||
:data="menuData"
|
||||
:props="defaultProps"
|
||||
:load="loadNode"
|
||||
lazy
|
||||
node-key="id"
|
||||
highlight-current
|
||||
@node-click="handleNodeClick"
|
||||
/>
|
||||
</div>
|
||||
</el-splitter-panel>
|
||||
|
||||
<el-splitter-panel>
|
||||
<div class="g3-container-right">
|
||||
<div style="width: 100%">
|
||||
<div style="display: flex; height: 32px; margin-bottom: 8px">
|
||||
<el-button type="primary" @click="handleAdd"> 新增 </el-button>
|
||||
<el-button type="danger" @click="handleDeleteRows"> 批量删除 </el-button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="g3-table">
|
||||
<vxe-table
|
||||
ref="tableRef"
|
||||
border
|
||||
stripe
|
||||
header-align="center"
|
||||
:data="tableData"
|
||||
:sort-config="{
|
||||
// showIcon: false,
|
||||
trigger: 'cell',
|
||||
}"
|
||||
:filter-config="{
|
||||
showIcon: false,
|
||||
}"
|
||||
:column-drag-config="{
|
||||
showIcon: false,
|
||||
trigger: 'cell',
|
||||
}"
|
||||
:row-config="{
|
||||
useKey: true,
|
||||
keyField: 'b_id',
|
||||
isCurrent: true,
|
||||
isHover: true,
|
||||
}"
|
||||
:column-config="{
|
||||
resizable: true,
|
||||
drag: true,
|
||||
}"
|
||||
show-overflow
|
||||
show-header-overflow
|
||||
height="100%"
|
||||
>
|
||||
<vxe-column type="checkbox" width="50" align="center" />
|
||||
<vxe-column
|
||||
v-for="column in columnData"
|
||||
:key="column.prop"
|
||||
:field="column.prop"
|
||||
:title="column.label"
|
||||
:width="column.width"
|
||||
sortable
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<template v-if="['b_visible'].includes(column.prop)">
|
||||
<el-checkbox
|
||||
v-model="row[column.prop]"
|
||||
disabled
|
||||
true-value="1"
|
||||
false-value="0"
|
||||
@change="handleCheckChange(row, column.prop)"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ row[column.prop] }}
|
||||
</template>
|
||||
</template>
|
||||
</vxe-column>
|
||||
|
||||
<vxe-column width="200" align="center" fixed="right" title="操作">
|
||||
<template #default="{ row }">
|
||||
<el-button type="primary" link @click="handleEditRow(row)"> 编辑 </el-button>
|
||||
|
||||
<el-button type="danger" link @click="handleDeleteRow(row)"> 删除 </el-button>
|
||||
</template>
|
||||
</vxe-column>
|
||||
</vxe-table>
|
||||
</div>
|
||||
</div>
|
||||
</el-splitter-panel>
|
||||
</el-splitter>
|
||||
</div>
|
||||
|
||||
<EditDrawer
|
||||
v-model="drawerVisible"
|
||||
:tree-id="treeId"
|
||||
:edit-id="editId"
|
||||
@change="handleEditChange"
|
||||
@closed="handleEditClosed"
|
||||
/>
|
||||
|
||||
<CategoryDialog v-model="showCategory" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
<style lang="scss" scoped>
|
||||
@use "@/styles/mixins.scss" as *;
|
||||
|
||||
.g3-flex-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #fff;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.g3-header {
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #f2f6fc;
|
||||
}
|
||||
|
||||
.g3-container {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.g3-container-left {
|
||||
height: 100%;
|
||||
border-right: 1px solid #f2f6fc;
|
||||
padding: 10px;
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.g3-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 14px;
|
||||
margin-bottom: 4px;
|
||||
@include flexGapRow(4px);
|
||||
}
|
||||
|
||||
.pagination {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.g3-container-right {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
padding: 10px;
|
||||
height: 100%;
|
||||
background-color: #ffffff;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.g3-table {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.g3-footer {
|
||||
padding-top: 8px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
@@ -1,7 +1,470 @@
|
||||
<script lang="js" setup></script>
|
||||
<script setup>
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import PopupTextarea from "./popupTextarea.vue";
|
||||
import { loadDataApi, loadViewColumnsApi, saveObjectApi } from "@/request/api";
|
||||
import { diffObject } from "@/utils/diff";
|
||||
import { getUniqueIdApi } from "@/request/api";
|
||||
import { updateColumnsApi } from "@/request/api";
|
||||
|
||||
const tableRef = ref();
|
||||
|
||||
const columns = ref([]);
|
||||
const tableData = ref([]);
|
||||
const tableDataoriginal = ref([]);
|
||||
const loading = reactive({
|
||||
table: false,
|
||||
save: false,
|
||||
});
|
||||
|
||||
let oldRowData = {};
|
||||
|
||||
const textareaFields = ["b_sql", "b_resultsql", "b_polyline", "b_polyline2", "b_graph_x", "b_img"];
|
||||
|
||||
const validRules = ref({
|
||||
b_id: [
|
||||
{ required: true, message: "必须填写" },
|
||||
{
|
||||
validator: ({ cellValue, rowIndex }) => {
|
||||
if (!cellValue) return;
|
||||
|
||||
const $table = tableRef.value;
|
||||
|
||||
const ids = $table.getFullData().map((r) => r.b_id);
|
||||
const firstIndex = ids.indexOf(cellValue);
|
||||
if (firstIndex !== rowIndex) {
|
||||
return new Error("编号 不可重复");
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// 列配置
|
||||
async function addColumn() {
|
||||
const result = await loadViewColumnsApi("s_awokemodule");
|
||||
if (result.success) {
|
||||
columns.value = result.data;
|
||||
} else {
|
||||
ElMessage.error(`加载失败: ${result.error}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function getsubtable(targetRef, originalRef, whereClause) {
|
||||
try {
|
||||
loading.table = true;
|
||||
|
||||
const response = await loadDataApi("s_awokemodule", whereClause);
|
||||
if (response.success) {
|
||||
const rawData = JSON.parse(JSON.stringify(response.data || []));
|
||||
targetRef.value = JSON.parse(JSON.stringify(rawData));
|
||||
originalRef.value = JSON.parse(JSON.stringify(rawData));
|
||||
} else {
|
||||
throw new Error(response.data?.message || "返回数据格式异常");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("API Error:", err);
|
||||
return { data: [], total: 0 };
|
||||
} finally {
|
||||
loading.table = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 进入单行编辑
|
||||
async function startEditRow(row) {
|
||||
const $table = tableRef.value;
|
||||
if ($table) {
|
||||
oldRowData = JSON.parse(JSON.stringify(row));
|
||||
await $table.setEditRow(row);
|
||||
}
|
||||
}
|
||||
|
||||
// 取消单行编辑
|
||||
async function cancelEditRow(row) {
|
||||
const $table = tableRef.value;
|
||||
if ($table) {
|
||||
await $table.clearEdit();
|
||||
// 还原数据
|
||||
await $table.revertData(row);
|
||||
}
|
||||
}
|
||||
|
||||
// 判断是否在编辑态
|
||||
function isRowEditing(row) {
|
||||
const $table = tableRef.value;
|
||||
if ($table) {
|
||||
return $table.isEditByRow(row);
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑关闭事件
|
||||
async function handleEditClosed({ row, rowIndex, column, columnIndex }) {
|
||||
const $table = tableRef.value;
|
||||
if ($table) {
|
||||
await $table.clearEdit();
|
||||
await $table.revertData(row);
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑激活事件
|
||||
function handleEditActivated({ row }) {
|
||||
startEditRow(row);
|
||||
}
|
||||
|
||||
// 保存本地提交
|
||||
async function commitEditRow(row) {
|
||||
const $table = tableRef.value;
|
||||
if ($table) {
|
||||
// 重新加载行
|
||||
await $table.reloadRow(row, {});
|
||||
await $table.clearEdit();
|
||||
}
|
||||
}
|
||||
|
||||
// 删除单行
|
||||
async function handleDeleteRow(row) {
|
||||
if (!row) return;
|
||||
const $table = tableRef.value;
|
||||
|
||||
if ($table) {
|
||||
deleteRows([row]);
|
||||
}
|
||||
}
|
||||
|
||||
// 删除多选行
|
||||
async function handleDeleteRows() {
|
||||
const $table = tableRef.value;
|
||||
if ($table) {
|
||||
deleteRows($table.getCheckboxRecords());
|
||||
}
|
||||
}
|
||||
|
||||
// 删除(传入行数组)
|
||||
async function deleteRows(rows = []) {
|
||||
const $table = tableRef.value;
|
||||
if (!$table) {
|
||||
return;
|
||||
}
|
||||
if (!rows.length) {
|
||||
ElMessage.warning("请先选择要删除的行");
|
||||
return;
|
||||
}
|
||||
|
||||
const delectedIds = rows.filter((row) => !$table.isInsertByRow(row)).map((row) => row.b_id);
|
||||
|
||||
const tipMessage = `确定要删除选中的用户提醒吗?`;
|
||||
|
||||
try {
|
||||
await ElMessageBox.confirm(tipMessage, "提示", {
|
||||
confirmButtonText: "确定",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
dangerouslyUseHTMLString: true,
|
||||
});
|
||||
|
||||
if (delectedIds.length === 0) {
|
||||
ElMessage.success("删除成功");
|
||||
|
||||
await getsubtable(tableData, tableDataoriginal, ``);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const requestData = [
|
||||
{
|
||||
tableName: "s_awokemodule",
|
||||
keyField: "b_id",
|
||||
inserts: [],
|
||||
updates: [],
|
||||
deletes: delectedIds.map((id) => ({ b_id: id })),
|
||||
},
|
||||
];
|
||||
|
||||
const response = await saveObjectApi(requestData);
|
||||
|
||||
if (response.success) {
|
||||
ElMessage.success("删除成功");
|
||||
|
||||
await getsubtable(tableData, tableDataoriginal, ``);
|
||||
} else {
|
||||
throw new Error(response.data?.message || "删除失败");
|
||||
}
|
||||
} catch (err) {
|
||||
if (err !== "cancel") {
|
||||
console.error("删除失败:", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAdd() {
|
||||
const $table = tableRef.value;
|
||||
if ($table) {
|
||||
if ($table.getInsertRecords().length > 0) {
|
||||
$table.setEditRow($table.getInsertRecords()[0], true);
|
||||
return;
|
||||
}
|
||||
|
||||
const idResponse = await getUniqueIdApi();
|
||||
|
||||
if (idResponse.success) {
|
||||
const record = {
|
||||
b_id: idResponse.data,
|
||||
b_name: "",
|
||||
};
|
||||
|
||||
const { row: newRow } = await $table.insertAt(record, -1);
|
||||
$table.setEditRow(newRow, true);
|
||||
$table.setCurrentRow(newRow, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function saveRow(row) {
|
||||
const $table = tableRef.value;
|
||||
const isNew = $table.isInsertByRow(row);
|
||||
|
||||
if (!row.b_id) {
|
||||
ElMessage.warning("编号不能为空");
|
||||
return;
|
||||
}
|
||||
|
||||
loading.save = true;
|
||||
|
||||
try {
|
||||
// 3. 组装请求
|
||||
let requestData = [];
|
||||
if (isNew) {
|
||||
// 新增
|
||||
const editedData = { ...toRaw(row) };
|
||||
delete editedData._X_ROW_KEY;
|
||||
|
||||
requestData = [
|
||||
{
|
||||
tableName: "s_awokemodule",
|
||||
keyField: "b_id",
|
||||
inserts: [editedData],
|
||||
updates: [],
|
||||
deletes: [],
|
||||
},
|
||||
];
|
||||
} else {
|
||||
// 更新
|
||||
const { isChanged, result } = diffObject(row, oldRowData, ["b_id"], ["_X_ROW_KEY"]);
|
||||
|
||||
if (!isChanged) {
|
||||
ElMessage.info("没有数据变更");
|
||||
return;
|
||||
}
|
||||
|
||||
requestData = [
|
||||
{
|
||||
tableName: "s_awokemodule",
|
||||
keyField: "b_id",
|
||||
inserts: [],
|
||||
updates: [result],
|
||||
deletes: [],
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// 4. 提交保存
|
||||
const response = await saveObjectApi(requestData);
|
||||
|
||||
if (response.success) {
|
||||
ElMessage.success("保存成功");
|
||||
|
||||
const $table = tableRef.value;
|
||||
if ($table) {
|
||||
if (isNew) {
|
||||
const currentData = tableData.value;
|
||||
currentData.push(row);
|
||||
commitEditRow(row);
|
||||
} else {
|
||||
commitEditRow(row);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new Error(response.data?.message || "保存失败");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("保存出错:", err);
|
||||
} finally {
|
||||
loading.save = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await addColumn();
|
||||
await getsubtable(tableData, tableDataoriginal, ``);
|
||||
});
|
||||
|
||||
async function handleRefresh(){
|
||||
await getsubtable(tableData, tableDataoriginal, ``);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class=""></div>
|
||||
<div class="g3-flex-container">
|
||||
<div class="g3-header">
|
||||
<div class="g3-header-left">
|
||||
<el-button type="primary" @click="handleAdd"> 新建 </el-button>
|
||||
<el-button type="danger" @click="handleDeleteRows"> 删除 </el-button>
|
||||
</div>
|
||||
<div class="g3-header-right">
|
||||
<div class="g3-svg-query" style="margin-right: 10px" @click="handleRefresh">
|
||||
<G3Icon icon="lucide:refresh-ccw" />
|
||||
</div>
|
||||
<el-dropdown>
|
||||
<div class="g3-svg-query">
|
||||
<G3Icon icon="lucide:ellipsis" />
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="updateColumnsApi('s_awokemodule')">
|
||||
栏位保存
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
<div class="g3-table">
|
||||
<vxe-table
|
||||
ref="tableRef"
|
||||
border
|
||||
stripe
|
||||
header-align="center"
|
||||
keep-source
|
||||
:data="tableData"
|
||||
:loading="loading.table"
|
||||
:filter-config="{
|
||||
showIcon: false,
|
||||
}"
|
||||
:column-drag-config="{
|
||||
showIcon: false,
|
||||
trigger: 'cell',
|
||||
}"
|
||||
:row-config="{
|
||||
useKey: true,
|
||||
// keyField: 'b_id',
|
||||
isCurrent: true,
|
||||
isHover: true,
|
||||
}"
|
||||
:column-config="{
|
||||
resizable: true,
|
||||
drag: true,
|
||||
}"
|
||||
:edit-config="{
|
||||
trigger: 'click',
|
||||
mode: 'row',
|
||||
autoClear: false,
|
||||
showStatus: true,
|
||||
showIcon: false,
|
||||
}"
|
||||
:edit-rules="validRules"
|
||||
height="100%"
|
||||
show-overflow
|
||||
show-header-overflow
|
||||
@edit-closed="handleEditClosed"
|
||||
@edit-activated="handleEditActivated"
|
||||
>
|
||||
<vxe-column type="checkbox" width="50" align="center" />
|
||||
<vxe-column
|
||||
v-for="column in columns"
|
||||
:key="column.col_FieldName"
|
||||
:field="column.col_FieldName"
|
||||
:title="column.col_Caption"
|
||||
:width="column.col_Width"
|
||||
:edit-render="{
|
||||
autoFocus: true,
|
||||
}"
|
||||
>
|
||||
<template #edit="{ row }">
|
||||
<PopupTextarea
|
||||
v-if="textareaFields.includes(column.col_FieldName)"
|
||||
v-model="row[column.col_FieldName]"
|
||||
/>
|
||||
<el-input v-else v-model="row[column.col_FieldName]" />
|
||||
</template>
|
||||
</vxe-column>
|
||||
<vxe-column title="操作" width="180" align="center" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<template v-if="isRowEditing(row)">
|
||||
<el-button
|
||||
type="success"
|
||||
link
|
||||
:loading="loading.save"
|
||||
@click="saveRow(row)"
|
||||
@click.stop
|
||||
>
|
||||
保存
|
||||
</el-button>
|
||||
<el-button link @click="cancelEditRow(row)" @click.stop> 取消 </el-button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<el-button
|
||||
:disabled="loading.save"
|
||||
type="primary"
|
||||
link
|
||||
@click.stop
|
||||
@click="startEditRow(row)"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
|
||||
<el-button :disabled="loading.save" type="danger" link @click="handleDeleteRow(row)">
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</template>
|
||||
</vxe-column>
|
||||
</vxe-table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
<style lang="scss" scoped>
|
||||
.g3-flex-container {
|
||||
display: flex;
|
||||
background-color: #fff;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.g3-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
.g3-header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.g3-table {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.g3-svg-query {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 50%;
|
||||
padding: 8px;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
border: 1px solid #dcdfe6;
|
||||
}
|
||||
|
||||
.g3-svg-query:hover {
|
||||
border-color: #badeff;
|
||||
background-color: #e8f4ff;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,64 @@
|
||||
<script setup>
|
||||
import { ref, watch } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
width: {
|
||||
type: Number,
|
||||
default: 360,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["update:modelValue"]);
|
||||
|
||||
const visible = ref(false);
|
||||
const tempValue = ref("");
|
||||
|
||||
watch(tempValue, (newValue) => {
|
||||
emit("update:modelValue", newValue);
|
||||
});
|
||||
|
||||
function onShow() {
|
||||
tempValue.value = props.modelValue;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<el-popover
|
||||
v-model:visible="visible"
|
||||
placement="bottom-start"
|
||||
:width="width"
|
||||
trigger="click"
|
||||
@show="onShow"
|
||||
>
|
||||
<!-- 弹出编辑区 -->
|
||||
<div class="popup-textarea-box">
|
||||
<el-input v-model="tempValue" type="textarea" resize="none" class="popup-textarea" />
|
||||
</div>
|
||||
|
||||
<!-- 触发显示区域 -->
|
||||
<template #reference>
|
||||
<el-input :model-value="modelValue" size="small" readonly class="popup-textarea-trigger" />
|
||||
</template>
|
||||
</el-popover>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.popup-textarea textarea {
|
||||
height: 150px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
:deep(.el-textarea__inner) {
|
||||
min-height: 150px !important;
|
||||
}
|
||||
|
||||
.popup-textarea-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
</style>
|
||||
@@ -246,7 +246,7 @@ export function uploadFile(blob, fileName, fileExt, fileSize, xh, moduleid, fath
|
||||
getAliyunKeyApi()
|
||||
.then(async (res) => {
|
||||
try {
|
||||
const aliyunKey = res.data.data
|
||||
const aliyunKey = res.data
|
||||
const formData = new FormData()
|
||||
|
||||
let uniqueName = `${Date.now()}_${Math.random()
|
||||
|
||||
Reference in new issue
Block a user