mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-26 22:31:21 +00:00
feat: 新增AI模型配置功能,优化前端组件与代码规范
1. 新增Redis AI_MODEL_CONFIG枚举与后端AI模型配置CRUD接口 2. 升级vue-img-cutter到3.1.1版本,更新前端依赖 3. 重构前端多处ElMessage提示逻辑,统一由拦截器处理 4. 替换ElDrawer为FaDrawer组件,统一弹窗组件库 5. 重构文章详情、评论组件,新增租户切换器与AI配置面板 6. 优化代码生成模板、菜单树表格逻辑与代码高亮样式 7. 修复前端路由与组件命名问题,更新快速入口配置
This commit is contained in:
@@ -26,9 +26,9 @@
|
||||
</ElForm>
|
||||
|
||||
<ul>
|
||||
<div class="pb-5 text-lg font-medium">评论 {{ comments.length }}</div>
|
||||
<div class="pb-5 text-lg font-medium">评论 {{ internalComments.length }}</div>
|
||||
<FaCommentItem
|
||||
v-for="comment in comments.slice().reverse()"
|
||||
v-for="comment in internalComments.slice().reverse()"
|
||||
:key="comment.id"
|
||||
:comment="comment"
|
||||
:show-reply-form="showReplyForm"
|
||||
@@ -42,16 +42,17 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from "vue";
|
||||
import { type Comment } from "@/mock/temp/commentDetail";
|
||||
import { commentList, Comment } from "@/mock/temp/commentDetail";
|
||||
import { ElMessage } from "element-plus";
|
||||
|
||||
defineOptions({ name: "FaCommentWidget" });
|
||||
|
||||
interface Props {
|
||||
/** 评论列表数据,不传则使用 mock 数据 */
|
||||
comments?: Comment[];
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
comments: () => [],
|
||||
});
|
||||
|
||||
@@ -62,6 +63,11 @@ interface Emits {
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
|
||||
/** 内部评论数据(优先使用 props,否则使用 mock 数据) */
|
||||
const internalComments = ref<Comment[]>(
|
||||
props.comments.length > 0 ? props.comments : commentList.value
|
||||
);
|
||||
|
||||
const newComment = ref<Partial<Comment>>({
|
||||
author: "",
|
||||
content: "",
|
||||
@@ -75,16 +81,19 @@ const addComment = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
emit("add-comment", {
|
||||
const newCommentData: Comment = {
|
||||
id: Date.now(),
|
||||
author: newComment.value.author.trim(),
|
||||
content: newComment.value.content.trim(),
|
||||
timestamp: new Date().toISOString(),
|
||||
replies: [],
|
||||
});
|
||||
};
|
||||
|
||||
internalComments.value.push(newCommentData);
|
||||
|
||||
newComment.value.author = "";
|
||||
newComment.value.content = "";
|
||||
emit("add-comment", newCommentData);
|
||||
ElMessage.success("评论发布成功");
|
||||
};
|
||||
|
||||
@@ -94,12 +103,35 @@ const addReply = (commentId: number, replyAuthor: string, replyContent: string)
|
||||
return;
|
||||
}
|
||||
|
||||
emit("add-reply", commentId, replyAuthor.trim(), replyContent.trim());
|
||||
showReplyForm.value = null;
|
||||
ElMessage.success("回复发布成功");
|
||||
const comment = findComment(internalComments.value, commentId);
|
||||
if (comment) {
|
||||
comment.replies.push({
|
||||
id: Date.now(),
|
||||
author: replyAuthor.trim(),
|
||||
content: replyContent.trim(),
|
||||
timestamp: new Date().toISOString(),
|
||||
replies: [],
|
||||
});
|
||||
showReplyForm.value = null;
|
||||
emit("add-reply", commentId, replyAuthor, replyContent);
|
||||
ElMessage.success("回复发布成功");
|
||||
}
|
||||
};
|
||||
|
||||
const toggleReply = (commentId: number) => {
|
||||
showReplyForm.value = showReplyForm.value === commentId ? null : commentId;
|
||||
};
|
||||
|
||||
const findComment = (comments: Comment[], commentId: number): Comment | undefined => {
|
||||
for (const comment of comments) {
|
||||
if (comment.id === commentId) {
|
||||
return comment;
|
||||
}
|
||||
const found = findComment(comment.replies, commentId);
|
||||
if (found) {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
</div>
|
||||
|
||||
<ul class="pl-2.5" v-if="comment.replies.length > 0">
|
||||
<CommentItem
|
||||
<FaCommentItem
|
||||
v-for="reply in comment.replies"
|
||||
:key="reply.id"
|
||||
:comment="reply"
|
||||
|
||||
@@ -172,6 +172,9 @@
|
||||
:icon="isDark ? 'ri:sun-fill' : 'ri:moon-line'"
|
||||
/>
|
||||
|
||||
<!-- 租户切换器(全局可见,1步切换) -->
|
||||
<FaTenantSwitcher />
|
||||
|
||||
<!-- 用户头像、菜单 -->
|
||||
<FaUserMenu />
|
||||
</div>
|
||||
@@ -197,6 +200,7 @@ import { mittBus, themeAnimation } from "@utils";
|
||||
import { useCommon } from "@/hooks/core/useCommon";
|
||||
import { useHeaderBar } from "@/hooks/core/useHeaderBar";
|
||||
import FaUserMenu from "./widgets/FaUserMenu.vue";
|
||||
import FaTenantSwitcher from "./widgets/FaTenantSwitcher.vue";
|
||||
|
||||
defineOptions({ name: "FaHeaderBar" });
|
||||
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
@close="onDrawerClosed"
|
||||
>
|
||||
<ElTabs v-model="activeTabRef" type="border-card">
|
||||
<ElTabPane label="AI 模型" name="aiModel">
|
||||
<FaAiModelConfigPanel />
|
||||
</ElTabPane>
|
||||
<ElTabPane label="接口白名单" name="apiWhitelist">
|
||||
<ElForm :model="configState" label-suffix=":" label-width="100px" label-position="right">
|
||||
<!-- 系统配置 -->
|
||||
@@ -195,6 +198,7 @@
|
||||
<template #footer>
|
||||
<ElButton @click="handleCloseDialog">取消</ElButton>
|
||||
<ElButton
|
||||
v-if="activeTabRef !== 'aiModel'"
|
||||
v-hasPerm="['module_system:config:update']"
|
||||
type="primary"
|
||||
:disabled="!hasChanges"
|
||||
@@ -213,6 +217,7 @@ import { useAppStore, useConfigStore } from "@stores";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { DeviceEnum } from "@/enums/settings/device.enum";
|
||||
import FaAiModelConfigPanel from "@/views/module_ai/chat/components/FaAiModelConfigPanel.vue";
|
||||
|
||||
defineOptions({ name: "FaConfigInfoDrawer" });
|
||||
|
||||
|
||||
@@ -1,36 +1,76 @@
|
||||
<!-- 租户切换器(顶栏版):头像左侧,始终可见 -->
|
||||
<!-- 顶栏租户切换器:头像左侧,始终可见 -->
|
||||
<template>
|
||||
<ElDropdown
|
||||
v-if="tenantList.length > 1"
|
||||
trigger="click"
|
||||
placement="bottom-start"
|
||||
:disabled="switching"
|
||||
@command="handleSwitch"
|
||||
placement="bottom"
|
||||
@visible-change="(v) => (dropdownVisible = v)"
|
||||
popper-class="fa-tenant-dropdown"
|
||||
>
|
||||
<span class="tenant-btn">
|
||||
<span class="tenant-icon">🏢</span>
|
||||
<span class="tenant-name">{{ currentTenantName }}</span>
|
||||
<FaSvgIcon icon="ri:arrow-down-s-line" class="arrow" />
|
||||
</span>
|
||||
<div
|
||||
class="tenant-switcher"
|
||||
:class="{ 'is-active': dropdownVisible, 'is-switching': switching }"
|
||||
:title="`当前租户:${currentTenantName}`"
|
||||
>
|
||||
<FaSvgIcon icon="ri:building-2-fill" class="icon" />
|
||||
<span class="name">{{ currentTenantName }}</span>
|
||||
<FaSvgIcon
|
||||
v-if="!switching"
|
||||
icon="ri:arrow-down-s-line"
|
||||
class="arrow"
|
||||
:class="{ rotated: dropdownVisible }"
|
||||
/>
|
||||
<ElIcon v-else class="arrow is-loading"><Loading /></ElIcon>
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<ElDropdownMenu>
|
||||
<div class="dropdown-header">
|
||||
<span class="dropdown-title">切换租户</span>
|
||||
<ElTag size="small" effect="plain" type="info"> 共 {{ tenantList.length }} 个 </ElTag>
|
||||
</div>
|
||||
<ElDropdownItem
|
||||
v-for="t in tenantList"
|
||||
:key="t.id"
|
||||
:command="t.id"
|
||||
:class="{ 'is-active': t.id === currentTenant?.id }"
|
||||
:disabled="switching"
|
||||
>
|
||||
<span class="dropdown-row">
|
||||
<span>{{ t.name }}</span>
|
||||
<FaSvgIcon v-if="t.id === currentTenant?.id" icon="ri:check-line" class="check" />
|
||||
</span>
|
||||
<div class="dropdown-item" :class="{ 'is-current': t.id === currentTenant?.id }">
|
||||
<div class="item-main">
|
||||
<FaSvgIcon
|
||||
:icon="t.id === currentTenant?.id ? 'ri:check-line' : 'ri:building-2-line'"
|
||||
class="item-icon"
|
||||
:class="{ active: t.id === currentTenant?.id }"
|
||||
/>
|
||||
<div class="item-text">
|
||||
<div class="item-name">{{ t.name }}</div>
|
||||
<div v-if="t.code" class="item-code">{{ t.code }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<ElTag v-if="t.id === currentTenant?.id" type="primary" size="small" effect="plain">
|
||||
当前
|
||||
</ElTag>
|
||||
</div>
|
||||
</ElDropdownItem>
|
||||
<div class="dropdown-footer-hint">
|
||||
<FaSvgIcon icon="ri:information-line" class="hint-icon" />
|
||||
<span>点击其他租户即可切换</span>
|
||||
</div>
|
||||
<div v-if="switching" class="dropdown-footer">
|
||||
<ElIcon class="is-loading"><Loading /></ElIcon>
|
||||
<span>切换中...</span>
|
||||
</div>
|
||||
</ElDropdownMenu>
|
||||
</template>
|
||||
</ElDropdown>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
import { ref, computed } from "vue";
|
||||
import { ElMessage } from "element-plus";
|
||||
import { Loading } from "@element-plus/icons-vue";
|
||||
import { useUserStore } from "@stores";
|
||||
import { storeToRefs } from "pinia";
|
||||
|
||||
@@ -39,29 +79,38 @@ defineOptions({ name: "FaTenantSwitcher" });
|
||||
const userStore = useUserStore();
|
||||
const { tenantList, currentTenant } = storeToRefs(userStore);
|
||||
|
||||
const dropdownVisible = ref(false);
|
||||
const switching = ref(false);
|
||||
|
||||
const currentTenantName = computed(
|
||||
() => currentTenant.value?.name || tenantList.value[0]?.name || "—"
|
||||
);
|
||||
|
||||
async function handleSwitch(tenantId: number) {
|
||||
if (tenantId === currentTenant.value?.id) return;
|
||||
if (switching.value) return;
|
||||
// 点击当前租户:给个明确提示(不再静默忽略)
|
||||
if (tenantId === currentTenant.value?.id) {
|
||||
ElMessage.info("已是当前租户");
|
||||
return;
|
||||
}
|
||||
switching.value = true;
|
||||
try {
|
||||
await userStore.selectTenant(tenantId);
|
||||
setTimeout(() => window.location.reload(), 200);
|
||||
} catch {
|
||||
// 静默失败
|
||||
switching.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.tenant-btn {
|
||||
.tenant-switcher {
|
||||
display: inline-flex;
|
||||
gap: 4px;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
max-width: 160px;
|
||||
height: 30px;
|
||||
padding: 0 10px 0 6px;
|
||||
max-width: 180px;
|
||||
height: 32px;
|
||||
padding: 0 10px;
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-primary);
|
||||
cursor: pointer;
|
||||
@@ -69,45 +118,169 @@ async function handleSwitch(tenantId: number) {
|
||||
background: var(--fa-gray-100);
|
||||
border: 1px solid var(--fa-gray-300);
|
||||
border-radius: 6px;
|
||||
transition:
|
||||
background 0.15s,
|
||||
border-color 0.15s;
|
||||
}
|
||||
transition: all 0.15s;
|
||||
|
||||
.tenant-btn:hover {
|
||||
background: var(--fa-gray-200);
|
||||
border-color: var(--el-color-primary-light-5);
|
||||
}
|
||||
&:hover {
|
||||
color: var(--el-color-primary);
|
||||
background: var(--el-color-primary-light-9);
|
||||
border-color: var(--el-color-primary-light-5);
|
||||
}
|
||||
|
||||
.tenant-icon {
|
||||
flex-shrink: 0;
|
||||
font-size: 15px;
|
||||
line-height: 1;
|
||||
}
|
||||
&.is-active {
|
||||
color: var(--el-color-primary);
|
||||
background: var(--el-color-primary-light-9);
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.tenant-name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
&.is-switching {
|
||||
cursor: wait;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
flex-shrink: 0;
|
||||
font-size: 11px;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
.icon {
|
||||
flex-shrink: 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.dropdown-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
.name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.check {
|
||||
flex-shrink: 0;
|
||||
color: var(--el-color-primary);
|
||||
.arrow {
|
||||
flex-shrink: 0;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
transition: transform 0.2s;
|
||||
|
||||
&.rotated {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
/* 全局样式:下拉面板(scoped 无法深入 ElDropdownMenu) */
|
||||
.fa-tenant-dropdown {
|
||||
min-width: 240px;
|
||||
padding: 0 !important;
|
||||
|
||||
.el-dropdown-menu__item {
|
||||
padding: 0 !important;
|
||||
|
||||
&.is-active {
|
||||
color: inherit;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
&:not(.is-disabled):hover {
|
||||
background: var(--fa-gray-200) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-header {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
|
||||
.dropdown-title {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-secondary);
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border-left: 2px solid transparent;
|
||||
border-radius: 0;
|
||||
transition: all 0.15s;
|
||||
|
||||
&.is-current {
|
||||
background: var(--el-color-primary-light-9);
|
||||
border-left-color: var(--el-color-primary);
|
||||
|
||||
&:hover {
|
||||
background: var(--el-color-primary-light-8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-footer-hint {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 6px 12px 8px;
|
||||
font-size: 11px;
|
||||
color: var(--el-text-color-secondary);
|
||||
border-top: 1px solid var(--el-border-color-lighter);
|
||||
|
||||
.hint-icon {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.item-main {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.item-icon {
|
||||
flex-shrink: 0;
|
||||
font-size: 15px;
|
||||
color: var(--el-text-color-secondary);
|
||||
|
||||
&.active {
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.item-text {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.item-name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-primary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.item-code {
|
||||
margin-top: 2px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 11px;
|
||||
color: var(--el-text-color-secondary);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dropdown-footer {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
color: var(--el-text-color-secondary);
|
||||
border-top: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -56,37 +56,6 @@
|
||||
</div>
|
||||
</div>
|
||||
<ul class="py-4 mt-3 border-t border-g-300/80">
|
||||
<li
|
||||
v-if="tenantList.length > 1"
|
||||
class="flex select-none cursor-pointer last:mb-0 hover:bg-(--fa-gray-200) flex-col! items-start! mb-4 p-2! rounded-lg bg-(--fa-gray-100)"
|
||||
>
|
||||
<span class="text-xs text-g-500 mb-2 block w-full">当前租户</span>
|
||||
<ElDropdown trigger="click" @command="handleTenantSwitch">
|
||||
<span
|
||||
class="flex items-center cursor-pointer w-full text-sm font-medium text-(--el-color-primary) hover:underline"
|
||||
>
|
||||
{{ currentTenantName }}
|
||||
<FaSvgIcon icon="ri:arrow-down-s-line" class="ml-1 text-xs" />
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<ElDropdownMenu>
|
||||
<ElDropdownItem
|
||||
v-for="t in tenantList"
|
||||
:key="t.id"
|
||||
:command="t.id"
|
||||
:class="{
|
||||
'text-(--el-color-primary) font-medium': t.id === currentTenant?.id,
|
||||
}"
|
||||
>
|
||||
<span class="flex items-center justify-between gap-4">
|
||||
<span>{{ t.name }}</span>
|
||||
<FaSvgIcon v-if="t.id === currentTenant?.id" icon="ri:check-line" />
|
||||
</span>
|
||||
</ElDropdownItem>
|
||||
</ElDropdownMenu>
|
||||
</template>
|
||||
</ElDropdown>
|
||||
</li>
|
||||
<li
|
||||
class="flex items-center p-2 mb-3 select-none rounded-md cursor-pointer last:mb-0 hover:bg-(--fa-gray-200)"
|
||||
@click="goPage('/fastlink/profile')"
|
||||
@@ -152,7 +121,6 @@ const { t } = useI18n();
|
||||
const userStore = useUserStore();
|
||||
|
||||
const { info: userInfo } = storeToRefs(userStore);
|
||||
const { tenantList, currentTenant } = storeToRefs(userStore);
|
||||
const userMenuPopover = ref();
|
||||
const paramDrawerVisible = ref(false);
|
||||
|
||||
@@ -170,22 +138,6 @@ const displayName = computed(
|
||||
|
||||
const displayEmail = computed(() => (userInfo.value as { email?: string })?.email || "");
|
||||
|
||||
const currentTenantName = computed(
|
||||
() => currentTenant.value?.name || tenantList.value[0]?.name || "—"
|
||||
);
|
||||
|
||||
async function handleTenantSwitch(tenantId: number) {
|
||||
closeUserMenu();
|
||||
try {
|
||||
await userStore.selectTenant(tenantId);
|
||||
setTimeout(() => {
|
||||
window.location.reload();
|
||||
}, 200);
|
||||
} catch {
|
||||
// silently fail
|
||||
}
|
||||
}
|
||||
|
||||
function openParamConfig(): void {
|
||||
closeUserMenu();
|
||||
paramDrawerVisible.value = true;
|
||||
|
||||
@@ -1,17 +1,26 @@
|
||||
<!-- 图片裁剪组件 github: https://github.com/acccccccb/vue-img-cutter/tree/master -->
|
||||
<!--
|
||||
图片裁剪组件 - 基于 vue-img-cutter 封装
|
||||
官方文档: https://gitee.com/GLUESTICK/vue-img-cutter
|
||||
|
||||
封装策略:
|
||||
- 所有 ImgCutter 原生 prop 以对应名称透传(驼峰→短横线映射由 Vue 自动处理)
|
||||
- 包装层额外提供 title/showPreview/previewTitle/downloadable 等增强功能
|
||||
- 仅将 ImgCutter 原生 prop 通过 v-bind 传递,避免污染
|
||||
-->
|
||||
<template>
|
||||
<div class="cutter-container">
|
||||
<div class="cutter-component">
|
||||
<div class="title">{{ title }}</div>
|
||||
<div v-if="title" class="title">{{ title }}</div>
|
||||
<ImgCutter
|
||||
ref="imgCutterModal"
|
||||
@cutDown="cutDownImg"
|
||||
@onPrintImg="cutterPrintImg"
|
||||
@onImageLoadComplete="handleImageLoadComplete"
|
||||
@onImageLoadError="handleImageLoadError"
|
||||
@onClearAll="handleClearAll"
|
||||
v-bind="cutterProps"
|
||||
class="img-cutter"
|
||||
v-bind="imgCutterProps"
|
||||
@cut-down="onCutDown"
|
||||
@on-print-img="onPrintImg"
|
||||
@on-choose-img="onChooseImg"
|
||||
@on-clear-all="onClearAll"
|
||||
@on-image-load-complete="onImageLoadComplete"
|
||||
@on-image-load-error="onImageLoadError"
|
||||
@error="onError"
|
||||
>
|
||||
<template #choose>
|
||||
<ElButton type="primary" plain v-ripple>选择图片</ElButton>
|
||||
@@ -20,98 +29,168 @@
|
||||
<ElButton type="danger" plain v-ripple>清除</ElButton>
|
||||
</template>
|
||||
<template #confirm>
|
||||
<!-- <ElButton type="primary" :style="'margin-left: 10px'">确定</ElButton> -->
|
||||
<div></div>
|
||||
<div ref="confirmElRef" />
|
||||
</template>
|
||||
</ImgCutter>
|
||||
</div>
|
||||
|
||||
<div v-if="showPreview" class="preview-container">
|
||||
<div class="title">{{ previewTitle }}</div>
|
||||
<div v-if="previewTitle" class="title">{{ previewTitle }}</div>
|
||||
<div
|
||||
class="preview-box"
|
||||
:style="{
|
||||
width: `${cutterProps.cutWidth}px`,
|
||||
height: `${cutterProps.cutHeight}px`,
|
||||
width: `${cutWidth}px`,
|
||||
height: `${cutHeight}px`,
|
||||
}"
|
||||
>
|
||||
<img class="preview-img" :src="temImgPath" alt="预览图" v-if="temImgPath" loading="eager" />
|
||||
<img v-if="temImgPath" class="preview-img" :src="temImgPath" alt="预览图" />
|
||||
</div>
|
||||
<div class="preview-actions">
|
||||
<ElButton
|
||||
v-if="downloadable"
|
||||
class="download-btn"
|
||||
:disabled="!temImgPath"
|
||||
v-ripple
|
||||
@click="handleDownload"
|
||||
>
|
||||
下载图片
|
||||
</ElButton>
|
||||
<ElButton type="primary" :disabled="!temImgPath" v-ripple @click="triggerCrop">
|
||||
确定
|
||||
</ElButton>
|
||||
</div>
|
||||
<ElButton class="download-btn" @click="downloadImg" :disabled="!temImgPath" v-ripple>
|
||||
下载图片
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, watch } from "vue";
|
||||
import ImgCutter from "vue-img-cutter";
|
||||
import "vue-img-cutter/vue-img-cutter.css";
|
||||
|
||||
defineOptions({ name: "FaCutterImg" });
|
||||
|
||||
interface Props {
|
||||
// 基础配置
|
||||
/** 是否模态框 */
|
||||
isModal?: boolean;
|
||||
/** 是否显示工具栏 */
|
||||
tool?: boolean;
|
||||
/** 工具栏背景色 */
|
||||
toolBgc?: string;
|
||||
/** 标题 */
|
||||
/* ============================================================
|
||||
* Props
|
||||
* ============================================================ */
|
||||
|
||||
interface FaCutterImgProps {
|
||||
// ── 包装层增强 ──
|
||||
/** 裁剪区域标题 */
|
||||
title?: string;
|
||||
/** 预览标题 */
|
||||
previewTitle?: string;
|
||||
/** 是否显示预览 */
|
||||
/** 是否显示预览区域 */
|
||||
showPreview?: boolean;
|
||||
|
||||
// 尺寸相关
|
||||
/** 容器宽度 */
|
||||
boxWidth?: number;
|
||||
/** 容器高度 */
|
||||
boxHeight?: number;
|
||||
/** 裁剪宽度 */
|
||||
cutWidth?: number;
|
||||
/** 裁剪高度 */
|
||||
cutHeight?: number;
|
||||
/** 是否允许大小调整 */
|
||||
sizeChange?: boolean;
|
||||
|
||||
// 移动和缩放
|
||||
/** 是否允许移动 */
|
||||
moveAble?: boolean;
|
||||
/** 是否允许图片移动 */
|
||||
imgMove?: boolean;
|
||||
/** 是否允许缩放 */
|
||||
scaleAble?: boolean;
|
||||
|
||||
// 图片相关
|
||||
/** 是否显示原始图片 */
|
||||
originalGraph?: boolean;
|
||||
/** 是否允许跨域 */
|
||||
crossOrigin?: boolean;
|
||||
/** 文件类型 */
|
||||
fileType?: "png" | "jpeg" | "webp";
|
||||
/** 质量 */
|
||||
quality?: number;
|
||||
|
||||
// 水印
|
||||
/** 水印文本 */
|
||||
watermarkText?: string;
|
||||
/** 水印字体大小 */
|
||||
watermarkFontSize?: number;
|
||||
/** 水印颜色 */
|
||||
watermarkColor?: string;
|
||||
|
||||
// 其他功能
|
||||
/** 是否保存裁剪位置 */
|
||||
saveCutPosition?: boolean;
|
||||
/** 是否预览模式 */
|
||||
previewMode?: boolean;
|
||||
|
||||
// 输入图片
|
||||
/** 预览区域标题 */
|
||||
previewTitle?: string;
|
||||
/** 是否显示下载按钮 */
|
||||
downloadable?: boolean;
|
||||
/** 远程图片地址(支持 v-model) */
|
||||
imgUrl?: string;
|
||||
|
||||
// ── ImgCutter 原生 prop ──
|
||||
isModal?: boolean;
|
||||
showChooseBtn?: boolean;
|
||||
lockScroll?: boolean;
|
||||
modalTitle?: string;
|
||||
boxWidth?: number;
|
||||
boxHeight?: number;
|
||||
cutWidth?: number;
|
||||
cutHeight?: number;
|
||||
tool?: boolean;
|
||||
toolBgc?: string;
|
||||
sizeChange?: boolean;
|
||||
moveAble?: boolean;
|
||||
imgMove?: boolean;
|
||||
originalGraph?: boolean;
|
||||
crossOrigin?: boolean;
|
||||
crossOriginHeader?: string;
|
||||
rate?: string;
|
||||
/** @deprecated 使用 watermarkText 代替 */
|
||||
WatermarkText?: string;
|
||||
/** 水印文字 */
|
||||
watermarkText?: string;
|
||||
/** 水印字体 (如 '12px Sans-serif') */
|
||||
watermarkTextFont?: string;
|
||||
/** 水印颜色 */
|
||||
watermarkTextColor?: string;
|
||||
/** 水印水平位置 (0-1) */
|
||||
watermarkTextX?: number;
|
||||
/** 水印垂直位置 (0-1) */
|
||||
watermarkTextY?: number;
|
||||
smallToUpload?: boolean;
|
||||
saveCutPosition?: boolean;
|
||||
scaleAble?: boolean;
|
||||
toolBoxOverflow?: boolean;
|
||||
index?: unknown;
|
||||
previewMode?: boolean;
|
||||
fileType?: "png" | "jpeg" | "webp";
|
||||
quality?: number;
|
||||
accept?: string;
|
||||
afterChooseImg?: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<FaCutterImgProps>(), {
|
||||
// ── 包装层 ──
|
||||
title: "",
|
||||
showPreview: true,
|
||||
previewTitle: "",
|
||||
downloadable: true,
|
||||
|
||||
// ── ImgCutter 默认值(对齐官方) ──
|
||||
isModal: false,
|
||||
showChooseBtn: true,
|
||||
lockScroll: true,
|
||||
modalTitle: "图片裁剪",
|
||||
boxWidth: 800,
|
||||
boxHeight: 400,
|
||||
cutWidth: 200,
|
||||
cutHeight: 200,
|
||||
tool: true,
|
||||
toolBgc: "#fff",
|
||||
sizeChange: true,
|
||||
moveAble: true,
|
||||
imgMove: true,
|
||||
originalGraph: false,
|
||||
crossOrigin: false,
|
||||
crossOriginHeader: "",
|
||||
rate: undefined,
|
||||
watermarkText: "",
|
||||
watermarkTextFont: "12px Sans-serif",
|
||||
watermarkTextColor: "#ffffff",
|
||||
watermarkTextX: 0.95,
|
||||
watermarkTextY: 0.95,
|
||||
smallToUpload: false,
|
||||
saveCutPosition: false,
|
||||
scaleAble: true,
|
||||
toolBoxOverflow: true,
|
||||
index: undefined,
|
||||
previewMode: true,
|
||||
fileType: "png",
|
||||
quality: 1,
|
||||
accept: "image/gif, image/jpeg ,image/png",
|
||||
});
|
||||
|
||||
/* ============================================================
|
||||
* Emits
|
||||
* ============================================================ */
|
||||
|
||||
interface FaCutterImgEmits {
|
||||
(e: "update:imgUrl", url: string): void;
|
||||
(e: "cut-down", result: CutterResult): void;
|
||||
(e: "error", error: Error): void;
|
||||
(e: "choose-img", result: unknown): void;
|
||||
(e: "print-img", result: { dataURL: string }): void;
|
||||
(e: "clear-all"): void;
|
||||
(e: "image-load-complete", result: unknown): void;
|
||||
(e: "image-load-error", error: Error): void;
|
||||
}
|
||||
|
||||
const emit = defineEmits<FaCutterImgEmits>();
|
||||
|
||||
/* ============================================================
|
||||
* Types
|
||||
* ============================================================ */
|
||||
|
||||
interface CutterResult {
|
||||
fileName: string;
|
||||
file: File;
|
||||
@@ -119,64 +198,130 @@ interface CutterResult {
|
||||
dataURL: string;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
// 基础配置默认值
|
||||
isModal: false,
|
||||
tool: true,
|
||||
toolBgc: "#fff",
|
||||
title: "",
|
||||
previewTitle: "",
|
||||
showPreview: true,
|
||||
|
||||
// 尺寸相关默认值
|
||||
boxWidth: 700,
|
||||
boxHeight: 458,
|
||||
cutWidth: 470,
|
||||
cutHeight: 270,
|
||||
sizeChange: true,
|
||||
|
||||
// 移动和缩放默认值
|
||||
moveAble: true,
|
||||
imgMove: true,
|
||||
scaleAble: true,
|
||||
|
||||
// 图片相关默认值
|
||||
originalGraph: true,
|
||||
crossOrigin: true,
|
||||
fileType: "png",
|
||||
quality: 0.9,
|
||||
|
||||
// 水印默认值
|
||||
watermarkText: "",
|
||||
watermarkFontSize: 20,
|
||||
watermarkColor: "#ffffff",
|
||||
|
||||
// 其他功能默认值
|
||||
saveCutPosition: true,
|
||||
previewMode: true,
|
||||
});
|
||||
|
||||
interface Emits {
|
||||
"update:imgUrl": [value: string];
|
||||
error: [error: any];
|
||||
imageLoadComplete: [result: CutterResult];
|
||||
imageLoadError: [error: any];
|
||||
}
|
||||
|
||||
const emit = defineEmits<Emits>();
|
||||
/* ============================================================
|
||||
* State
|
||||
* ============================================================ */
|
||||
|
||||
const temImgPath = ref("");
|
||||
const imgCutterModal = ref();
|
||||
const confirmElRef = ref<HTMLElement>();
|
||||
|
||||
// 计算属性:整合所有ImgCutter的props
|
||||
const cutterProps = computed(() => ({
|
||||
...props,
|
||||
WatermarkText: props.watermarkText,
|
||||
WatermarkFontSize: props.watermarkFontSize,
|
||||
WatermarkColor: props.watermarkColor,
|
||||
}));
|
||||
/* ============================================================
|
||||
* Computed: pass only native ImgCutter props
|
||||
* ============================================================ */
|
||||
|
||||
const IMG_CUTTER_PROP_KEYS = new Set([
|
||||
"isModal",
|
||||
"showChooseBtn",
|
||||
"lockScroll",
|
||||
"modalTitle",
|
||||
"boxWidth",
|
||||
"boxHeight",
|
||||
"cutWidth",
|
||||
"cutHeight",
|
||||
"tool",
|
||||
"toolBgc",
|
||||
"sizeChange",
|
||||
"moveAble",
|
||||
"imgMove",
|
||||
"originalGraph",
|
||||
"crossOrigin",
|
||||
"crossOriginHeader",
|
||||
"rate",
|
||||
"WatermarkText",
|
||||
"WatermarkTextFont",
|
||||
"WatermarkTextColor",
|
||||
"WatermarkTextX",
|
||||
"WatermarkTextY",
|
||||
"smallToUpload",
|
||||
"saveCutPosition",
|
||||
"scaleAble",
|
||||
"toolBoxOverflow",
|
||||
"index",
|
||||
"previewMode",
|
||||
"fileType",
|
||||
"quality",
|
||||
"accept",
|
||||
"afterChooseImg",
|
||||
]);
|
||||
|
||||
const imgCutterProps = computed(() => {
|
||||
const result: Record<string, unknown> = {};
|
||||
|
||||
for (const key of IMG_CUTTER_PROP_KEYS) {
|
||||
const k = key as keyof FaCutterImgProps;
|
||||
if (props[k] != null) {
|
||||
result[key] = props[k];
|
||||
}
|
||||
}
|
||||
|
||||
// 水印文字兼容 deprecated WatermarkText
|
||||
if (props.watermarkText) {
|
||||
result.WatermarkText = props.watermarkText;
|
||||
}
|
||||
// 水印字体映射
|
||||
if (props.watermarkTextFont) {
|
||||
result.WatermarkTextFont = props.watermarkTextFont;
|
||||
}
|
||||
// 水印颜色映射
|
||||
if (props.watermarkTextColor) {
|
||||
result.WatermarkTextColor = props.watermarkTextColor;
|
||||
}
|
||||
// 水印位置映射
|
||||
if (props.watermarkTextX != null) {
|
||||
result.WatermarkTextX = props.watermarkTextX;
|
||||
}
|
||||
if (props.watermarkTextY != null) {
|
||||
result.WatermarkTextY = props.watermarkTextY;
|
||||
}
|
||||
// 比例映射
|
||||
if (props.rate) {
|
||||
result.rate = props.rate;
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
/* ============================================================
|
||||
* Methods: ImgCutter 事件处理 → emit camelCase 事件
|
||||
* ============================================================ */
|
||||
|
||||
function onCutDown(result: CutterResult) {
|
||||
emit("update:imgUrl", result.dataURL);
|
||||
emit("cut-down", result);
|
||||
}
|
||||
|
||||
function onPrintImg(result: { dataURL: string }) {
|
||||
temImgPath.value = result.dataURL;
|
||||
emit("print-img", result);
|
||||
}
|
||||
|
||||
function onChooseImg(result: unknown) {
|
||||
emit("choose-img", result);
|
||||
}
|
||||
|
||||
function onClearAll() {
|
||||
temImgPath.value = "";
|
||||
emit("clear-all");
|
||||
}
|
||||
|
||||
function onImageLoadComplete(result: unknown) {
|
||||
emit("image-load-complete", result);
|
||||
}
|
||||
|
||||
function onImageLoadError(error: Error) {
|
||||
emit("error", error);
|
||||
emit("image-load-error", error);
|
||||
}
|
||||
|
||||
function onError(error: Error) {
|
||||
emit("error", error);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
* Methods: 图片预加载
|
||||
* ============================================================ */
|
||||
|
||||
// 图片预加载
|
||||
function preloadImage(url: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
@@ -187,7 +332,6 @@ function preloadImage(url: string): Promise<void> {
|
||||
});
|
||||
}
|
||||
|
||||
// 初始化裁剪器
|
||||
async function initImgCutter() {
|
||||
if (props.imgUrl) {
|
||||
try {
|
||||
@@ -197,21 +341,16 @@ async function initImgCutter() {
|
||||
src: props.imgUrl,
|
||||
});
|
||||
} catch (error) {
|
||||
emit("error", error);
|
||||
emit("error", error as Error);
|
||||
console.error("图片加载失败:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 生命周期钩子
|
||||
onMounted(() => {
|
||||
if (props.imgUrl) {
|
||||
temImgPath.value = props.imgUrl;
|
||||
initImgCutter();
|
||||
}
|
||||
});
|
||||
/* ============================================================
|
||||
* Watch: imgUrl 变化
|
||||
* ============================================================ */
|
||||
|
||||
// 监听图片URL变化
|
||||
watch(
|
||||
() => props.imgUrl,
|
||||
(newVal) => {
|
||||
@@ -222,39 +361,27 @@ watch(
|
||||
}
|
||||
);
|
||||
|
||||
// 实时预览
|
||||
function cutterPrintImg(result: { dataURL: string }) {
|
||||
temImgPath.value = result.dataURL;
|
||||
}
|
||||
onMounted(() => {
|
||||
if (props.imgUrl) {
|
||||
temImgPath.value = props.imgUrl;
|
||||
initImgCutter();
|
||||
}
|
||||
});
|
||||
|
||||
// 裁剪完成
|
||||
function cutDownImg(result: CutterResult) {
|
||||
emit("update:imgUrl", result.dataURL);
|
||||
}
|
||||
/* ============================================================
|
||||
* Methods: 下载
|
||||
* ============================================================ */
|
||||
|
||||
// 图片加载完成
|
||||
function handleImageLoadComplete(result: any) {
|
||||
emit("imageLoadComplete", result);
|
||||
}
|
||||
|
||||
// 图片加载失败
|
||||
function handleImageLoadError(error: any) {
|
||||
emit("error", error);
|
||||
emit("imageLoadError", error);
|
||||
}
|
||||
|
||||
// 清除所有
|
||||
function handleClearAll() {
|
||||
temImgPath.value = "";
|
||||
}
|
||||
|
||||
// 下载图片
|
||||
function downloadImg() {
|
||||
function handleDownload() {
|
||||
const a = document.createElement("a");
|
||||
a.href = temImgPath.value;
|
||||
a.download = "image.png";
|
||||
a.click();
|
||||
}
|
||||
|
||||
function triggerCrop() {
|
||||
confirmElRef.value?.dispatchEvent(new MouseEvent("click", { bubbles: true }));
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -274,7 +401,7 @@ function downloadImg() {
|
||||
|
||||
.preview-container {
|
||||
.preview-box {
|
||||
background-color: var(--fa-active-color) !important;
|
||||
background-color: var(--art-active-color) !important;
|
||||
|
||||
.preview-img {
|
||||
width: 100%;
|
||||
@@ -283,9 +410,11 @@ function downloadImg() {
|
||||
}
|
||||
}
|
||||
|
||||
.download-btn {
|
||||
display: block;
|
||||
margin: 20px auto;
|
||||
.preview-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -354,66 +483,3 @@ function downloadImg() {
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<!--
|
||||
案例
|
||||
<template>
|
||||
<div class="page-content">
|
||||
<ArtCutterImg
|
||||
v-model:imgUrl="imageUrl"
|
||||
:boxWidth="530"
|
||||
:boxHeight="300"
|
||||
:cutWidth="360"
|
||||
:cutHeight="200"
|
||||
:quality="1"
|
||||
:tool="true"
|
||||
:watermarkText="'My Watermark'"
|
||||
watermarkColor="#ff0000"
|
||||
:showPreview="true"
|
||||
:originalGraph="false"
|
||||
:title="'图片裁剪'"
|
||||
:previewTitle="'预览效果'"
|
||||
@error="handleError"
|
||||
@imageLoadComplete="handleLoadComplete"
|
||||
@imageLoadError="handleLoadError"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import lockImg from '@imgs/lock/bg_dark.webp'
|
||||
|
||||
defineOptions({ name: 'WidgetsImageCrop' })
|
||||
|
||||
/**
|
||||
* 图片 URL
|
||||
*/
|
||||
const imageUrl = ref(lockImg)
|
||||
|
||||
/**
|
||||
* 处理裁剪错误
|
||||
* @param error 错误对象
|
||||
*/
|
||||
const handleError = (error: Error) => {
|
||||
console.error('裁剪错误:', error)
|
||||
ElMessage.error('图片裁剪失败')
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理图片加载完成
|
||||
* @param result 加载结果
|
||||
*/
|
||||
const handleLoadComplete = (result: { url: string; width: number; height: number }) => {
|
||||
console.log('图片加载完成:', result)
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理图片加载错误
|
||||
* @param error 错误对象
|
||||
*/
|
||||
const handleLoadError = (error: Error) => {
|
||||
console.error('图片加载失败:', error)
|
||||
ElMessage.error('图片加载失败')
|
||||
}
|
||||
</script>
|
||||
-->
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
destroy-on-close
|
||||
v-bind="dialogAttrs"
|
||||
@close="emit('close')"
|
||||
@closed="emit('closed')"
|
||||
@opened="emit('opened')"
|
||||
>
|
||||
<template #header="{ titleId, titleClass, close }">
|
||||
@@ -83,6 +84,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
interface Emits {
|
||||
"update:modelValue": [v: boolean];
|
||||
close: [];
|
||||
closed: [];
|
||||
opened: [];
|
||||
"fullscreen-change": [isFullscreen: boolean];
|
||||
/** 点击取消按钮 */
|
||||
|
||||
+8
-9
@@ -26,11 +26,9 @@ interface MenuTreeTableProps {
|
||||
* 负责:搜索过滤、级联勾选、父子联动、展开/收起、初始化选中、对外方法
|
||||
*/
|
||||
export function useMenuTreeTable(rawProps: MenuTreeTableProps) {
|
||||
const props = {
|
||||
menuTree: rawProps.menuTree,
|
||||
checkedIds: rawProps.checkedIds ?? [],
|
||||
loading: rawProps.loading,
|
||||
};
|
||||
// 直接使用父组件传入的 props(vue 3 中子组件 props 本身就是 reactive proxy)
|
||||
// 不要再 reactive() 一次,否则会丢失响应式(只能拿到首次快照)
|
||||
const props = rawProps;
|
||||
// ---- 状态 ----
|
||||
const filterText = ref("");
|
||||
const isExpanded = ref(true);
|
||||
@@ -336,22 +334,23 @@ export function useMenuTreeTable(rawProps: MenuTreeTableProps) {
|
||||
function refresh() {
|
||||
filterText.value = "";
|
||||
const tree = props.menuTree;
|
||||
parentChildLinked.value = checkParentChildLinked(props.checkedIds, tree);
|
||||
const ids = props.checkedIds ?? [];
|
||||
parentChildLinked.value = checkParentChildLinked(ids, tree);
|
||||
tableData.value = filterTableData(tree);
|
||||
nextTick(() => initSelection(props.checkedIds));
|
||||
nextTick(() => initSelection(ids));
|
||||
}
|
||||
|
||||
// ---- 数据变化监听 ----
|
||||
watch(
|
||||
() => [props.menuTree, props.checkedIds] as const,
|
||||
() => refresh(),
|
||||
{ immediate: false }
|
||||
{ immediate: true, deep: true }
|
||||
);
|
||||
|
||||
watch(filterText, () => {
|
||||
const tree = filteredMenuTree.value;
|
||||
tableData.value = filterTableData(tree);
|
||||
nextTick(() => initSelection(props.checkedIds));
|
||||
nextTick(() => initSelection(props.checkedIds ?? []));
|
||||
if (filterText.value) setAllRowsExpanded(true);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
<!-- 单图上传组件 -->
|
||||
<template>
|
||||
<div class="single-image-upload">
|
||||
<ElDialog
|
||||
<FaDialog
|
||||
v-model="cropVisible"
|
||||
:title="cropDialogTitle"
|
||||
width="640px"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
class="single-image-upload__crop-dialog"
|
||||
width="960px"
|
||||
:draggable="false"
|
||||
@closed="onCropDialogClosed"
|
||||
>
|
||||
<FaCutterImg
|
||||
@@ -28,7 +26,7 @@
|
||||
@update:img-url="onCropConfirm"
|
||||
@error="onCropError"
|
||||
/>
|
||||
</ElDialog>
|
||||
</FaDialog>
|
||||
|
||||
<ElUpload
|
||||
v-model:file-list="internalFileList"
|
||||
@@ -82,6 +80,7 @@ defineOptions({ name: "FaUpload" });
|
||||
|
||||
import { ref, watch } from "vue";
|
||||
import { UploadRawFile, UploadRequestOptions, ElMessage, type UploadUserFile } from "element-plus";
|
||||
import { CircleCloseFilled } from "@element-plus/icons-vue";
|
||||
import ParamsAPI from "@/api/module_system/params";
|
||||
import { dataURLToFile } from "@utils";
|
||||
|
||||
@@ -146,11 +145,11 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
tipText: "",
|
||||
enablePreview: true,
|
||||
enableCrop: false,
|
||||
cropBoxWidth: 520,
|
||||
cropBoxHeight: 360,
|
||||
cropCutWidth: 400,
|
||||
cropCutHeight: 300,
|
||||
cropQuality: 0.92,
|
||||
cropBoxWidth: 530,
|
||||
cropBoxHeight: 300,
|
||||
cropCutWidth: 360,
|
||||
cropCutHeight: 200,
|
||||
cropQuality: 1,
|
||||
cropFileType: "jpeg",
|
||||
cropDialogTitle: "裁剪图片",
|
||||
cropInnerTitle: "调整图片",
|
||||
|
||||
Reference in New Issue
Block a user