first commit
This commit is contained in:
110
frontend/src/module/ose-card/components/OseCardCouponDrawer.vue
Normal file
110
frontend/src/module/ose-card/components/OseCardCouponDrawer.vue
Normal file
@@ -0,0 +1,110 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
:model-value="modelValue"
|
||||
title="搭配優惠代碼功能說明"
|
||||
direction="ltr"
|
||||
size="50%"
|
||||
@update:model-value="$emit('update:modelValue', $event)"
|
||||
>
|
||||
<template #default>
|
||||
<article>
|
||||
<el-row class="tw-space-y-5">
|
||||
<el-col>
|
||||
<el-row class="tw-items-center">
|
||||
<el-col :span="3" class="tw-font-light">優惠代碼</el-col>
|
||||
<el-col :span="21">
|
||||
<el-input
|
||||
:input-style="{ fontSize: 'medium' }"
|
||||
size="large"
|
||||
:model-value="couponCode"
|
||||
@update:model-value="$emit('update:couponCode', $event)"
|
||||
/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-col>
|
||||
|
||||
<el-col>
|
||||
<el-row class="tw-items-center">
|
||||
<el-col :span="3" class="tw-font-light">推薦代碼</el-col>
|
||||
<el-col :span="21">
|
||||
<el-input
|
||||
:input-style="{ fontSize: 'medium' }"
|
||||
size="large"
|
||||
:model-value="affiliateCode"
|
||||
@update:model-value="$emit('update:affiliateCode', $event)"
|
||||
/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-col>
|
||||
|
||||
<el-col>
|
||||
<el-radio-group
|
||||
:model-value="couponType"
|
||||
@update:model-value="$emit('update:couponType', $event)"
|
||||
>
|
||||
<el-row>
|
||||
<el-col :span="8" class="tw-pb-3">
|
||||
<el-radio label="優惠代碼" size="large" border>優惠代碼</el-radio>
|
||||
</el-col>
|
||||
<el-col :span="8" class="tw-pb-3">
|
||||
<el-radio label="推薦代碼" size="large" border>推薦代碼</el-radio>
|
||||
</el-col>
|
||||
<el-col :span="8" class="tw-pb-3">
|
||||
<el-radio label="優惠代碼+推薦代碼" size="large" border>優惠代碼+推薦代碼</el-radio>
|
||||
</el-col>
|
||||
<el-col :span="8" class="tw-pb-3">
|
||||
<el-radio label="行銷圖卡+優惠代碼" size="large" border>行銷圖卡+優惠代碼</el-radio>
|
||||
</el-col>
|
||||
<el-col :span="8" class="tw-pb-3">
|
||||
<el-radio label="行銷圖卡+推薦代碼" size="large" border>行銷圖卡+推薦代碼</el-radio>
|
||||
</el-col>
|
||||
<el-col :span="8" class="tw-pb-3">
|
||||
<el-radio label="行銷圖卡+優惠代碼+推薦代碼" size="large" border
|
||||
>行銷圖卡+優惠代碼+推薦代碼</el-radio
|
||||
>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-radio-group>
|
||||
</el-col>
|
||||
|
||||
<el-col>
|
||||
<p>網址:</p>
|
||||
<a class="tw-text-blue-600" :href="cardCodeUrl">{{ cardCodeUrl }}</a>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</article>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
couponType: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
couponCode: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
affiliateCode: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
cardCodeUrl: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
})
|
||||
|
||||
defineEmits([
|
||||
"update:modelValue",
|
||||
"update:couponType",
|
||||
"update:couponCode",
|
||||
"update:affiliateCode",
|
||||
])
|
||||
</script>
|
||||
@@ -0,0 +1,65 @@
|
||||
<template>
|
||||
<el-drawer
|
||||
:model-value="modelValue"
|
||||
title="選擇 / 上傳圖片"
|
||||
direction="ltr"
|
||||
size="50%"
|
||||
@update:model-value="$emit('update:modelValue', $event)"
|
||||
>
|
||||
<template #default>
|
||||
<section class="-tw-my-5 tw-space-y-7 tw-h-full">
|
||||
<el-upload drag :http-request="onImageUpload" :show-file-list="false">
|
||||
<el-icon class="el-icon--upload">
|
||||
<upload-filled />
|
||||
</el-icon>
|
||||
<div class="el-upload__text">拖移檔案至此 或 <em>點擊上傳</em></div>
|
||||
</el-upload>
|
||||
<el-card style="height: 73%" :body-style="{ height: '100%' }">
|
||||
<div class="tw-grid tw-grid-cols-4 tw-gap-4 tw-h-full tw-overflow-auto">
|
||||
<div
|
||||
v-for="item in images"
|
||||
:key="item.url"
|
||||
style="height: 150px"
|
||||
>
|
||||
<el-image
|
||||
:src="item.url"
|
||||
fit="fill"
|
||||
:class="[
|
||||
normalizeImageUrl(item.url) === selectedImageUrl ? 'tw-border-4 tw-border-blue-500' : '',
|
||||
'tw-h-full tw-w-full tw-rounded-md',
|
||||
]"
|
||||
loading="lazy"
|
||||
@click="$emit('select', normalizeImageUrl(item.url))"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</section>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
images: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
selectedImageUrl: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
onImageUpload: {
|
||||
type: Function,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
defineEmits(["update:modelValue", "select"])
|
||||
|
||||
const normalizeImageUrl = (url) => url.replace("adapi", "pass")
|
||||
</script>
|
||||
153
frontend/src/module/ose-card/components/OseCardPreviewPanel.vue
Normal file
153
frontend/src/module/ose-card/components/OseCardPreviewPanel.vue
Normal file
@@ -0,0 +1,153 @@
|
||||
<template>
|
||||
<el-main class="tw-h-full">
|
||||
<article class="tw-h-full tw-w-full tw-grid tw-justify-items-center">
|
||||
<div class="tw-h-full tw-space-y-8" style="min-width: 375px; max-width: 375px">
|
||||
<section class="tw-space-x-3 tw-flex tw-justify-between">
|
||||
<span class="tw-font-bold tw-text-2xl">樣式</span>
|
||||
<el-select
|
||||
:model-value="cardStyle"
|
||||
placeholder="Select"
|
||||
size="large"
|
||||
style="width: 240px"
|
||||
@update:model-value="$emit('update:cardStyle', $event)"
|
||||
>
|
||||
<el-option key="1" label="1" value="1" />
|
||||
<el-option key="2" label="2" value="2" />
|
||||
</el-select>
|
||||
</section>
|
||||
|
||||
<section class="preview-card-container tw-space-y-3">
|
||||
<p class="tw-font-bold tw-text-2xl">原始圖卡</p>
|
||||
<div class="card-container">
|
||||
<ose_card v-if="data.card_style == 1" :model-value="data" />
|
||||
<ose_card2 v-else-if="data.card_style == 2" :model-value="data" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="preview-hint-container tw-space-y-3">
|
||||
<p class="tw-font-bold tw-text-2xl">縮小圖卡</p>
|
||||
<div class="tw-space-y-5">
|
||||
<div class="tw-space-y-1">
|
||||
<p class="tw-font-normal tw-text-base">
|
||||
<el-icon size="15" :color="'#2f91ee'">
|
||||
<Iphone />
|
||||
</el-icon>
|
||||
手機版
|
||||
</p>
|
||||
<div class="mini-card-container">
|
||||
<ose_mini_card
|
||||
:timer="data.timer"
|
||||
:minicard_transparency="data.minicard_phone_transparency"
|
||||
:minicard_bg="data.minicard_phone_bg"
|
||||
:minicard_word="data.minicard_phone_word"
|
||||
:minicard_word_color="data.minicard_phone_word_color"
|
||||
:minicard_word_align="data.minicard_phone_word_align"
|
||||
:minicard_timer_start_word="data.minicard_phone_timer_start_word"
|
||||
:minicard_timer_start_word_color="data.minicard_phone_timer_start_word_color"
|
||||
:minicard_timer_end_word="data.minicard_phone_timer_end_word"
|
||||
:minicard_timer_end_word_color="data.minicard_phone_timer_end_word_color"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tw-space-y-1">
|
||||
<p class="tw-font-normal tw-text-base">
|
||||
<el-icon size="15" :color="'#2f91ee'">
|
||||
<Monitor />
|
||||
</el-icon>
|
||||
電腦版
|
||||
</p>
|
||||
<div class="mini-card-container">
|
||||
<ose_mini_card
|
||||
:timer="data.timer"
|
||||
:minicard_transparency="data.minicard_computer_transparency"
|
||||
:minicard_bg="data.minicard_computer_bg"
|
||||
:minicard_word="data.minicard_computer_word"
|
||||
:minicard_word_color="data.minicard_computer_word_color"
|
||||
:minicard_word_align="data.minicard_computer_word_align"
|
||||
:minicard_timer_start_word="data.minicard_computer_timer_start_word"
|
||||
:minicard_timer_start_word_color="data.minicard_computer_timer_start_word_color"
|
||||
:minicard_timer_end_word="data.minicard_computer_timer_end_word"
|
||||
:minicard_timer_end_word_color="data.minicard_computer_timer_end_word_color"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="preview-dynamic-container">
|
||||
<p class="tw-font-bold tw-text-2xl tw-pb-3">實際效果呈現</p>
|
||||
<div>
|
||||
<el-row>
|
||||
<el-col :span="12" class="tw-space-x-3">
|
||||
<span>
|
||||
<el-icon size="15" :color="'#2f91ee'">
|
||||
<Iphone />
|
||||
</el-icon>
|
||||
<span class="tw-text-base tw-font-normal"> 手機版</span>
|
||||
</span>
|
||||
<el-switch
|
||||
:model-value="dynamicPhoneVisible"
|
||||
inline-prompt
|
||||
active-text="是"
|
||||
inactive-text="否"
|
||||
:active-value="true"
|
||||
:inactive-value="false"
|
||||
@update:model-value="$emit('update:dynamicPhoneVisible', $event)"
|
||||
/>
|
||||
</el-col>
|
||||
<el-col :span="12" class="tw-space-x-3">
|
||||
<span>
|
||||
<el-icon size="15" :color="'#2f91ee'">
|
||||
<Monitor />
|
||||
</el-icon>
|
||||
<span class="tw-text-base tw-font-normal"> 電腦版</span>
|
||||
</span>
|
||||
<el-switch
|
||||
:model-value="dynamicComputerVisible"
|
||||
inline-prompt
|
||||
active-text="是"
|
||||
inactive-text="否"
|
||||
:active-value="true"
|
||||
:inactive-value="false"
|
||||
@update:model-value="$emit('update:dynamicComputerVisible', $event)"
|
||||
/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</article>
|
||||
</el-main>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ose_card from "@/module/ose-card/components/card.vue"
|
||||
import ose_card2 from "@/module/ose-card/components/card2.vue"
|
||||
import ose_mini_card from "@/module/ose-card/components/mini-card.vue"
|
||||
|
||||
defineProps({
|
||||
data: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
cardStyle: {
|
||||
type: [String, Number],
|
||||
default: 1,
|
||||
},
|
||||
dynamicPhoneVisible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
dynamicComputerVisible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
defineEmits([
|
||||
"update:cardStyle",
|
||||
"update:dynamicPhoneVisible",
|
||||
"update:dynamicComputerVisible",
|
||||
])
|
||||
</script>
|
||||
@@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<section class="ose-card-table-actions">
|
||||
<el-button size="default" @click="$emit('preview')">預覽</el-button>
|
||||
<el-button size="default" @click="$emit('edit')">編輯</el-button>
|
||||
<el-button size="default" @click="$emit('copy')">複製</el-button>
|
||||
<el-button size="default" @click="$emit('delete')">刪除</el-button>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineEmits(["preview", "edit", "copy", "delete"]);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ose-card-table-actions {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
87
frontend/src/module/ose-card/components/card.vue
Normal file
87
frontend/src/module/ose-card/components/card.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<div class="ose-card"
|
||||
style="overflow: hidden; display: flex; flex-direction: column; width: 100%; min-width: 375px; box-shadow: rgba(100, 100, 111, 0.2) 0px 7px 29px 0px;">
|
||||
<article :class="['count-down-timer']" :hidden="modelValue.timer?false:true" style="">
|
||||
<div class="ose-time_container"
|
||||
:style="`text-align: center; background-color: ${modelValue.timer_bg}; padding-top: 3px; padding-bottom: 3px;
|
||||
display: flex; justify-content: center; align-items: center; font-weight: bold; font-size: 18px;`">
|
||||
<section class="ose-t_description" :style="`margin-right: 0.3em; color: ${modelValue.timer_start_word_color};`">
|
||||
{{ modelValue.timer_start_word }}
|
||||
</section>
|
||||
<section class="ose-t_days"
|
||||
:style="`text-align: center; min-width: 24px; color: ${modelValue.timer_day_number_color};`">00
|
||||
</section>
|
||||
<section class="ose-t_days_unit" :style="`color: ${modelValue.timer_day_word_color};`">
|
||||
{{modelValue.timer_day_word}}
|
||||
</section>
|
||||
<section class="ose-t_hours"
|
||||
:style="`text-align: center; width: 24px; color: ${modelValue.timer_hour_number_color};`">00
|
||||
</section>
|
||||
<section class="ose-t_hours_unit" :style="`margin-right: 0.3em; color: ${modelValue.timer_hour_word_color};`">
|
||||
{{modelValue.timer_hour_word}}
|
||||
</section>
|
||||
<section class="ose-t_minutes"
|
||||
:style="`text-align: center; width: 23px; color: ${modelValue.timer_minute_number_color};`">00
|
||||
</section>
|
||||
<section class="ose-t_minutes_unit" :style="`margin-right: 0.3em; color: ${modelValue.timer_minute_word_color};`">
|
||||
{{ modelValue.timer_minute_word }}
|
||||
</section>
|
||||
<section class="ose-t_seconds"
|
||||
:style="`text-align: center; width: 24px; color: ${modelValue.timer_second_number_color};`">00
|
||||
</section>
|
||||
<section class="ose-t_seconds_unit" :style="`color: ${modelValue.timer_second_word_color};`">
|
||||
{{modelValue.timer_second_word}}
|
||||
</section>
|
||||
<section class="ose-t_description_end" :style="`margin-left: 0.3em; color: ${modelValue.timer_end_word_color};`">
|
||||
{{ modelValue.timer_end_word }}
|
||||
</section>
|
||||
</div>
|
||||
</article>
|
||||
<article class="ose-card-content"
|
||||
style="display: flex; flex-direction: row; overflow: hidden; max-height: 100px">
|
||||
<img v-if="!modelValue.card_img_url"
|
||||
src="@a/ose-logo2.png"
|
||||
class="ose-card-img"
|
||||
:style="`min-width: 100px; max-width: 100px;
|
||||
width: 100px; height: 100px; min-height: 100px; max-height: 100px; background-color:${modelValue.card_img_bg}`">
|
||||
<img v-if="modelValue.card_img_url"
|
||||
:src="modelValue.card_img_url"
|
||||
class="card-img"
|
||||
:style="`min-width: 100px; max-width: 100px;
|
||||
width: 100px; height: 100px; min-height: 100px; max-height: 100px; background-color:${modelValue.card_img_bg}`">
|
||||
|
||||
<section class="ose-card-text" style="width: 100%;">
|
||||
<div class="ose-card-header"
|
||||
:style="`min-height: 1em; background-color: ${modelValue.card_title_bg}; position: relative;`">
|
||||
<h3
|
||||
:style="`text-align: ${modelValue.card_title_word_align} ;color: ${modelValue.card_title_word_color}; padding: 3px 15px 3px 12px; font-size: 20px; margin: 0px
|
||||
; font-weight: bold; line-height: normal;`">
|
||||
{{ modelValue.card_title_word }}
|
||||
</h3>
|
||||
<div class="ose-close-card-btn"
|
||||
:style="`position: absolute; color: ${modelValue.card_cancel_btn_color}; font-style: normal; right: 12px; top: 4px; cursor: pointer;`">
|
||||
✕
|
||||
</div>
|
||||
</div>
|
||||
<section class="ose-card-text-content "
|
||||
:style="` padding-left: 12px; height: 100%; background-color: ${modelValue.card_text_bg}; font-size: 16px;`" v-html="modelValue.card_text_content">
|
||||
</section>
|
||||
</section>
|
||||
</article>
|
||||
<article class="ose-card-footer"
|
||||
:style="`min-height: 1em; background-color: ${modelValue.card_footer_bg};`">
|
||||
<h3
|
||||
:style="`text-align: ${modelValue.card_footer_word_align}; margin: 0px; font-size: 18px; font-weight: bold;
|
||||
letter-spacing: 3px; color:${modelValue.card_footer_word_color}; padding: 5px; line-height: normal;`">
|
||||
{{ modelValue.card_footer_word }}
|
||||
</h3>
|
||||
</article>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {watchEffect , ref} from "vue";
|
||||
|
||||
const props = defineProps(['modelValue'])
|
||||
</script>
|
||||
|
||||
61
frontend/src/module/ose-card/components/card2.vue
Normal file
61
frontend/src/module/ose-card/components/card2.vue
Normal file
@@ -0,0 +1,61 @@
|
||||
<template>
|
||||
<div class="ose-card"
|
||||
style="overflow: hidden; display: flex; flex-direction: column; width: 100%; min-width: 375px; box-shadow: rgba(100, 100, 111, 0.2) 0px 7px 29px 0px;">
|
||||
<article class="ose-card-content"
|
||||
style="display: flex; flex-direction: row; overflow: hidden; max-height: 135px;position: relative">
|
||||
<img v-if="!modelValue.card_img_url"
|
||||
src="@a/ose-logo2.png"
|
||||
class="ose-card-img"
|
||||
:style="`width: 100%; height: auto; background-color:${modelValue.card_img_bg}`">
|
||||
<img v-if="modelValue.card_img_url"
|
||||
:src="modelValue.card_img_url"
|
||||
class="card-img"
|
||||
:style="`width: 100%; height: auto; background-color:${modelValue.card_img_bg}`">
|
||||
<div class="ose-close-card-btn"
|
||||
:style="`position: absolute; color: ${modelValue.card_cancel_btn_color}; font-style: normal; right: 12px; top: 4px; cursor: pointer;`">
|
||||
✕
|
||||
</div>
|
||||
<article :class="['count-down-timer']" :hidden="modelValue.timer?false:true"
|
||||
:style="`position: absolute;left: 0px; right: ${modelValue.timer_x_axis}px; bottom: ${modelValue.timer_y_axis}px; background-color: ${modelValue.timer_bg};`"
|
||||
>
|
||||
<div class="ose-time_container"
|
||||
:style="`text-align: center; background-color: ${modelValue.timer_bg}; padding-top: 3px; padding-bottom: 3px;
|
||||
display: flex; justify-content: center; align-items: center; font-weight: bold; font-size: ${modelValue.timer_font_size}px; gap:${modelValue.timer_space}px`"
|
||||
>
|
||||
<section class="ose-t_days"
|
||||
:style="`text-align: center; min-width: 24px; color: ${modelValue.timer_day_number_color};`">00
|
||||
</section>
|
||||
<section class="ose-t_days_unit" :style="`color: ${modelValue.timer_day_word_color};`">
|
||||
{{ modelValue.timer_day_word }}
|
||||
</section>
|
||||
<section class="ose-t_hours"
|
||||
:style="`text-align: center; color: ${modelValue.timer_hour_number_color};`">00
|
||||
</section>
|
||||
<section class="ose-t_hours_unit" :style="`color: ${modelValue.timer_hour_word_color};`">
|
||||
{{ modelValue.timer_hour_word }}
|
||||
</section>
|
||||
<section class="ose-t_minutes"
|
||||
:style="`text-align: center; color: ${modelValue.timer_minute_number_color};`">00
|
||||
</section>
|
||||
<section class="ose-t_minutes_unit"
|
||||
:style="`color: ${modelValue.timer_minute_word_color};`">
|
||||
{{ modelValue.timer_minute_word }}
|
||||
</section>
|
||||
<section class="ose-t_seconds"
|
||||
:style="`text-align: center; color: ${modelValue.timer_second_number_color};`">00
|
||||
</section>
|
||||
<section class="ose-t_seconds_unit" :style="`color: ${modelValue.timer_second_word_color};`">
|
||||
{{ modelValue.timer_second_word }}
|
||||
</section>
|
||||
</div>
|
||||
</article>
|
||||
</article>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {watchEffect, ref} from "vue";
|
||||
|
||||
const props = defineProps(['modelValue'])
|
||||
</script>
|
||||
|
||||
48
frontend/src/module/ose-card/components/mini-card.vue
Normal file
48
frontend/src/module/ose-card/components/mini-card.vue
Normal file
@@ -0,0 +1,48 @@
|
||||
<template>
|
||||
<div class="ose-mini-card"
|
||||
:style="`width: 100%; min-height: 1em; background-color: ${minicard_bg};
|
||||
color: ${minicard_word_color}; text-align: ${minicard_word_align};
|
||||
cursor: pointer; padding: 5px 0px; font-weight: bold; font-size: 14px; opacity:${minicard_transparency}`">
|
||||
<div class="ose-mini-card-text-container"> {{ minicard_word }}</div>
|
||||
<article :class="['count-down-timer']" :hidden="timer?false:true" style="">
|
||||
<div class="ose-time_container"
|
||||
style="text-align: center; background-color: transparent; padding-top: 3px; padding-bottom: 3px;
|
||||
display: flex; justify-content: center; align-items: center; font-weight: bold; font-size: 14px;">
|
||||
<section class="ose-t_description" :style="`margin-right: 0.3em; color:${minicard_timer_start_word_color};`"> {{minicard_timer_start_word}}
|
||||
</section>
|
||||
<section class="ose-t_days"
|
||||
style="text-align: center; min-width: 24px; color: rgb(255, 255, 255);">00
|
||||
</section>
|
||||
<section class="ose-t_days_unit" style="color: rgb(255, 255, 255);"> 日</section>
|
||||
<section class="ose-t_hours"
|
||||
style="text-align: center; width: 24px; color: rgb(255, 255, 255);">00
|
||||
</section>
|
||||
<section class="ose-t_hours_unit" style="margin-right: 0.3em; color: rgb(255, 255, 255);"> 小時
|
||||
</section>
|
||||
<section class="ose-t_minutes"
|
||||
style="text-align: center; width: 23px; color: rgb(255, 255, 255);">00
|
||||
</section>
|
||||
<section class="ose-t_minutes_unit" style="margin-right: 0.3em; color: rgb(255, 255, 255);">
|
||||
分
|
||||
</section>
|
||||
<section class="ose-t_seconds"
|
||||
style="text-align: center; width: 24px; color: rgb(255, 255, 255);">00
|
||||
</section>
|
||||
<section class="ose-t_seconds_unit" style="color: rgb(255, 255, 255);"> 秒</section>
|
||||
<section class="ose-t_description_end" :style="`margin-left: 0.3em; color: ${minicard_timer_end_word_color};`"> {{minicard_timer_end_word}}
|
||||
</section>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps([
|
||||
'minicard_word_color','minicard_bg','minicard_word_align','minicard_word','minicard_transparency',
|
||||
'minicard_timer_start_word','minicard_timer_start_word_color','minicard_timer_end_word','minicard_timer_end_word_color','timer'
|
||||
])
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
67
frontend/src/module/ose-card/model/table.js
Normal file
67
frontend/src/module/ose-card/model/table.js
Normal file
@@ -0,0 +1,67 @@
|
||||
export default [
|
||||
{
|
||||
id:"id",
|
||||
name:"序列",
|
||||
header_setup:{
|
||||
sortable:true,
|
||||
"show-overflow-tooltip":false,
|
||||
width:"100",
|
||||
}
|
||||
},
|
||||
{
|
||||
id:"card_name",
|
||||
name:"圖卡名稱",
|
||||
header_setup:{
|
||||
sortable:false,
|
||||
"show-overflow-tooltip":true,
|
||||
width:"280"
|
||||
}
|
||||
},
|
||||
{
|
||||
id:"card_code",
|
||||
name:"圖卡代碼",
|
||||
header_setup:{
|
||||
sortable:true,
|
||||
"show-overflow-tooltip":false,
|
||||
width:"150"
|
||||
}
|
||||
},
|
||||
{
|
||||
id:"start_date",
|
||||
name:"開始時間",
|
||||
header_setup:{
|
||||
sortable:true,
|
||||
"show-overflow-tooltip":false,
|
||||
width:"180"
|
||||
}
|
||||
},
|
||||
{
|
||||
id:"end_date",
|
||||
name:"結束時間",
|
||||
header_setup:{
|
||||
sortable:true,
|
||||
"show-overflow-tooltip":false,
|
||||
width:"180"
|
||||
}
|
||||
},
|
||||
{
|
||||
id:"carousel_days",
|
||||
name:"循環天數",
|
||||
header_setup:{
|
||||
sortable:true,
|
||||
"show-overflow-tooltip":false,
|
||||
width:"110"
|
||||
}
|
||||
},
|
||||
{
|
||||
id:"origin",
|
||||
name:"網域",
|
||||
header_setup:{
|
||||
sortable:true,
|
||||
"show-overflow-tooltip":false,
|
||||
width:"250"
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
]
|
||||
102
frontend/src/module/ose-card/service/card-service.js
Normal file
102
frontend/src/module/ose-card/service/card-service.js
Normal file
@@ -0,0 +1,102 @@
|
||||
import {
|
||||
createItem,
|
||||
deleteItem,
|
||||
readFiles,
|
||||
readFolders,
|
||||
readItems,
|
||||
updateFile,
|
||||
updateItem,
|
||||
uploadFiles,
|
||||
} from "@directus/sdk";
|
||||
import { buildDirectusAssetUrl } from "@/config/env";
|
||||
|
||||
export const listCards = async (client, corporateCustomerId) => {
|
||||
return await client.request(
|
||||
readItems("marketing_card", {
|
||||
filter: {
|
||||
corporate_customer: {
|
||||
_eq: corporateCustomerId,
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
export const deleteCardById = async (client, id) => {
|
||||
return await client.request(deleteItem("marketing_card", id));
|
||||
};
|
||||
|
||||
export const listCardsByFilter = async (client, filter) => {
|
||||
return await client.request(
|
||||
readItems("marketing_card", {
|
||||
filter,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
export const saveCardItem = async (client, options) => {
|
||||
if (options.mode === "edit") {
|
||||
return await client.request(
|
||||
updateItem("marketing_card", options.id, options.item)
|
||||
);
|
||||
}
|
||||
|
||||
return await client.request(createItem("marketing_card", options.item));
|
||||
};
|
||||
|
||||
export const listCardImages = async (client) => {
|
||||
const files = await client.request(
|
||||
readFiles({
|
||||
filter: {
|
||||
type: {
|
||||
_contains: "image",
|
||||
},
|
||||
folder: {
|
||||
name: {
|
||||
_eq: "OSE Card",
|
||||
},
|
||||
},
|
||||
},
|
||||
fields: ["id"],
|
||||
sort: ["-uploaded_on"],
|
||||
})
|
||||
);
|
||||
|
||||
return files.map((item) => ({
|
||||
url: buildDirectusAssetUrl(item.id),
|
||||
}));
|
||||
};
|
||||
|
||||
export const getCardFolderId = async (client) => {
|
||||
const folders = await client.request(
|
||||
readFolders({
|
||||
fields: ["id"],
|
||||
filter: {
|
||||
name: "OSE Card",
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
return folders[0]?.id || null;
|
||||
};
|
||||
|
||||
export const moveCardFileToFolder = async (client, fileId, folderId) => {
|
||||
return await client.request(
|
||||
updateFile(fileId, {
|
||||
folder: folderId,
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
export const uploadCardImage = async (client, file, folderId) => {
|
||||
const result = await client.request(uploadFiles(file));
|
||||
if (folderId) {
|
||||
await moveCardFileToFolder(client, result.id, folderId);
|
||||
}
|
||||
|
||||
return {
|
||||
id: result.id,
|
||||
url: buildDirectusAssetUrl(result.id),
|
||||
};
|
||||
};
|
||||
|
||||
87
frontend/src/module/ose-card/service/card-url.js
Normal file
87
frontend/src/module/ose-card/service/card-url.js
Normal file
@@ -0,0 +1,87 @@
|
||||
const buildUrl = (landingPage) => {
|
||||
try {
|
||||
return new URL(landingPage);
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const buildOseCardPreviewUrl = (landingPage, cardCode) => {
|
||||
const url = buildUrl(landingPage);
|
||||
|
||||
if (!url) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (cardCode) {
|
||||
url.searchParams.set("ose-card", cardCode);
|
||||
}
|
||||
|
||||
return url.toString();
|
||||
};
|
||||
|
||||
export const buildOseCardCouponUrl = (
|
||||
landingPage,
|
||||
cardCode,
|
||||
couponType,
|
||||
couponCode,
|
||||
affiliateCode
|
||||
) => {
|
||||
const url = buildUrl(landingPage);
|
||||
|
||||
if (!url) {
|
||||
return "";
|
||||
}
|
||||
|
||||
switch (couponType) {
|
||||
case "優惠代碼":
|
||||
if (couponCode) {
|
||||
url.searchParams.set("oc", couponCode);
|
||||
}
|
||||
break;
|
||||
case "推薦代碼":
|
||||
if (affiliateCode) {
|
||||
url.searchParams.set("ac", affiliateCode);
|
||||
}
|
||||
break;
|
||||
case "優惠代碼+推薦代碼":
|
||||
if (couponCode) {
|
||||
url.searchParams.set("oc", couponCode);
|
||||
}
|
||||
if (affiliateCode) {
|
||||
url.searchParams.set("ac", affiliateCode);
|
||||
}
|
||||
break;
|
||||
case "行銷圖卡+優惠代碼":
|
||||
if (cardCode) {
|
||||
url.searchParams.set("mc", cardCode);
|
||||
}
|
||||
if (couponCode) {
|
||||
url.searchParams.set("oc", couponCode);
|
||||
}
|
||||
break;
|
||||
case "行銷圖卡+推薦代碼":
|
||||
if (cardCode) {
|
||||
url.searchParams.set("mc", cardCode);
|
||||
}
|
||||
if (affiliateCode) {
|
||||
url.searchParams.set("ac", affiliateCode);
|
||||
}
|
||||
break;
|
||||
case "行銷圖卡+優惠代碼+推薦代碼":
|
||||
if (cardCode) {
|
||||
url.searchParams.set("mc", cardCode);
|
||||
}
|
||||
if (couponCode) {
|
||||
url.searchParams.set("oc", couponCode);
|
||||
}
|
||||
if (affiliateCode) {
|
||||
url.searchParams.set("ac", affiliateCode);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
|
||||
return url.toString();
|
||||
};
|
||||
104
frontend/src/module/ose-card/service/timer.js
Normal file
104
frontend/src/module/ose-card/service/timer.js
Normal file
@@ -0,0 +1,104 @@
|
||||
function querySelectorAllForEachInnerHtml(documents, value) {
|
||||
documents.forEach((dom) => {
|
||||
dom.innerText = value
|
||||
})
|
||||
}
|
||||
|
||||
function getCountDownUnit(distance) {
|
||||
|
||||
var days = Math.floor(distance / (1000 * 60 * 60 * 24));
|
||||
var hours = Math.floor((distance % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
|
||||
var minutes = Math.floor((distance % (1000 * 60 * 60)) / (1000 * 60));
|
||||
var seconds = Math.floor((distance % (1000 * 60)) / 1000);
|
||||
days = days.toString()
|
||||
if (days.length === 1) {
|
||||
days = '0' + days
|
||||
}
|
||||
hours = hours.toString()
|
||||
if (hours.length === 1) {
|
||||
hours = '0' + hours
|
||||
}
|
||||
minutes = minutes.toString()
|
||||
if (minutes.length === 1) {
|
||||
minutes = '0' + minutes
|
||||
}
|
||||
seconds = seconds.toString()
|
||||
if (seconds.length === 1) {
|
||||
seconds = '0' + seconds
|
||||
}
|
||||
|
||||
var result = {
|
||||
t_days: days,
|
||||
t_hours: hours,
|
||||
t_minutes: minutes,
|
||||
t_seconds: seconds
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function getCarouselDateDistance(now,end_at,carousel_days){
|
||||
var distance_days = Math.ceil((now-end_at) / (1000 * 60 * 60 * 24))
|
||||
var freq = Math.ceil(distance_days/carousel_days)
|
||||
var increase_days_times = freq * carousel_days * (1000 * 60 * 60 * 24)
|
||||
var distance = end_at + increase_days_times - now
|
||||
return distance
|
||||
}
|
||||
|
||||
function initTimer(start_at, end_at, init,carousel_days) {
|
||||
|
||||
var init_now = new Date().getTime();
|
||||
var init_distance = end_at - init_now;
|
||||
if ((init_distance < 0 && (carousel_days == 0 || !carousel_days)) || init_now < start_at || init) {
|
||||
querySelectorAllForEachInnerHtml(document.querySelectorAll(".ose-t_days"), '00')
|
||||
querySelectorAllForEachInnerHtml(document.querySelectorAll(".ose-t_hours"), '00')
|
||||
querySelectorAllForEachInnerHtml(document.querySelectorAll(".ose-t_minutes"), '00')
|
||||
querySelectorAllForEachInnerHtml(document.querySelectorAll(".ose-t_seconds"), '00')
|
||||
return null
|
||||
}
|
||||
|
||||
/*if (init_now < start_at) {
|
||||
init_distance = end_at - start_at;
|
||||
}
|
||||
|
||||
var init_show_units = getCountDownUnit(init_distance)
|
||||
var init_units = Object.keys(init_show_units)
|
||||
init_units.forEach(function(unit){
|
||||
querySelectorAllForEachInnerHtml(document.querySelectorAll("."+unit),init_show_units[unit])
|
||||
})
|
||||
if (init_now < start_at) {
|
||||
return null
|
||||
}*/
|
||||
|
||||
|
||||
var timer = setInterval(function () {
|
||||
var now = new Date().getTime();
|
||||
|
||||
var distance = end_at - now;
|
||||
|
||||
if (distance < 0 && (carousel_days == 0 || !carousel_days)) {
|
||||
clearInterval(timer);
|
||||
querySelectorAllForEachInnerHtml(document.querySelectorAll(".ose-t_days"), '00')
|
||||
querySelectorAllForEachInnerHtml(document.querySelectorAll(".ose-t_hours"), '00')
|
||||
querySelectorAllForEachInnerHtml(document.querySelectorAll(".ose-t_minutes"), '00')
|
||||
querySelectorAllForEachInnerHtml(document.querySelectorAll(".ose-t_seconds"), '00')
|
||||
return null
|
||||
}
|
||||
else if(distance < 0 && carousel_days > 0){
|
||||
distance = getCarouselDateDistance(now,end_at,carousel_days)
|
||||
}
|
||||
|
||||
var show_units = getCountDownUnit(distance)
|
||||
//console.log(show_units)
|
||||
var units = Object.keys(show_units)
|
||||
units.forEach(function (unit) {
|
||||
querySelectorAllForEachInnerHtml(document.querySelectorAll(".ose-" + unit), show_units[unit])
|
||||
})
|
||||
|
||||
}, 1000);
|
||||
return timer
|
||||
}
|
||||
|
||||
|
||||
export default {
|
||||
initTimer
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { ref, watch } from "vue"
|
||||
|
||||
import timer from "@/module/ose-card/service/timer"
|
||||
|
||||
export function useOseCardDynamicPreview({ data, store }) {
|
||||
const dynamicPhoneVisible = ref(false)
|
||||
const dynamicComputerVisible = ref(false)
|
||||
|
||||
const initializeDynamicCard = (cardContainerClassName, miniCardContainerClassName, isPhone) => {
|
||||
if (store.state.ose_card_store.timer_id) {
|
||||
clearInterval(store.state.ose_card_store.timer_id)
|
||||
}
|
||||
|
||||
timer.initTimer(new Date(data.start_date).getTime(), new Date(data.end_date).getTime(), true)
|
||||
|
||||
const dynamicPhoneContainer = document.querySelector(".dynamic_phone_container")
|
||||
const dynamicComputerContainer = document.querySelector(".dynamic_computer_container")
|
||||
const dynamicCardContainer = document.querySelector(`.${cardContainerClassName}`)
|
||||
const miniDynamicCardContainer = document.querySelector(`.${miniCardContainerClassName}`)
|
||||
|
||||
if (isPhone) {
|
||||
dynamicCardContainer.style.display = "none"
|
||||
miniDynamicCardContainer.style.display = "none"
|
||||
} else {
|
||||
dynamicCardContainer.style.display = ""
|
||||
miniDynamicCardContainer.style.display = "none"
|
||||
}
|
||||
|
||||
data.dynamic_phone_container = dynamicPhoneContainer.outerHTML
|
||||
data.dynamic_computer_container = dynamicComputerContainer.outerHTML
|
||||
dynamicCardContainer.style.display = ""
|
||||
miniDynamicCardContainer.style.display = "none"
|
||||
}
|
||||
|
||||
const bindDynamicCardControl = (cardContainerClassName, miniCardContainerClassName) => {
|
||||
const dynamicCardContainer = document.querySelector(`.${cardContainerClassName}`)
|
||||
const miniDynamicCardContainer = document.querySelector(`.${miniCardContainerClassName}`)
|
||||
const closeButton = dynamicCardContainer?.querySelector(".ose-close-card-btn")
|
||||
|
||||
closeButton?.addEventListener("click", () => {
|
||||
dynamicCardContainer.style.display = "none"
|
||||
miniDynamicCardContainer.style.display = ""
|
||||
})
|
||||
|
||||
miniDynamicCardContainer?.addEventListener("click", () => {
|
||||
dynamicCardContainer.style.display = ""
|
||||
miniDynamicCardContainer.style.display = "none"
|
||||
})
|
||||
}
|
||||
|
||||
const prepareDynamicPreviewForSave = () => {
|
||||
initializeDynamicCard("dynamic_computer_card_container", "mini_dynamic_computer_card_container")
|
||||
initializeDynamicCard("dynamic_phone_card_container", "mini_dynamic_phone_card_container", true)
|
||||
}
|
||||
|
||||
watch(() => dynamicComputerVisible.value, () => {
|
||||
bindDynamicCardControl("dynamic_computer_card_container", "mini_dynamic_computer_card_container")
|
||||
})
|
||||
|
||||
watch(() => dynamicPhoneVisible.value, () => {
|
||||
bindDynamicCardControl("dynamic_phone_card_container", "mini_dynamic_phone_card_container")
|
||||
})
|
||||
|
||||
watch(
|
||||
() => [data.start_date, data.end_date, data.carousel_days],
|
||||
() => {
|
||||
if (data.start_date && data.end_date) {
|
||||
if (store.state.ose_card_store.timer_id) {
|
||||
clearInterval(store.state.ose_card_store.timer_id)
|
||||
}
|
||||
|
||||
store.state.ose_card_store.timer_id = timer.initTimer(
|
||||
new Date(data.start_date).getTime(),
|
||||
new Date(data.end_date).getTime(),
|
||||
null,
|
||||
data.carousel_days
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => data.landing_page,
|
||||
() => {
|
||||
try {
|
||||
data.origin = new URL(data.landing_page).origin
|
||||
} catch (error) {
|
||||
// Ignore malformed URLs while the user is still typing.
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
dynamicPhoneVisible,
|
||||
dynamicComputerVisible,
|
||||
prepareDynamicPreviewForSave,
|
||||
}
|
||||
}
|
||||
108
frontend/src/module/ose-card/service/use-ose-card-edit-page.js
Normal file
108
frontend/src/module/ose-card/service/use-ose-card-edit-page.js
Normal file
@@ -0,0 +1,108 @@
|
||||
import { ref } from "vue"
|
||||
|
||||
import { ElMessage } from "element-plus"
|
||||
|
||||
export function useOseCardEditPage({ data, props, query, router, store, beforeSave }) {
|
||||
const previewButtonDisabled = ref(true)
|
||||
const pageLoading = ref(false)
|
||||
|
||||
const initializePage = async () => {
|
||||
for (const key in props) {
|
||||
data[key] = props[key]
|
||||
}
|
||||
|
||||
if (store.state.ose_card_store.timer_id) {
|
||||
clearInterval(store.state.ose_card_store.timer_id)
|
||||
}
|
||||
|
||||
if (query.id) {
|
||||
for (const key in data) {
|
||||
data[key] = store.state.ose_card_store.edit_card_data[key]
|
||||
}
|
||||
}
|
||||
|
||||
data.corporate_customer = store.state.user.current_corporate_customer
|
||||
|
||||
if (query.mode === "edit") {
|
||||
previewButtonDisabled.value = false
|
||||
}
|
||||
|
||||
if (store.state.ose_card_store.images.length === 0) {
|
||||
await store.dispatch("ose_card_store/getImages")
|
||||
}
|
||||
}
|
||||
|
||||
const goBack = () => {
|
||||
router.go(-1)
|
||||
}
|
||||
|
||||
const saveRecord = async () => {
|
||||
pageLoading.value = true
|
||||
|
||||
const params = {
|
||||
mode: query.mode,
|
||||
id: query.id,
|
||||
item: data,
|
||||
}
|
||||
|
||||
if (!data.card_name || !data.landing_page || !data.card_code || !data.start_date || !data.end_date) {
|
||||
ElMessage({
|
||||
showClose: true,
|
||||
message: "【圖卡名稱、LandingPage、圖卡代碼、開始/結束時間】不得為空。",
|
||||
type: "error",
|
||||
})
|
||||
pageLoading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
const filterResult = await store.dispatch("ose_card_store/saveCardFilter", params)
|
||||
|
||||
if (filterResult.length > 0) {
|
||||
ElMessage({
|
||||
showClose: true,
|
||||
message: "該圖卡有重複到【LandingPage、圖卡代碼、開始/結束時間】,請再次檢查。",
|
||||
type: "warning",
|
||||
})
|
||||
pageLoading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
if (beforeSave) {
|
||||
beforeSave()
|
||||
}
|
||||
|
||||
await store.dispatch("ose_card_store/saveCard", params)
|
||||
|
||||
ElMessage({
|
||||
showClose: true,
|
||||
message: "圖卡保存完成!",
|
||||
type: "success",
|
||||
})
|
||||
|
||||
if (query.mode !== "edit") {
|
||||
router.go(-1)
|
||||
}
|
||||
|
||||
pageLoading.value = false
|
||||
}
|
||||
|
||||
const applyColorChange = (target, color) => {
|
||||
if (typeof target === "string") {
|
||||
data[target] = color
|
||||
return
|
||||
}
|
||||
|
||||
for (const key of target) {
|
||||
data[key] = color
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
pageLoading,
|
||||
previewButtonDisabled,
|
||||
initializePage,
|
||||
goBack,
|
||||
saveRecord,
|
||||
applyColorChange,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { computed, ref } from "vue"
|
||||
|
||||
import { buildOseCardCouponUrl, buildOseCardPreviewUrl } from "@/module/ose-card/service/card-url"
|
||||
|
||||
export function useOseCardEditSupport({ data, store }) {
|
||||
const imageDrawerVisible = ref(false)
|
||||
const couponDrawerVisible = ref(false)
|
||||
const couponType = ref("")
|
||||
const couponForm = ref({
|
||||
coupon_code: "",
|
||||
affiliate_code: "",
|
||||
})
|
||||
|
||||
const cardCodeUrl = computed(() =>
|
||||
buildOseCardCouponUrl(
|
||||
data.landing_page,
|
||||
data.card_code,
|
||||
couponType.value,
|
||||
couponForm.value.coupon_code,
|
||||
couponForm.value.affiliate_code
|
||||
)
|
||||
)
|
||||
|
||||
const openImageDrawer = () => {
|
||||
imageDrawerVisible.value = true
|
||||
}
|
||||
|
||||
const openCouponDrawer = () => {
|
||||
couponDrawerVisible.value = true
|
||||
}
|
||||
|
||||
const previewCard = () => {
|
||||
window.open(buildOseCardPreviewUrl(data.landing_page, data.card_code))
|
||||
}
|
||||
|
||||
// Keep upload and image selection side effects close to the drawer state,
|
||||
// so the main edit page can focus on the card form itself.
|
||||
const uploadImage = async (event) => {
|
||||
const form = new FormData()
|
||||
form.append("file", event.file)
|
||||
await store.dispatch("ose_card_store/uploadImages", form)
|
||||
}
|
||||
|
||||
const selectImage = (imageUrl) => {
|
||||
data.card_img_url = imageUrl
|
||||
}
|
||||
|
||||
return {
|
||||
imageDrawerVisible,
|
||||
couponDrawerVisible,
|
||||
couponType,
|
||||
couponForm,
|
||||
cardCodeUrl,
|
||||
openImageDrawer,
|
||||
openCouponDrawer,
|
||||
previewCard,
|
||||
uploadImage,
|
||||
selectImage,
|
||||
}
|
||||
}
|
||||
115
frontend/src/module/ose-card/service/use-ose-card-list-page.js
Normal file
115
frontend/src/module/ose-card/service/use-ose-card-list-page.js
Normal file
@@ -0,0 +1,115 @@
|
||||
import { computed, ref } from "vue";
|
||||
import { ElMessage, ElMessageBox } from "element-plus";
|
||||
import { buildOseCardPreviewUrl } from "@/module/ose-card/service/card-url";
|
||||
|
||||
export const useOseCardListPage = ({ store, router, tableColumns }) => {
|
||||
const search = ref("");
|
||||
const carouselDaysSearch = ref(false);
|
||||
const tableLoad = ref(true);
|
||||
|
||||
const filterTableData = computed(() => {
|
||||
let searchResult = store.state.ose_card_store.card_list;
|
||||
|
||||
if (search.value) {
|
||||
searchResult = searchResult.filter((data) => {
|
||||
for (const column of tableColumns) {
|
||||
try {
|
||||
if (
|
||||
data[column.id]
|
||||
.toLowerCase()
|
||||
.includes(search.value.toLowerCase())
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
if (carouselDaysSearch.value) {
|
||||
searchResult = searchResult.filter((data) => data.carousel_days > 0);
|
||||
}
|
||||
|
||||
return searchResult;
|
||||
});
|
||||
|
||||
const openEditPage = async (queryParams) => {
|
||||
tableLoad.value = true;
|
||||
await router.push({
|
||||
name: "ose-card-edit",
|
||||
query: queryParams,
|
||||
});
|
||||
tableLoad.value = false;
|
||||
};
|
||||
|
||||
const loadCardList = async () => {
|
||||
tableLoad.value = true;
|
||||
await store.dispatch(
|
||||
"ose_card_store/getCardList",
|
||||
store.state.user.current_corporate_customer
|
||||
);
|
||||
tableLoad.value = false;
|
||||
};
|
||||
|
||||
const previewCard = (rowItem) => {
|
||||
window.open(buildOseCardPreviewUrl(rowItem.landing_page, rowItem.card_code));
|
||||
};
|
||||
|
||||
const editCard = (rowItem) => {
|
||||
store.commit("ose_card_store/edit_card_data", rowItem);
|
||||
openEditPage({
|
||||
id: rowItem.id,
|
||||
mode: "edit",
|
||||
});
|
||||
};
|
||||
|
||||
const copyCard = (rowItem) => {
|
||||
store.commit("ose_card_store/edit_card_data", rowItem);
|
||||
openEditPage({
|
||||
id: rowItem.id,
|
||||
mode: "copy",
|
||||
});
|
||||
};
|
||||
|
||||
const deleteCard = async (rowItem) => {
|
||||
try {
|
||||
await ElMessageBox.confirm("請再次確認是否該刪除?", "注意", {
|
||||
confirmButtonText: "確認",
|
||||
cancelButtonText: "取消",
|
||||
type: "warning",
|
||||
});
|
||||
|
||||
tableLoad.value = true;
|
||||
await store.dispatch("ose_card_store/deleteCard", rowItem.id);
|
||||
tableLoad.value = false;
|
||||
|
||||
ElMessage({
|
||||
type: "success",
|
||||
message: "刪除成功",
|
||||
});
|
||||
} catch (error) {
|
||||
tableLoad.value = false;
|
||||
ElMessage({
|
||||
type: "info",
|
||||
message: "刪除取消",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
search,
|
||||
carouselDaysSearch,
|
||||
tableLoad,
|
||||
filterTableData,
|
||||
openEditPage,
|
||||
loadCardList,
|
||||
previewCard,
|
||||
editCard,
|
||||
copyCard,
|
||||
deleteCard,
|
||||
};
|
||||
};
|
||||
121
frontend/src/module/ose-card/store/ose_card_store.js
Normal file
121
frontend/src/module/ose-card/store/ose_card_store.js
Normal file
@@ -0,0 +1,121 @@
|
||||
import {
|
||||
deleteCardById,
|
||||
getCardFolderId,
|
||||
listCardImages,
|
||||
listCards,
|
||||
listCardsByFilter,
|
||||
moveCardFileToFolder,
|
||||
saveCardItem,
|
||||
uploadCardImage,
|
||||
} from "@/module/ose-card/service/card-service";
|
||||
|
||||
export default {
|
||||
namespaced: true,
|
||||
state: {
|
||||
timer_id: null,
|
||||
card_list: [],
|
||||
edit_card_data: JSON.parse(sessionStorage.getItem('edit_card_data')) || {},
|
||||
images: []
|
||||
},
|
||||
actions: {
|
||||
async getCardList({rootState, commit, dispatch}, corporate_customer_id) {
|
||||
const client = rootState.directus.client
|
||||
const result = await listCards(client, corporate_customer_id);
|
||||
commit("card_list", result)
|
||||
},
|
||||
async deleteCard({rootState, commit, dispatch}, id) {
|
||||
const client = rootState.directus.client
|
||||
await deleteCardById(client, id);
|
||||
await dispatch("getCardList", rootState.user.current_corporate_customer)
|
||||
},
|
||||
async saveCardFilter({rootState, commit, dispatch}, options) {
|
||||
let start_date = new Date(options.item.start_date)
|
||||
start_date.setHours(new Date(options.item.start_date).getHours()+8)
|
||||
let end_date = new Date(options.item.end_date)
|
||||
end_date.setHours(new Date(options.item.end_date).getHours()+8)
|
||||
let filter = {
|
||||
_and: [
|
||||
{
|
||||
origin: {
|
||||
_eq: options.item.origin
|
||||
}
|
||||
},
|
||||
{
|
||||
card_code: {
|
||||
_eq: options.item.card_code
|
||||
}
|
||||
},
|
||||
{
|
||||
_or: [
|
||||
{
|
||||
start_date: {
|
||||
_between: [
|
||||
start_date.toISOString(),
|
||||
end_date.toISOString()
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
end_date: {
|
||||
_between: [
|
||||
start_date.toISOString(),
|
||||
end_date.toISOString()
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
if (options.mode == 'edit')
|
||||
filter._and.push({
|
||||
id: {
|
||||
_neq: options.id
|
||||
}
|
||||
})
|
||||
const client = rootState.directus.client
|
||||
const result = await listCardsByFilter(client, filter);
|
||||
return result
|
||||
},
|
||||
async saveCard({rootState, commit, dispatch}, options) {
|
||||
const client = rootState.directus.client
|
||||
return await saveCardItem(client, options)
|
||||
},
|
||||
async getImages({rootState, commit, dispatch}) {
|
||||
const client = rootState.directus.client
|
||||
const result = await listCardImages(client)
|
||||
commit("images", result)
|
||||
},
|
||||
async getFolder({rootState, commit, dispatch}) {
|
||||
const client = rootState.directus.client
|
||||
return await getCardFolderId(client)
|
||||
},
|
||||
async changeFileFolder({rootState, commit, dispatch}, fileId) {
|
||||
const client = rootState.directus.client
|
||||
const folderId = await dispatch('getFolder')
|
||||
await moveCardFileToFolder(client, fileId, folderId)
|
||||
},
|
||||
async uploadImages({rootState, commit, dispatch}, file) {
|
||||
const client = rootState.directus.client
|
||||
const folderId = await dispatch('getFolder')
|
||||
const result = await uploadCardImage(client, file, folderId)
|
||||
commit("addNewImage", {url: result.url})
|
||||
}
|
||||
},
|
||||
mutations: {
|
||||
card_list(state, val) {
|
||||
state.card_list = val
|
||||
},
|
||||
edit_card_data(state, val) {
|
||||
state.edit_card_data = val
|
||||
},
|
||||
images(state, val) {
|
||||
state.images = val
|
||||
},
|
||||
addNewImage(state, val) {
|
||||
state.images.unshift(val)
|
||||
}
|
||||
|
||||
},
|
||||
getters: {}
|
||||
}
|
||||
1917
frontend/src/module/ose-card/view/edit.vue
Normal file
1917
frontend/src/module/ose-card/view/edit.vue
Normal file
File diff suppressed because it is too large
Load Diff
102
frontend/src/module/ose-card/view/main.vue
Normal file
102
frontend/src/module/ose-card/view/main.vue
Normal file
@@ -0,0 +1,102 @@
|
||||
<template>
|
||||
<el-main class="tw-h-full">
|
||||
<el-row class="tw-h-full">
|
||||
<el-col class="tw-h-full">
|
||||
<el-card shadow="never" class="tw-h-full" body-class="tw-h-full">
|
||||
<section class="tw-space-y-6">
|
||||
<legacy-module-banner
|
||||
title="ose-card 目前仍是 legacy 行銷圖卡模組"
|
||||
description="它會先維持營運可用,之後再對照新的 experiment / editor 主線逐步重構;這一區目前不作為新功能主入口。"
|
||||
/>
|
||||
|
||||
<section class="tw-flex tw-items-start tw-justify-between tw-gap-6">
|
||||
<div class="tw-space-y-2">
|
||||
<p class="tw-text-sm tw-font-medium tw-uppercase tw-tracking-[0.2em] tw-text-slate-400">
|
||||
OSE Card
|
||||
</p>
|
||||
<div class="tw-space-y-1">
|
||||
<h2 class="tw-text-3xl tw-font-bold tw-text-slate-900">行銷圖卡管理</h2>
|
||||
<p class="tw-text-sm tw-leading-6 tw-text-slate-500">
|
||||
先維持既有圖卡模組可用,同時把列表、編輯與預覽流程逐步收斂成更乾淨的模組邊界。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-button type="primary" size="large" :color="'#2f91ee'" @click="openEditPage()" plain>
|
||||
新增設定檔
|
||||
</el-button>
|
||||
</section>
|
||||
|
||||
<el-row class="tw-items-center">
|
||||
<el-col :span="12" class="tw-flex tw-gap-6 tw-items-center">
|
||||
<el-input
|
||||
v-model="search"
|
||||
size="large"
|
||||
placeholder="搜尋"
|
||||
suffix-icon="Search"/>
|
||||
<p class="tw-w-full">
|
||||
查詢開啟循環天數
|
||||
<el-switch v-model="carouselDaysSearch" aria-label="test"/>
|
||||
</p>
|
||||
|
||||
</el-col>
|
||||
</el-row>
|
||||
</section>
|
||||
<el-divider></el-divider>
|
||||
<el-table stripe border style="height: 87%" :data="filterTableData" v-loading="tableLoad"
|
||||
:default-sort="{prop:'id',order:'descending'}">
|
||||
<el-table-column v-for="(item,index) in table" :prop="item.id" :label="item.name"
|
||||
:width="item.header_setup.width" :sortable="item.header_setup.sortable"
|
||||
:show-overflow-tooltip="item.header_setup['show-overflow-tooltip']"/>
|
||||
<el-table-column fixed="right" label="操作" width="150">
|
||||
<template #default="scope">
|
||||
<ose-card-table-actions
|
||||
@preview="previewCard(scope.row)"
|
||||
@edit="editCard(scope.row)"
|
||||
@copy="copyCard(scope.row)"
|
||||
@delete="deleteCard(scope.row)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
</el-table>
|
||||
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-main>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import LegacyModuleBanner from "@/components/LegacyModuleBanner.vue";
|
||||
import OseCardTableActions from "@/module/ose-card/components/OseCardTableActions.vue";
|
||||
import {useRouter} from "vue-router";
|
||||
import {useStore} from "vuex";
|
||||
import table from "@/module/ose-card/model/table";
|
||||
import { useOseCardListPage } from "@/module/ose-card/service/use-ose-card-list-page";
|
||||
|
||||
const store = useStore();
|
||||
const router = useRouter()
|
||||
const {
|
||||
search,
|
||||
carouselDaysSearch,
|
||||
tableLoad,
|
||||
filterTableData,
|
||||
openEditPage,
|
||||
loadCardList,
|
||||
previewCard,
|
||||
editCard,
|
||||
copyCard,
|
||||
deleteCard,
|
||||
} = useOseCardListPage({
|
||||
store,
|
||||
router,
|
||||
tableColumns: table,
|
||||
});
|
||||
|
||||
loadCardList()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
Reference in New Issue
Block a user