完成保存草稿到作品库预览编辑功能,添加防篡改水印功能
This commit is contained in:
parent
46fe97c589
commit
6a108992e2
|
|
@ -209,3 +209,16 @@
|
|||
[2026-07-25 10:06:42,035] [INFO] [D:\AAA-Code\flaunt-web\service\core\router\index.cjs:28] GET 200 /api/user?nonce=57df84ed41a34157f3de139fa046d5f4&signature=c580ad55611e2949914ce98f0e4b68df×tamp=1784945202
|
||||
[2026-07-25 10:06:42,037] [INFO] [unknow:0] GET 200 /favicon.ico
|
||||
[2026-07-25 10:06:42,179] [INFO] [D:\AAA-Code\flaunt-web\service\core\router\index.cjs:28] GET 200 /api/order?nonce=180858c062a7d37f8a3bc5addc1f7c5f&signature=ed4d810b96feff3c2bcb8354b9396516&status=2×tamp=1784945202
|
||||
[2026-07-25 10:40:31,791] [INFO] [unknow:0] GET 200 /favicon.ico
|
||||
[2026-07-25 10:40:32,709] [INFO] [unknow:0] GET 200 /assets/works.js
|
||||
[2026-07-25 10:40:32,710] [INFO] [unknow:0] GET 200 /assets/works.css
|
||||
[2026-07-25 10:40:32,734] [INFO] [unknow:0] GET 200 /about?t=1784947232719
|
||||
[2026-07-25 10:40:32,888] [INFO] [unknow:0] GET 200 /favicon.ico
|
||||
[2026-07-25 10:40:33,018] [INFO] [D:\AAA-Code\flaunt-web\service\core\router\index.cjs:28] GET 200 /api/user?nonce=51cd4c69c8f40c85725c4c7012572837&signature=493b3172a42ce9e7aa87bbde0a0e648a×tamp=1784947233
|
||||
[2026-07-25 10:40:33,020] [INFO] [D:\AAA-Code\flaunt-web\service\core\router\index.cjs:28] GET 200 /api/user/config?nonce=41b001a436a201a06776f30ab1a27f3c&signature=df2393a0cc5cdce01cb19394ecbe20a3×tamp=1784947233
|
||||
[2026-07-25 10:40:35,661] [INFO] [D:\AAA-Code\flaunt-web\service\core\router\index.cjs:28] GET 200 /api/app/code?nonce=36d0e906c36c3dfba2c57dae63071af4&signature=c16ede81b8db3bd367d5469dfba193ea×tamp=1784947235
|
||||
[2026-07-25 10:43:11,325] [INFO] [D:\AAA-Code\flaunt-web\service\core\router\index.cjs:28] GET 200 /api/app/code?nonce=60c325a3f3639913aa1ced29496ec98d&signature=3318713827559bc10a9922aaae5ad1de×tamp=1784947391
|
||||
[2026-07-25 10:43:13,507] [INFO] [unknow:0] GET 200 /.well-known/appspecific/com.chrome.devtools.json
|
||||
[2026-07-25 10:43:20,485] [INFO] [D:\AAA-Code\flaunt-web\service\core\router\index.cjs:28] GET 200 /api/app/code?nonce=fa79dee27ab021c46d92f63fb3385ec9&signature=04c18efa72c875e51b9044e78b25e86d×tamp=1784947400
|
||||
[2026-07-25 10:46:56,136] [INFO] [D:\AAA-Code\flaunt-web\service\core\router\index.cjs:28] GET 200 /api/user?nonce=8f01b34875563a6e6098e726aa7b7a2e&signature=ebea6add12ddea8167b1a77318a6aa6f×tamp=1784947616
|
||||
[2026-07-25 10:46:56,138] [INFO] [unknow:0] GET 200 /favicon.ico
|
||||
|
|
|
|||
|
|
@ -82,8 +82,8 @@ export default {
|
|||
* @param {*} data
|
||||
* @returns
|
||||
*/
|
||||
async getAppCode(data) {
|
||||
return await get(appConfig.baseURL, "/app/code", data);
|
||||
async getDownloadCode(data) {
|
||||
return await get(appConfig.baseURL, "/web/download", data);
|
||||
}
|
||||
|
||||
};
|
||||
|
|
|
|||
|
|
@ -71,9 +71,13 @@
|
|||
</div>
|
||||
|
||||
<LoginDialog ref="loginDialogRef" />
|
||||
<BaseDialog ref="appCodeRef" v-model="showAppCode" width="260px" @close="showAppCode = false" title="下载APP">
|
||||
<div class="qrCode-box">
|
||||
<img style="width: 200px;height: 200px;margin: 0 auto;" :src="appCodeImg" alt="">
|
||||
<BaseDialog ref="appCodeRef" v-model="showAppCode" width="494px" class="app-code-dialog"
|
||||
@close="showAppCode = false" title="下载APP">
|
||||
<div class="app-download-wrapper">
|
||||
<div class="content-box">
|
||||
<div class="scan-title">扫码下载装样大师手机版</div>
|
||||
<img class="qrcode" :src="appCodeImg" alt="">
|
||||
</div>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
</header>
|
||||
|
|
@ -102,10 +106,10 @@ async function openAppCodeDialog() {
|
|||
showAppCode.value = true
|
||||
if (!appCodeImg.value) {
|
||||
try {
|
||||
const res = await authService.getAppCode()
|
||||
const res = await authService.getDownloadCode()
|
||||
console.log('获取APP二维码响应结果:', res)
|
||||
const data = res.data || res
|
||||
appCodeImg.value = data.qrcode || data.qr_code || data.qrcode_url || data.url || (typeof data === 'string' ? data : '')
|
||||
appCodeImg.value = data.android_qrcode
|
||||
} catch (e) {
|
||||
console.log(e.message)
|
||||
}
|
||||
|
|
@ -559,11 +563,53 @@ function openLoginDialog() {
|
|||
height: 100%;
|
||||
}
|
||||
|
||||
.qrCode-box {
|
||||
.app-code-dialog {
|
||||
:deep(.el-dialog) {
|
||||
padding: 0 !important;
|
||||
background-color: transparent !important;
|
||||
box-shadow: 0 0 0 0 transparent !important;
|
||||
}
|
||||
|
||||
|
||||
:deep(.close) {
|
||||
top: calc(100% + 18px);
|
||||
|
||||
right: calc(50% - 34px);
|
||||
}
|
||||
}
|
||||
|
||||
.app-download-wrapper {
|
||||
width: 494px;
|
||||
height: 504px;
|
||||
background-image: url('@/public/other/gongzidan/download-app-bg.png');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
position: relative;
|
||||
|
||||
.content-box {
|
||||
position: absolute;
|
||||
top: 45%;
|
||||
/* 大约在白底开始的位置,根据高保真微调 */
|
||||
left: 0;
|
||||
width: 100%;
|
||||
padding-left: 40px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px 0;
|
||||
|
||||
.scan-title {
|
||||
font-weight: 500;
|
||||
font-size: 20px;
|
||||
color: #1A1A1A;
|
||||
line-height: 29px;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.qrcode {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="phone-box">
|
||||
<div class="phone-box" ref="containerRef">
|
||||
<img class="phone-bg" src="@/public/simulator/ios-phone.png" alt="">
|
||||
<div class="iphone-frame" :class="theme" id="capture-frame">
|
||||
<slot></slot>
|
||||
|
|
@ -12,6 +12,8 @@
|
|||
<script setup>
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { ref } from 'vue'
|
||||
import { useWatermarkGuard } from '@/hooks/useWatermarkGuard'
|
||||
|
||||
defineProps({
|
||||
theme: {
|
||||
|
|
@ -22,6 +24,9 @@ defineProps({
|
|||
|
||||
const userStore = useUserStore()
|
||||
const { isVip } = storeToRefs(userStore)
|
||||
|
||||
const containerRef = ref(null)
|
||||
useWatermarkGuard(containerRef, isVip)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,176 @@
|
|||
import { onMounted, onBeforeUnmount, ref } from 'vue';
|
||||
import { ElMessage } from 'element-plus';
|
||||
|
||||
/**
|
||||
* 保护水印节点防篡改
|
||||
* @param {Ref<HTMLElement>} containerRef - 包含水印的父容器的 Ref
|
||||
* @param {String} watermarkSelector - 水印节点的类名选择器,默认 '.watermark-overlay'
|
||||
* @param {Ref<Boolean>} isVipRef - 用户的 VIP 状态引用,如果是 VIP 则不进行检查
|
||||
*/
|
||||
export function useWatermarkGuard(containerRef, isVipRef, watermarkSelector = '.watermark-overlay') {
|
||||
let observer = null;
|
||||
let pollTimer = null;
|
||||
const isDestroyed = ref(false);
|
||||
|
||||
const enforcePunishment = () => {
|
||||
if (isDestroyed.value) return;
|
||||
if (!containerRef.value) return;
|
||||
|
||||
// 锁定原始尺寸,防止清空内部元素(如撑起宽高的图片)后导致外层盒子塌陷
|
||||
const width = containerRef.value.offsetWidth;
|
||||
const height = containerRef.value.offsetHeight;
|
||||
if (width && height) {
|
||||
containerRef.value.style.width = width + 'px';
|
||||
containerRef.value.style.height = height + 'px';
|
||||
}
|
||||
|
||||
// 触发惩罚机制:将容器内容彻底清空并显示红色警告
|
||||
isDestroyed.value = true;
|
||||
containerRef.value.innerHTML = `
|
||||
<div style="
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: rgba(245, 247, 250, 0.85);
|
||||
backdrop-filter: blur(8px);
|
||||
z-index: 999999;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
">
|
||||
<div style="
|
||||
background: #ffffff;
|
||||
border-radius: 20px;
|
||||
padding: 40px 24px;
|
||||
box-shadow: 0 12px 32px rgba(139, 126, 250, 0.15);
|
||||
width: 85%;
|
||||
max-width: 320px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
">
|
||||
<div style="
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
background: linear-gradient(135deg, #e0c3fc 0%, #8ec5fc 100%);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 32px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 4px 12px rgba(142, 197, 252, 0.4);
|
||||
">🛡️</div>
|
||||
<h3 style="color: #333333; font-size: 18px; font-weight: 600; margin: 0 0 12px 0;">预览已受保护</h3>
|
||||
<p style="color: #666666; font-size: 14px; line-height: 1.6; margin: 0 0 28px 0;">检测到水印显示异常<br>为保护创作者权益,预览已终止</p>
|
||||
<button onclick="window.location.reload()" style="
|
||||
background: linear-gradient(90deg, #9b81ec 0%, #7d65f5 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 24px;
|
||||
padding: 10px 32px;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 12px rgba(155, 129, 236, 0.3);
|
||||
transition: transform 0.2s;
|
||||
" onmouseover="this.style.transform='scale(1.05)'" onmouseout="this.style.transform='scale(1)'">
|
||||
重新加载
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
ElMessage.warning('由于水印显示异常,预览已被终止');
|
||||
|
||||
if (observer) {
|
||||
observer.disconnect();
|
||||
}
|
||||
};
|
||||
|
||||
const checkWatermarkIntegrity = () => {
|
||||
if (!containerRef.value || isDestroyed.value) return;
|
||||
// 如果是 VIP,不需要水印,也不需要防篡改检查
|
||||
if (isVipRef && isVipRef.value === true) return;
|
||||
|
||||
const watermarkNode = containerRef.value.querySelector(watermarkSelector);
|
||||
|
||||
// 检查 1:节点是否被直接删除
|
||||
if (!watermarkNode) {
|
||||
enforcePunishment();
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查 2:是否被通过 style 修改导致隐藏
|
||||
const styles = window.getComputedStyle(watermarkNode);
|
||||
if (
|
||||
styles.display === 'none' ||
|
||||
styles.visibility === 'hidden' ||
|
||||
styles.opacity === '0' ||
|
||||
parseFloat(styles.opacity) < 0.1 ||
|
||||
parseFloat(styles.width) < 10 ||
|
||||
parseFloat(styles.height) < 10 ||
|
||||
styles.position === 'static'
|
||||
) {
|
||||
enforcePunishment();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (!containerRef.value) return;
|
||||
|
||||
// 初次检查 (延迟一下等节点渲染完毕)
|
||||
setTimeout(() => {
|
||||
checkWatermarkIntegrity();
|
||||
}, 800);
|
||||
|
||||
// 监听容器内的节点变动
|
||||
observer = new MutationObserver((mutations) => {
|
||||
if (isVipRef && isVipRef.value === true) return;
|
||||
let shouldCheck = false;
|
||||
|
||||
for (const mutation of mutations) {
|
||||
// 如果是子节点树发生变化(可能删除了节点)
|
||||
if (mutation.type === 'childList') {
|
||||
shouldCheck = true;
|
||||
break;
|
||||
}
|
||||
// 如果是属性发生了变化(包含自身和所有子节点的样式属性改变)
|
||||
if (mutation.type === 'attributes') {
|
||||
shouldCheck = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldCheck) {
|
||||
// 使用 setTimeout 避免在 DOM 更新周期中同步导致卡死,给恶意篡改一个反应时间
|
||||
setTimeout(() => checkWatermarkIntegrity(), 50);
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(containerRef.value, {
|
||||
childList: true, // 监听子节点增删
|
||||
subtree: true, // 监听所有后代节点
|
||||
attributes: true, // 监听属性变化
|
||||
attributeFilter: ['style', 'class'] // 只关注样式和类的变化
|
||||
});
|
||||
|
||||
// 终极防御:轮询检查(防止修改全局 CSS 绕过 MutationObserver)
|
||||
pollTimer = setInterval(() => {
|
||||
checkWatermarkIntegrity();
|
||||
}, 1500);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (observer) {
|
||||
observer.disconnect();
|
||||
}
|
||||
if (pollTimer) {
|
||||
clearInterval(pollTimer);
|
||||
}
|
||||
});
|
||||
|
||||
return { isDestroyed };
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 137 KiB |
|
|
@ -87,6 +87,9 @@ export const useUserStore = defineStore('user', () => {
|
|||
}
|
||||
} catch (e) {
|
||||
console.error("同步后台用户信息异常:", e)
|
||||
// 关键防护:网络断开或异常时,强制降级高级权限,防止断网 + 篡改缓存白嫖
|
||||
isVip.value = false
|
||||
tokenValid.value = false
|
||||
} finally {
|
||||
fetchPromise = null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@
|
|||
<div class="dialog-title">
|
||||
<div class="bar"></div>全部订单
|
||||
</div>
|
||||
<div class="orders-list">
|
||||
<div v-if="orders.length > 0" class="orders-list">
|
||||
<div v-for="(o, index) in orders" :key="index" class="order-item">
|
||||
<div class="order-left">
|
||||
<span class="order-plan">{{ planLabel(o.goods_name) }}</span>
|
||||
|
|
@ -100,6 +100,9 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="empty-state">
|
||||
<img src="@/public/profile/empty.png" class="empty-img" />
|
||||
</div>
|
||||
</div>
|
||||
</BaseDialog>
|
||||
</div>
|
||||
|
|
@ -147,7 +150,6 @@ onMounted(async () => {
|
|||
Object.assign(userInfo, local)
|
||||
} catch (e) { }
|
||||
|
||||
loadStats();
|
||||
getOrders();
|
||||
})
|
||||
|
||||
|
|
@ -383,6 +385,10 @@ function statusLabel(s) {
|
|||
.section-container {
|
||||
margin-bottom: 13px;
|
||||
|
||||
.order-content {
|
||||
min-height: 240px;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -450,6 +456,8 @@ function statusLabel(s) {
|
|||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
|
@ -461,7 +469,6 @@ function statusLabel(s) {
|
|||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
width: 100%;
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
</div>
|
||||
<img class="stay-tuned-img" src="@/public/common/stay-tuned.png" alt="">
|
||||
</div>
|
||||
<div class="main-box">
|
||||
<div class="main-box" ref="mainBoxRef">
|
||||
<img class="main-bg" src="@/public/other/gongzidan/gongzidan.png" alt="">
|
||||
<span class="item-info" style="left: 156px;bottom: 210px;">{{ formData.job }}</span>
|
||||
<span class="item-info" style="left: 202px;bottom: 210px;">{{ formData.name }}</span>
|
||||
|
|
@ -28,7 +28,7 @@
|
|||
<span class="item-info" style="left: 646px;bottom: 210px;">{{ formData.netSalary }}</span>
|
||||
<span class="item-info time" style="left: 434px; bottom: 298px;">{{ formData.year }}</span>
|
||||
<span class="item-info time" style="left: 488px; bottom: 298px;">{{ formData.month }}</span>
|
||||
<div class="watermark-overlay"></div>
|
||||
<div v-if="!isVip" class="watermark-overlay"></div>
|
||||
</div>
|
||||
<!-- <div class="right-box"></div> -->
|
||||
<div class="right-box content-box">
|
||||
|
|
@ -102,11 +102,52 @@
|
|||
<WarningFooter />
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { ref, computed, watch, inject, onMounted } from 'vue'
|
||||
import WarningFooter from '@/components/common/WarningFooter.vue'
|
||||
import { all } from 'axios'
|
||||
import { dayOrDaysToDate, ElMessage } from 'element-plus'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import html2canvas from 'html2canvas'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { useWatermarkGuard } from '@/hooks/useWatermarkGuard'
|
||||
|
||||
const showPayment = inject('showPayment')
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const workId = ref('')
|
||||
const originalTitle = ref('')
|
||||
|
||||
onMounted(async () => {
|
||||
const id = route.query.id || route.query.workId
|
||||
if (!id) return
|
||||
|
||||
try {
|
||||
let drafts = []
|
||||
try {
|
||||
drafts = JSON.parse(localStorage.getItem('local_drafts') || '[]')
|
||||
if (!Array.isArray(drafts)) drafts = []
|
||||
} catch (e) { }
|
||||
|
||||
const work = drafts.find(w => String(w.id) === String(id))
|
||||
if (work) {
|
||||
workId.value = id
|
||||
originalTitle.value = work.title || ''
|
||||
if (work.data_json) {
|
||||
const configData = typeof work.data_json === 'string' ? JSON.parse(work.data_json) : work.data_json
|
||||
Object.assign(formData.value, configData)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取草稿失败:', e)
|
||||
}
|
||||
})
|
||||
|
||||
const userStore = useUserStore()
|
||||
const { isVip } = storeToRefs(userStore)
|
||||
|
||||
const mainBoxRef = ref(null)
|
||||
useWatermarkGuard(mainBoxRef, isVip)
|
||||
|
||||
const currentYear = new Date().getFullYear().toString()
|
||||
const currentMonth = (new Date().getMonth() + 1).toString()
|
||||
|
|
@ -178,11 +219,115 @@ const randomGenerate = () => {
|
|||
// TODO: 实现随机生成逻辑
|
||||
}
|
||||
|
||||
const saveDraft = () => {
|
||||
ElMessage.success('草稿已保存')
|
||||
const saveDraft = async () => {
|
||||
if (!localStorage.getItem('token')) {
|
||||
ElMessage.warning('请先登录')
|
||||
return
|
||||
}
|
||||
try {
|
||||
const defaultTitle = originalTitle.value || '未命名工资单'
|
||||
const { value: title } = await ElMessageBox.prompt('请输入作品名称', '保存草稿', {
|
||||
confirmButtonText: '保存',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: defaultTitle,
|
||||
inputPlaceholder: '输入名称(同名将提示覆盖,异名将新建)',
|
||||
})
|
||||
if (!title) return
|
||||
|
||||
let drafts = []
|
||||
try {
|
||||
drafts = JSON.parse(localStorage.getItem('local_drafts') || '[]')
|
||||
if (!Array.isArray(drafts)) drafts = []
|
||||
} catch (e) {
|
||||
drafts = []
|
||||
}
|
||||
|
||||
let draftId = ''
|
||||
let isOverwriteOther = false
|
||||
|
||||
if (title !== originalTitle.value) {
|
||||
const existIndex = drafts.findIndex(w => w.title === title && w.type === 'gongzidan')
|
||||
if (existIndex !== -1) {
|
||||
await ElMessageBox.confirm(`已存在同名作品「${title}」,是否覆盖该作品?`, '提示', {
|
||||
confirmButtonText: '覆盖',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
draftId = drafts[existIndex].id
|
||||
isOverwriteOther = true
|
||||
} else {
|
||||
draftId = 'draft_' + Date.now()
|
||||
}
|
||||
} else {
|
||||
draftId = workId.value || ('draft_' + Date.now())
|
||||
}
|
||||
|
||||
let coverImg = ''
|
||||
const el = document.querySelector('.main-box')
|
||||
if (el) {
|
||||
try {
|
||||
const canvas = await html2canvas(el, {
|
||||
scale: 300 / el.offsetWidth, // 固定生成 150px 宽度的图片
|
||||
useCORS: true
|
||||
})
|
||||
coverImg = canvas.toDataURL('image/jpeg', 0.6)
|
||||
} catch (err) {
|
||||
console.error('截图生成失败', err)
|
||||
}
|
||||
}
|
||||
|
||||
const payload = {
|
||||
id: draftId,
|
||||
title: title,
|
||||
type: 'gongzidan',
|
||||
page_router: '/gongzidan',
|
||||
data_json: JSON.stringify(formData.value),
|
||||
cover_img: coverImg,
|
||||
update_time: Date.now()
|
||||
}
|
||||
|
||||
let isSelfUpdate = false
|
||||
if (isOverwriteOther) {
|
||||
const existIndex = drafts.findIndex(w => String(w.id) === String(draftId))
|
||||
if (existIndex !== -1) drafts[existIndex] = payload
|
||||
} else {
|
||||
const selfIndex = drafts.findIndex(w => String(w.id) === String(draftId))
|
||||
if (selfIndex !== -1) {
|
||||
drafts[selfIndex] = payload
|
||||
isSelfUpdate = true
|
||||
} else {
|
||||
drafts.unshift(payload)
|
||||
}
|
||||
}
|
||||
|
||||
localStorage.setItem('local_drafts', JSON.stringify(drafts))
|
||||
workId.value = payload.id
|
||||
originalTitle.value = payload.title
|
||||
|
||||
if (!route.query.id || isOverwriteOther) {
|
||||
router.replace({ query: { id: draftId } })
|
||||
}
|
||||
|
||||
let msg = '已新增草稿'
|
||||
if (isOverwriteOther) msg = '草稿已覆盖保存'
|
||||
else if (isSelfUpdate) msg = '草稿已更新'
|
||||
ElMessage.success(msg)
|
||||
} catch (e) {
|
||||
if (e !== 'cancel') {
|
||||
console.error(e)
|
||||
ElMessage.error('保存失败')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const downloadPaper = async () => {
|
||||
if (!localStorage.getItem('token')) { ElMessage.warning('请先登录后再下载'); return }
|
||||
if (!userStore.isVip) {
|
||||
showPayment?.()
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
const el = document.querySelector('.main-box')
|
||||
if (!el) {
|
||||
ElMessage.error('预览未就绪,请稍后重试')
|
||||
|
|
@ -342,7 +487,7 @@ const downloadPaper = async () => {
|
|||
background-size: 125px 125px;
|
||||
background-repeat: repeat;
|
||||
pointer-events: none;
|
||||
z-index: 999;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.item-info {
|
||||
|
|
|
|||
|
|
@ -29,16 +29,19 @@
|
|||
|
||||
<!-- 悬浮遮罩与操作按钮 -->
|
||||
<div class="hover-overlay">
|
||||
<div class="close-btn">
|
||||
<div class="close-btn" @click.stop="handleDelete(item.id)">
|
||||
<img style="width: 18px;height: 18px;" src="@/public/header/close.png" alt="">
|
||||
</div>
|
||||
<div class="work-title">
|
||||
<span class="title-text" :title="item.title || '未命名作品'">{{ item.title || '未命名作品' }}</span>
|
||||
</div>
|
||||
<div class="action-btns">
|
||||
<button class="action-btn edit-btn">
|
||||
<button class="action-btn edit-btn" @click.stop="handleEdit(item)">
|
||||
<img class="icon-img" src="@/public/edit.png" alt=""> 编辑
|
||||
</button>
|
||||
<button class="action-btn download-btn">
|
||||
<!-- <button class="action-btn download-btn" @click.stop="handleDownload(item)">
|
||||
<img class="icon-img" src="@/public/direct-inbox.png" alt=""> 下载
|
||||
</button>
|
||||
</button> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -51,38 +54,89 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { Search, Close, EditPen, Download } from '@element-plus/icons-vue'
|
||||
import WarningFooter from '@/components/common/WarningFooter.vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
const router = useRouter()
|
||||
const searchQuery = ref('')
|
||||
const tabs = ['全部', '微信模拟', '娱乐模拟']
|
||||
const activeTab = ref('全部')
|
||||
|
||||
// 模拟瀑布流数据,包含不同高度以展现错落有致的效果
|
||||
const masonryItems = ref([
|
||||
{ id: 1, height: 100, img: "https://fuss10.elemecdn.com/8/27/f01c15bb73e1ef3793e64e6b7bbccjpeg.jpeg" },
|
||||
{ id: 2, height: 260, img: "https://cube.elemecdn.com/6/94/4d3ea53c084bad6931a56d5158a48jpeg.jpeg" },
|
||||
{ id: 3, height: 560, img: "https://fuss10.elemecdn.com/3/28/bbf893f792f03a54408b3b7a7ebf0jpeg.jpeg" },
|
||||
{ id: 4, height: 320, img: "https://fastly.picsum.photos/id/167/300/200.jpg?hmac=2eeD-Byn4LjWFNUeCaFtQHSCpKs0vaozPXwFdkpzOP0" },
|
||||
{ id: 1, height: 100, img: "https://fuss10.elemecdn.com/8/27/f01c15bb73e1ef3793e64e6b7bbccjpeg.jpeg" },
|
||||
{ id: 5, height: 380, img: "https://fastly.picsum.photos/id/513/200/200.jpg?hmac=xMRZhdrttvlfIvOf0Qm9J4texbmA0HS2pBNVM-Pho-U" },
|
||||
{ id: 1, height: 100, img: "https://fuss10.elemecdn.com/8/27/f01c15bb73e1ef3793e64e6b7bbccjpeg.jpeg" },
|
||||
{ id: 2, height: 260, img: "https://cube.elemecdn.com/6/94/4d3ea53c084bad6931a56d5158a48jpeg.jpeg" },
|
||||
{ id: 3, height: 560, img: "https://fuss10.elemecdn.com/3/28/bbf893f792f03a54408b3b7a7ebf0jpeg.jpeg" },
|
||||
{ id: 4, height: 320, img: "https://fastly.picsum.photos/id/167/300/200.jpg?hmac=2eeD-Byn4LjWFNUeCaFtQHSCpKs0vaozPXwFdkpzOP0" },
|
||||
{ id: 5, height: 380, img: "https://fastly.picsum.photos/id/513/200/200.jpg?hmac=xMRZhdrttvlfIvOf0Qm9J4texbmA0HS2pBNVM-Pho-U" },
|
||||
{ id: 1, height: 100, img: "https://fuss10.elemecdn.com/8/27/f01c15bb73e1ef3793e64e6b7bbccjpeg.jpeg" },
|
||||
{ id: 11, height: 270, img: "https://fastly.picsum.photos/id/513/200/200.jpg?hmac=xMRZhdrttvlfIvOf0Qm9J4texbmA0HS2pBNVM-Pho-U" },
|
||||
{ id: 1, height: 100, img: "https://fuss10.elemecdn.com/8/27/f01c15bb73e1ef3793e64e6b7bbccjpeg.jpeg" },
|
||||
{ id: 2, height: 260, img: "https://cube.elemecdn.com/6/94/4d3ea53c084bad6931a56d5158a48jpeg.jpeg" },
|
||||
{ id: 3, height: 560, img: "https://fuss10.elemecdn.com/3/28/bbf893f792f03a54408b3b7a7ebf0jpeg.jpeg" },
|
||||
{ id: 4, height: 320, img: "https://fastly.picsum.photos/id/167/300/200.jpg?hmac=2eeD-Byn4LjWFNUeCaFtQHSCpKs0vaozPXwFdkpzOP0" },
|
||||
{ id: 5, height: 380, img: "https://fastly.picsum.photos/id/513/200/200.jpg?hmac=xMRZhdrttvlfIvOf0Qm9J4texbmA0HS2pBNVM-Pho-U" },
|
||||
{ id: 4, height: 320, img: "https://fastly.picsum.photos/id/167/300/200.jpg?hmac=2eeD-Byn4LjWFNUeCaFtQHSCpKs0vaozPXwFdkpzOP0" },
|
||||
{ id: 2, height: 260, img: "https://cube.elemecdn.com/6/94/4d3ea53c084bad6931a56d5158a48jpeg.jpeg" },
|
||||
{ id: 3, height: 560, img: "https://fuss10.elemecdn.com/3/28/bbf893f792f03a54408b3b7a7ebf0jpeg.jpeg" },
|
||||
])
|
||||
const localDrafts = ref([])
|
||||
|
||||
onMounted(() => {
|
||||
loadDrafts()
|
||||
})
|
||||
|
||||
function loadDrafts() {
|
||||
try {
|
||||
const drafts = JSON.parse(localStorage.getItem('local_drafts') || '[]')
|
||||
localDrafts.value = Array.isArray(drafts) ? drafts : []
|
||||
} catch (e) {
|
||||
localDrafts.value = []
|
||||
}
|
||||
}
|
||||
|
||||
// 动态计算展示的数据
|
||||
const masonryItems = computed(() => {
|
||||
let list = localDrafts.value
|
||||
|
||||
if (activeTab.value === '微信模拟') {
|
||||
list = list.filter(item => item.type && item.type.startsWith('wx-'))
|
||||
} else if (activeTab.value === '娱乐模拟') {
|
||||
list = list.filter(item => item.type && !item.type.startsWith('wx-'))
|
||||
}
|
||||
|
||||
if (searchQuery.value) {
|
||||
const keyword = searchQuery.value.toLowerCase()
|
||||
list = list.filter(item => item.title && item.title.toLowerCase().includes(keyword))
|
||||
}
|
||||
|
||||
return list.map(item => {
|
||||
return {
|
||||
...item,
|
||||
id: item.id,
|
||||
img: item.cover_img || '',
|
||||
height: 'auto'
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
function handleDelete(id) {
|
||||
ElMessageBox.confirm('确定要删除该草稿吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
const drafts = localDrafts.value.filter(item => item.id !== id)
|
||||
localStorage.setItem('local_drafts', JSON.stringify(drafts))
|
||||
localDrafts.value = drafts
|
||||
ElMessage.success('已删除')
|
||||
}).catch(() => { })
|
||||
}
|
||||
|
||||
function handleEdit(item) {
|
||||
if (item.type === 'wx-balance' || item.type === 'gongzidan' || item.type === 'wx-chat') {
|
||||
router.push(`${item.page_router}?id=${item.id}`)
|
||||
} else {
|
||||
// 后续添加更多类型跳转
|
||||
ElMessage.info('敬请期待')
|
||||
}
|
||||
}
|
||||
|
||||
function handleDownload(item) {
|
||||
if (!item.cover_img) {
|
||||
ElMessage.warning('没有可下载的预览图')
|
||||
return
|
||||
}
|
||||
const link = document.createElement('a')
|
||||
link.download = `${item.title || '草稿截图'}.png`
|
||||
link.href = item.cover_img
|
||||
link.click()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
@ -241,6 +295,31 @@ const masonryItems = ref([
|
|||
}
|
||||
}
|
||||
|
||||
.work-title {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
margin-bottom: 8px;
|
||||
width: 100%;
|
||||
|
||||
.title-text {
|
||||
color: #ffffff;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
letter-spacing: 1px;
|
||||
text-shadow: 0 2px 10px rgba(0, 0, 0, 0.7);
|
||||
padding: 0 10px;
|
||||
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 3;
|
||||
overflow: hidden;
|
||||
word-break: break-all;
|
||||
}
|
||||
}
|
||||
|
||||
.action-btns {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
|
@ -253,7 +332,7 @@ const masonryItems = ref([
|
|||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 44px;
|
||||
height: 18px;
|
||||
padding: 4px 0;
|
||||
gap: 2px;
|
||||
color: #FFFFFF;
|
||||
background: linear-gradient(215deg, #A679F4 7.14%, #778CF5 100%);
|
||||
|
|
@ -264,7 +343,7 @@ const masonryItems = ref([
|
|||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.1);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.icon-img {
|
||||
|
|
|
|||
|
|
@ -150,16 +150,39 @@ const workId = ref(null)
|
|||
const originalTitle = ref('')
|
||||
|
||||
onMounted(async () => {
|
||||
const id = route.query.workId
|
||||
const id = route.query.id || route.query.workId
|
||||
if (!id) return
|
||||
|
||||
try {
|
||||
const work = await api.works.get(id)
|
||||
let drafts = []
|
||||
try {
|
||||
drafts = JSON.parse(localStorage.getItem('local_drafts') || '[]')
|
||||
if (!Array.isArray(drafts)) drafts = []
|
||||
} catch (e) { }
|
||||
|
||||
const work = drafts.find(w => String(w.id) === String(id))
|
||||
if (work) {
|
||||
workId.value = id
|
||||
originalTitle.value = work.title || ''
|
||||
if (work.data_json) {
|
||||
Object.assign(balanceSettings, JSON.parse(work.data_json))
|
||||
const configData = typeof work.data_json === 'string' ? JSON.parse(work.data_json) : work.data_json
|
||||
Object.assign(balanceSettings, configData)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const remoteWork = await api.works.get(id)
|
||||
if (remoteWork) {
|
||||
workId.value = id
|
||||
originalTitle.value = remoteWork.title || ''
|
||||
if (remoteWork.data_json) {
|
||||
const configData = typeof remoteWork.data_json === 'string' ? JSON.parse(remoteWork.data_json) : remoteWork.data_json
|
||||
Object.assign(balanceSettings, configData)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('获取草稿失败:', e)
|
||||
}
|
||||
} catch { }
|
||||
})
|
||||
|
||||
async function handleSave() {
|
||||
|
|
@ -170,36 +193,120 @@ async function handleSave() {
|
|||
confirmButtonText: '保存',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: defaultTitle,
|
||||
inputPlaceholder: '输入名称,同名作品将覆盖',
|
||||
inputPlaceholder: '输入名称(同名将提示覆盖,异名将新建)',
|
||||
})
|
||||
if (!title) return
|
||||
const payload = { type: 'balance', data_json: JSON.stringify(balanceSettings), title }
|
||||
|
||||
// 从作品库进入的,直接覆盖原作品
|
||||
if (workId.value) {
|
||||
await api.works.update(workId.value, payload)
|
||||
originalTitle.value = title
|
||||
ElMessage.success('草稿已保存')
|
||||
return
|
||||
let drafts = []
|
||||
try {
|
||||
drafts = JSON.parse(localStorage.getItem('local_drafts') || '[]')
|
||||
if (!Array.isArray(drafts)) drafts = []
|
||||
} catch (e) {
|
||||
drafts = []
|
||||
}
|
||||
|
||||
// 新建:查找同类型同名作品,存在则覆盖
|
||||
const listRes = await api.works.list({ keyword: title, type: 'balance', per_page: 1 })
|
||||
const existing = (listRes.items || []).find(w => w.title === title)
|
||||
let draftId = ''
|
||||
let isOverwriteOther = false
|
||||
|
||||
if (existing) {
|
||||
await ElMessageBox.confirm(`已存在同名作品「${title}」,是否覆盖?`, '提示', { confirmButtonText: '覆盖', cancelButtonText: '取消' })
|
||||
await api.works.update(existing.id, payload)
|
||||
ElMessage.success('草稿已覆盖保存')
|
||||
// 只有当修改了名称(或新建作品)时,才去检查重名/新建
|
||||
if (title !== originalTitle.value) {
|
||||
const existIndex = drafts.findIndex(w => w.title === title && w.type === 'wx-balance')
|
||||
if (existIndex !== -1) {
|
||||
await ElMessageBox.confirm(`已存在同名作品「${title}」,是否覆盖该作品?`, '提示', {
|
||||
confirmButtonText: '覆盖',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
draftId = drafts[existIndex].id
|
||||
isOverwriteOther = true
|
||||
} else {
|
||||
await api.works.create(payload)
|
||||
ElMessage.success('草稿已保存')
|
||||
// 名称改变且无冲突,则新增为新草稿
|
||||
draftId = 'draft_' + Date.now()
|
||||
}
|
||||
} else {
|
||||
// 名称未改变,直接更新当前正在编辑的草稿
|
||||
draftId = workId.value || ('draft_' + Date.now())
|
||||
}
|
||||
|
||||
let coverImg = ''
|
||||
const el = document.getElementById('capture-frame') || previewRef.value?.$el
|
||||
if (el) {
|
||||
try {
|
||||
const canvas = await html2canvas(el, {
|
||||
scale: 300 / el.offsetWidth, // 固定生成 150px 宽度的图片
|
||||
useCORS: true,
|
||||
onclone: (clonedDoc) => {
|
||||
const box = clonedDoc.querySelector('.phone-box')
|
||||
if (box) box.style.transform = 'none'
|
||||
const frame = clonedDoc.getElementById('capture-frame')
|
||||
if (frame) {
|
||||
frame.style.borderRadius = '0'
|
||||
if (!userStore.isVip) {
|
||||
const watermark = clonedDoc.querySelector('.watermark-overlay')
|
||||
if (watermark) {
|
||||
watermark.style.borderRadius = '0'
|
||||
frame.appendChild(watermark)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
coverImg = canvas.toDataURL('image/jpeg', 0.6)
|
||||
} catch (err) {
|
||||
console.error('截图生成失败', err)
|
||||
}
|
||||
}
|
||||
|
||||
const payload = {
|
||||
id: draftId,
|
||||
title: title,
|
||||
type: 'wx-balance',
|
||||
page_router: '/balance',
|
||||
data_json: JSON.stringify(balanceSettings),
|
||||
cover_img: coverImg,
|
||||
update_time: Date.now()
|
||||
}
|
||||
|
||||
let isSelfUpdate = false
|
||||
if (isOverwriteOther) {
|
||||
const existIndex = drafts.findIndex(w => String(w.id) === String(draftId))
|
||||
if (existIndex !== -1) drafts[existIndex] = payload
|
||||
} else {
|
||||
const selfIndex = drafts.findIndex(w => String(w.id) === String(draftId))
|
||||
if (selfIndex !== -1) {
|
||||
drafts[selfIndex] = payload
|
||||
isSelfUpdate = true
|
||||
} else {
|
||||
drafts.unshift(payload)
|
||||
}
|
||||
}
|
||||
|
||||
localStorage.setItem('local_drafts', JSON.stringify(drafts))
|
||||
workId.value = payload.id
|
||||
originalTitle.value = payload.title
|
||||
|
||||
let msg = '已新增草稿'
|
||||
if (isOverwriteOther) msg = '草稿已覆盖保存'
|
||||
else if (isSelfUpdate) msg = '草稿已更新'
|
||||
ElMessage.success(msg)
|
||||
} catch (e) {
|
||||
if (e !== 'cancel') {
|
||||
console.error(e)
|
||||
ElMessage.error('保存失败')
|
||||
}
|
||||
}
|
||||
} catch (e) { }
|
||||
}
|
||||
|
||||
async function handleExport() {
|
||||
if (!localStorage.getItem('token')) { ElMessage.warning('请先登录后再下载'); return }
|
||||
|
||||
// 关键防护:触发导出前强制等待最新的后台用户信息校验完成(防篡改本地缓存与时间差)
|
||||
try {
|
||||
if (userStore.fetchProfile) {
|
||||
await userStore.fetchProfile()
|
||||
}
|
||||
} catch (e) { }
|
||||
|
||||
if (!userStore.isVip) {
|
||||
showPayment?.()
|
||||
return
|
||||
|
|
|
|||
|
|
@ -419,7 +419,7 @@
|
|||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, inject, onMounted, watch, nextTick } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Top, Bottom, Delete, EditPen, ArrowDown } from '@element-plus/icons-vue'
|
||||
import ChatPreview from '@/components/chat/ChatPreview.vue'
|
||||
|
|
@ -432,6 +432,10 @@ import { checkUserVip } from '@/utils/appUtils'
|
|||
const userStore = useUserStore()
|
||||
const showPayment = inject('showPayment')
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const workId = ref('')
|
||||
const originalTitle = ref('')
|
||||
|
||||
const layoutRef = ref()
|
||||
const previewRef = ref()
|
||||
|
|
@ -508,8 +512,6 @@ const selectedMsg = computed(() => {
|
|||
return null
|
||||
})
|
||||
|
||||
const workId = ref(null)
|
||||
const originalTitle = ref('')
|
||||
const typeLabels = { text: '文字', time: '时间', redpacket: '红包', transfer: '转账', system: '系统提示语', call: '通话', voice: '语音' }
|
||||
|
||||
const getTypeLabel = (msg) => {
|
||||
|
|
@ -531,18 +533,39 @@ function handleSystemQuickAdd(command) {
|
|||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const id = route.query.workId
|
||||
const id = route.query.id || route.query.workId
|
||||
if (!id) return
|
||||
|
||||
try {
|
||||
let drafts = []
|
||||
try {
|
||||
drafts = JSON.parse(localStorage.getItem('local_drafts') || '[]')
|
||||
} catch (e) { }
|
||||
|
||||
const localWork = drafts.find(w => String(w.id) === String(id))
|
||||
if (localWork) {
|
||||
workId.value = id
|
||||
originalTitle.value = localWork.title || ''
|
||||
if (localWork.data_json) {
|
||||
const data = typeof localWork.data_json === 'string' ? JSON.parse(localWork.data_json) : localWork.data_json
|
||||
if (data.settings) Object.assign(settings, data.settings)
|
||||
// 这里的 map(m => ({ ...m, id: msgIdCounter++ })) 确保回显后新发送的消息ID不冲突,并且正常驱动视图
|
||||
if (data.messages) messages.value = data.messages.map(m => ({ ...m, id: msgIdCounter++ }))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
const work = await api.works.get(id)
|
||||
workId.value = id
|
||||
originalTitle.value = work.title || ''
|
||||
if (work.data_json) {
|
||||
const data = JSON.parse(work.data_json)
|
||||
const data = typeof work.data_json === 'string' ? JSON.parse(work.data_json) : work.data_json
|
||||
if (data.messages) messages.value = data.messages.map(m => ({ ...m, id: msgIdCounter++ }))
|
||||
if (data.settings) Object.assign(settings, data.settings)
|
||||
}
|
||||
} catch { }
|
||||
} catch (e) {
|
||||
console.error('加载草稿异常:', e)
|
||||
}
|
||||
})
|
||||
|
||||
function getMessageBrief(msg) {
|
||||
|
|
@ -722,28 +745,110 @@ async function handleSave() {
|
|||
if (!localStorage.getItem('token')) { ElMessage.warning('请先登录'); return }
|
||||
try {
|
||||
const defaultTitle = originalTitle.value || settings.title || '未命名作品'
|
||||
const { value: title } = await ElMessageBox.prompt('请输入作品名称', '保存草稿', { confirmButtonText: '保存', cancelButtonText: '取消', inputValue: defaultTitle })
|
||||
const { value: title } = await ElMessageBox.prompt('请输入作品名称', '保存草稿', {
|
||||
confirmButtonText: '保存',
|
||||
cancelButtonText: '取消',
|
||||
inputValue: defaultTitle,
|
||||
inputPlaceholder: '输入名称(同名将提示覆盖,异名将新建)',
|
||||
})
|
||||
if (!title) return
|
||||
const payload = { type: 'chat', data_json: JSON.stringify({ messages: messages.value, settings }), title }
|
||||
|
||||
if (workId.value) {
|
||||
await api.works.update(workId.value, payload)
|
||||
originalTitle.value = title
|
||||
ElMessage.success('草稿已保存')
|
||||
return
|
||||
let drafts = []
|
||||
try {
|
||||
drafts = JSON.parse(localStorage.getItem('local_drafts') || '[]')
|
||||
if (!Array.isArray(drafts)) drafts = []
|
||||
} catch (e) {
|
||||
drafts = []
|
||||
}
|
||||
|
||||
const listRes = await api.works.list({ keyword: title, type: 'chat', per_page: 1 })
|
||||
const existing = (listRes.items || []).find(w => w.title === title)
|
||||
if (existing) {
|
||||
await ElMessageBox.confirm(`已存在同名作品「${title}」,是否覆盖?`, '提示', { confirmButtonText: '覆盖', cancelButtonText: '取消' })
|
||||
await api.works.update(existing.id, payload)
|
||||
ElMessage.success('草稿已覆盖保存')
|
||||
let draftId = ''
|
||||
let isOverwriteOther = false
|
||||
|
||||
// 只有当修改了名称(或新建作品)时,才去检查重名/新建
|
||||
if (title !== originalTitle.value) {
|
||||
const existIndex = drafts.findIndex(w => w.title === title && w.type === 'wx-chat')
|
||||
if (existIndex !== -1) {
|
||||
await ElMessageBox.confirm(`已存在同名作品「${title}」,是否覆盖该作品?`, '提示', {
|
||||
confirmButtonText: '覆盖',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
})
|
||||
draftId = drafts[existIndex].id
|
||||
isOverwriteOther = true
|
||||
} else {
|
||||
await api.works.create(payload)
|
||||
ElMessage.success('草稿已保存')
|
||||
// 名称改变且无冲突,则新增为新草稿
|
||||
draftId = 'draft_' + Date.now()
|
||||
}
|
||||
} else {
|
||||
// 名称未改变,直接更新当前正在编辑的草稿
|
||||
draftId = workId.value || ('draft_' + Date.now())
|
||||
}
|
||||
|
||||
let coverImg = ''
|
||||
const el = document.getElementById('capture-frame') || previewRef.value?.$el
|
||||
if (el) {
|
||||
try {
|
||||
const canvas = await html2canvas(el, {
|
||||
scale: 300 / el.offsetWidth, // 生成缩略图
|
||||
useCORS: true,
|
||||
onclone: (clonedDoc) => {
|
||||
const box = clonedDoc.querySelector('.phone-box')
|
||||
if (box) box.style.transform = 'none'
|
||||
const frame = clonedDoc.getElementById('capture-frame')
|
||||
if (frame) {
|
||||
frame.style.borderRadius = '0'
|
||||
if (!userStore.isVip) {
|
||||
const watermark = clonedDoc.querySelector('.watermark-overlay')
|
||||
if (watermark) {
|
||||
watermark.style.borderRadius = '0'
|
||||
frame.appendChild(watermark)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
coverImg = canvas.toDataURL('image/jpeg', 0.8)
|
||||
} catch (err) {
|
||||
console.error('生成封面失败', err)
|
||||
}
|
||||
}
|
||||
|
||||
const payload = {
|
||||
id: draftId,
|
||||
type: 'wx-chat',
|
||||
page_router: '/chat',
|
||||
title,
|
||||
cover_img: coverImg,
|
||||
data_json: JSON.stringify({ messages: messages.value, settings }),
|
||||
updated_at: new Date().toISOString(),
|
||||
created_at: new Date().toISOString()
|
||||
}
|
||||
|
||||
const existingIndex = drafts.findIndex(d => d.id === draftId)
|
||||
if (existingIndex !== -1) {
|
||||
// 保持原有创建时间
|
||||
payload.created_at = drafts[existingIndex].created_at
|
||||
drafts[existingIndex] = payload
|
||||
} else {
|
||||
drafts.unshift(payload)
|
||||
}
|
||||
|
||||
localStorage.setItem('local_drafts', JSON.stringify(drafts))
|
||||
|
||||
// 如果是覆盖了别的作品,或者新建了作品,更新当前状态
|
||||
originalTitle.value = title
|
||||
if (!workId.value || isOverwriteOther) {
|
||||
workId.value = draftId
|
||||
router.replace({ query: { id: draftId } })
|
||||
}
|
||||
|
||||
ElMessage.success('草稿保存成功')
|
||||
} catch (e) {
|
||||
if (e !== 'cancel') {
|
||||
console.error(e)
|
||||
ElMessage.error('保存失败')
|
||||
}
|
||||
}
|
||||
} catch (e) { }
|
||||
}
|
||||
|
||||
async function handleExport() {
|
||||
|
|
|
|||
Loading…
Reference in New Issue