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' );
/**
* 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_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 '' . "\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();
?>
cart->get_cart_contents_count() ); ?>
__( '關於我們', '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 .= '' . esc_html( $label ) . '';
}
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 '
';
echo '';
echo '參與商品
';
$prods = get_posts( array( 'post_type' => 'product', 'posts_per_page' => -1, 'post_status' => 'publish', 'orderby' => 'title', 'order' => 'ASC' ) );
foreach ( $prods as $p ) {
printf(
'
',
$p->ID, checked( in_array( $p->ID, $selected, true ), true, false ), esc_html( $p->post_title )
);
}
if ( ! $prods ) { echo '
尚無商品。
'; }
echo '
';
}
function ming_campaign_stats_box( $post ) {
$products = array_map( 'intval', (array) get_post_meta( $post->ID, 'ming_products', true ) );
if ( ! $products ) { echo '儲存並選擇參與商品後,這裡會顯示投票統計。
'; return; }
$goal = (int) get_post_meta( $post->ID, 'ming_goal', true );
$goal = $goal ? $goal : ming_vote_goal();
echo '| 商品 / 顏色 | 票數 | 目標達成 |
';
$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 '| ' . esc_html( get_the_title( $pid ) ) . ' | ' . $v . ' | ' . $pct . '% |
';
$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 '| └ ' . esc_html( $cname ? $cname : ( '變體 #' . $vid ) ) . ' | ' . $sv . ' | |
';
}
}
}
echo '| 總票數 | ' . $total . ' | |
|---|
';
$voters = get_users( array( 'meta_key' => 'ming_voted_c' . $post->ID, 'fields' => array( 'ID', 'display_name', 'user_email' ) ) );
if ( $voters ) {
echo '投票會員(' . count( $voters ) . ')
| 會員 | 信箱 | 支持的商品 |
';
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 '| ' . esc_html( $u->display_name ) . ' | ' . esc_html( $u->user_email ) . ' | ' . esc_html( implode( '、', $names ) ) . ' |
';
}
echo '
';
} else {
echo '尚無會員投票。
';
}
}
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 '匯款資訊
';
if ( $gateway->instructions ) { echo wpautop( wptexturize( $gateway->instructions ) ); }
$accounts = $gateway->account_details;
if ( $accounts ) {
echo '
';
foreach ( $accounts as $a ) {
echo '- ';
if ( ! empty( $a['account_name'] ) ) { echo '戶名' . esc_html( $a['account_name'] ) . ''; }
if ( ! empty( $a['bank_name'] ) ) { echo '銀行' . esc_html( $a['bank_name'] ) . ''; }
if ( ! empty( $a['account_number'] ) ) { echo '帳號' . esc_html( $a['account_number'] ) . ''; }
if ( ! empty( $a['sort_code'] ) ) { echo '分行代碼' . esc_html( $a['sort_code'] ) . ''; }
echo '
';
}
echo '
';
}
echo '
請於匯款時備註訂單編號 #' . esc_html( $order->get_order_number() ) . ',確認到帳後我們將盡快為您出貨。
';
}
/* 投票會員 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 '現貨';
} else {
echo '缺貨';
}
}
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(
'聯絡我們',
'任何商品、訂單或合作問題,都歡迎與我們聯繫,我們將於 1–2 個工作日內回覆。
'
. '客服管道
- 客服信箱:請見頁腳「公司資訊」之客服信箱
- 服務時間:週一至週五 10:00–18:00(國定假日除外)
'
. '大宗採購與企業禮贈
企業禮贈、經銷通路與大宗訂購需求,請來信註明公司名稱、需求數量與預計用途,我們將由專人與您聯繫報價。
',
),
'faq' => array(
'常見問題 FAQ',
'陶瓷內膽可以裝碳酸飲料或牛奶嗎?
可以。純陶瓷內膽抗酸鹼、耐腐蝕、不殘留味道,咖啡、茶、果汁、牛奶都適用;使用後建議儘快清洗晾乾。
'
. '如何清洗與保養?
杯蓋、吸管與內膽皆可拆洗;請以中性清潔劑與海綿手洗,勿使用鋼刷,勿放入洗碗機高溫烘碗模式。
'
. '下單後多久出貨?
現貨商品於確認款項後 1–3 個工作日內出貨;如遇缺貨或預購,商品頁會標示預計出貨時間。
'
. '如何查詢訂單?
登入會員後可於「會員中心 → 訂單」查詢訂單狀態與匯款資訊。
',
),
'ordering-guide' => array(
'訂購須知',
'購物流程
選購商品 → 加入購物車 → 填寫收件資料 → 選擇付款方式 → 完成訂單。未註冊會員亦可直接結帳。
'
. '付款方式
目前提供銀行轉帳(ATM/網銀匯款)。下單後請於訂單頁面查看匯款帳號,並於 3 日內完成轉帳,逾期訂單將自動取消。
'
. '運費與配送
台灣本島常溫配送,運費依結帳頁顯示為準;離島與海外配送請先來信詢問。
',
),
'return-policy' => array(
'退換貨政策',
'七天鑑賞期
依消費者保護法,商品到貨翌日起算七天內可申請退貨(鑑賞期非試用期)。退回商品須為全新未使用、包裝完整。
'
. '不適用退貨之情形
- 已使用過或經清洗之飲具(衛生考量)
- 客製化商品
- 包裝或配件缺損
'
. '瑕疵與換貨
如收到瑕疵品或寄錯商品,請於到貨七天內拍照並來信客服信箱,我們將安排換貨並負擔來回運費。
'
. '退款方式
退貨驗收完成後 7–10 個工作日內,依原付款方式退款。
',
),
'privacy-policy' => array(
'隱私權政策',
'MING 明命(以下稱本公司)重視您的個人資料保護,本政策說明我們如何蒐集、使用與保護您的資料。
'
. '資料蒐集
您於本站註冊、訂購或參與活動時,我們會蒐集姓名、聯絡方式、收件地址與訂單紀錄等必要資料。
'
. '使用目的
僅用於訂單處理、配送、售後服務與會員活動通知;未經您同意,不會提供予無關之第三方。
'
. '資料安全與權利
您的資料將以合理之技術與程序保護。您可隨時聯繫客服查詢、更正或請求刪除您的個人資料。
',
),
);
}
/**
* 主題啟用時:建立頁腳資訊頁與三欄頁腳選單(已存在則跳過)。
*/
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 分享標籤 ---------- */
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\n";
echo '' . "\n";
echo '' . "\n";
echo '' . "\n";
echo '' . "\n";
echo '' . "\n";
echo '' . "\n";
if ( $image ) {
echo '' . "\n";
}
echo '' . "\n";
echo '' . "\n";
echo '' . "\n";
if ( $image ) {
echo '' . "\n";
}
echo "\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;
?>
array(
array(
'slug' => 'why-ceramic-liner',
'title' => '為什麼是陶瓷?—— 一只內膽杯的起點',
'imgkey' => '雙用',
'body' => '每個品牌都有自己的起點。MING 的起點,來自一個很日常的困擾:保溫杯裡的金屬味。
咖啡放進不鏽鋼杯,下午再喝,總有一絲說不上來的味道;裝過牛奶的杯子,隔天泡茶,茶香裡總混著別的氣息。我們開始問:有沒有一種材質,可以忠實地裝下每一種飲品,又什麼都不留下?
答案是陶瓷。純陶瓷內膽抗酸鹼、耐腐蝕,不與飲品產生反應,也不殘留味道——裝過手沖咖啡的杯子,洗淨後再裝冷泡茶,味道依然乾淨。
讓器物退到飲品後面,是我們對「好杯子」的定義。
於是我們把陶瓷放進雙層真空結構裡,讓它既有陶瓷的純淨,也有保溫杯的鎖溫能力。這是 MING 的第一只杯子,也是我們想一直做下去的事。
',
),
array(
'slug' => 'meaning-of-ming',
'title' => '明與命:我們給品牌取名字的緣由',
'imgkey' => '漸層',
'body' => 'MING,取自「明」與「命」。
「明」,是光明,是看見,是對美好生活的嚮往;「命」,是生命,是人與自然之間的連結,也是萬物生生不息的延續。
品牌標誌的外圓,以時鐘與田地為意象——時間的流轉與土地的循環。我們相信,器物不是孤立的存在:一只杯子連結著裝進去的茶葉與咖啡豆,連結著種出它們的土地,也連結著使用它的每一個日常。
以時間為序,以土地為根,以光明為引。
這句話寫在 MING 的首頁,也寫在每一只杯子的設計裡。希望它陪伴你的每一天,都明亮而有根。
',
),
),
'media-coverage' => array(
array(
'slug' => 'media-plastic-free-trend',
'title' => '媒體報導|陶瓷內膽杯成為減塑新寵,MING 以「不串味」突圍',
'imgkey' => '冰霸',
'body' => '(範例報導稿,實際上刊後可替換為媒體原文與連結)
隨著自帶杯政策與減塑意識升溫,隨行杯市場競爭激烈。近期主打「純陶瓷內膽」的品牌 MING 明命受到關注:有別於市售不鏽鋼內膽,陶瓷材質不殘留味道,裝咖啡、茶飲、牛奶都不串味,成為許多手搖飲愛好者的新選擇。
報導指出,MING 產品線涵蓋大容量冰霸杯、泡茶直飲雙用杯與輕巧迷你杯,全系列內膽皆可拆洗,瞄準「味道潔癖」與「好清洗」兩大痛點。
「讓每一杯飲品,都喝得到它本來的味道。」——報導引述品牌理念
未來若有媒體正式報導,可於後台直接編輯本篇,換上真實媒體名稱、日期與原文連結。
',
),
array(
'slug' => 'media-brand-interview',
'title' => '媒體專訪|從辦公室水杯到生活儀式:MING 的品牌觀察',
'imgkey' => '隨行',
'body' => '(範例專訪稿,實際上刊後可替換為媒體原文與連結)
「杯子是少數一天會被使用好幾次的器物。」訪談中,MING 團隊談起品牌觀察:早上的咖啡、午後的茶、運動後的冰水——杯子參與了生活的每一段節奏,卻很少被認真對待。
因此 MING 在設計上格外重視「長期使用」的細節:抗酸鹼的陶瓷內膽、可全拆洗的結構、一提就走的提把。團隊表示,未來也將透過「新品許願池」邀請使用者投票決定下一批新品的顏色與容量,讓產品從使用者的日常裡長出來。
未來若有正式專訪刊出,可於後台直接編輯本篇內容。
',
),
),
'unboxing' => array(
array(
'slug' => 'unboxing-bingba-1000',
'title' => '開箱|冰霸杯:裝得下整個下午的冰美式',
'imgkey' => '冰霸',
'body' => '(範例開箱文,歡迎於後台替換為真實使用者心得)
第一眼的印象是「大」。寬口設計讓加冰塊變得很從容,整杯手搖飲倒進去也沒問題。提把是一體成型的加粗設計,裝滿水提起來不勒手,從辦公室走到健身房一路拎著也不彆扭。
最驚豔的是內膽。下午裝了冰美式,晚上洗淨後裝溫水——完全沒有咖啡味殘留。陶瓷內膽摸起來是溫潤的霧面質感,和金屬內膽的冰冷感很不一樣。
- 保冷實測:早上 8 點裝的冰飲,下午 2 點冰塊尚未全融
- 拆洗:杯蓋、吸管、內膽皆可拆,海綿可以伸到底
- 缺點:大容量裝滿後較有份量,適合定點或短程攜帶
',
),
array(
'slug' => 'unboxing-brew-tea-week',
'title' => '實測|雙用杯泡茶一週心得:清洗真的不麻煩嗎?',
'imgkey' => '雙用',
'body' => '(範例開箱文,歡迎於後台替換為真實使用者心得)
這一週我把辦公室的馬克杯換成 MING 雙用杯,早上直飲裝拿鐵,下午換上茶倉泡烏龍。重力鎖水的設計讓直飲與泡茶兩種模式切換很直覺,不用額外帶茶具。
原本最擔心的是清洗——茶垢向來是杯子的敵人。實測一週下來,陶瓷內膽用海綿加中性清潔劑就能洗淨,沒有用到科技海綿,也沒有殘留茶色。窄縫處搭配附贈概念中的小刷具會更徹底。
- 優點:不串味是真的,茶與咖啡同杯無壓力
- 優點:茶倉濾網細,茶葉不會喝到嘴裡
- 注意:裝熱飲時外層仍建議握杯身,避免直接碰杯口金屬件
',
),
),
);
}
/**
* 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 = '';
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(
'',
( $selected === $slug ? ' is-active' : '' ),
esc_attr( $slug ),
esc_attr( $term->name ),
esc_attr( $hex ? $hex : '#D8D2C4' ),
esc_html( $term->name )
);
}
$btns .= '
';
// 原生 select 保留(WooCommerce 變體 JS 需要),僅視覺隱藏.
return $btns . '' . $html . '
';
}
/* ============================================================
* 海報生成器(後台:產品 → 海報生成器)
* 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;
?>
MING 海報生成器
選擇商品與文案,一鍵生成 1080×1350 社群豎版海報。每次生成的版式、配色與裝飾皆隨機,適合 LINE / IG / FB 分享。
注意:主機 PHP 未啟用 GD / FreeType 擴充,目前無法生成海報。
生成記錄( 筆,保留最近 100 筆)
尚無記錄。