This commit is contained in:
13121765685 2026-01-20 16:57:14 +08:00
parent 4f93347199
commit 2a992810fe
37 changed files with 1291 additions and 144 deletions

View File

@ -269,10 +269,10 @@ const handleUserAction = async (command) => {
localStorage.removeItem('user')
localStorage.removeItem('permissionButtonList')
localStorage.removeItem('permissionRouter')
router.push('/login')
setTimeout(() => {
window.location.reload();
}, 50);
router.replace('/login')
// setTimeout(() => {
// window.location.reload();
// }, 50);
})
break
}

View File

@ -23,7 +23,12 @@ export default {
all: 'All',
operation:'Operation',
},
greetingCard: {
cancelButton: 'Cancel',
exportButton: 'Export',
greetingCard: 'Greeting Card',
exportSuccess: 'Export Successful',
},
// Shop Management
shop: {
pleaseEnterAddress: 'Please enter address',
@ -602,6 +607,7 @@ export default {
dateRange: 'Date Range',
orderNumber: 'Order Number',
customer: 'Customer',
series: 'Series',
total: 'Total Amount',
payment: 'Payment Method',
date: 'Order Date',
@ -636,6 +642,7 @@ export default {
logisticsStatus: 'Logistics Status',
currentLocation: 'Current Location',
noLogisticsData: 'No logistics data available',
previewCard: 'Preview Card',
stats: {
total: 'Total Orders',
pending: 'Pending',
@ -1040,6 +1047,8 @@ export default {
productName: 'Product Name',
enterProductName: 'Enter Product Name',
productImage: 'Product Image',
mark: 'Mark',
enterMark: 'Enter Product Mark',
imageUploadTip: 'Support JPG, PNG, GIF format, max size 5MB',
imageTypeError: 'Please upload image file only',
imageSizeError: 'Image size cannot exceed 5MB',

View File

@ -1,5 +1,11 @@
// 中文语言包
export default {
greetingCard: {
cancelButton: '取消',
exportButton: '导出',
greetingCard: '贺卡',
exportSuccess: '导出成功',
},
orderManagement: {
title: '订单',
description: '查看和管理您的购买和订阅信息',
@ -454,6 +460,7 @@ orderManagement: {
dateRange: '日期范围',
orderNumber: '订单号',
customer: '客户',
series: '系列',
total: '总金额',
payment: '支付方式',
date: '下单日期',
@ -488,6 +495,7 @@ orderManagement: {
logisticsStatus: '物流状态',
currentLocation: '当前位置',
noLogisticsData: '暂无物流信息',
previewCard: '预览贺卡',
stats: {
total: '总订单',
pending: '待处理',
@ -893,6 +901,8 @@ orderManagement: {
productName: '产品名称',
enterProductName: '请输入产品名称',
productImage: '产品图片',
mark: '标识',
enterMark: '请输入产品标识',
imageUploadTip: '支持JPG、PNG、GIF格式最大5MB',
imageTypeError: '请上传图片文件',
imageSizeError: '图片大小不能超过5MB',

View File

@ -328,7 +328,6 @@ const routes = [
requiresAuth: true
},
children: [
{
path: 'disassembly-orders/:id',
name: 'AdminDisassemblyDetail',

View File

@ -12,6 +12,7 @@ export const useAuthStore = defineStore('auth', {
router: null,
routesUpdated: 0,//路由更新次数
routerList: window.location.hostname.indexOf('local') === -1 ? [] : permissionRoutes,//侧边栏路由
// routerList:[],//侧边栏路由
}),
getters: {

View File

@ -155,7 +155,7 @@ const handleLogin = async () => {
let data = response.data;
authStore.login(data,()=>{
window.location.reload();
// router.push('/admin')
// router.replace('/')
})
} catch (error) {
ElMessage.error(t('admin.login.loginFailed'))

View File

@ -124,6 +124,11 @@
{{ (row?.order_info?.shipping?.firstName || '-')+(row?.order_info?.shipping?.lastName || '-') }}
</template>
</el-table-column>
<el-table-column prop="series" :label="t('admin.orders.series')" width="120">
<template #default="{ row }">
{{ row?.order_info?.series || '通用' }}
</template>
</el-table-column>
<el-table-column prop="actual_amount" :label="t('admin.orders.total')" width="120">
<template #default="{ row }">
${{ row.actual_amount.toFixed(2) }}
@ -187,6 +192,9 @@
<el-descriptions-item :label="t('admin.orders.customer')">
{{ selectedOrder?.order_info?.shipping?.firstName || '-'+selectedOrder?.order_info?.shipping?.lastName || '-' }}
</el-descriptions-item>
<el-descriptions-item :label="t('admin.orders.series')">
{{ selectedOrder?.order_info?.series || '通用' }}
</el-descriptions-item>
<el-descriptions-item :label="t('admin.orders.total')">
${{ selectedOrder?.actual_amount?.toFixed(2) || '-' }}
</el-descriptions-item>
@ -420,6 +428,16 @@
>
{{ t('admin.disassemblyOrders.list.disassembly') }}
</el-button>
<!-- 预览贺卡 - 仅A1系列显示 -->
<el-button
v-if="selectedOrderForAction?.order_info?.series === 'A1'"
type="warning"
size="large"
class="action-btn"
@click="handlePreviewCard(selectedOrderForAction)"
>
{{ t('admin.orders.previewCard') }}
</el-button>
<!-- 待支付状态修改支付状态为已支付 -->
<el-button
v-if="getStatusTagType(selectedOrderForAction).type=='dzf'"
@ -474,6 +492,12 @@
</div>
</div>
</el-dialog>
<DtCardPreview
v-model="previewVisible"
:image-url="currentPreviewCard.imageUrl"
:message="currentPreviewCard.message"
:card-message="currentPreviewCard.cardMessage"
:id="currentPreviewCard.id+''" />
</div>
</template>
@ -492,7 +516,13 @@ import {
} from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import {AdminOrders} from './AdminOrders'
const previewVisible = ref(false)
const currentPreviewCard = ref({
imageUrl: '',
message: '',
cardMessage: '',
id: ''
})
//
const logisticsService = new LogistIcsService()
const adminOrders = new AdminOrders()
@ -542,6 +572,15 @@ const handleDisassemble = (order) => {
params: { id: order.id }
})
}
// - A1
const handlePreviewCard = (order) => {
currentPreviewCard.value.imageUrl = order?.order_info?.greetingCard?.imageUrl || ''
currentPreviewCard.value.message = order?.order_info?.greetingCard?.message || ''
currentPreviewCard.value.cardMessage = order?.order_info?.greetingCard?.cardMessage || ''
currentPreviewCard.value.id = order?.order_info?.greetingCard?.id || ''
previewVisible.value = true
}
//
const shippingForm = reactive({
trackingNo: '',

View File

@ -117,7 +117,7 @@
<el-input v-model="userForm.fullName" :placeholder="t('admin.userList.fullNamePlaceholder')" />
</el-form-item>
<el-form-item :label="t('admin.userList.email')" prop="email">
<el-input v-model="userForm.email" :placeholder="t('admin.userList.emailPlaceholder')" type="email" />
<el-input v-model="userForm.email" :placeholder="t('admin.userList.emailPlaceholder')" type="text" />
</el-form-item>
<el-form-item v-if="!isEditMode" :label="t('admin.userList.password')" prop="password">
<el-input v-model="userForm.password" :placeholder="t('admin.userList.passwordPlaceholder')" type="password" show-password />
@ -261,7 +261,7 @@ const userFormRules = reactive({
],
email: [
{ required: true, message: t('admin.userList.emailRequired'), trigger: 'blur' },
{ type: 'email', message: t('admin.userList.emailFormat'), trigger: 'blur' }
{ message: t('admin.userList.emailFormat'), trigger: 'blur' }
],
password: [
{ required: true, message: t('admin.userList.passwordRequired'), trigger: 'blur' },

View File

@ -314,7 +314,12 @@ export class AdminRoleManagement {
userId: data.id,
newPassword: data.newPassword
}
return requestUtils.common(adminApi.default.resetAdminUserPassword, params);
const requestUrl = {
method: adminApi.default.resetAdminUserPassword.method,
url: adminApi.default.resetAdminUserPassword.url.replace('{userId}', data.id)+'?newPassword='+data.newPassword,
isLoading: adminApi.default.resetAdminUserPassword.isLoading
}
return requestUtils.common(requestUrl, params);
}
//根据用户ID查询管理员用户详情
async getAdminUserDetail(data) {

View File

@ -143,6 +143,13 @@
</div>
</el-form-item>
<el-form-item :label="t('admin.productManagement.mark')">
<el-input
v-model="formData.mark"
:placeholder="t('admin.productManagement.enterMark')"
/>
</el-form-item>
<el-form-item :label="t('admin.productManagement.description')" required>
<el-input
v-model="formData.description"
@ -314,7 +321,8 @@ const formData = ref({
amount: 0,
currency: 'USD',
current_price_id: '',
image: ''
image: '',
mark: ''
})
//
@ -364,7 +372,8 @@ const showAddDialog = () => {
amount: 0,
currency: 'USD',
current_price_id: '',
image: ''
image: '',
mark: ''
}
dialogVisible.value = true
}
@ -381,7 +390,8 @@ const showEditDialog = async (product) => {
amount: product.amount,
currency: product.currency,
current_price_id: product.current_price_id || '',
image: response.product_info?.image || ''
image: response.product_info?.image || '',
mark: response.product_info?.mark || ''
}
dialogVisible.value = true
}

View File

@ -9,6 +9,7 @@ export class ProductManagement {
currency: data.currency,//货币类型默认USD
product_info:{
image:data.image,//产品图片
mark:data.mark,//产品标识
}
}
return await requestUtils.common(adminApi.default.createProduct, params);
@ -39,6 +40,7 @@ export class ProductManagement {
"current_price_id": data.current_price_id,//当前价格ID
product_info:{
image:data.image,//产品图片
mark:data.mark,//产品标识
}
}
return await requestUtils.common(adminApi.default.updateProduct, params);

View File

@ -29,7 +29,6 @@
"country-state-city": "^3.2.1",
"dayjs": "^1.11.13",
"element-plus": "^2.11.7",
"html2canvas": "^1.4.1",
"install": "^0.13.0",
"jose": "^6.1.1",
"motion-v": "^1.7.4",
@ -37,7 +36,6 @@
"nprogress": "^0.2.0",
"pinia": "^3.0.4",
"pinia-plugin-persistedstate": "^4.7.1",
"qrcode": "^1.5.4",
"three": "^0.180.0",
"twind": "^0.16.19",
"vue": "^3.5.24",

View File

@ -101,6 +101,13 @@ onMounted(() => {
<footerCom v-if="isCn()" />
</template>
<style>
.qrjzt{
font-weight: 700;
font-family: 'Georgia', 'Times New Roman', 'Playfair Display', 'Brush Script MT', cursive;
text-shadow: 0 2px 4px rgba(214, 51, 132, 0.2);
letter-spacing: 1px;
word-wrap: break-word;
}
*{
box-sizing: border-box;
outline:0;

View File

@ -48,6 +48,7 @@
:placeholder="$t('checkout.ipNamePlaceholder')"
class="ip-name-input"
/>
<div class="ip-name-hint" v-if="series=='A1'">{{ $t('checkout.ipNameHint') }}</div>
</div>
<div class="config-item shop-select">
<div class="label">{{ $t('checkout.shop') }}</div>
@ -71,6 +72,62 @@
</div>
</div>
<!-- 贺卡选择区域 -->
<div class="content-block greeting-card-block" >
<h2 class="block-title greeting-card-title">
<span class="title-icon">💝</span>
{{ $t('checkout.greetingCard') }}
</h2>
<div class="greeting-card-section">
<div v-if="loadingGreetingCards" class="loading-greeting-cards">
<div class="loading-spinner"></div>
<span>{{ $t('checkout.loading') }}</span>
</div>
<div v-else-if="greetingCards.length === 0" class="no-greeting-cards">
<span>{{ $t('checkout.noGreetingCards') }}</span>
</div>
<el-select
v-else
v-model="selectedGreetingCard"
:placeholder="$t('checkout.greetingCardPlaceholder')"
class="greeting-card-select"
value-key="id"
clearable
teleported
>
<template #prefix>
<div class="heka qrjzt" v-if="selectedGreetingCard" >
<img
:src="selectedGreetingCard.imageUrl"
class="greeting-card-prefix"
/>
{{ selectedGreetingCard.cardMessage }}
</div>
<span v-else class="greeting-card-prefix-placeholder">💝</span>
</template>
<el-option
v-for="card in greetingCards"
:key="card.id"
:value="card"
class="greeting-card-option"
>
<div class="greeting-card-option-content">
<img :src="card.imageUrl" :alt="card.name" class="greeting-card-thumb" />
<div class="greeting-card-info">
<!-- <div class="greeting-card-name">{{ card.name }}</div> -->
<div class="greeting-card-message qrjzt">{{ card.cardMessage }}</div>
</div>
</div>
</el-option>
</el-select>
<div v-if="selectedGreetingCard" class="greeting-card-preview">
<div class="preview-card">
<div class="preview-message qrjzt">{{ selectedGreetingCard.message }}</div>
</div>
</div>
</div>
</div>
<!-- 优惠券选择区域 -->
<div class="content-block">
<h2 class="block-title">
@ -208,7 +265,6 @@
<button class="buy-btn" v-if="!isWeChatBrowser()" :disabled="isPayButtonDisabled" @click="goShopify">{{ $t('checkout.buy') }}</button>
<button class="buy-btn" v-else @click="goShopify">{{ $t('checkout.buy') }}</button>
</div>
<!-- Stripe Payment Overlay -->
<div v-if="showStripe" class="stripe-overlay" @click="onStripeCancel">
<div class="stripe-card" @click.stop>
@ -238,8 +294,10 @@ import { useI18n } from 'vue-i18n'
import { PayServer,isWeChatBrowser } from '@deotaland/utils'
import { requestUtils,clientApi,environmentUtils,WechatBus } from '@deotaland/utils'
import { PurchaseModal as PurchaseModalClass } from './index.js'
import { GreetingCard } from '@/views/GreetingCard/index.js'
const payserver = new PayServer();
const purchaseModal = new PurchaseModalClass();
const greetingCard = new GreetingCard();
const props = defineProps({
modelData: { type: Object, default: () => ({}) },
show: { type: Boolean, default: false },
@ -247,7 +305,6 @@ const props = defineProps({
})
const emit = defineEmits(['close'])
const onClose = () => emit('close')
const addons = ref({ matte:false, gloss:false, base:false })
const qty = ref(1)
const ipName = ref('doll')
const contact = ref({ emailOrPhone:'', subscribe:false })
@ -264,6 +321,11 @@ const voucherList = ref([])
const selectedVoucher = ref(null)
const loadingVouchers = ref(false)
const discount_amount= ref(0);
//
const greetingCards = ref([])
const selectedGreetingCard = ref(null);
const loadingGreetingCards = ref(false);
//
const amountCents = computed(() => {
let base = price.value //
@ -308,6 +370,28 @@ const getVoucherList = async () => {
}
}
const getGreetingCardList = async () => {
loadingGreetingCards.value = true
try {
const res = await greetingCard.getMyGreetingCardList()
if (res.code === 200) {
const rows = res.rows || []
greetingCards.value = rows.map(card => ({
id: card.id,
name: card.title,
imageUrl: card.extraInfo?.image?.replace(/`/g, '').trim() || '',
message: card?.content,
cardMessage: card.extraInfo?.cardMessage || ''
}))
}
} catch (error) {
console.error('获取贺卡列表失败:', error)
greetingCards.value = []
} finally {
loadingGreetingCards.value = false
}
}
const onVoucherSelect = (voucher) => {
if (selectedVoucher.value?.id === voucher.id) {
selectedVoucher.value = null
@ -346,7 +430,7 @@ const getPrice = async () => {
const res = await requestUtils.common(clientApi.default.getProductList)
if(res.code === 0){
const data = res.data.list || []
const item = data.find(item => item.name === props.series)
const item = data.find(item => item.product_info?.mark === props.series)
if(item){
price.value = item.price?.amount || 0
unt.value = item.price?.currency || ''
@ -382,6 +466,7 @@ const goShopify = () => {//用户点击购买
}
//
const order_info = {
series: props.series,
quantity: qty.value,
ipName: ipName.value,
contact: {
@ -400,7 +485,8 @@ const goShopify = () => {//用户点击购买
address2: shipping.value.address2,
phone: shipping.value.phone
},
modelData:project_details
modelData:project_details,
greetingCard:selectedGreetingCard.value,
}
let params ={
quantity:qty.value,
@ -490,6 +576,7 @@ onMounted(() => {
getPrice();
getVoucherList();
fetchShopList();
getGreetingCardList();
} catch (e) {}
})
watch(() => shipping.value.country, () => { updateStates() })
@ -802,6 +889,13 @@ const updateStates = () => {
color: #ffffff;
}
.ip-name-hint {
font-size: 12px;
color: rgba(255,255,255,0.6);
margin-top: 4px;
line-height: 1.4;
}
.shop-select-input :deep(.el-select__popper) {
background: rgba(17,24,39,0.95);
border-color: rgba(255,255,255,0.2);
@ -828,7 +922,9 @@ const updateStates = () => {
}
.loading-vouchers,
.no-vouchers {
.no-vouchers,
.loading-greeting-cards,
.no-greeting-cards {
display: flex;
align-items: center;
justify-content: center;
@ -938,6 +1034,219 @@ const updateStates = () => {
font-weight: bold;
}
/* 贺卡选择区域 - 情人节主题 */
.greeting-card-block {
background: linear-gradient(135deg, #fff5f7 0%, #ffe4e8 50%, #ffd1dc 100%);
border: 2px solid rgba(255, 182, 193, 0.5);
}
.greeting-card-title {
color: #d63384;
display: flex;
align-items: center;
gap: 8px;
}
.title-icon {
font-size: 24px;
animation: pulse 2s infinite;
}
@keyframes pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.1); }
}
.greeting-card-section {
display: flex;
flex-direction: column;
gap: 16px;
}
.greeting-card-select {
width: 100%;
}
.greeting-card-select :deep(.el-select__wrapper) {
background: linear-gradient(135deg, #ffffff 0%, #fff5f7 100%);
border: 2px solid #ffb6c1;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(255, 107, 157, 0.15);
height: 56px;
font-size: 16px;
color: #d63384;
font-weight: 500;
}
.greeting-card-select :deep(.el-select__wrapper:hover) {
border-color: #ff6b9d;
box-shadow: 0 6px 16px rgba(255, 107, 157, 0.25);
}
.greeting-card-select :deep(.el-select__placeholder) {
color: #ff8a80;
}
.greeting-card-select :deep(.el-icon) {
color: #ff6b9d;
}
.greeting-card-select :deep(.el-select__prefix) {
display: flex;
align-items: center;
padding-left: 12px;
}
.greeting-card-prefix {
width: 36px;
height: 36px;
border-radius: 6px;
object-fit: cover;
border: 1px solid #ffb6c1;
}
.greeting-card-prefix-placeholder {
font-size: 20px;
}
.greeting-card-select :deep(.el-select__input) {
margin-left: 8px;
color: #d63384;
font-weight: 500;
}
.greeting-card-select :deep(.el-select__wrapper) {
padding-left: 8px;
}
.greeting-card-select :deep(.el-select-dropdown__item) {
padding: 0 12px;
height: auto;
line-height: normal;
}
.greeting-card-select :deep(.el-select-dropdown__item::after) {
display: none;
}
.greeting-card-select :deep(.el-select-dropdown) {
max-height: 300px !important;
z-index: 9999 !important;
position: relative !important;
background: #ffffff !important;
border: 1px solid #ffb6c1 !important;
border-radius: 12px !important;
box-shadow: 0 8px 24px rgba(255, 107, 157, 0.2) !important;
}
.greeting-card-select :deep(.el-select-dropdown__list) {
padding: 8px !important;
}
.greeting-card-select :deep(.el-select-dropdown__wrap) {
max-height: 300px !important;
}
.greeting-card-select :deep(.el-popper) {
z-index: 9999 !important;
}
.greeting-card-option {
padding: 8px 12px !important;
height: auto !important;
line-height: normal !important;
}
.greeting-card-option:hover {
background-color: #fff5f7 !important;
}
.greeting-card-option.is-selected {
background-color: #ffe4e8 !important;
}
.greeting-card-option-content {
display: flex;
align-items: center;
gap: 12px;
padding: 4px 0;
visibility: visible !important;
opacity: 1 !important;
min-height: 50px;
}
.greeting-card-thumb {
width: 50px;
height: 50px;
min-width: 50px;
border-radius: 8px;
object-fit: cover;
border: 2px solid #ffb6c1;
display: block;
}
.greeting-card-info {
flex: 1;
min-width: 0;
}
.greeting-card-name {
font-weight: 600;
color: #d63384;
font-size: 14px;
margin-bottom: 4px;
}
.greeting-card-message {
font-size: 12px;
color: #666;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.greeting-card-preview {
margin-top: 8px;
}
.preview-card {
background: linear-gradient(135deg, #ffffff 0%, #fff5f7 100%);
border-radius: 16px;
overflow: hidden;
box-shadow: 0 8px 24px rgba(255, 107, 157, 0.2);
border: 2px solid #ffb6c1;
position: relative;
}
.preview-image {
width: 100%;
height: 180px;
object-fit: cover;
}
.preview-message {
padding: 16px;
font-size: 14px;
color: #4a5568;
line-height: 1.6;
text-align: center;
background: linear-gradient(135deg, #fff5f7 0%, #ffe4e8 100%);
}
.preview-badge {
position: absolute;
top: 12px;
right: 12px;
background: linear-gradient(135deg, #ff6b9d 0%, #ff8a80 100%);
color: white;
padding: 6px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
box-shadow: 0 4px 12px rgba(255, 107, 157, 0.4);
}
/* Info Row Layout */
.info-row {
display: grid;
@ -1470,7 +1779,12 @@ const updateStates = () => {
text-align: center;
line-height: 1.4;
}
.heka{
display: flex;
align-items: center;
color: #ff8a80;
gap: 10px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }

View File

@ -24,9 +24,8 @@
</div>
</div>
</div>
<!-- 是否需要挂钩 - 仅当series为E1时显示 -->
<div class="form-section" v-if="series === 'E1'">
<!-- 是否需要挂钩 - 仅当series为E1或A1时显示 -->
<div class="form-section" v-if="series === 'E1'||series === 'A1'">
<div class="expression-info">
<span class="expression-description">
{{ $t('iPandCardLeft.needHook') }}

View File

@ -802,6 +802,7 @@ export default {
quantity: '数量',
ipName: 'IP名称',
ipNamePlaceholder: '请输入IP名称',
ipNameHint: '地台的铭牌中会使用当前名称',
shop: '店铺',
chooseShop: '选择店铺',
buy: '购买',
@ -815,6 +816,8 @@ export default {
noVouchers: '暂无可用优惠券',
minOrder: '最低订单金额',
expireAt: '过期时间',
greetingCard: '选择贺卡',
greetingCardPlaceholder: '选择一张贺卡',
error: {
firstNameRequired: '名不能为空',
lastNameRequired: '姓不能为空',
@ -1601,6 +1604,9 @@ export default {
messageLabel: '祝福寄语',
messagePlaceholder: '写下您想对TA说的情话...',
messageRequired: '请输入祝福寄语',
cardMessageLabel: '贺卡寄语',
cardMessagePlaceholder: '写下您想对TA说的话...',
cardMessageRequired: '请输入贺卡寄语',
imageRequired: '请上传一张图片',
cancelButton: '取消',
saveButton: '保存',
@ -2562,6 +2568,7 @@ export default {
quantity: 'Quantity',
ipName: 'IP Name',
ipNamePlaceholder: 'Please enter IP name',
ipNameHint: 'The current name will be used on the nameplate',
shop: 'Shop',
chooseShop: 'Select Shop',
buy: 'Buy',
@ -2575,6 +2582,8 @@ export default {
noVouchers: 'No available vouchers',
minOrder: 'Minimum order amount',
expireAt: 'Expiration date',
greetingCard: 'Select Greeting Card',
greetingCardPlaceholder: 'Select a greeting card',
error: {
firstNameRequired: 'First name cannot be empty',
lastNameRequired: 'Last name cannot be empty',
@ -3195,6 +3204,9 @@ export default {
messageLabel: 'Blessing Message',
messagePlaceholder: 'Write your love message...',
messageRequired: 'Please enter a blessing message',
cardMessageLabel: 'Card Message',
cardMessagePlaceholder: 'Write your message to your loved one...',
cardMessageRequired: 'Please enter a card message',
imageRequired: 'Please upload an image',
cancelButton: 'Cancel',
saveButton: 'Save',

View File

@ -288,12 +288,11 @@ router.beforeEach(async (to, from, next) => {
// console.log(info,'infoinfo');
const user_role = authStore.user?.userRole;
if ((user_role == 1 || user_role == 2) && router.getRoutes().length == routes.length) {
// 添加动态路由
addDynamicRoutes();
if (isDynamicRoute(to.path)) {
next('/czhome')
setTimeout(() => {
router.push(to.path)
router.push({ path: to.path, query: to.query })
}, 20);
return
}

View File

@ -257,8 +257,7 @@ const openProject = (project) => {
}
const createNewProject = (series) => {
console.log(series,'seriesseriesseries');
router.push(`/project/new/${series.name}`)
router.push(`/project/new/${series.product_info.mark}`)
}
//

View File

@ -113,7 +113,17 @@
type="textarea"
:rows="4"
:placeholder="$t('greetingCard.messagePlaceholder')"
maxlength="200"
:maxlength="messageMaxLength"
show-word-limit
/>
</el-form-item>
<el-form-item :label="$t('greetingCard.cardMessageLabel')" prop="cardMessage">
<el-input
v-model="form.cardMessage"
type="textarea"
:rows="4"
:placeholder="$t('greetingCard.cardMessagePlaceholder')"
:maxlength="cardMessageMaxLength"
show-word-limit
/>
</el-form-item>
@ -147,35 +157,43 @@
</span>
</template>
</el-dialog>
<!-- Card Preview Dialog -->
<CardPreview v-model="previewVisible" :image-url="currentPreviewCard.imageUrl" :message="currentPreviewCard.message" />
<DtCardPreview :card-message="currentPreviewCard.cardMessage" :id="currentPreviewCard.id" v-model="previewVisible" :image-url="currentPreviewCard.imageUrl" :message="currentPreviewCard.message" />
</div>
</template>
<script setup>
import { ref, reactive, computed } from 'vue';
import { ref, reactive, computed, onMounted } from 'vue';
import { ElMessage } from 'element-plus';
import { Plus, Edit, Delete, View, UploadFilled } from '@element-plus/icons-vue';
import dayjs from 'dayjs';
import CardPreview from './components/CardPreview.vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
import {FileServer} from '@deotaland/utils'
import { GreetingCard } from './index.js';
const fileServer = new FileServer();
const greetingCardApi = new GreetingCard();
const { t, locale } = useI18n();
// --- State ---
const greetingCards = ref([
{
id: 1,
imageUrl: 'https://images.unsplash.com/photo-1518199266791-5375a83190b7?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=80',
message: '在这个特别的日子里,想告诉你:你是我生命中最美好的遇见,愿我们的爱情永远甜蜜如初。',
createTime: '2023-08-22T10:00:00.000Z'
},
{
id: 2,
imageUrl: 'https://images.unsplash.com/photo-1529333166437-7750a6dd5a70?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=80',
message: '情人节快乐!愿我们的爱情像玫瑰一样绽放,永远充满芬芳与美丽。',
createTime: '2023-08-20T14:30:00.000Z'
const greetingCards = ref([]);
//
const loadGreetingCards = async () => {
try {
const result = await greetingCardApi.getMyGreetingCardList();
greetingCards.value = result.rows.map(card => ({
id: card.id,
imageUrl: card.extraInfo.image.replace(/`/g, '').trim(),
message: card.content,
createTime: card.createdAt,
cardMessage: card.extraInfo.cardMessage,
}));
} catch (error) {
ElMessage.error(error.message || t('greetingCard.loadError'));
}
]);
};
//
onMounted(() => {
loadGreetingCards();
});
const dialogVisible = ref(false);
const isEdit = ref(false);
@ -185,19 +203,23 @@ const deleteDialogVisible = ref(false);
const currentDeleteCardId = ref(null);
const currentPreviewCard = reactive({
imageUrl: '',
message: ''
message: '',
id: null,
cardMessage:''
});
const formRef = ref(null);
const form = reactive({
id: null,
imageUrl: '',
message: ''
message: '',
cardMessage: '',
});
const rules = computed(() => ({
imageUrl: [{ required: true, message: t('greetingCard.imageRequired'), trigger: 'change' }],
message: [{ required: true, message: t('greetingCard.messageRequired'), trigger: 'blur' }]
message: [{ required: true, message: t('greetingCard.messageRequired'), trigger: 'blur' }],
cardMessage: [{ required: true, message: t('greetingCard.cardMessageRequired'), trigger: 'blur' }]
}));
const dialogWidth = computed(() => {
@ -210,17 +232,23 @@ const deleteDialogWidth = computed(() => {
return window.innerWidth < 768 ? '90%' : '400px';
});
// --- Actions ---
const messageMaxLength = computed(() => {
return locale.value === 'zh' ? 150 : 200;
});
const cardMessageMaxLength = computed(() => {
return locale.value === 'zh' ? 10 : 30;
});
// --- Actions ---
const formatDate = (date) => {
return dayjs(date).format('YYYY-MM-DD HH:mm');
};
const openAddDialog = () => {
isEdit.value = false;
form.id = null;
form.imageUrl = '';
form.message = '';
form.cardMessage = '';
dialogVisible.value = true;
};
@ -229,12 +257,15 @@ const editCard = (card) => {
form.id = card.id;
form.imageUrl = card.imageUrl;
form.message = card.message;
form.cardMessage = card.cardMessage || '';
dialogVisible.value = true;
};
const previewCardAction = (card) => {
currentPreviewCard.imageUrl = card.imageUrl;
currentPreviewCard.message = card.message;
currentPreviewCard.id = card.id;
currentPreviewCard.cardMessage = card.cardMessage;
previewVisible.value = true;
};
@ -243,28 +274,34 @@ const openDeleteDialog = (card) => {
deleteDialogVisible.value = true;
};
const confirmDelete = () => {
const confirmDelete = async () => {
if (currentDeleteCardId.value) {
greetingCards.value = greetingCards.value.filter(card => card.id !== currentDeleteCardId.value);
ElMessage.success(t('greetingCard.deleteSuccess'));
deleteDialogVisible.value = false;
currentDeleteCardId.value = null;
try {
await greetingCardApi.deleteGreetingCard({
id: currentDeleteCardId.value
});
greetingCards.value = greetingCards.value.filter(card => card.id !== currentDeleteCardId.value);
ElMessage.success(t('greetingCard.deleteSuccess'));
deleteDialogVisible.value = false;
currentDeleteCardId.value = null;
} catch (error) {
ElMessage.error(error.message || t('greetingCard.deleteError'));
}
}
};
const handleFileChange = (uploadFile) => {
const file = uploadFile.raw;
if (!file) return;
const isImage = file.type.startsWith('image/');
if (!isImage) {
ElMessage.error('请上传图片文件');
return;
}
const reader = new FileReader();
reader.onload = (e) => {
form.imageUrl = e.target.result;
reader.onload = async (e) => {
const url = await fileServer.uploadFile(e.target.result);
form.imageUrl = url;
if (formRef.value) {
formRef.value.validateField('imageUrl');
}
@ -275,30 +312,33 @@ const handleFileChange = (uploadFile) => {
const saveCard = async () => {
if (!formRef.value) return;
await formRef.value.validate((valid) => {
await formRef.value.validate(async (valid) => {
if (valid) {
saving.value = true;
// Simulate API call
setTimeout(() => {
try {
if (isEdit.value) {
const index = greetingCards.value.findIndex(c => c.id === form.id);
if (index !== -1) {
greetingCards.value[index] = { ...greetingCards.value[index], ...form };
ElMessage.success(t('greetingCard.updateSuccess'));
}
await greetingCardApi.updateGreetingCard({
id: form.id,
content: form.message,
image: form.imageUrl,
cardMessage: form.cardMessage
});
ElMessage.success(t('greetingCard.updateSuccess'));
} else {
const newCard = {
id: Date.now(),
imageUrl: form.imageUrl,
message: form.message,
createTime: new Date().toISOString()
};
greetingCards.value.unshift(newCard);
await greetingCardApi.createGreetingCard({
content: form.message,
image: form.imageUrl,
cardMessage: form.cardMessage
});
ElMessage.success(t('greetingCard.createSuccess'));
}
await loadGreetingCards();
saving.value = false;
dialogVisible.value = false;
}, 500);
} catch (error) {
saving.value = false;
ElMessage.error(error.message || t('greetingCard.saveError'));
}
}
});
};

View File

@ -0,0 +1,110 @@
import { requestUtils,clientApi } from "@deotaland/utils";
export class GreetingCard {
constructor() {
}
// 创建贺卡
async createGreetingCard(data) {
let parmas = {
title:'贺卡',//写死不需要传递
content:data.content,
extraInfo:{
image:data.image,//上传的图片
cardMessage:data.cardMessage//贺卡内容
}
}
return requestUtils.common(clientApi.default.CREATE_GREETING_CARD,parmas)
}
// 更新贺卡
async updateGreetingCard(data) {
let parmas = {
id:data.id,
title:'贺卡',//写死不需要传递
content:data.content,//贺语内容
extraInfo:{
image:data.image,//上传的图片
cardMessage:data.cardMessage//贺卡内容
}
}
return requestUtils.common(clientApi.default.UPDATE_GREETING_CARD,parmas)
}
// 删除贺卡
async deleteGreetingCard(data) {
let parmas = {
id:data.id,
}
const requestUrl = {
method: clientApi.default.DELETE_GREETING_CARD.method,
url: clientApi.default.DELETE_GREETING_CARD.url.replace('{id}',data.id),
isLoading: clientApi.default.DELETE_GREETING_CARD.isLoading,
}
return requestUtils.common(requestUrl,parmas)
}
// 查询贺卡详情
async getGreetingCardDetail(data) {
let parmas = {
id:data.id,
}
const requestUrl = {
method: clientApi.default.GET_GREETING_CARD_DETAIL.method,
url: clientApi.default.GET_GREETING_CARD_DETAIL.url.replace('{id}',data.id),
isLoading: clientApi.default.GET_GREETING_CARD_DETAIL.isLoading,
}
return requestUtils.common(requestUrl,parmas)
/**
返回示例
{
"code": 0,
"success": true,
"data": {
"id": 9007199254740991,
"title": "string",
"content": "string",
"creatorId": 9007199254740991,
"creatorNickname": "string",
"extraInfo": {
"image": "string",//上传的图片
"cardMessage": "string"//贺卡内容
},
"createdAt": "2026-01-20T03:11:14.573Z",
"updatedAt": "2026-01-20T03:11:14.573Z"
},
"message": "操作成功"
}
*
*/
}
// 查询我的贺卡列表
async getMyGreetingCardList() {
let parmas = {
}
const requestUrl = {
method: clientApi.default.GET_MY_GREETING_CARD_LIST.method,
url: clientApi.default.GET_MY_GREETING_CARD_LIST.url,
isLoading: clientApi.default.GET_MY_GREETING_CARD_LIST.isLoading,
}
return requestUtils.common(requestUrl,parmas)
/**
返回示例
{
"total": 9007199254740991,
"rows": [
{
"id": 9007199254740991,
"title": "string",
"content": "string",
"creatorId": 9007199254740991,
"creatorNickname": "string",
"extraInfo": {
"image": "string",//上传的图片
"cardMessage": "string"//贺卡内容
},
"createdAt": "2026-01-20T03:11:32.331Z",
"updatedAt": "2026-01-20T03:11:32.331Z"
}
],
"code": 1073741824,
"msg": "string"
}
*/
}
}

View File

@ -1136,7 +1136,7 @@ const init = ()=>{
const route = useRoute();
projectId.value = route.params.id;
series.value = route.params.series;
if(series.value!='D1'&&series.value!='E1'){
if(series.value!='D1'&&series.value!='E1'&&series.value!='A1'){
series.value = 'D1';
router.replace(`/project/${projectId.value}/${series.value}`);
return

View File

@ -730,7 +730,7 @@ export class Project {
}, 1000);
}
//获取动态提示词
async getCombinedPrompt(series,config={}) {//series:项目系列D1 E1
async getCombinedPrompt(series,config={}) {//series:项目系列D1 E1 A1
try {
return new Promise(async (resolve, reject) => {
const res = await requestUtils.common(clientApi.default.combined)
@ -742,12 +742,12 @@ export class Project {
return !item.title.includes('头部挂钩');
});
}
if (series === 'E1') {
if (series === 'E1'||series === 'A1') {
data = data.filter(item => {
return !item.title.includes('动物坐姿') && !item.title.includes('人物姿势') && item.type != 'D1';
});
} else if (series === 'D1') {// 如果是Done系列过滤掉type为E1的提示词
data = data.filter(item => item.type !== 'E1');
} else if (series === 'D1') {// 如果是Done系列过滤掉type为E1和A1的提示词
data = data.filter(item => (item.type !== 'E1'&&item.type !== 'A1'));
}
// 初始化返回数据结构
const result = {
@ -763,7 +763,7 @@ export class Project {
// 按sortOrder排序
data.sort((a, b) => a.sortOrder - b.sortOrder);
// 处理person和general类型的数据
const personAndGeneral = data.filter(item => item.type === 'person' || item.type === 'general' || item.type === 'E1');
const personAndGeneral = data.filter(item => item.type === 'person' || item.type === 'general' || item.type === 'E1'|| item.type === 'A1');
personAndGeneral.forEach(item => {
// 拼接content
result.person.content += item.content;
@ -780,7 +780,7 @@ export class Project {
}
});
// 处理animal和general类型的数据
const animalAndGeneral = data.filter(item => item.type === 'animal' || item.type === 'general' || item.type === 'E1');
const animalAndGeneral = data.filter(item => item.type === 'animal' || item.type === 'general' || item.type === 'E1'|| item.type === 'A1');
animalAndGeneral.forEach(item => {
// 拼接content
result.animal.content += item.content;

View File

@ -31,13 +31,17 @@
<script setup>
import { ref, onMounted, onUnmounted, computed, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRoute } from 'vue-router'
import { useThemeStore } from '@/stores/theme'
import * as THREE from 'three'
import { Hands } from '@mediapipe/hands'
import { Camera } from '@mediapipe/camera_utils'
import { GreetingCard } from '../GreetingCard/index.js'
const { t, locale } = useI18n()
const route = useRoute()
const themeStore = useThemeStore()
const greetingCard = new GreetingCard()
const canvasContainer = ref(null)
const videoElement = ref(null)
@ -47,20 +51,10 @@ const showImage = ref(false)
const isMobile = ref(window.innerWidth < 768)
const cameraInitFailed = ref(false)
const isManuallyTriggered = ref(false)
// 1.
const confessionText = computed(() => {
if (locale.value === 'zh') {
return isMobile.value
? `Dear, Meeting you is the luckiest thing in my life. Your smile is my greatest motivation, I want to accompany you through every season. Happy Valentine's Day, I love you!💖`
: `Dear, Meeting you is the luckiest thing in my life. Your smile is my greatest motivation, I want to accompany you through every season. Happy Valentine's Day, I love you!💖`
} else {
return 'I LOVE YOU\nFOREVER & ALWAYS'
}
})
const confessionImage = ref('https://images.unsplash.com/photo-1518199266791-5375a83190b7?w=400&h=400&fit=crop')
const confessionText = ref(``)
const confessionImage = ref('')
const isImageLoaded = ref(false)
let scene, camera, renderer, particles
let hands, cameraUtils
let animationId
@ -107,17 +101,13 @@ const initThreeScene = () => {
scene = new THREE.Scene()
const themeColors = getThemeColors()
scene.background = new THREE.Color(themeColors.background)
camera = new THREE.PerspectiveCamera(60, width / height, 0.1, 1000)
// (110)
camera.position.z = isMobile.value ? 110 : 60
renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true })
renderer.setSize(width, height)
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2))
canvasContainer.value.appendChild(renderer.domElement)
createParticles()
animate()
}
@ -177,7 +167,6 @@ const createParticles = () => {
const getTextPositions = (text) => {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
// 1.
const fontSize = isMobile.value ? 36 : 50
const font = `bold ${fontSize}px "Noto Serif SC", serif`
@ -187,7 +176,7 @@ const getTextPositions = (text) => {
// 80%PC
// 350pxPC 800px
const maxLineWidth = isMobile.value ? 350 : 800
const maxLineWidth = isMobile.value ? 450 : 800
const rawLines = text.split('\n') //
const lines = []
@ -244,9 +233,7 @@ const getTextPositions = (text) => {
// === ===
// 3D
const textWorldHeight = lines.length * lineHeight * 0.08
let shiftY = isMobile.value ? -12 : -15 //
if (isMobile.value) {
// CSS
const vw = window.innerWidth
@ -277,7 +264,7 @@ const getTextPositions = (text) => {
const imgBottomWorldY = (1 - 2 * imgBottomRatio) * (visibleHeight / 2)
// 5
const safeTopY = imgBottomWorldY - 5
const safeTopY = imgBottomWorldY - 0
//
// Top = Center + HalfHeight => Center = Top - HalfHeight
@ -544,6 +531,27 @@ const handleResize = () => {
renderer.setSize(width, height)
}
const preloadImage = (url) => {
return new Promise((resolve, reject) => {
if (!url) {
resolve(false)
return
}
const img = new Image()
img.onload = () => {
isImageLoaded.value = true
resolve(true)
}
img.onerror = () => {
console.error('Image preload failed:', url)
isImageLoaded.value = false
resolve(false)
}
img.src = url
})
}
watch(locale, () => {
if (isGestureDetected.value) {
isAssembling = false
@ -558,11 +566,27 @@ watch(() => themeStore.isDark, () => {
}
})
onMounted(() => {
onMounted(async () => {
initThreeScene()
initHandTracking()
initCamera()
window.addEventListener('resize', handleResize)
console.log(route,'routeroute');
const cardId = route.query.id
if (cardId) {
try {
const response = await greetingCard.getGreetingCardDetail({ id: cardId })
if (response.success && response.data) {
confessionText.value = response.data.content
confessionImage.value = response.data.extraInfo?.image || ''
if (confessionImage.value) {
await preloadImage(confessionImage.value)
}
}
} catch (error) {
console.error('Failed to fetch greeting card detail:', error)
}
}
})
onUnmounted(() => {
@ -706,7 +730,7 @@ html.dark .confession-card-container {
@media (max-width: 768px) {
.image-display {
top: 12%;
top: 3%;
}
.confession-image {

View File

@ -66,8 +66,8 @@ export default defineConfig({
// 配置代理解决CORS问题
proxy: {
'/api': {
target: 'https://api.deotaland.ai',
// target: 'http://api.deotaland.local',
// target: 'https://api.deotaland.ai',
target: 'http://api.deotaland.local',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}

View File

@ -2,7 +2,7 @@
FROM node:22-alpine
# 安装 pnpm
RUN npm install -g pnpm
RUN corepack enable && corepack prepare pnpm@9.3.0 --activate
# 设置工作目录
WORKDIR /build

30
dockerfile copy Normal file
View File

@ -0,0 +1,30 @@
# 使用官方 Node.js 镜像
FROM node:22-alpine
# 安装 pnpm
RUN npm install -g pnpm
# 设置工作目录
WORKDIR /build
# 先复制配置文件以利用 Docker 缓存
COPY pnpm-workspace.yaml ./
COPY package.json ./
COPY pnpm-lock.yaml ./
COPY turbo.json ./
# 复制所有 workspace 包和应用
COPY packages ./packages
COPY apps ./apps
# 使用 pnpm 安装依赖
RUN pnpm install
# 先构建需要构建的包
RUN pnpm --filter @deotaland/ui build
# 构建应用
RUN pnpm run build
# 确认构建输出存在
CMD ["sh", "-c", "find /build -name 'dist' -type d -exec ls -la {} \\;"]

View File

@ -41,5 +41,10 @@
"element-plus"
],
"author": "Deotaland AI Team",
"license": "MIT"
"license": "MIT",
"dependencies": {
"dom-to-image-more": "^3.7.2",
"html2canvas": "^1.4.1",
"qrcode": "^1.5.4"
}
}

View File

@ -346,7 +346,6 @@ async function fetchImage(url) {
});
const blob = await response.blob();
const imgurl = URL.createObjectURL(blob);
console.log(imgurl,'imgurlimgurlimgurl');
return imgurl;
}
const handleTouchMove = (e) => {

View File

@ -1,7 +1,6 @@
<template>
<el-dialog
v-model="visible"
:title="$t('greetingCard.previewDialogTitle')"
:width="dialogWidth"
custom-class="card-preview-dialog"
destroy-on-close
@ -12,8 +11,11 @@
<div class="preview-container">
<div ref="cardRef" class="card-preview">
<div class="card-content">
<div class="card-image-section">
<img :src="imageUrl" alt="Card Image" class="preview-image" />
<div class="card-image-section" :style="{ height: imageHeight + 'px' }">
<div v-if="!urlImage" class="image-loading">
<div class="loading-spinner"></div>
</div>
<img v-else-if="urlImage" :src="urlImage" alt="Card Image" class="preview-image" @load="handleImageLoad" @error="handleImageError" />
</div>
<div class="card-message-section">
<p class="preview-message">{{ $t('greetingCard.cardMessage') }}</p>
@ -36,15 +38,13 @@
</template>
<script setup>
import { ref, watch, nextTick, computed } from 'vue';
import { ref, watch, nextTick, computed, onMounted } from 'vue';
import { ElMessage } from 'element-plus';
import { Download } from '@element-plus/icons-vue';
import QRCode from 'qrcode';
import html2canvas from 'html2canvas';
import domtoimage from 'dom-to-image-more';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const props = defineProps({
modelValue: {
type: Boolean,
@ -59,9 +59,29 @@ const props = defineProps({
default: ''
}
});
const urlImage = ref('');
const imageLoading = ref(true);
const imageHeight = ref(280);
watch(() => props.imageUrl, async (newVal) => {
imageLoading.value = true;
urlImage.value = '';
urlImage.value = await fetchImage(newVal);
})
const handleImageLoad = (e) => {
imageLoading.value = false;
const img = e.target;
if (img && img.naturalHeight) {
imageHeight.value = Math.min(img.naturalHeight, 400);
}
};
const handleImageError = () => {
imageLoading.value = false;
ElMessage.error('图片加载失败');
};
const emit = defineEmits(['update:modelValue']);
const visible = ref(false);
const cardRef = ref(null);
const qrcodeRef = ref(null);
@ -113,11 +133,45 @@ const exportImage = async () => {
exporting.value = true;
try {
await nextTick();
const canvas = await html2canvas(cardRef.value, {
scale: 2,
useCORS: true,
backgroundColor: '#fff5f7',
logging: false
allowTaint: true,
backgroundColor: null,
logging: false,
imageTimeout: 0,
removeContainer: true,
foreignObjectRendering: false,
onclone: (clonedDoc) => {
const clonedCard = clonedDoc.querySelector('.card-preview');
if (clonedCard) {
clonedCard.style.width = '360px';
clonedCard.style.maxWidth = '360px';
clonedCard.style.boxShadow = '0 8px 32px rgba(255, 107, 157, 0.2)';
clonedCard.style.borderRadius = '0px';
}
const clonedImageSection = clonedDoc.querySelector('.card-image-section');
if (clonedImageSection) {
clonedImageSection.style.height = imageHeight.value + 'px';
// clonedImageSection.style.background = 'linear-gradient(135deg, #ffe4e8 0%, #fff5f7 100%)';
}
const clonedImage = clonedDoc.querySelector('.preview-image');
if (clonedImage) {
clonedImage.style.objectFit = 'cover';
clonedImage.style.width = '100%';
clonedImage.style.height = '100%';
}
const clonedMessageSection = clonedDoc.querySelector('.card-message-section');
if (clonedMessageSection) {
clonedMessageSection.style.background = 'linear-gradient(135deg, #fff5f7 0%, #ffe4e8 100%)';
}
const clonedQrcodeSection = clonedDoc.querySelector('.card-qrcode-section');
if (clonedQrcodeSection) {
clonedQrcodeSection.style.background = 'linear-gradient(135deg, #fff5f7 0%, #ffe4e8 100%)';
}
}
});
const link = document.createElement('a');
@ -133,7 +187,18 @@ const exportImage = async () => {
exporting.value = false;
}
};
async function fetchImage(url) {
const cacheBusterUrl = url + '?v=1.0.0';
const response = await fetch(cacheBusterUrl, {
method: 'GET',
mode: 'cors',
credentials: 'omit',
cache: 'no-cache'
});
const blob = await response.blob();
const imgurl = URL.createObjectURL(blob);
return imgurl;
}
const handleClose = () => {
visible.value = false;
};
@ -146,7 +211,6 @@ const handleClose = () => {
align-items: center;
padding: 20px;
border-radius: 12px;
background: linear-gradient(135deg, #fff5f7 0%, #ffe4e8 100%);
}
.card-preview {
@ -167,15 +231,53 @@ const handleClose = () => {
.card-image-section {
width: 100%;
height: 280px;
min-height: 200px;
max-height: 400px;
overflow: hidden;
position: relative;
transition: height 0.3s ease;
}
.image-loading {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #ffe4e8 0%, #fff5f7 100%);
}
.loading-spinner {
width: 40px;
height: 40px;
border: 3px solid rgba(214, 51, 132, 0.2);
border-top-color: #d63384;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.preview-image {
width: 100%;
height: 100%;
object-fit: cover;
animation: fadeIn 0.5s ease-in-out;
background-color: none;
z-index: 200;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.card-message-section {
@ -249,7 +351,8 @@ const handleClose = () => {
}
.card-image-section {
height: 240px;
min-height: 180px;
max-height: 320px;
}
.card-message-section {

View File

@ -0,0 +1,409 @@
<template>
<el-dialog
v-model="visible"
:width="dialogWidth"
custom-class="card-preview-dialog"
destroy-on-close
:close-on-click-modal="false"
append-to-body
@close="handleClose"
>
<div class="preview-container">
<div ref="cardRef" class="card-preview">
<div class="card-content">
<div class="card-image-section" :style="{ height: imageHeight + 'px' }">
<div v-if="!urlImage" class="image-loading">
<div class="loading-spinner"></div>
</div>
<img v-else-if="urlImage" :src="urlImage" alt="Card Image" class="preview-image" @load="handleImageLoad" @error="handleImageError" />
</div>
<div class="card-message-section">
<p class="preview-message">{{ cardMessage }}</p>
</div>
<div class="card-qrcode-section">
<div ref="qrcodeRef" class="qrcode-container"></div>
</div>
</div>
</div>
</div>
<template #footer>
<span class="dialog-footer">
<el-button @click="handleClose">{{ $t('greetingCard.cancelButton') }}</el-button>
<el-button type="primary" class="export-btn" @click="exportImage" :loading="exporting">
<el-icon><Download /></el-icon> {{ $t('greetingCard.exportButton') }}
</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup>
import { ref, watch, nextTick, computed, onMounted } from 'vue';
import { Download } from '@element-plus/icons-vue';
import { ElMessage, ElIcon, ElButton, ElInput, ElDialog } from 'element-plus'
import QRCode from 'qrcode';
import html2canvas from 'html2canvas';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const props = defineProps({
id: {
type: String,
default: ''
},
modelValue: {
type: Boolean,
default: false
},
imageUrl: {
type: String,
default: ''
},
message: {
type: String,
default: ''
},
cardMessage: {
type: String,
default: ''
}
});
const urlImage = ref('');
const imageLoading = ref(true);
const imageHeight = ref(280);
watch(() => props.imageUrl, async (newVal) => {
imageLoading.value = true;
urlImage.value = '';
urlImage.value = await fetchImage(newVal);
}, { immediate: true })
const handleImageLoad = (e) => {
imageLoading.value = false;
const img = e.target;
if (img && img.naturalHeight) {
imageHeight.value = Math.min(img.naturalHeight, 400);
}
};
const handleImageError = () => {
imageLoading.value = false;
ElMessage.error('图片加载失败');
};
const emit = defineEmits(['update:modelValue']);
const visible = ref(false);
const cardRef = ref(null);
const qrcodeRef = ref(null);
const exporting = ref(false);
const dialogWidth = computed(() => {
if (typeof window === 'undefined') return '400px';
return window.innerWidth < 768 ? '95%' : '400px';
});
watch(() => props.modelValue, (newVal) => {
visible.value = newVal;
if (newVal) {
nextTick(() => {
generateQRCode();
});
}
});
watch(visible, (newVal) => {
emit('update:modelValue', newVal);
});
const generateQRCode = async () => {
const Loadurl = `${window.location.origin}/#/confession-electronic-card?id=${props.id}`;
if (!qrcodeRef.value) return;
try {
qrcodeRef.value.innerHTML = '';
const canvas = document.createElement('canvas');
await QRCode.toCanvas(canvas,Loadurl, {
width: 70,
margin: 0,
color: {
dark: '#d63384',
light: '#ffffff'
}
});
qrcodeRef.value.appendChild(canvas);
} catch (error) {
console.error('QR Code generation failed:', error);
}
};
const exportImage = async () => {
if (!cardRef.value) return;
exporting.value = true;
try {
await nextTick();
const canvas = await html2canvas(cardRef.value, {
scale: 2, //
useCORS: true,
allowTaint: true,
backgroundColor: '#ffffff', //
logging: false,
imageTimeout: 0,
removeContainer: true,
foreignObjectRendering: false,
onclone: (clonedDoc) => {
const clonedCard = clonedDoc.querySelector('.card-preview');
if (clonedCard) {
clonedCard.style.width = '360px';
clonedCard.style.maxWidth = '360px';
clonedCard.style.boxShadow = 'none'; //
clonedCard.style.borderRadius = '0px';
}
const clonedImageSection = clonedDoc.querySelector('.card-image-section');
if (clonedImageSection) {
clonedImageSection.style.height = imageHeight.value + 'px';
}
const clonedImage = clonedDoc.querySelector('.preview-image');
if (clonedImage) {
// --- ---
//
clonedImage.style.animation = 'none';
clonedImage.style.transition = 'none';
clonedImage.style.opacity = '1';
// --- ---
clonedImage.style.objectFit = 'cover';//
clonedImage.style.width = '100%';
clonedImage.style.height = '100%';
}
//
const clonedMessageSection = clonedDoc.querySelector('.card-message-section');
if (clonedMessageSection) {
clonedMessageSection.style.background = 'linear-gradient(135deg, #fff5f7 0%, #ffe4e8 100%)';
}
//
const clonedQrcodeSection = clonedDoc.querySelector('.card-qrcode-section');
if (clonedQrcodeSection) {
clonedQrcodeSection.style.background = 'linear-gradient(135deg, #fff5f7 0%, #ffe4e8 100%)';
}
}
});
const link = document.createElement('a');
link.download = `greeting-card-${Date.now()}.png`;
link.href = canvas.toDataURL('image/png');
link.click();
ElMessage.success(t('greetingCard.exportSuccess'));
} catch (error) {
console.error('Export failed:', error);
ElMessage.error(t('greetingCard.exportFailed'));
} finally {
exporting.value = false;
}
};
async function fetchImage(url) {
const cacheBusterUrl = url + '?v=1.0.0';
const response = await fetch(cacheBusterUrl, {
method: 'GET',
mode: 'cors',
credentials: 'omit',
cache: 'no-cache'
});
const blob = await response.blob();
const imgurl = URL.createObjectURL(blob);
return imgurl;
}
const handleClose = () => {
visible.value = false;
};
</script>
<style scoped>
.preview-container {
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
border-radius: 12px;
}
.card-preview {
width: 100%;
max-width: 360px;
background: white;
border-radius: 16px;
overflow: hidden;
box-shadow: 0 8px 32px rgba(255, 107, 157, 0.2);
}
.card-content {
position: relative;
display: flex;
flex-direction: column;
}
.card-image-section {
width: 100%;
min-height: 200px;
max-height: 400px;
overflow: hidden;
position: relative;
transition: height 0.3s ease;
}
.image-loading {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #ffe4e8 0%, #fff5f7 100%);
}
.loading-spinner {
width: 40px;
height: 40px;
border: 3px solid rgba(214, 51, 132, 0.2);
border-top-color: #d63384;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.preview-image {
width: 100%;
height: 100%;
object-fit: cover;
animation: fadeIn 0.5s ease-in-out;
background-color: none;
z-index: 200;
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.card-message-section {
padding: 24px 20px;
background: linear-gradient(135deg, #fff5f7 0%, #ffe4e8 100%);
display: flex;
align-items: center;
justify-content: center;
}
.preview-message {
margin: 0;
color: #d63384;
font-size: 1.3rem;
line-height: 1.8;
text-align: center;
font-weight: 700;
font-family: 'Georgia', 'Times New Roman', 'Playfair Display', 'Brush Script MT', cursive;
text-shadow: 0 2px 4px rgba(214, 51, 132, 0.2);
letter-spacing: 1px;
word-wrap: break-word;
}
.card-qrcode-section {
display: flex;
justify-content: center;
align-items: center;
padding: 16px 0;
background: linear-gradient(135deg, #fff5f7 0%, #ffe4e8 100%);
}
.qrcode-container {
display: flex;
align-items: center;
justify-content: center;
}
.qrcode-container canvas {
border-radius: 4px;
}
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
}
.export-btn {
background: linear-gradient(135deg, #ff6b9d 0%, #ff8a80 100%);
border: none;
font-weight: 600;
transition: all 0.3s ease;
box-shadow: 0 4px 12px rgba(255, 107, 157, 0.3);
padding: 12px 24px;
font-size: 1rem;
}
.export-btn:hover {
background: linear-gradient(135deg, #ff5a8d 0%, #ff7a70 100%);
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(255, 107, 157, 0.4);
}
@media (max-width: 768px) {
.preview-container {
padding: 12px;
}
.card-preview {
max-width: 100%;
}
.card-image-section {
min-height: 180px;
max-height: 320px;
}
.card-message-section {
padding: 20px 16px;
}
.preview-message {
font-size: 1.15rem;
font-family: 'Georgia', 'Times New Roman', 'Playfair Display', 'Brush Script MT', cursive;
}
.card-qrcode-section {
bottom: 12px;
right: 12px;
padding: 6px;
}
.qrcode-container canvas {
width: 60px !important;
height: 60px !important;
}
.dialog-footer {
flex-direction: column-reverse;
gap: 10px;
}
.dialog-footer .el-button {
width: 100%;
}
.export-btn {
margin-left: 0;
}
}
</style>

View File

@ -1,10 +1,10 @@
import 'element-plus/dist/index.css'
import './style.css'
// UI组件库入口文件
import LoadingCom from './components/LoadingCom/index.vue'
import CardPreview from './components/CardPreview/CardPreview.vue'
import CanvasEditor from './components/CanvasEditor/CanvasEditor.vue'
import './style.css'
// 创建带有Dt前缀的组件
const DtLoadingCom = {
...LoadingCom,
@ -20,17 +20,26 @@ const DtCanvasEditor = {
app.component('DtCanvasEditor', DtCanvasEditor)
}
}
const DtCardPreview = {
...CardPreview,
name: 'DtCardPreview',
install(app) {
app.component('DtCardPreview', DtCardPreview)
}
}
// 组件列表
const components = [
DtLoadingCom,
DtCanvasEditor
DtCanvasEditor,
DtCardPreview
]
// 导出组件
export {
DtLoadingCom,
DtCanvasEditor
DtCanvasEditor,
DtCardPreview
}
// 批量注册组件的函数

View File

@ -1,8 +1,6 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
import { fileURLToPath, URL } from 'node:url'
export default defineConfig({
plugins: [vue()],
build: {

View File

@ -0,0 +1,9 @@
const greetingCardApi = {
GENERATE_IMAGE:{url:'/api-core/front/gemini/generate-image',method:'POST',isLoading: true,},// 生图模型任务创建
CREATE_GREETING_CARD:{url:'/api-base/greeting-card',method:'POST',isLoading: true,},// 创建贺卡
UPDATE_GREETING_CARD:{url:'/api-base/greeting-card/update',method:'POST',isLoading: true,},// 更新贺卡
DELETE_GREETING_CARD:{url:'/api-base/greeting-card/delete/{id}',method:'POST',isLoading: true,},// 删除贺卡
GET_GREETING_CARD_DETAIL:{url:'/api-base/greeting-card/public/{id}',method:'GET',isLoading: true,},// 查询贺卡详情
GET_MY_GREETING_CARD_LIST:{url:'/api-base/greeting-card/my-list',method:'GET',isLoading: true,},// 查询我的贺卡列表
}
export default greetingCardApi;

View File

@ -11,6 +11,7 @@ import rechargeconfig from './rechargeconfig.js';
import voucher from './voucher.js';
import shop from './shop.js';
import agreement from './agreement.js';
import greetingCard from './greetingCard.js';
export default {
...meshy,
...login,
@ -25,4 +26,5 @@ export default {
...voucher,
...shop,
...agreement,
...greetingCard,
};

View File

@ -45,9 +45,8 @@ export class MeshyServer extends FileServer {
ai_model: 'latest',
enable_pbr: false,
should_remesh: false,
should_texture: false,//是否生成纹理
should_texture: false,
save_pre_remeshed_model: true,
// target_polycount:300000,
...config
}
}

View File

@ -17,7 +17,7 @@ const getEnvBaseURL = () => {
// }
var baseURL = '';
const hostname = window.location.hostname;
if(hostname=='localhost'||hostname=='192.168.101.2'){
if(hostname=='localhost'||hostname=='192.168.0.146'){
baseURL = '/api'
}else if(hostname.indexOf('deotaland.ai')>-1||hostname.indexOf('deota.cn')>-1){
baseURL = 'https://api.deotaland.ai'

View File

@ -177,9 +177,6 @@ importers:
element-plus:
specifier: ^2.11.7
version: 2.11.7(vue@3.5.24)
html2canvas:
specifier: ^1.4.1
version: 1.4.1
install:
specifier: ^0.13.0
version: 0.13.0
@ -201,9 +198,6 @@ importers:
pinia-plugin-persistedstate:
specifier: ^4.7.1
version: 4.7.1(pinia@3.0.4)
qrcode:
specifier: ^1.5.4
version: 1.5.4
three:
specifier: ^0.180.0
version: 0.180.0
@ -280,9 +274,18 @@ importers:
packages/ui:
dependencies:
dom-to-image-more:
specifier: ^3.7.2
version: 3.7.2
element-plus:
specifier: ^2.0.0
version: 2.11.7(vue@3.5.24)
html2canvas:
specifier: ^1.4.1
version: 1.4.1
qrcode:
specifier: ^1.5.4
version: 1.5.4
vue:
specifier: ^3.0.0
version: 3.5.24
@ -2242,6 +2245,10 @@ packages:
entities: 2.2.0
dev: false
/dom-to-image-more@3.7.2:
resolution: {integrity: sha512-uQf+pHv6eQhgfI8t2bFuinV0KsPyT8TZgCLwcSU8uBVgN9v6leb0mMpvp6HQAlAcplP3NCcGjxbdqef6pTzvmw==}
dev: false
/dom7@3.0.0:
resolution: {integrity: sha512-oNlcUdHsC4zb7Msx7JN3K0Nro1dzJ48knvBOnDPKJ2GV9wl1i5vydJZUSyOfrkKFDZEud/jBsTk92S/VGSAe/g==}
dependencies: