Files
mingxu/ming-theme/functions.php
T

1768 lines
75 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* MING 明命 theme functions
*
* @package MING
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
define( 'MING_VERSION', '1.7.4' );
/**
* Theme setup.
*/
function ming_setup() {
load_theme_textdomain( 'ming', get_template_directory() . '/languages' );
add_theme_support( 'automatic-feed-links' );
add_theme_support( 'title-tag' );
add_theme_support( 'post-thumbnails' );
add_theme_support(
'html5',
array( 'search-form', 'comment-form', 'comment-list', 'gallery', 'caption', 'style', 'script' )
);
add_theme_support(
'custom-logo',
array(
'height' => 200,
'width' => 200,
'flex-height' => true,
'flex-width' => true,
)
);
// WooCommerce support.
add_theme_support( 'woocommerce' );
add_theme_support( 'wc-product-gallery-zoom' );
add_theme_support( 'wc-product-gallery-lightbox' );
add_theme_support( 'wc-product-gallery-slider' );
register_nav_menus(
array(
'primary' => __( '主選單', 'ming' ),
'footer' => __( '頁腳選單', 'ming' ),
'footer_service' => __( '頁腳:顧客服務', 'ming' ),
'footer_guide' => __( '頁腳:購物指南', 'ming' ),
'footer_brand' => __( '頁腳:品牌探索', 'ming' ),
)
);
}
add_action( 'after_setup_theme', 'ming_setup' );
/**
* Return the site's WordPress locale in a form suitable for comparisons.
*
* @return string
*/
function ming_site_locale() {
return strtolower( str_replace( '-', '_', get_locale() ) );
}
/**
* Build font stacks for the language selected in WordPress settings.
*
* Self-hosted TC font files are used for Traditional Chinese. Other site
* languages use the appropriate platform font stack and do not request a
* Chinese Traditional web font.
*
* @return string CSS custom-property declarations.
*/
function ming_font_stack_css() {
$locale = ming_site_locale();
$fonts = array(
'sans' => "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
'serif' => "Georgia, 'Times New Roman', serif",
'latin' => "'Cormorant Garamond', Georgia, 'Times New Roman', serif",
);
if ( preg_match( '/^zh_(?:hant(?:_|$)|tw(?:_|$)|hk(?:_|$)|mo(?:_|$))/', $locale ) ) {
$fonts = array(
'sans' => "'Noto Sans TC', 'Noto Sans CJK TC', 'PingFang TC', 'Microsoft JhengHei', 'Heiti TC', sans-serif",
'serif' => "'Noto Serif TC', 'Noto Serif CJK TC', 'Songti TC', 'PMingLiU', 'MingLiU', serif",
'latin' => "'Cormorant Garamond', 'Noto Serif TC', 'Noto Serif CJK TC', 'Songti TC', serif",
);
} elseif ( preg_match( '/^zh_/', $locale ) ) {
$fonts = array(
'sans' => "'Noto Sans SC', 'Noto Sans CJK SC', 'PingFang SC', 'Microsoft YaHei', 'Heiti SC', sans-serif",
'serif' => "'Noto Serif SC', 'Noto Serif CJK SC', 'Songti SC', SimSun, serif",
'latin' => "'Cormorant Garamond', 'Noto Serif SC', 'Noto Serif CJK SC', 'Songti SC', serif",
);
} elseif ( preg_match( '/^ja(?:_|$)/', $locale ) ) {
$fonts = array(
'sans' => "'Noto Sans JP', 'Hiragino Kaku Gothic ProN', 'Yu Gothic', Meiryo, sans-serif",
'serif' => "'Noto Serif JP', 'Hiragino Mincho ProN', 'Yu Mincho', serif",
'latin' => "'Cormorant Garamond', 'Noto Serif JP', 'Hiragino Mincho ProN', serif",
);
} elseif ( preg_match( '/^ko(?:_|$)/', $locale ) ) {
$fonts = array(
'sans' => "'Noto Sans KR', 'Apple SD Gothic Neo', 'Malgun Gothic', sans-serif",
'serif' => "'Noto Serif KR', AppleMyungjo, Batang, serif",
'latin' => "'Cormorant Garamond', 'Noto Serif KR', AppleMyungjo, serif",
);
}
return sprintf(
":root {\n\t--serif: %1\$s;\n\t--latin: %2\$s;\n\t--sans: %3\$s;\n}\n",
$fonts['serif'],
$fonts['latin'],
$fonts['sans']
);
}
/**
* Enqueue assets.
*/
function ming_assets() {
// 自托管字體(v1.5.6 起替代 Google Fonts 外鏈,避免 fonts.googleapis.com 加載緩慢)
wp_enqueue_style(
'ming-fonts',
get_template_directory_uri() . '/assets/css/fonts.css',
array(),
MING_VERSION
);
wp_enqueue_style( 'ming-main', get_template_directory_uri() . '/assets/css/main.css', array( 'ming-fonts' ), MING_VERSION );
wp_add_inline_style( 'ming-main', ming_font_stack_css() );
wp_enqueue_script( 'ming-main', get_template_directory_uri() . '/assets/js/main.js', array(), MING_VERSION, true );
// Pass image directory to JS (product grid).
wp_localize_script(
'ming-main',
'MING_ASSETS',
get_template_directory_uri() . '/assets/img/'
);
// Vote module data.
wp_localize_script(
'ming-main',
'MING_VOTE',
array(
'ajaxurl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'ming_vote' ),
'goal' => ming_vote_goal(),
)
);
}
add_action( 'wp_enqueue_scripts', 'ming_assets' );
/**
* Theme favicon.
*/
function ming_favicon() {
echo '<link rel="icon" type="image/png" href="' . esc_url( get_template_directory_uri() . '/assets/img/logo-mark.png' ) . '">' . "\n";
}
add_action( 'wp_head', 'ming_favicon' );
/**
* Find a front-end URL for a page using a given template file.
*
* @param string $template Template file name, e.g. 'template-brand.php'.
* @param string $fallback Fallback URL.
* @return string
*/
function ming_page_url_by_template( $template, $fallback = '#' ) {
$pages = get_pages(
array(
'meta_key' => '_wp_page_template',
'meta_value' => $template,
'number' => 1,
)
);
if ( ! empty( $pages ) ) {
return get_permalink( $pages[0]->ID );
}
return $fallback;
}
/**
* WooCommerce shop URL (or home fallback when WooCommerce is inactive).
*
* @return string
*/
function ming_shop_url() {
if ( function_exists( 'wc_get_page_permalink' ) ) {
return wc_get_page_permalink( 'shop' );
}
return home_url( '/' );
}
/**
* Customizer: brand contact & social links.
*
* @param WP_Customize_Manager $wp_customize Customizer manager.
*/
function ming_customize_register( $wp_customize ) {
$wp_customize->add_section(
'ming_brand',
array(
'title' => __( 'MING 品牌資訊與社群', 'ming' ),
'description' => __( '頁腳的公司資訊、客服信箱與社群連結。留空的社群連結不會顯示。', 'ming' ),
'priority' => 30,
)
);
$fields = array(
'ming_company' => array( __( '公司名稱', 'ming' ), '明序有限公司', 'text' ),
'ming_email' => array( __( '客服信箱', 'ming' ), '', 'email' ),
'ming_phone' => array( __( '客服電話', 'ming' ), '', 'text' ),
'ming_address' => array( __( '公司地址', 'ming' ), '', 'text' ),
'ming_taxid' => array( __( '統一編號', 'ming' ), '', 'text' ),
'ming_facebook' => array( __( 'Facebook 網址', 'ming' ), '', 'url' ),
'ming_instagram' => array( __( 'Instagram 網址', 'ming' ), '', 'url' ),
'ming_line' => array( __( 'LINE 官方帳號網址', 'ming' ), '', 'url' ),
'ming_vote_goal' => array( __( '許願池目標人數', 'ming' ), '100', 'number' ),
);
foreach ( $fields as $id => $field ) {
$wp_customize->add_setting(
$id,
array(
'default' => $field[1],
'sanitize_callback' => ( 'url' === $field[2] ) ? 'esc_url_raw' : ( ( 'email' === $field[2] ) ? 'sanitize_email' : 'sanitize_text_field' ),
)
);
$wp_customize->add_control(
$id,
array(
'section' => 'ming_brand',
'label' => $field[0],
'type' => ( 'text' === $field[2] ) ? 'text' : $field[2],
)
);
}
// 品牌宣傳片(首頁影片區塊).
$wp_customize->add_section(
'ming_video',
array(
'title' => __( 'MING 品牌宣傳片', 'ming' ),
'description' => __( '首頁影片區塊。支援 YouTube 連結或後台媒體庫上傳的 mp4,站內播放不跳轉。', 'ming' ),
'priority' => 31,
)
);
$wp_customize->add_setting( 'ming_video_enable', array( 'default' => '', 'sanitize_callback' => 'wp_validate_boolean' ) );
$wp_customize->add_control( 'ming_video_enable', array( 'section' => 'ming_video', 'label' => __( '啟用宣傳片區塊', 'ming' ), 'type' => 'checkbox' ) );
$wp_customize->add_setting( 'ming_video_source', array( 'default' => 'youtube', 'sanitize_callback' => 'sanitize_text_field' ) );
$wp_customize->add_control(
'ming_video_source',
array(
'section' => 'ming_video',
'label' => __( '影片來源', 'ming' ),
'type' => 'radio',
'choices' => array(
'youtube' => __( 'YouTube 連結', 'ming' ),
'mp4' => __( '本機 mp4(媒體庫網址)', 'ming' ),
),
)
);
$wp_customize->add_setting( 'ming_video_url', array( 'default' => '', 'sanitize_callback' => 'esc_url_raw' ) );
$wp_customize->add_control( 'ming_video_url', array( 'section' => 'ming_video', 'label' => __( '影片網址', 'ming' ), 'type' => 'url' ) );
$wp_customize->add_setting( 'ming_video_title', array( 'default' => '', 'sanitize_callback' => 'sanitize_text_field' ) );
$wp_customize->add_control( 'ming_video_title', array( 'section' => 'ming_video', 'label' => __( '標題', 'ming' ), 'type' => 'text' ) );
$wp_customize->add_setting( 'ming_video_desc', array( 'default' => '', 'sanitize_callback' => 'sanitize_textarea_field' ) );
$wp_customize->add_control( 'ming_video_desc', array( 'section' => 'ming_video', 'label' => __( '介紹詞', 'ming' ), 'type' => 'textarea' ) );
}
add_action( 'customize_register', 'ming_customize_register' );
/**
* AJAX cart fragment: keep the header cart count in sync.
*
* @param array $fragments Fragments.
* @return array
*/
function ming_cart_count_fragment( $fragments ) {
ob_start();
?>
<span class="hcount"><?php echo esc_html( WC()->cart->get_cart_contents_count() ); ?></span>
<?php
$fragments['span.hcount'] = ob_get_clean();
return $fragments;
}
if ( function_exists( 'WC' ) || class_exists( 'WooCommerce' ) ) {
add_filter( 'woocommerce_add_to_cart_fragments', 'ming_cart_count_fragment' );
}
/**
* Related products: 3 items in 3 columns.
*
* @param array $args Args.
* @return array
*/
function ming_related_products_args( $args ) {
$args['posts_per_page'] = 3;
$args['columns'] = 3;
return $args;
}
add_filter( 'woocommerce_output_related_products_args', 'ming_related_products_args' );
/**
* Fallback primary menu: brand + shop structure.
*
* @param array $args Menu args.
* @return string
*/
function ming_fallback_menu( $args = array() ) {
$base = is_front_page() ? '' : esc_url( home_url( '/' ) );
$items = array(
ming_page_url_by_template( 'template-brand.php', $base . '#philosophy' ) => __( '關於我們', 'ming' ),
ming_page_url_by_template( 'template-ceramic.php', $base . '#core' ) => __( '陶瓷內膽', 'ming' ),
ming_page_url_by_template( 'template-journal.php', $base . '#philosophy' ) => __( '品牌專欄', 'ming' ),
ming_shop_url() => __( '全部商品', 'ming' ),
ming_page_url_by_template( 'template-vote.php', $base . '#top' ) => __( '新品許願', 'ming' ),
$base . '#contact' => __( '聯絡我們', 'ming' ),
);
$out = '';
foreach ( $items as $url => $label ) {
$out .= '<a href="' . esc_attr( $url ) . '">' . esc_html( $label ) . '</a>';
}
return $out;
}
/* ============================================================
投票活動系統(多活動版)
============================================================ */
function ming_vote_goal() {
return max( 1, absint( get_theme_mod( 'ming_vote_goal', 100 ) ) );
}
function ming_campaign_expired( $cid ) {
$d = get_post_meta( $cid, 'ming_deadline', true );
return $d && strtotime( $d ) < current_time( 'timestamp' );
}
function ming_campaign_total_votes( $cid ) {
$total = 0;
foreach ( (array) get_post_meta( $cid, 'ming_products', true ) as $pid ) {
$total += (int) get_post_meta( $pid, 'ming_votes_c' . $cid, true );
}
return $total;
}
/* 商品顏色(嚴格取自商品顏色屬性 / 變體 SKU 選項) */
function ming_product_colors( $prod ) {
$hexmap = array(
'黑' => '#2B2B26', '白' => '#F3EDE2', '米' => '#F3EDE2', '粉' => '#F0C7CE',
'紅' => '#C0392B', '黃' => '#F2C230', '藍' => '#A8CBE0', '綠' => '#CFE3CE',
'薄荷' => '#A8D8D2', '紫' => '#B9B1D6', '灰' => '#C0C4C0', '銀' => '#C9CDD2',
'橘' => '#E8A04C', '金' => '#BFA06A', '棕' => '#8B6B4A',
);
$names = array();
foreach ( $prod->get_attributes() as $att ) {
$label = $att->is_taxonomy() ? wc_attribute_label( $att->get_name() ) : $att->get_name();
if ( ! preg_match( '/色|顏色|color|colour/iu', $label ) ) { continue; }
foreach ( preg_split( '/[,,、|]/u', $prod->get_attribute( $att->get_name() ) ) as $n ) {
$n = trim( $n );
if ( '' !== $n ) { $names[] = $n; }
}
}
$out = array();
foreach ( array_unique( $names ) as $n ) {
$hex = '#D8D2C4';
foreach ( $hexmap as $k => $v ) {
if ( false !== mb_strpos( $n, $k ) ) { $hex = $v; break; }
}
$out[] = array( 'name' => $n, 'hex' => $hex );
}
return array_slice( $out, 0, 6 );
}
/**
* 商品顏色選項(含變體圖與預選連結):色卡點點切換卡片圖用。
* 優先取 pa_color 變體(圖=變體圖,連結帶 ?attribute_pa_color= 預選);無變體時退回 ming_product_colors()。
*/
function ming_product_color_options( $prod ) {
$opts = array();
if ( $prod && $prod->is_type( 'variable' ) ) {
// 不用 get_available_variations():它會略過未定價變體,許願池需要全部顏色.
foreach ( $prod->get_children() as $vid ) {
$var = wc_get_product( $vid );
if ( ! $var || 'publish' !== $var->get_status() ) { continue; }
$aval = $var->get_attribute( 'pa_color' ); // 可能回傳 term name 或 slug.
if ( '' === $aval ) { continue; }
$term = get_term_by( 'slug', $aval, 'pa_color' );
if ( ! $term ) { $term = get_term_by( 'name', $aval, 'pa_color' ); }
if ( ! $term ) { continue; }
$hex = get_term_meta( $term->term_id, 'ming_hex', true );
$img = $var->get_image_id() ? wp_get_attachment_image_url( $var->get_image_id(), 'medium_large' ) : '';
$opts[] = array(
'name' => $term->name,
'slug' => $term->slug,
'hex' => $hex ? $hex : '#D8D2C4',
'img' => $img ? $img : '',
'vid' => (int) $vid,
'link' => add_query_arg( 'attribute_pa_color', $term->slug, get_permalink( $prod->get_id() ) ),
);
}
}
if ( ! $opts ) {
foreach ( ming_product_colors( $prod ) as $c ) {
$c['slug'] = '';
$c['img'] = '';
$c['vid'] = 0;
$c['link'] = get_permalink( $prod->get_id() );
$opts[] = $c;
}
}
return $opts;
}
/* ---------- 投票活動 CPT(掛在「產品」選單下) ---------- */
add_action( 'init', 'ming_register_campaign' );
function ming_register_campaign() {
register_post_type( 'ming_campaign', array(
'labels' => array(
'name' => __( '投票活動', 'ming' ),
'singular_name' => __( '投票活動', 'ming' ),
'add_new_item' => __( '發起新投票活動', 'ming' ),
'edit_item' => __( '編輯投票活動', 'ming' ),
),
'public' => true,
'show_in_menu' => 'edit.php?post_type=product',
'supports' => array( 'title' ),
'rewrite' => array( 'slug' => 'campaign' ),
'show_in_rest' => false,
) );
}
/* ---------- 活動設定與投票情況(編輯畫面) ---------- */
add_action( 'add_meta_boxes', 'ming_campaign_metaboxes' );
function ming_campaign_metaboxes() {
add_meta_box( 'ming_campaign_settings', __( '活動設定', 'ming' ), 'ming_campaign_settings_box', 'ming_campaign', 'normal', 'high' );
add_meta_box( 'ming_campaign_stats', __( '投票情況', 'ming' ), 'ming_campaign_stats_box', 'ming_campaign', 'normal', 'default' );
}
function ming_campaign_settings_box( $post ) {
wp_nonce_field( 'ming_campaign_save', 'ming_campaign_nonce' );
$goal = get_post_meta( $post->ID, 'ming_goal', true );
$goal = $goal ? $goal : ming_vote_goal();
$deadline = get_post_meta( $post->ID, 'ming_deadline', true );
$selected = array_map( 'intval', (array) get_post_meta( $post->ID, 'ming_products', true ) );
echo '<p><label><strong>目標人數</strong><br><input type="number" name="ming_goal" value="' . esc_attr( $goal ) . '" min="1" style="width:140px"></label></p>';
echo '<p><label><strong>截止時間</strong>(留空表示長期進行)<br><input type="datetime-local" name="ming_deadline" value="' . esc_attr( $deadline ) . '"></label></p>';
echo '<p><strong>參與商品</strong></p><div style="max-height:260px;overflow:auto;border:1px solid #ddd;padding:8px">';
$prods = get_posts( array( 'post_type' => 'product', 'posts_per_page' => -1, 'post_status' => 'publish', 'orderby' => 'title', 'order' => 'ASC' ) );
foreach ( $prods as $p ) {
printf(
'<label style="display:block;margin:4px 0"><input type="checkbox" name="ming_products[]" value="%d"%s> %s</label>',
$p->ID, checked( in_array( $p->ID, $selected, true ), true, false ), esc_html( $p->post_title )
);
}
if ( ! $prods ) { echo '<p>尚無商品。</p>'; }
echo '</div>';
}
function ming_campaign_stats_box( $post ) {
$products = array_map( 'intval', (array) get_post_meta( $post->ID, 'ming_products', true ) );
if ( ! $products ) { echo '<p>儲存並選擇參與商品後,這裡會顯示投票統計。</p>'; return; }
$goal = (int) get_post_meta( $post->ID, 'ming_goal', true );
$goal = $goal ? $goal : ming_vote_goal();
echo '<table class="widefat striped"><thead><tr><th>商品 / 顏色</th><th>票數</th><th>目標達成</th></tr></thead><tbody>';
$total = 0;
foreach ( $products as $pid ) {
$v = (int) get_post_meta( $pid, 'ming_votes_c' . $post->ID, true );
$total += $v;
$pct = min( 100, round( $v / $goal * 100 ) );
echo '<tr><td><a href="' . esc_url( get_edit_post_link( $pid ) ) . '">' . esc_html( get_the_title( $pid ) ) . '</a></td><td><strong>' . $v . '</strong></td><td>' . $pct . '%</td></tr>';
$prod = wc_get_product( $pid );
if ( $prod && $prod->is_type( 'variable' ) ) {
foreach ( $prod->get_children() as $vid ) {
$sv = (int) get_post_meta( $vid, 'ming_votes_c' . $post->ID, true );
$color = wc_get_product( $vid );
$cname = $color ? $color->get_attribute( 'pa_color' ) : '';
echo '<tr><td style="padding-left:2em;color:#666">└ ' . esc_html( $cname ? $cname : ( '變體 #' . $vid ) ) . '</td><td>' . $sv . '</td><td></td></tr>';
}
}
}
echo '</tbody><tfoot><tr><th>總票數</th><th>' . $total . '</th><th></th></tr></tfoot></table>';
$voters = get_users( array( 'meta_key' => 'ming_voted_c' . $post->ID, 'fields' => array( 'ID', 'display_name', 'user_email' ) ) );
if ( $voters ) {
echo '<h4>投票會員(' . count( $voters ) . ')</h4><table class="widefat striped"><thead><tr><th>會員</th><th>信箱</th><th>支持的商品</th></tr></thead><tbody>';
foreach ( $voters as $u ) {
$voted = (array) get_user_meta( $u->ID, 'ming_voted_c' . $post->ID, true );
$names = array();
foreach ( $voted as $e ) {
$parts = explode( ':', (string) $e );
$label = get_the_title( (int) $parts[0] );
if ( ! empty( $parts[1] ) ) {
$vp = wc_get_product( (int) $parts[1] );
if ( $vp ) { $label .= '(' . $vp->get_attribute( 'pa_color' ) . ')'; }
}
$names[] = $label;
}
echo '<tr><td>' . esc_html( $u->display_name ) . '</td><td>' . esc_html( $u->user_email ) . '</td><td>' . esc_html( implode( '、', $names ) ) . '</td></tr>';
}
echo '</tbody></table>';
} else {
echo '<p>尚無會員投票。</p>';
}
}
add_action( 'save_post_ming_campaign', 'ming_save_campaign' );
function ming_save_campaign( $post_id ) {
if ( ! isset( $_POST['ming_campaign_nonce'] ) || ! wp_verify_nonce( $_POST['ming_campaign_nonce'], 'ming_campaign_save' ) ) { return; }
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) { return; }
if ( ! current_user_can( 'edit_post', $post_id ) ) { return; }
update_post_meta( $post_id, 'ming_goal', max( 1, absint( $_POST['ming_goal'] ?? ming_vote_goal() ) ) );
$deadline = sanitize_text_field( $_POST['ming_deadline'] ?? '' );
update_post_meta( $post_id, 'ming_deadline', $deadline );
$products = array_map( 'absint', (array) ( $_POST['ming_products'] ?? array() ) );
update_post_meta( $post_id, 'ming_products', $products );
}
/* ---------- 列表欄位 ---------- */
add_filter( 'manage_ming_campaign_posts_columns', function( $cols ) {
$cols['ming_goal'] = __( '目標人數', 'ming' );
$cols['ming_deadline'] = __( '截止時間', 'ming' );
$cols['ming_items'] = __( '參與商品', 'ming' );
$cols['ming_votes'] = __( '總票數', 'ming' );
$cols['ming_status'] = __( '狀態', 'ming' );
unset( $cols['date'] );
return $cols;
} );
add_action( 'manage_ming_campaign_posts_custom_column', function( $col, $post_id ) {
if ( 'ming_goal' === $col ) { echo esc_html( get_post_meta( $post_id, 'ming_goal', true ) ?: ming_vote_goal() ); }
if ( 'ming_deadline' === $col ) { $d = get_post_meta( $post_id, 'ming_deadline', true ); echo $d ? esc_html( date_i18n( 'Y/m/d H:i', strtotime( $d ) ) ) : '長期'; }
if ( 'ming_items' === $col ) { echo count( (array) get_post_meta( $post_id, 'ming_products', true ) ); }
if ( 'ming_votes' === $col ) { echo ming_campaign_total_votes( $post_id ); }
if ( 'ming_status' === $col ) { echo ming_campaign_expired( $post_id ) ? '🔒 已截止' : '🟢 進行中'; }
}, 10, 2 );
/* ---------- 投票 AJAX ---------- */
add_action( 'wp_ajax_ming_vote', 'ming_handle_vote' );
add_action( 'wp_ajax_nopriv_ming_vote', 'ming_handle_vote' );
function ming_handle_vote() {
check_ajax_referer( 'ming_vote', 'nonce' );
if ( ! is_user_logged_in() ) {
$login = function_exists( 'wc_get_page_permalink' ) ? wc_get_page_permalink( 'myaccount' ) : wp_login_url();
wp_send_json_error( array( 'need_login' => true, 'login_url' => $login ) );
}
$cid = isset( $_POST['campaign_id'] ) ? absint( $_POST['campaign_id'] ) : 0;
$pid = isset( $_POST['product_id'] ) ? absint( $_POST['product_id'] ) : 0;
if ( ! $cid || 'ming_campaign' !== get_post_type( $cid ) || 'publish' !== get_post_status( $cid ) ) {
wp_send_json_error( array( 'message' => 'invalid_campaign' ) );
}
if ( ming_campaign_expired( $cid ) ) {
wp_send_json_error( array( 'message' => 'closed' ) );
}
if ( ! in_array( $pid, array_map( 'intval', (array) get_post_meta( $cid, 'ming_products', true ) ), true ) ) {
wp_send_json_error( array( 'message' => 'invalid_product' ) );
}
$vid = isset( $_POST['variation_id'] ) ? absint( $_POST['variation_id'] ) : 0;
if ( $vid && ( 'product_variation' !== get_post_type( $vid ) || (int) wp_get_post_parent_id( $vid ) !== $pid ) ) {
$vid = 0; // 變體必須屬於該商品,否則視為未選色.
}
$uid = get_current_user_id();
$ukey = 'ming_voted_c' . $cid;
$pkey = 'ming_votes_c' . $cid;
$voted_raw = (array) get_user_meta( $uid, $ukey, true );
$voted = array();
foreach ( $voted_raw as $e ) { $voted[] = (int) explode( ':', (string) $e )[0]; } // 相容舊純 pid 與新 pid:vid 格式.
$count = (int) get_post_meta( $pid, $pkey, true );
if ( in_array( $pid, $voted, true ) ) {
wp_send_json_error( array( 'already' => true, 'count' => $count ) );
}
$count++;
update_post_meta( $pid, $pkey, $count );
$sku_count = 0;
if ( $vid ) {
$sku_count = (int) get_post_meta( $vid, $pkey, true ) + 1;
update_post_meta( $vid, $pkey, $sku_count );
}
$voted_raw[] = $vid ? ( $pid . ':' . $vid ) : (string) $pid;
update_user_meta( $uid, $ukey, $voted_raw );
wp_send_json_success( array( 'count' => $count, 'vid' => $vid, 'sku_count' => $sku_count ) );
}
/* ---------- 舊投票資料遷移到預設活動 ---------- */
add_action( 'init', 'ming_migrate_votes', 20 );
function ming_migrate_votes() {
if ( get_option( 'ming_votes_migrated' ) ) { return; }
if ( ! post_type_exists( 'product' ) ) { return; }
if ( get_posts( array( 'post_type' => 'ming_campaign', 'posts_per_page' => 1, 'fields' => 'ids', 'post_status' => 'any' ) ) ) {
update_option( 'ming_votes_migrated', 1 );
return;
}
$old = get_posts( array( 'post_type' => 'product', 'posts_per_page' => -1, 'meta_key' => 'ming_votes', 'fields' => 'ids', 'post_status' => 'any' ) );
if ( ! $old ) { update_option( 'ming_votes_migrated', 1 ); return; }
$all = get_posts( array( 'post_type' => 'product', 'posts_per_page' => -1, 'post_status' => 'publish', 'fields' => 'ids' ) );
$cid = wp_insert_post( array( 'post_type' => 'ming_campaign', 'post_title' => '新品許願(第一期)', 'post_status' => 'publish' ) );
if ( ! $cid ) { return; }
update_post_meta( $cid, 'ming_goal', ming_vote_goal() );
update_post_meta( $cid, 'ming_products', array_map( 'intval', $all ) );
foreach ( $old as $pid ) {
update_post_meta( $pid, 'ming_votes_c' . $cid, (int) get_post_meta( $pid, 'ming_votes', true ) );
}
foreach ( get_users( array( 'meta_key' => 'ming_voted_products', 'fields' => 'ID' ) ) as $uid ) {
$v = (array) get_user_meta( $uid, 'ming_voted_products', true );
if ( $v ) { update_user_meta( $uid, 'ming_voted_c' . $cid, array_map( 'intval', $v ) ); }
}
update_option( 'ming_votes_migrated', 1 );
}
/* ---------- 訂單詳情頁顯示銀行匯款資訊 ---------- */
add_action( 'woocommerce_view_order', 'ming_bacs_on_view_order', 5 );
function ming_bacs_on_view_order( $order_id ) {
$order = wc_get_order( $order_id );
// bacs 下單後訂單狀態為「保留」(on-hold),needs_payment() 只對待付款/失敗有效,故改用 is_paid() 反向判斷
if ( ! $order || 'bacs' !== $order->get_payment_method() || $order->is_paid() ) { return; }
$gateway = new WC_Gateway_BACS();
echo '<div class="ming-bacs-box"><h2 class="h2">匯款資訊</h2>';
if ( $gateway->instructions ) { echo wpautop( wptexturize( $gateway->instructions ) ); }
$accounts = $gateway->account_details;
if ( $accounts ) {
echo '<ul class="bacs-accounts">';
foreach ( $accounts as $a ) {
echo '<li>';
if ( ! empty( $a['account_name'] ) ) { echo '<span><b>戶名</b>' . esc_html( $a['account_name'] ) . '</span>'; }
if ( ! empty( $a['bank_name'] ) ) { echo '<span><b>銀行</b>' . esc_html( $a['bank_name'] ) . '</span>'; }
if ( ! empty( $a['account_number'] ) ) { echo '<span><b>帳號</b>' . esc_html( $a['account_number'] ) . '</span>'; }
if ( ! empty( $a['sort_code'] ) ) { echo '<span><b>分行代碼</b>' . esc_html( $a['sort_code'] ) . '</span>'; }
echo '</li>';
}
echo '</ul>';
}
echo '<p class="bacs-note">請於匯款時備註訂單編號 <b>#' . esc_html( $order->get_order_number() ) . '</b>,確認到帳後我們將盡快為您出貨。</p></div>';
}
/* 投票會員 meta 註冊(便於管理員校正) */
add_action( 'init', function() {
foreach ( get_posts( array( 'post_type' => 'ming_campaign', 'posts_per_page' => -1, 'post_status' => 'any', 'fields' => 'ids' ) ) as $cid ) {
register_meta( 'user', 'ming_voted_c' . $cid, array(
'type' => 'array', 'single' => true,
'show_in_rest' => array( 'schema' => array( 'type' => 'array', 'items' => array( 'type' => 'integer' ) ) ),
'auth_callback' => function( $a, $k, $uid ) { return current_user_can( 'edit_user', $uid ); },
) );
}
}, 25 );
/* ============================================================
* v1.6.0 — 宣傳片 / 搜尋 / 頁腳三欄
* ==========================================================*/
/**
* 解析 YouTube 影片 ID(支援 watch、youtu.be、shorts、embed 等格式)。
*
* @param string $url YouTube 網址。
* @return string
*/
function ming_youtube_id( $url ) {
if ( preg_match( '~(?:youtube\.com/(?:watch\?.*v=|shorts/|embed/)|youtu\.be/)([A-Za-z0-9_-]{6,20})~', $url, $m ) ) {
return $m[1];
}
return '';
}
/**
* 商品列表卡片上的供貨狀態標籤(現貨/缺貨)——以絕對定位貼在商品圖左上角。
*/
function ming_loop_stock_badge() {
global $product;
if ( ! $product || ! is_a( $product, 'WC_Product' ) ) { return; }
if ( $product->is_in_stock() ) {
echo '<span class="ming-stock in">現貨</span>';
} else {
echo '<span class="ming-stock out">缺貨</span>';
}
}
add_action( 'woocommerce_before_shop_loop_item_title', 'ming_loop_stock_badge', 9 );
/**
* 頁腳資訊頁占位文案(繁中草稿,後台可自由修改)。
*
* @return array slug => [標題, 內容HTML]
*/
function ming_info_pages() {
return array(
'contact-us' => array(
'聯絡我們',
'<p>任何商品、訂單或合作問題,都歡迎與我們聯繫,我們將於 1–2 個工作日內回覆。</p>'
. '<h3>客服管道</h3><ul><li>客服信箱:請見頁腳「公司資訊」之客服信箱</li><li>服務時間:週一至週五 10:00–18:00(國定假日除外)</li></ul>'
. '<h3>大宗採購與企業禮贈</h3><p>企業禮贈、經銷通路與大宗訂購需求,請來信註明公司名稱、需求數量與預計用途,我們將由專人與您聯繫報價。</p>',
),
'faq' => array(
'常見問題 FAQ',
'<h3>陶瓷內膽可以裝碳酸飲料或牛奶嗎?</h3><p>可以。純陶瓷內膽抗酸鹼、耐腐蝕、不殘留味道,咖啡、茶、果汁、牛奶都適用;使用後建議儘快清洗晾乾。</p>'
. '<h3>如何清洗與保養?</h3><p>杯蓋、吸管與內膽皆可拆洗;請以中性清潔劑與海綿手洗,勿使用鋼刷,勿放入洗碗機高溫烘碗模式。</p>'
. '<h3>下單後多久出貨?</h3><p>現貨商品於確認款項後 1–3 個工作日內出貨;如遇缺貨或預購,商品頁會標示預計出貨時間。</p>'
. '<h3>如何查詢訂單?</h3><p>登入會員後可於「會員中心 → 訂單」查詢訂單狀態與匯款資訊。</p>',
),
'ordering-guide' => array(
'訂購須知',
'<h3>購物流程</h3><p>選購商品 → 加入購物車 → 填寫收件資料 → 選擇付款方式 → 完成訂單。未註冊會員亦可直接結帳。</p>'
. '<h3>付款方式</h3><p>目前提供銀行轉帳(ATM/網銀匯款)。下單後請於訂單頁面查看匯款帳號,並於 3 日內完成轉帳,逾期訂單將自動取消。</p>'
. '<h3>運費與配送</h3><p>台灣本島常溫配送,運費依結帳頁顯示為準;離島與海外配送請先來信詢問。</p>',
),
'return-policy' => array(
'退換貨政策',
'<h3>七天鑑賞期</h3><p>依消費者保護法,商品到貨翌日起算七天內可申請退貨(鑑賞期非試用期)。退回商品須為全新未使用、包裝完整。</p>'
. '<h3>不適用退貨之情形</h3><ul><li>已使用過或經清洗之飲具(衛生考量)</li><li>客製化商品</li><li>包裝或配件缺損</li></ul>'
. '<h3>瑕疵與換貨</h3><p>如收到瑕疵品或寄錯商品,請於到貨七天內拍照並來信客服信箱,我們將安排換貨並負擔來回運費。</p>'
. '<h3>退款方式</h3><p>退貨驗收完成後 7–10 個工作日內,依原付款方式退款。</p>',
),
'privacy-policy' => array(
'隱私權政策',
'<p>MING 明命(以下稱本公司)重視您的個人資料保護,本政策說明我們如何蒐集、使用與保護您的資料。</p>'
. '<h3>資料蒐集</h3><p>您於本站註冊、訂購或參與活動時,我們會蒐集姓名、聯絡方式、收件地址與訂單紀錄等必要資料。</p>'
. '<h3>使用目的</h3><p>僅用於訂單處理、配送、售後服務與會員活動通知;未經您同意,不會提供予無關之第三方。</p>'
. '<h3>資料安全與權利</h3><p>您的資料將以合理之技術與程序保護。您可隨時聯繫客服查詢、更正或請求刪除您的個人資料。</p>',
),
);
}
/**
* 主題啟用時:建立頁腳資訊頁與三欄頁腳選單(已存在則跳過)。
*/
function ming_seed_footer_content() {
$pages = ming_info_pages();
$pageid = array();
foreach ( $pages as $slug => $p ) {
$exist = get_page_by_path( $slug );
if ( $exist ) {
$pageid[ $slug ] = $exist->ID;
continue;
}
$pageid[ $slug ] = wp_insert_post(
array(
'post_type' => 'page',
'post_status' => 'publish',
'post_name' => $slug,
'post_title' => $p[0],
'post_content' => $p[1],
)
);
}
$menus = array(
'footer_service' => array( '顧客服務', array( 'contact-us', 'faq' ) ),
'footer_guide' => array( '購物指南', array( 'ordering-guide', 'return-policy', 'privacy-policy' ) ),
'footer_brand' => array( '品牌探索', array() ), // 品牌探索改以自訂連結建置.
);
$locations = get_theme_mod( 'nav_menu_locations', array() );
foreach ( $menus as $loc => $m ) {
if ( ! empty( $locations[ $loc ] ) && wp_get_nav_menu_object( $locations[ $loc ] ) ) { continue; }
$menu_id = wp_create_nav_menu( $m[0] );
if ( is_wp_error( $menu_id ) ) { continue; }
foreach ( $m[1] as $slug ) {
if ( ! empty( $pageid[ $slug ] ) && ! is_wp_error( $pageid[ $slug ] ) ) {
wp_update_nav_menu_item(
$menu_id,
0,
array(
'menu-item-object-id' => $pageid[ $slug ],
'menu-item-object' => 'page',
'menu-item-type' => 'post_type',
'menu-item-status' => 'publish',
)
);
}
}
$locations[ $loc ] = $menu_id;
}
// 品牌探索:品牌故事 / 陶瓷內膽 / 新品許願(自訂連結,依模板找頁面).
$brand_menu = isset( $locations['footer_brand'] ) ? $locations['footer_brand'] : 0;
if ( $brand_menu && ! wp_get_nav_menu_items( $brand_menu ) ) {
$links = array(
'品牌故事' => ming_page_url_by_template( 'template-brand.php', home_url( '/' ) . '#philosophy' ),
'陶瓷內膽' => ming_page_url_by_template( 'template-ceramic.php', home_url( '/' ) . '#core' ),
'新品許願' => ming_page_url_by_template( 'template-vote.php', home_url( '/' ) ),
'線上商店' => ming_shop_url(),
);
foreach ( $links as $title => $url ) {
wp_update_nav_menu_item(
$brand_menu,
0,
array(
'menu-item-title' => $title,
'menu-item-url' => $url,
'menu-item-type' => 'custom',
'menu-item-status' => 'publish',
)
);
}
}
set_theme_mod( 'nav_menu_locations', $locations );
}
add_action( 'after_switch_theme', 'ming_seed_footer_content' );
/* ============================================================
* v1.7.0 — OG 標籤 / 許願分享 / 品牌專欄 / SKU 色卡 / 海報生成器
* ==========================================================*/
/* ---------- OG / Twitter 分享標籤 ---------- */
/**
* Return an Open Graph locale derived from the WordPress site language.
*
* @return string
*/
function ming_og_locale() {
$locale = ming_site_locale();
if ( 0 === strpos( $locale, 'zh_' ) ) {
if ( preg_match( '/(?:hk)/', $locale ) ) {
return 'zh_HK';
}
if ( preg_match( '/(?:mo)/', $locale ) ) {
return 'zh_MO';
}
return preg_match( '/(?:hant|tw)/', $locale ) ? 'zh_TW' : 'zh_CN';
}
$parts = explode( '_', $locale );
$language = isset( $parts[0] ) ? $parts[0] : 'en';
$region = '';
foreach ( array_reverse( $parts ) as $part ) {
if ( 2 === strlen( $part ) && ctype_alpha( $part ) ) {
$region = strtoupper( $part );
break;
}
}
if ( $region ) {
return $language . '_' . $region;
}
$default_regions = array(
'ar' => 'SA',
'de' => 'DE',
'en' => 'US',
'es' => 'ES',
'fr' => 'FR',
'it' => 'IT',
'ja' => 'JP',
'ko' => 'KR',
'nl' => 'NL',
'pl' => 'PL',
'pt' => 'BR',
'ru' => 'RU',
);
return isset( $default_regions[ $language ] ) ? $language . '_' . $default_regions[ $language ] : 'en_US';
}
add_action( 'wp_head', 'ming_og_tags', 5 );
function ming_og_tags() {
if ( is_admin() || ( function_exists( 'is_embed' ) && is_embed() ) ) { return; }
$site = get_bloginfo( 'name' );
$title = wp_get_document_title();
$desc = '';
$type = 'website';
$url = home_url( '/' );
$image = get_template_directory_uri() . '/assets/img/hero.jpg';
if ( is_singular() ) {
$p = get_post();
if ( $p ) {
$url = get_permalink( $p );
$desc = trim( wp_strip_all_tags( $p->post_excerpt ? $p->post_excerpt : $p->post_content ) );
$desc = mb_substr( $desc, 0, 120 );
if ( has_post_thumbnail( $p ) ) {
$thumb = get_the_post_thumbnail_url( $p, 'large' );
if ( $thumb ) { $image = $thumb; }
}
}
if ( function_exists( 'is_product' ) && is_product() ) {
$type = 'product';
$prod = wc_get_product( get_the_ID() );
if ( $prod && '' === $desc ) {
$desc = trim( wp_strip_all_tags( $prod->get_short_description() ) );
}
} elseif ( is_single() ) {
$type = 'article';
}
} elseif ( is_front_page() || is_home() ) {
$desc = get_bloginfo( 'description' );
if ( '' === $desc ) {
$desc = '以時間為序、以土地為根、以光明為引。MING 明命純陶瓷內膽杯,連結人與自然、生活與生命。';
}
}
if ( '' === $desc ) { $desc = $site; }
if ( $image && 0 === strpos( $image, '/' ) ) { $image = home_url( $image ); }
echo "\n<!-- MING OG -->\n";
echo '<meta property="og:site_name" content="' . esc_attr( $site ) . '">' . "\n";
echo '<meta property="og:title" content="' . esc_attr( $title ) . '">' . "\n";
echo '<meta property="og:description" content="' . esc_attr( $desc ) . '">' . "\n";
echo '<meta property="og:url" content="' . esc_url( $url ) . '">' . "\n";
echo '<meta property="og:type" content="' . esc_attr( $type ) . '">' . "\n";
echo '<meta property="og:locale" content="' . esc_attr( ming_og_locale() ) . '">' . "\n";
if ( $image ) {
echo '<meta property="og:image" content="' . esc_url( $image ) . '">' . "\n";
}
echo '<meta name="twitter:card" content="summary_large_image">' . "\n";
echo '<meta name="twitter:title" content="' . esc_attr( $title ) . '">' . "\n";
echo '<meta name="twitter:description" content="' . esc_attr( $desc ) . '">' . "\n";
if ( $image ) {
echo '<meta name="twitter:image" content="' . esc_url( $image ) . '">' . "\n";
}
echo "<!-- /MING OG -->\n";
}
/* ---------- 社群分享列(LINE / Facebook / 複製連結 / 原生分享) ---------- */
/**
* 輸出分享按鈕列。行動裝置上由 JS 顯示系統原生分享按鈕。
*
* @param array $args { title, text, url, label }
*/
function ming_share_bar( $args = array() ) {
$args = wp_parse_args(
$args,
array(
'title' => wp_get_document_title(),
'text' => '',
'url' => get_permalink(),
'label' => __( '分享', 'ming' ),
)
);
$url = $args['url'] ? $args['url'] : home_url( add_query_arg( null, null ) );
$title = $args['title'];
$text = $args['text'] ? $args['text'] : $title;
?>
<div class="ming-share" data-title="<?php echo esc_attr( $title ); ?>" data-text="<?php echo esc_attr( $text ); ?>" data-url="<?php echo esc_url( $url ); ?>">
<span class="ming-share-label"><?php echo esc_html( $args['label'] ); ?></span>
<button type="button" class="ming-share-btn native" hidden>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v12"/><path d="M7 8l5-5 5 5"/><path d="M5 13v6a1.5 1.5 0 0 0 1.5 1.5h11A1.5 1.5 0 0 0 19 19v-6"/></svg>
<?php esc_html_e( '分享', 'ming' ); ?>
</button>
<a class="ming-share-btn line" target="_blank" rel="noopener"
href="https://social-plugins.line.me/lineit/share?url=<?php echo rawurlencode( $url ); ?>&text=<?php echo rawurlencode( $text ); ?>">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 3C6.9 3 3 6.2 3 10.2c0 3.5 3.1 6.4 7.2 7 .3.1.7.2.8.4.1.2.1.5 0 .7l-.2 1.1c0 .2-.2.8.7.4.9-.4 4.7-2.7 6.4-4.7 1.2-1.3 1.8-2.7 1.8-4.4C21.5 6.2 17.1 3 12 3z"/></svg>
LINE
</a>
<a class="ming-share-btn fb" target="_blank" rel="noopener"
href="https://www.facebook.com/sharer/sharer.php?u=<?php echo rawurlencode( $url ); ?>&quote=<?php echo rawurlencode( $text ); ?>">
<svg viewBox="0 0 24 24" fill="currentColor"><path d="M13.5 21v-7h2.4l.4-3h-2.8V9.1c0-.9.3-1.5 1.6-1.5h1.3V4.9c-.2 0-1-.1-1.9-.1-1.9 0-3.3 1.2-3.3 3.4V11H8.5v3h2.6v7h2.4z"/></svg>
Facebook
</a>
<button type="button" class="ming-share-btn copy">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M10 14a5 5 0 0 0 7.1 0l2.1-2.1a5 5 0 0 0-7.1-7.1l-1.2 1.2"/><path d="M14 10a5 5 0 0 0-7.1 0l-2.1 2.1a5 5 0 0 0 7.1 7.1l1.2-1.2"/></svg>
<?php esc_html_e( '複製連結', 'ming' ); ?>
</button>
</div>
<?php
}
/* ---------- 品牌專欄:分類 / 範例文章 / 頁面與選單種子 ---------- */
/**
* 範例文章定義(繁中草稿,後台可自由修改)。
*
* @return array
*/
function ming_journal_seed_posts() {
return array(
'brand-journal' => array(
array(
'slug' => 'why-ceramic-liner',
'title' => '為什麼是陶瓷?—— 一只內膽杯的起點',
'imgkey' => '雙用',
'body' => '<p>每個品牌都有自己的起點。MING 的起點,來自一個很日常的困擾:保溫杯裡的金屬味。</p><p>咖啡放進不鏽鋼杯,下午再喝,總有一絲說不上來的味道;裝過牛奶的杯子,隔天泡茶,茶香裡總混著別的氣息。我們開始問:有沒有一種材質,可以忠實地裝下每一種飲品,又什麼都不留下?</p><p>答案是陶瓷。純陶瓷內膽抗酸鹼、耐腐蝕,不與飲品產生反應,也不殘留味道——裝過手沖咖啡的杯子,洗淨後再裝冷泡茶,味道依然乾淨。</p><blockquote>讓器物退到飲品後面,是我們對「好杯子」的定義。</blockquote><p>於是我們把陶瓷放進雙層真空結構裡,讓它既有陶瓷的純淨,也有保溫杯的鎖溫能力。這是 MING 的第一只杯子,也是我們想一直做下去的事。</p>',
),
array(
'slug' => 'meaning-of-ming',
'title' => '明與命:我們給品牌取名字的緣由',
'imgkey' => '漸層',
'body' => '<p>MING,取自「明」與「命」。</p><p>「明」,是光明,是看見,是對美好生活的嚮往;「命」,是生命,是人與自然之間的連結,也是萬物生生不息的延續。</p><p>品牌標誌的外圓,以時鐘與田地為意象——時間的流轉與土地的循環。我們相信,器物不是孤立的存在:一只杯子連結著裝進去的茶葉與咖啡豆,連結著種出它們的土地,也連結著使用它的每一個日常。</p><blockquote>以時間為序,以土地為根,以光明為引。</blockquote><p>這句話寫在 MING 的首頁,也寫在每一只杯子的設計裡。希望它陪伴你的每一天,都明亮而有根。</p>',
),
),
'media-coverage' => array(
array(
'slug' => 'media-plastic-free-trend',
'title' => '媒體報導|陶瓷內膽杯成為減塑新寵,MING 以「不串味」突圍',
'imgkey' => '冰霸',
'body' => '<p>(範例報導稿,實際上刊後可替換為媒體原文與連結)</p><p>隨著自帶杯政策與減塑意識升溫,隨行杯市場競爭激烈。近期主打「純陶瓷內膽」的品牌 MING 明命受到關注:有別於市售不鏽鋼內膽,陶瓷材質不殘留味道,裝咖啡、茶飲、牛奶都不串味,成為許多手搖飲愛好者的新選擇。</p><p>報導指出,MING 產品線涵蓋大容量冰霸杯、泡茶直飲雙用杯與輕巧迷你杯,全系列內膽皆可拆洗,瞄準「味道潔癖」與「好清洗」兩大痛點。</p><blockquote>「讓每一杯飲品,都喝得到它本來的味道。」——報導引述品牌理念</blockquote><p>未來若有媒體正式報導,可於後台直接編輯本篇,換上真實媒體名稱、日期與原文連結。</p>',
),
array(
'slug' => 'media-brand-interview',
'title' => '媒體專訪|從辦公室水杯到生活儀式:MING 的品牌觀察',
'imgkey' => '隨行',
'body' => '<p>(範例專訪稿,實際上刊後可替換為媒體原文與連結)</p><p>「杯子是少數一天會被使用好幾次的器物。」訪談中,MING 團隊談起品牌觀察:早上的咖啡、午後的茶、運動後的冰水——杯子參與了生活的每一段節奏,卻很少被認真對待。</p><p>因此 MING 在設計上格外重視「長期使用」的細節:抗酸鹼的陶瓷內膽、可全拆洗的結構、一提就走的提把。團隊表示,未來也將透過「新品許願池」邀請使用者投票決定下一批新品的顏色與容量,讓產品從使用者的日常裡長出來。</p><p>未來若有正式專訪刊出,可於後台直接編輯本篇內容。</p>',
),
),
'unboxing' => array(
array(
'slug' => 'unboxing-bingba-1000',
'title' => '開箱|冰霸杯:裝得下整個下午的冰美式',
'imgkey' => '冰霸',
'body' => '<p>(範例開箱文,歡迎於後台替換為真實使用者心得)</p><p>第一眼的印象是「大」。寬口設計讓加冰塊變得很從容,整杯手搖飲倒進去也沒問題。提把是一體成型的加粗設計,裝滿水提起來不勒手,從辦公室走到健身房一路拎著也不彆扭。</p><p>最驚豔的是內膽。下午裝了冰美式,晚上洗淨後裝溫水——完全沒有咖啡味殘留。陶瓷內膽摸起來是溫潤的霧面質感,和金屬內膽的冰冷感很不一樣。</p><ul><li>保冷實測:早上 8 點裝的冰飲,下午 2 點冰塊尚未全融</li><li>拆洗:杯蓋、吸管、內膽皆可拆,海綿可以伸到底</li><li>缺點:大容量裝滿後較有份量,適合定點或短程攜帶</li></ul>',
),
array(
'slug' => 'unboxing-brew-tea-week',
'title' => '實測|雙用杯泡茶一週心得:清洗真的不麻煩嗎?',
'imgkey' => '雙用',
'body' => '<p>(範例開箱文,歡迎於後台替換為真實使用者心得)</p><p>這一週我把辦公室的馬克杯換成 MING 雙用杯,早上直飲裝拿鐵,下午換上茶倉泡烏龍。重力鎖水的設計讓直飲與泡茶兩種模式切換很直覺,不用額外帶茶具。</p><p>原本最擔心的是清洗——茶垢向來是杯子的敵人。實測一週下來,陶瓷內膽用海綿加中性清潔劑就能洗淨,沒有用到科技海綿,也沒有殘留茶色。窄縫處搭配附贈概念中的小刷具會更徹底。</p><ul><li>優點:不串味是真的,茶與咖啡同杯無壓力</li><li>優點:茶倉濾網細,茶葉不會喝到嘴裡</li><li>注意:裝熱飲時外層仍建議握杯身,避免直接碰杯口金屬件</li></ul>',
),
),
);
}
/**
* v1.7.0 主題啟用種子:專欄分類 + 範例文章 + 專欄頁 + 關於我們更名 + 選單入口。
*/
function ming_seed_journal_content() {
// 1. 三個分類.
$cats = array(
'brand-journal' => '品牌專欄',
'media-coverage' => '媒體報導',
'unboxing' => '開箱評測',
);
$cat_ids = array();
foreach ( $cats as $slug => $name ) {
$cat = get_category_by_slug( $slug );
if ( $cat ) {
$cat_ids[ $slug ] = $cat->term_id;
} else {
$term = wp_insert_term( $name, 'category', array( 'slug' => $slug ) );
if ( ! is_wp_error( $term ) ) { $cat_ids[ $slug ] = $term['term_id']; }
}
}
// 2. 專欄頁(模板:template-journal.php).
$journal_page = get_page_by_path( 'journal' );
if ( ! $journal_page ) {
$journal_id = wp_insert_post( array(
'post_type' => 'page',
'post_status' => 'publish',
'post_name' => 'journal',
'post_title' => '品牌專欄',
'post_content' => '',
) );
} else {
$journal_id = $journal_page->ID;
}
if ( $journal_id && ! is_wp_error( $journal_id ) ) {
update_post_meta( $journal_id, '_wp_page_template', 'template-journal.php' );
}
// 3. 收集商品特色圖(供範例文章配圖).
$thumb_by_key = array();
$thumb_pool = array();
if ( post_type_exists( 'product' ) ) {
foreach ( get_posts( array( 'post_type' => 'product', 'posts_per_page' => -1, 'post_status' => 'publish' ) ) as $pp ) {
$tid = (int) get_post_thumbnail_id( $pp->ID );
if ( ! $tid ) { continue; }
$thumb_pool[] = $tid;
foreach ( array( '冰霸', '雙用', '小清新', '漸層', '迷你', '隨行' ) as $key ) {
if ( false !== mb_strpos( $pp->post_title, $key ) && ! isset( $thumb_by_key[ $key ] ) ) {
$thumb_by_key[ $key ] = $tid;
}
}
}
}
// 4. 範例文章(依 slug 跳過已存在).
$pool_i = 0;
foreach ( ming_journal_seed_posts() as $cat_slug => $posts ) {
if ( empty( $cat_ids[ $cat_slug ] ) ) { continue; }
foreach ( $posts as $p ) {
$exist = get_page_by_path( $p['slug'], OBJECT, 'post' );
if ( $exist ) { continue; }
$pid = wp_insert_post( array(
'post_type' => 'post',
'post_status' => 'publish',
'post_name' => $p['slug'],
'post_title' => $p['title'],
'post_content' => $p['body'],
'post_excerpt' => wp_trim_words( wp_strip_all_tags( $p['body'] ), 40, '…' ),
'post_category' => array( $cat_ids[ $cat_slug ] ),
) );
if ( ! $pid || is_wp_error( $pid ) ) { continue; }
$tid = isset( $thumb_by_key[ $p['imgkey'] ] ) ? $thumb_by_key[ $p['imgkey'] ] : ( $thumb_pool ? $thumb_pool[ $pool_i % count( $thumb_pool ) ] : 0 );
$pool_i++;
if ( $tid ) { set_post_thumbnail( $pid, $tid ); }
}
}
// 5. 品牌故事頁更名為「關於我們」.
foreach ( get_posts( array( 'post_type' => 'page', 'posts_per_page' => -1, 'post_status' => 'any', 'meta_key' => '_wp_page_template', 'meta_value' => 'template-brand.php' ) ) as $bp ) {
if ( '品牌故事' === $bp->post_title ) {
wp_update_post( array( 'ID' => $bp->ID, 'post_title' => '關於我們' ) );
}
}
// 6. 選單入口:主選單與頁腳「品牌探索」補上品牌專欄,並同步更名品牌故事→關於我們.
$locations = get_theme_mod( 'nav_menu_locations', array() );
$journal_url = $journal_id && ! is_wp_error( $journal_id ) ? get_permalink( $journal_id ) : home_url( '/journal/' );
foreach ( array( 'primary', 'footer_brand' ) as $loc ) {
if ( empty( $locations[ $loc ] ) ) { continue; }
$menu_id = $locations[ $loc ];
$items = wp_get_nav_menu_items( $menu_id );
if ( ! $items ) { continue; }
$has_journal = false;
foreach ( $items as $it ) {
if ( '品牌故事' === $it->title ) {
wp_update_nav_menu_item( $menu_id, $it->ID, array(
'menu-item-title' => '關於我們',
'menu-item-url' => $it->url,
'menu-item-object-id' => $it->object_id,
'menu-item-object' => $it->object,
'menu-item-type' => $it->type,
'menu-item-status' => 'publish',
) );
}
if ( false !== mb_strpos( $it->title, '專欄' ) ) { $has_journal = true; }
}
if ( ! $has_journal ) {
wp_update_nav_menu_item( $menu_id, 0, array(
'menu-item-title' => '品牌專欄',
'menu-item-url' => $journal_url,
'menu-item-type' => 'custom',
'menu-item-status' => 'publish',
) );
}
}
}
add_action( 'after_switch_theme', 'ming_seed_journal_content' );
/* ---------- SKU:全域顏色屬性 + 可變商品 + 色票占位圖 ---------- */
/* 伺服器 PATH_INFO 不支援非 ASCII slug:存檔時自動把含中文的 slug 轉為 ASCII */
add_action( 'wp_insert_post', 'ming_force_ascii_slug', 20, 2 );
function ming_force_ascii_slug( $post_id, $post ) {
if ( wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) ) { return; }
if ( in_array( $post->post_status, array( 'auto-draft', 'inherit', 'trash' ), true ) ) { return; }
if ( ! $post->post_name || ! preg_match( '/[^\x21-\x7E]/', $post->post_name ) ) { return; }
if ( ! in_array( $post->post_type, array( 'product', 'post', 'page', 'ming_campaign' ), true ) ) { return; }
if ( 'product' === $post->post_type ) {
$code = ming_series_code( $post->post_title );
$base = 'ming-' . strtolower( $code ? $code : 'item' );
if ( preg_match( '/(\d+)\s*(ml|毫升)/iu', $post->post_title, $mm ) ) { $base .= '-' . $mm[1]; }
} elseif ( 'ming_campaign' === $post->post_type ) {
$base = 'wish';
} else {
$base = $post->post_type;
}
$new = wp_unique_post_slug( $base, $post_id, $post->post_status, $post->post_type, $post->post_parent );
if ( $new && $new !== $post->post_name ) {
wp_update_post( array( 'ID' => $post_id, 'post_name' => $new ) );
}
}
/** 預設顏色(代碼對應 SKU 後綴,與亿看 ERP PROD_CD 編碼相容) */
function ming_color_presets() {
return array(
array( 'name' => '米白', 'slug' => 'off-white', 'code' => 'WH', 'hex' => '#F1EAD9', 'dark' => false ),
array( 'name' => '墨綠', 'slug' => 'ink-green', 'code' => 'GN', 'hex' => '#3E4A3F', 'dark' => true ),
array( 'name' => '暖棕', 'slug' => 'warm-brown', 'code' => 'BR', 'hex' => '#A4835F', 'dark' => true ),
array( 'name' => '黛藍', 'slug' => 'mist-blue', 'code' => 'BL', 'hex' => '#5B7089', 'dark' => true ),
);
}
/** 系列代碼(依商品名關鍵字) */
function ming_series_code( $title ) {
$map = array(
'冰霸' => 'FATE', '雙用' => 'BREW', '小清新' => 'FRESH',
'漸層' => 'GRAD', '迷你' => 'MINI', '隨行' => 'HNDL',
);
foreach ( $map as $k => $v ) {
if ( false !== mb_strpos( $title, $k ) ) { return $v; }
}
return '';
}
/**
* 以 GD 產生色票占位圖(色塊底 + 顏色名 + 商品名),回傳 attachment ID。
* 已存在同名檔案時直接重用。
*
* @param string $color_name 顏色名(米白…)
* @param string $hex 色值
* @param bool $dark 深色底(文字用淺色)
* @param string $caption 商品名/容量說明
* @return int attachment ID(失敗回 0)
*/
function ming_make_swatch_image( $color_name, $hex, $dark, $caption ) {
if ( ! function_exists( 'imagettftext' ) ) { return 0; }
$upload = wp_upload_dir();
$slug = sanitize_title( $color_name . '-' . $caption );
$fname = 'ming-swatch-' . md5( $color_name . '|' . $caption ) . '.jpg';
$path = $upload['path'] . '/' . $fname;
if ( ! file_exists( $path ) ) {
$font = get_template_directory() . '/assets/fonts/NotoSerifTC-GD.ttf';
if ( ! file_exists( $font ) ) { return 0; }
$w = 800; $h = 800;
$im = imagecreatetruecolor( $w, $h );
list( $r, $g, $b ) = sscanf( $hex, '#%02x%02x%02x' );
$bg = imagecolorallocate( $im, $r, $g, $b );
$fg = $dark ? imagecolorallocate( $im, 245, 242, 234 ) : imagecolorallocate( $im, 44, 51, 40 );
$gold = $dark ? imagecolorallocate( $im, 191, 160, 106 ) : imagecolorallocate( $im, 158, 128, 74 );
imagefilledrectangle( $im, 0, 0, $w, $h, $bg );
// 外框細線.
imagerectangle( $im, 34, 34, $w - 34, $h - 34, $gold );
// 顏色名(大字,置中偏上).
$size = 120;
$bb = imagettfbbox( $size, 0, $font, $color_name );
$tw = $bb[2] - $bb[0];
imagettftext( $im, $size, 0, (int) ( ( $w - $tw ) / 2 ), 400, $fg, $font, $color_name );
// 分隔線.
imageline( $im, (int) ( $w / 2 ) - 60, 452, (int) ( $w / 2 ) + 60, 452, $gold );
// 商品名/容量.
$size2 = 34;
$bb2 = imagettfbbox( $size2, 0, $font, $caption );
$tw2 = $bb2[2] - $bb2[0];
imagettftext( $im, $size2, 0, (int) ( ( $w - $tw2 ) / 2 ), 530, $fg, $font, $caption );
// 品牌字.
$size3 = 26;
$brand = 'MING 明命 · 陶瓷內膽';
$bb3 = imagettfbbox( $size3, 0, $font, $brand );
$tw3 = $bb3[2] - $bb3[0];
imagettftext( $im, $size3, 0, (int) ( ( $w - $tw3 ) / 2 ), 620, $gold, $font, $brand );
imagejpeg( $im, $path, 88 );
imagedestroy( $im );
}
// 重用已存在的 attachment.
global $wpdb;
$exist = $wpdb->get_var( $wpdb->prepare(
"SELECT post_id FROM {$wpdb->postmeta} WHERE meta_key = '_wp_attached_file' AND meta_value = %s LIMIT 1",
ltrim( str_replace( $upload['basedir'], '', $path ), '/' )
) );
if ( $exist ) { return (int) $exist; }
$aid = wp_insert_attachment( array(
'guid' => $upload['url'] . '/' . $fname,
'post_mime_type' => 'image/jpeg',
'post_title' => $color_name . ' ' . $caption . ' 色票',
'post_content' => '',
'post_status' => 'inherit',
), $path );
if ( ! $aid || is_wp_error( $aid ) ) { return 0; }
require_once ABSPATH . 'wp-admin/includes/image.php';
wp_update_attachment_metadata( $aid, wp_generate_attachment_metadata( $aid, $path ) );
return $aid;
}
/**
* v1.7.0 主題啟用種子:建立「顏色」全域屬性,將 6 款商品轉為可變商品(4 色 SKU)。
*/
function ming_seed_skus() {
if ( ! class_exists( 'WooCommerce' ) || ! post_type_exists( 'product' ) ) { return; }
if ( ! function_exists( 'imagettftext' ) ) { return; }
// 1. 建立全域屬性 pa_color.
$tax = 'pa_color';
if ( ! taxonomy_exists( $tax ) && function_exists( 'wc_create_attribute' ) ) {
$existing = wc_get_attribute_taxonomies();
$found = false;
foreach ( $existing as $att ) {
if ( 'color' === $att->attribute_name || '顏色' === $att->attribute_label ) { $found = true; }
}
if ( ! $found ) {
wc_create_attribute( array(
'name' => '顏色',
'slug' => 'color',
'type' => 'select',
'order_by' => 'menu_order',
'has_archives' => false,
) );
delete_transient( 'wc_attribute_taxonomies' );
}
}
if ( ! taxonomy_exists( $tax ) ) {
// 同一個 request 內手動註冊,才能立即建 term.
register_taxonomy( $tax, 'product', array(
'hierarchical' => false,
'show_ui' => false,
'query_var' => true,
'rewrite' => false,
) );
}
// 2. 建立顏色 term + 記錄 hex.
$term_map = array(); // slug => ['term'=>WP_Term,'code'=>..,'hex'=>..,'dark'=>..]
foreach ( ming_color_presets() as $c ) {
$t = get_term_by( 'slug', $c['slug'], $tax );
if ( ! $t ) {
$r = wp_insert_term( $c['name'], $tax, array( 'slug' => $c['slug'] ) );
if ( is_wp_error( $r ) ) { continue; }
$t = get_term( $r['term_id'], $tax );
}
update_term_meta( $t->term_id, 'ming_hex', $c['hex'] );
$term_map[ $c['slug'] ] = array_merge( $c, array( 'term_id' => (int) $t->term_id ) );
}
if ( ! $term_map ) { return; }
// 3. 逐商品轉可變商品.
$attr_tax_id = 0;
foreach ( wc_get_attribute_taxonomies() as $att ) {
if ( 'pa_color' === wc_attribute_taxonomy_name( $att->attribute_name ) ) {
$attr_tax_id = (int) $att->attribute_id;
}
}
$products = get_posts( array(
'post_type' => 'product',
'posts_per_page' => -1,
'post_status' => 'publish',
) );
foreach ( $products as $pp ) {
$product = wc_get_product( $pp->ID );
if ( ! $product || $product->is_type( 'variable' ) ) { continue; }
$price = $product->get_regular_price();
if ( '' === $price ) { $price = $product->get_price(); }
$stock = $product->get_stock_status();
$base = 'MING-' . ( ming_series_code( $pp->post_title ) ?: 'P' . $pp->ID );
// 屬性設定.
$attr = new WC_Product_Attribute();
$attr->set_id( $attr_tax_id );
$attr->set_name( $tax );
$attr->set_options( array_map( function( $m ) { return $m['term_id']; }, $term_map ) );
$attr->set_position( 0 );
$attr->set_visible( true );
$attr->set_variation( true );
$new = new WC_Product_Variable( $pp->ID );
$new->set_attributes( array( $attr ) );
$new->save();
// 預設色:米白.
$default_slug = 'off-white';
if ( isset( $term_map[ $default_slug ] ) ) {
update_post_meta( $pp->ID, '_default_attributes', array( 'color' => $default_slug ) );
}
// 建立變體.
foreach ( $term_map as $slug => $c ) {
// 容量取自標題本身,勿再拼接(避免「1200ml 1200ml」重複).
$caption = trim( $pp->post_title );
$img_id = ming_make_swatch_image( $c['name'], $c['hex'], $c['dark'], $caption );
$v = new WC_Product_Variation();
$v->set_parent_id( $pp->ID );
$v->set_attributes( array( $tax => $slug ) );
if ( '' !== $price ) { $v->set_regular_price( $price ); }
$v->set_sku( $base . '-' . $c['code'] );
$v->set_stock_status( $stock );
$v->set_status( 'publish' );
if ( $img_id ) { $v->set_image_id( $img_id ); }
$v->save();
}
wc_delete_product_transients( $pp->ID );
}
}
add_action( 'after_switch_theme', 'ming_seed_skus' );
/* ---------- 商品頁:顏色下拉換成色卡圓鈕 ---------- */
add_filter( 'woocommerce_dropdown_variation_attribute_options_html', 'ming_variation_swatches', 10, 2 );
function ming_variation_swatches( $html, $args ) {
$attribute = isset( $args['attribute'] ) ? $args['attribute'] : '';
if ( 'pa_color' !== $attribute ) { return $html; }
$product = $args['product'];
$options = $args['options'];
$selected = $args['selected'];
$btns = '<div class="ming-swatches" data-attribute="' . esc_attr( $attribute ) . '">';
foreach ( $options as $slug ) {
$term = get_term_by( 'slug', $slug, $attribute );
if ( ! $term ) { continue; }
$hex = get_term_meta( $term->term_id, 'ming_hex', true );
if ( ! $hex ) {
foreach ( ming_color_presets() as $c ) {
if ( $c['slug'] === $slug ) { $hex = $c['hex']; break; }
}
}
$btns .= sprintf(
'<button type="button" class="ming-swatch%s" data-value="%s" aria-label="%s"><i style="background:%s"></i><span>%s</span></button>',
( $selected === $slug ? ' is-active' : '' ),
esc_attr( $slug ),
esc_attr( $term->name ),
esc_attr( $hex ? $hex : '#D8D2C4' ),
esc_html( $term->name )
);
}
$btns .= '</div>';
// 原生 select 保留(WooCommerce 變體 JS 需要),僅視覺隱藏.
return $btns . '<div class="ming-select-hidden" aria-hidden="false">' . $html . '</div>';
}
/* ============================================================
* 海報生成器(後台:產品 → 海報生成器)
* GD 合成 1080×1350 社群豎版海報,隨機版式 × 文案 × 配色,保存生成記錄。
* ==========================================================*/
/** 品牌文案庫(可於後台自行擴充:ming_poster_copies filter) */
function ming_poster_copies() {
$copies = array(
'純陶瓷內膽,喝得到本來的味道',
'裝得下冰美式,也裝得下山的清晨',
'不與味道妥協',
'把土地的溫度,握在手心',
'以時間為序,以光明為引',
'好杯子,讓飲品說話',
'從今天起,只喝本來的味道',
'明亮生命,自然連結',
'一杯,一天,一種生活節奏',
'陶瓷的純,時間的溫',
);
return apply_filters( 'ming_poster_copies', $copies );
}
/** 海報配色方案(背景 / 主文字 / 點綴) */
function ming_poster_palettes() {
return array(
array( '#F3EDE2', '#2C3328', '#BFA06A' ), // 米白 × 深綠 × 金
array( '#2C3328', '#F3EDE2', '#BFA06A' ), // 深綠 × 米白 × 金
array( '#A4835F', '#FBF8F0', '#2C3328' ), // 暖棕 × 米白 × 深綠
array( '#5B7089', '#F5F2EA', '#E9D9B8' ), // 黛藍 × 米白 × 淡金
array( '#EFE7D8', '#3E4A3F', '#BFA06A' ), // 紙色 × 墨綠 × 金
);
}
/**
* 依字體寬度自動換行(中文按字元切)。
*
* @return array 各行文字
*/
function ming_poster_wrap( $font, $size, $text, $max_w ) {
$text = trim( $text );
if ( '' === $text ) { return array(); }
$lines = array();
$line = '';
$chars = preg_split( '//u', $text, -1, PREG_SPLIT_NO_EMPTY );
foreach ( $chars as $ch ) {
$try = $line . $ch;
$bb = imagettfbbox( $size, 0, $font, $try );
$w = $bb[2] - $bb[0];
if ( $w > $max_w && '' !== $line ) {
$lines[] = $line;
$line = $ch;
} else {
$line = $try;
}
}
if ( '' !== $line ) { $lines[] = $line; }
return $lines;
}
/** 置中寫一行字,回傳基線 y 之後的高度資訊 */
function ming_poster_text_center( $im, $font, $size, $y, $text, $color, $w = 1080 ) {
$bb = imagettfbbox( $size, 0, $font, $text );
$tw = $bb[2] - $bb[0];
imagettftext( $im, $size, 0, (int) ( ( $w - $tw ) / 2 - $bb[0] ), $y, $color, $font, $text );
}
/**
* 生成一張海報,寫入 uploads 並建立 attachment。
*
* @param int $product_id 商品 ID
* @param string $copy 主文案
* @return array|WP_Error { attachment_id, url, path }
*/
function ming_generate_poster( $product_id, $copy ) {
if ( ! function_exists( 'imagettftext' ) ) {
return new WP_Error( 'no_gd', '主機 PHP 未啟用 GD / FreeType,無法生成海報。' );
}
$font = get_template_directory() . '/assets/fonts/NotoSerifTC-GD.ttf';
if ( ! file_exists( $font ) ) {
return new WP_Error( 'no_font', '找不到海報字型檔(assets/fonts/NotoSerifTC-GD.ttf)。' );
}
$prod = $product_id ? wc_get_product( $product_id ) : null;
$W = 1080; $H = 1350;
$palettes = ming_poster_palettes();
$pal = $palettes[ array_rand( $palettes ) ];
$layout = mt_rand( 1, 3 );
$im = imagecreatetruecolor( $W, $H );
list( $br, $bgc, $bb_ ) = sscanf( $pal[0], '#%02x%02x%02x' );
list( $tr, $tg, $tb ) = sscanf( $pal[1], '#%02x%02x%02x' );
list( $ar, $ag, $ab ) = sscanf( $pal[2], '#%02x%02x%02x' );
$c_bg = imagecolorallocate( $im, $br, $bgc, $bb_ );
$c_txt = imagecolorallocate( $im, $tr, $tg, $tb );
$c_gold = imagecolorallocate( $im, $ar, $ag, $ab );
imagefilledrectangle( $im, 0, 0, $W, $H, $c_bg );
// 外框(雙細線).
imagerectangle( $im, 36, 36, $W - 36, $H - 36, $c_gold );
imagerectangle( $im, 46, 46, $W - 46, $H - 46, $c_gold );
// 商品圖(載入,依版式決定繪製時機).
$img_res = null;
if ( $prod ) {
$thumb_id = get_post_thumbnail_id( $prod->get_id() );
$f = $thumb_id ? get_attached_file( $thumb_id ) : '';
if ( $f && file_exists( $f ) ) {
$src = @imagecreatefromstring( file_get_contents( $f ) );
if ( $src ) { $img_res = $src; }
}
}
// 品牌字標(上方).
$brand_y = ( 1 === $layout ) ? 120 : 92;
ming_poster_text_center( $im, $font, 30, $brand_y, 'M I N G 明 命', $c_gold );
// 依版式先畫上方圖,或僅設定文字起始位置.
$text_top = 300;
$sw = 0; $sh = 0;
if ( $img_res ) {
$sw = imagesx( $img_res ); $sh = imagesy( $img_res );
if ( 2 === $layout ) {
// 紙卡圖在上、文字在下.
$card_w = 720; $card_h = 620;
$cx = (int) ( ( $W - $card_w ) / 2 ); $cy = 120;
$paper = imagecolorallocate( $im, 251, 248, 240 );
imagefilledrectangle( $im, $cx, $cy, $cx + $card_w, $cy + $card_h, $paper );
imagerectangle( $im, $cx, $cy, $cx + $card_w, $cy + $card_h, $c_gold );
$dw = $card_w - 60; $dh = (int) ( $dw * $sh / $sw );
$maxh = $card_h - 60;
if ( $dh > $maxh ) { $dh = $maxh; $dw = (int) ( $dh * $sw / $sh ); }
imagecopyresampled( $im, $img_res, (int) ( $cx + ( $card_w - $dw ) / 2 ), (int) ( $cy + ( $card_h - $dh ) / 2 ), 0, 0, $dw, $dh, $sw, $sh );
$text_top = $cy + $card_h + 100;
} elseif ( 3 === $layout ) {
// 全幅上半圖 + 底部文字帶.
$dw = $W - 92; $dh = (int) ( $dw * $sh / $sw );
$maxh = 620;
if ( $dh > $maxh ) { $dh = $maxh; $dw = (int) ( $dh * $sw / $sh ); }
$dx = (int) ( ( $W - $dw ) / 2 ); $dy = 46;
imagecopyresampled( $im, $img_res, $dx, $dy, 0, 0, $dw, $dh, $sw, $sh );
imageline( $im, 92, $dy + $dh + 40, $W - 92, $dy + $dh + 40, $c_gold );
$text_top = $dy + $dh + 120;
} else {
// 字在上、圖在下(圖待文字排完後依剩餘空間繪製).
$text_top = 220;
}
}
// 主文案(自動換行,依行數調整字級;避免末行孤字).
$copy = trim( $copy );
$size = 76;
$lines = ming_poster_wrap( $font, $size, $copy, $W - 160 );
if ( count( $lines ) > 2 ) { $size = 62; $lines = ming_poster_wrap( $font, $size, $copy, $W - 160 ); }
while ( count( $lines ) > 1 && mb_strlen( end( $lines ) ) <= 2 && $size > 52 ) {
$size -= 8;
$lines = ming_poster_wrap( $font, $size, $copy, $W - 160 );
}
$y = $text_top;
foreach ( $lines as $ln ) {
ming_poster_text_center( $im, $font, $size, $y, $ln, $c_txt );
$y += (int) ( $size * 1.75 );
}
// 裝飾線.
imageline( $im, (int) ( $W / 2 ) - 70, $y - 18, (int) ( $W / 2 ) + 70, $y - 18, $c_gold );
$y += 56;
if ( $img_res && 1 === $layout ) {
// 圖在下:依剩餘高度等比縮放;商品資訊併入底部一行,避免重疊.
$avail = ( $H - 250 ) - $y;
$dw = 620; $dh = (int) ( $dw * $sh / $sw );
if ( $dh > $avail ) { $dh = max( 220, $avail ); $dw = (int) ( $dh * $sw / $sh ); }
$dx = (int) ( ( $W - $dw ) / 2 ); $dy = (int) ( $y + ( $avail - $dh ) / 2 );
imageellipse( $im, (int) ( $W / 2 ), $dy + (int) ( $dh / 2 ), 720, 720, $c_gold );
imagecopyresampled( $im, $img_res, $dx, $dy, 0, 0, $dw, $dh, $sw, $sh );
$foot = '';
if ( $prod ) {
$foot = $prod->get_name();
if ( '' !== $prod->get_price() ) {
$foot .= ' | NT$ ' . number_format( (float) $prod->get_price() );
}
}
if ( $foot ) { ming_poster_text_center( $im, $font, 28, $H - 168, $foot, $c_txt ); }
} elseif ( $prod ) {
// 商品資訊(文字流內).
ming_poster_text_center( $im, $font, 34, $y, $prod->get_name(), $c_txt );
$y += 64;
$price = $prod->get_price();
if ( '' !== $price ) {
ming_poster_text_center( $im, $font, 30, $y, 'NT$ ' . number_format( (float) $price ), $c_gold );
}
}
if ( $img_res ) { imagedestroy( $img_res ); }
// 底部小字.
ming_poster_text_center( $im, $font, 24, $H - 92, '純陶瓷內膽 · 抗酸鹼 · 不串味', $c_txt );
// 存檔.
$upload = wp_upload_dir();
$dir = $upload['basedir'] . '/ming-posters';
if ( ! is_dir( $dir ) ) { wp_mkdir_p( $dir ); }
$fname = 'poster-' . gmdate( 'Ymd-His' ) . '-' . mt_rand( 100, 999 ) . '.jpg';
$path = $dir . '/' . $fname;
imagejpeg( $im, $path, 90 );
imagedestroy( $im );
$aid = wp_insert_attachment( array(
'guid' => $upload['baseurl'] . '/ming-posters/' . $fname,
'post_mime_type' => 'image/jpeg',
'post_title' => '海報 ' . ( $prod ? $prod->get_name() : '通用' ) . ' ' . gmdate( 'Y-m-d H:i' ),
'post_content' => '',
'post_status' => 'inherit',
), $path );
if ( ! $aid || is_wp_error( $aid ) ) { return new WP_Error( 'attach_fail', '海報已生成但寫入媒體庫失敗。' ); }
require_once ABSPATH . 'wp-admin/includes/image.php';
wp_update_attachment_metadata( $aid, wp_generate_attachment_metadata( $aid, $path ) );
return array(
'attachment_id' => $aid,
'url' => wp_get_attachment_url( $aid ),
'path' => $path,
);
}
/** 生成記錄(option,最多保留 100 筆) */
function ming_poster_log( $entry = null ) {
$log = get_option( 'ming_poster_log', array() );
if ( null === $entry ) { return $log; }
array_unshift( $log, $entry );
$log = array_slice( $log, 0, 100 );
update_option( 'ming_poster_log', $log, false );
return $log;
}
/* ---------- 海報生成器:後台頁面 ---------- */
add_action( 'admin_menu', function() {
add_submenu_page(
'edit.php?post_type=product',
'MING 海報生成器',
'海報生成器',
'manage_woocommerce',
'ming-poster',
'ming_poster_admin_page'
);
} );
/** 生成動作處理 */
add_action( 'admin_post_ming_poster_generate', function() {
if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_die( '權限不足' ); }
check_admin_referer( 'ming_poster_generate' );
$pid = isset( $_POST['product_id'] ) ? absint( $_POST['product_id'] ) : 0;
$copy = '';
if ( isset( $_POST['copy_custom'] ) && '' !== trim( $_POST['copy_custom'] ) ) {
$copy = sanitize_textarea_field( wp_unslash( $_POST['copy_custom'] ) );
} elseif ( isset( $_POST['copy_pick'] ) && '' !== $_POST['copy_pick'] ) {
$copy = sanitize_text_field( wp_unslash( $_POST['copy_pick'] ) );
} else {
$copies = ming_poster_copies();
$copy = $copies[ array_rand( $copies ) ];
}
$copy = str_replace( array( "\r\n", "\r" ), ' ', $copy );
$res = ming_generate_poster( $pid, $copy );
if ( is_wp_error( $res ) ) {
wp_safe_redirect( admin_url( 'edit.php?post_type=product&page=ming-poster&err=' . rawurlencode( $res->get_error_message() ) ) );
exit;
}
ming_poster_log( array(
'time' => current_time( 'Y-m-d H:i' ),
'product' => $pid ? get_the_title( $pid ) : '通用',
'copy' => $copy,
'aid' => $res['attachment_id'],
'url' => $res['url'],
) );
wp_safe_redirect( admin_url( 'edit.php?post_type=product&page=ming-poster&created=' . $res['attachment_id'] ) );
exit;
} );
/** 重新生成(沿用記錄的商品與文案,重新抽版式配色) */
add_action( 'admin_post_ming_poster_regen', function() {
if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_die( '權限不足' ); }
$idx = isset( $_GET['idx'] ) ? absint( $_GET['idx'] ) : -1;
check_admin_referer( 'ming_poster_regen_' . $idx );
$log = ming_poster_log();
if ( ! isset( $log[ $idx ] ) ) { wp_safe_redirect( admin_url( 'edit.php?post_type=product&page=ming-poster' ) ); exit; }
$entry = $log[ $idx ];
$pid = 0;
if ( '通用' !== $entry['product'] ) {
$p = get_page_by_title( $entry['product'], OBJECT, 'product' );
if ( $p ) { $pid = $p->ID; }
}
$res = ming_generate_poster( $pid, $entry['copy'] );
if ( is_wp_error( $res ) ) {
wp_safe_redirect( admin_url( 'edit.php?post_type=product&page=ming-poster&err=' . rawurlencode( $res->get_error_message() ) ) );
exit;
}
ming_poster_log( array(
'time' => current_time( 'Y-m-d H:i' ),
'product' => $entry['product'],
'copy' => $entry['copy'],
'aid' => $res['attachment_id'],
'url' => $res['url'],
) );
wp_safe_redirect( admin_url( 'edit.php?post_type=product&page=ming-poster&created=' . $res['attachment_id'] ) );
exit;
} );
/** 後台頁面輸出 */
function ming_poster_admin_page() {
if ( ! current_user_can( 'manage_woocommerce' ) ) { wp_die( '權限不足' ); }
$copies = ming_poster_copies();
$products = get_posts( array( 'post_type' => 'product', 'posts_per_page' => -1, 'post_status' => 'publish', 'orderby' => 'title', 'order' => 'ASC' ) );
$log = ming_poster_log();
$created = isset( $_GET['created'] ) ? absint( $_GET['created'] ) : 0;
?>
<div class="wrap">
<h1>MING 海報生成器</h1>
<p class="description">選擇商品與文案,一鍵生成 1080×1350 社群豎版海報。每次生成的版式、配色與裝飾皆隨機,適合 LINE / IG / FB 分享。</p>
<?php if ( isset( $_GET['err'] ) ) : ?>
<div class="notice notice-error"><p><?php echo esc_html( wp_unslash( $_GET['err'] ) ); ?></p></div>
<?php endif; ?>
<?php if ( $created ) : $curl = wp_get_attachment_url( $created ); ?>
<div class="notice notice-success"><p>海報已生成!<?php if ( $curl ) : ?><a href="<?php echo esc_url( $curl ); ?>" target="_blank">預覽</a> / <a href="<?php echo esc_url( $curl ); ?>" download>下載</a><?php endif; ?></p></div>
<?php endif; ?>
<?php if ( ! function_exists( 'imagettftext' ) ) : ?>
<div class="notice notice-warning"><p>注意:主機 PHP 未啟用 GD / FreeType 擴充,目前無法生成海報。</p></div>
<?php endif; ?>
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" style="background:#fff;border:1px solid #dcdcde;padding:20px;max-width:760px">
<input type="hidden" name="action" value="ming_poster_generate">
<?php wp_nonce_field( 'ming_poster_generate' ); ?>
<h2 style="margin-top:0">生成新海報</h2>
<p>
<label><strong>商品</strong>(圖片與品名會合成進海報;選「通用」則只排文字)<br>
<select name="product_id" style="min-width:320px">
<option value="0">通用(不帶商品)</option>
<?php foreach ( $products as $p ) : ?>
<option value="<?php echo esc_attr( $p->ID ); ?>"><?php echo esc_html( $p->post_title ); ?></option>
<?php endforeach; ?>
</select></label>
</p>
<p><strong>文案</strong>(隨機抽一句,或指定):</p>
<div style="max-height:180px;overflow:auto;border:1px solid #ddd;padding:10px;margin-bottom:10px">
<label style="display:block;margin:4px 0"><input type="radio" name="copy_pick" value="" checked> 隨機抽一句</label>
<?php foreach ( $copies as $c ) : ?>
<label style="display:block;margin:4px 0"><input type="radio" name="copy_pick" value="<?php echo esc_attr( $c ); ?>"> <?php echo esc_html( $c ); ?></label>
<?php endforeach; ?>
</div>
<p>
<label><strong>自訂文案</strong>(填寫後優先於上方選項,建議 16 字以內)<br>
<textarea name="copy_custom" rows="2" style="width:100%;max-width:560px" placeholder="例如:新款上市,陪你過整個夏天"></textarea></label>
</p>
<p><button type="submit" class="button button-primary button-hero">生成海報(隨機版式)</button></p>
</form>
<h2 style="margin-top:32px">生成記錄(<?php echo count( $log ); ?> 筆,保留最近 100 筆)</h2>
<?php if ( ! $log ) : ?>
<p>尚無記錄。</p>
<?php else : ?>
<table class="widefat striped" style="max-width:960px">
<thead><tr><th style="width:90px">縮圖</th><th>時間</th><th>商品</th><th>文案</th><th>操作</th></tr></thead>
<tbody>
<?php foreach ( $log as $i => $e ) : ?>
<tr>
<td><?php if ( ! empty( $e['aid'] ) ) : ?><a href="<?php echo esc_url( $e['url'] ); ?>" target="_blank"><?php echo wp_get_attachment_image( $e['aid'], array( 72, 90 ) ); ?></a><?php endif; ?></td>
<td><?php echo esc_html( $e['time'] ); ?></td>
<td><?php echo esc_html( $e['product'] ); ?></td>
<td><?php echo esc_html( $e['copy'] ); ?></td>
<td>
<a class="button button-small" href="<?php echo esc_url( $e['url'] ); ?>" target="_blank">預覽</a>
<a class="button button-small" href="<?php echo esc_url( $e['url'] ); ?>" download>下載</a>
<a class="button button-small" href="<?php echo esc_url( wp_nonce_url( admin_url( 'admin-post.php?action=ming_poster_regen&idx=' . $i ), 'ming_poster_regen_' . $i ) ); ?>">換一版</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<?php
}