222
This commit is contained in:
parent
4f93347199
commit
2a992810fe
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -328,7 +328,6 @@ const routes = [
|
|||
requiresAuth: true
|
||||
},
|
||||
children: [
|
||||
|
||||
{
|
||||
path: 'disassembly-orders/:id',
|
||||
name: 'AdminDisassemblyDetail',
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export const useAuthStore = defineStore('auth', {
|
|||
router: null,
|
||||
routesUpdated: 0,//路由更新次数
|
||||
routerList: window.location.hostname.indexOf('local') === -1 ? [] : permissionRoutes,//侧边栏路由
|
||||
// routerList:[],//侧边栏路由
|
||||
}),
|
||||
|
||||
getters: {
|
||||
|
|
|
|||
|
|
@ -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'))
|
||||
|
|
|
|||
|
|
@ -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: '',
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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); }
|
||||
|
|
|
|||
|
|
@ -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') }}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}`)
|
||||
}
|
||||
|
||||
// 处理系列选择确认
|
||||
|
|
|
|||
|
|
@ -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'));
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 端固定宽度
|
||||
// 这里我们用一个虚拟像素值,比如手机 350px,PC 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 {
|
||||
|
|
|
|||
|
|
@ -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/, '')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {} \\;"]
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
// 批量注册组件的函数
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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,
|
||||
};
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Reference in New Issue