仰望星辰工作室

feat(zjmf): 完成魔方财务代理分销云服务器插件功能集成

Y yang1145 提交于 2026-08-12 13:17 · e3f6c89 ·父提交 60d09a8
feat(zjmf): 完成魔方财务代理分销云服务器插件功能集成

1.  新增插件后端API与管理后台功能,支持商品同步、定价、订单与主机管理
2.  新增前台商品展示页、用户中心商城、主机管理、订单页面
3.  优化商品简介渲染逻辑,支持原生li列表格式展示
4.  适配前端SPA路由与侧边栏导航,完整打通前后端流程
18 个文件变更 +1427 -53 13172193298@163.com
•app_plugins/zjmfmanager_reserve/assets/style.css +4 -0
•app_plugins/zjmfmanager_reserve/bootstrap.php +150 -6
•app_plugins/zjmfmanager_reserve/lib/upstream.php +188 -25
•app_plugins/zjmfmanager_reserve/lib/zjmf.php +63 -20
•app_plugins/zjmfmanager_reserve/views/admin/products.php +58 -0
•app_plugins/zjmfmanager_reserve/views/order.php +1 -1
•app_plugins/zjmfmanager_reserve/views/shop.php +1 -1
•templates/tdesign/account/_spa_boot.php +1 -0
•templates/tdesign/home/index.php +1 -0
•templates/tdesign/spa/src/account/api/plugins.js +34 -0
•templates/tdesign/spa/src/account/layouts/AccountLayout.vue +45 -0
•templates/tdesign/spa/src/account/router/index.js +18 -0
•templates/tdesign/spa/src/account/views/ZjmfAssetsView.vue +309 -0
•templates/tdesign/spa/src/account/views/ZjmfOrdersView.vue +189 -0
•templates/tdesign/spa/src/account/views/ZjmfShopView.vue +279 -0
•templates/tdesign/spa/src/home/api/zjmf.js +13 -0
•templates/tdesign/spa/src/home/styles/home.scss +13 -0
•templates/tdesign/spa/src/home/views/LandingView.vue +60 -0
变更内容
diff --git a/app_plugins/zjmfmanager_reserve/assets/style.css b/app_plugins/zjmfmanager_reserve/assets/style.css
index f9c88c6..f070bf3 100644
--- a/app_plugins/zjmfmanager_reserve/assets/style.css
+++ b/app_plugins/zjmfmanager_reserve/assets/style.css
@@ -27,6 +27,10 @@
 .zj-card-desc img{max-width:100%;}
 .zj-desc{padding:10px 2px 18px;font-size:13px;color:#666;line-height:1.7;overflow-wrap:break-word;}
 .zj-desc img{max-width:100%;}
+/* 上游简介 li 列表:显式声明保证每项独占一行、带圆点(不受全局 reset 影响) */
+.zj-card-desc ul,.zj-desc ul{margin:6px 0 6px 18px;padding:0;}
+.zj-card-desc li,.zj-desc li{display:list-item;list-style:disc;margin:3px 0;padding:0;line-height:1.6;}
+.zj-card-desc ul li ul,.zj-desc ul li ul{list-style:circle;}
 .zj-price{display:flex;gap:14px;padding:12px 20px;background:#fafbfc;border-top:1px solid #f0f0f0;flex-wrap:wrap;}
 .zj-price-item{display:flex;align-items:center;gap:4px;font-size:12px;}
 .zj-price-label{background:#e8e8e8;color:#666;padding:1px 6px;border-radius:3px;}
diff --git a/app_plugins/zjmfmanager_reserve/bootstrap.php b/app_plugins/zjmfmanager_reserve/bootstrap.php
index 477f0f3..a0a7ab4 100644
--- a/app_plugins/zjmfmanager_reserve/bootstrap.php
+++ b/app_plugins/zjmfmanager_reserve/bootstrap.php
@@ -378,6 +378,56 @@ mnbt_register_ajax('admin', 'p_zjmf_admin_save_product', function () {
 	json_exit_success('已保存');
 });
 
+// 保存商品各周期售价(管理员直接定价,覆盖加价规则)
+mnbt_register_ajax('admin', 'p_zjmf_admin_save_cycles', function () {
+	mnbt_plugin_require_admin();
+	global $DB, $date;
+
+	$id = (int)($_POST['id'] ?? 0);
+	$product = zjmf_product_get($id);
+	if (!$product) {
+		json_exit_error('商品不存在');
+	}
+
+	$overrides = (isset($_POST['overrides']) && is_array($_POST['overrides']))
+		? $_POST['overrides'] : [];
+	if ($overrides === []) {
+		json_exit_error('请至少填写一个周期的售价');
+	}
+
+	$now = $date ?: date('Y-m-d H:i:s');
+	$known = zjmf_cycles();
+	$agentCents = max(0, (int)$product['agent_price_cents']);
+	$old = zjmf_product_cycles($product);
+	$save = [];
+	foreach ($overrides as $cycle => $val) {
+		$cycle = trim((string)$cycle);
+		if ($cycle === '' || !is_numeric($val)) {
+			continue;
+		}
+		$override = max(0, (int)round((float)$val * 100));
+		$entry = $old[$cycle] ?? [];
+		$save[$cycle] = [
+			'cycle'             => $cycle,
+			'name'              => (string)($entry['name'] ?? $known[$cycle]['name'] ?? $cycle),
+			'agent_price_cents' => (int)($entry['agent_price_cents'] ?? $agentCents),
+			'price_cents'       => $override,
+			'override'          => $override,
+		];
+	}
+	if ($save === []) {
+		json_exit_error('周期售价参数无效');
+	}
+	$ok = $DB->query_prepare(
+		"UPDATE MN_plugin_zjmf_product SET cycles=?, updated_at=? WHERE id=?",
+		[json_encode(array_values($save), JSON_UNESCAPED_UNICODE), $now, $id]
+	);
+	if (!$ok) {
+		json_exit_error('保存失败');
+	}
+	json_exit_success('周期售价已保存');
+});
+
 // 切换商品上架/下架
 mnbt_register_ajax('admin', 'p_zjmf_admin_toggle_product', function () {
 	mnbt_plugin_require_admin();
@@ -410,6 +460,28 @@ mnbt_register_route('GET', '/reserve/shop', function ($params, $ctx) {
 	]);
 });
 
+// 公开商品列表 API(供官网首页/游客展示,无需登录)
+mnbt_register_route('GET', '/reserve/api/public_products', function ($params, $ctx) {
+	$list = [];
+	foreach (zjmf_product_list_active() as $p) {
+		$min = 0;
+		foreach (zjmf_product_cycles($p) as $cfg) {
+			$c = (int)$cfg['price_cents'];
+			if ($c > 0 && ($min === 0 || $c < $min)) {
+				$min = $c;
+			}
+		}
+		$list[] = [
+			'id'             => (int)$p['id'],
+			'name'           => (string)$p['name'],
+			'description'    => zjmf_render_description((string)($p['description'] ?? '')),
+			'currency'       => (string)($p['currency'] ?? ''),
+			'min_price_cents'=> $min,
+		];
+	}
+	zjmf_json('ok', ['list' => $list]);
+});
+
 // 下单页(选周期 + 支付方式)
 mnbt_register_route('GET', '/reserve/product/{product_id}', function ($params, $ctx) {
 	zjmf_require_user();
@@ -422,7 +494,7 @@ mnbt_register_route('GET', '/reserve/product/{product_id}', function ($params, $
 	}
 	if (!zjmf_supplier_usable((int)$product['supplier_id'])) {
 		http_response_code(404);
-		echo '商品所属供应商已停用';
+		echo '该商品已暂停销售';
 		return;
 	}
 	$methods = function_exists('mnbt_get_enabled_payment_methods')
@@ -456,7 +528,7 @@ mnbt_register_route('POST', '/reserve/api/create_order', function ($params, $ctx
 		zjmf_json('商品不存在或已下架');
 	}
 	if (!zjmf_supplier_usable((int)$product['supplier_id'])) {
-		zjmf_json('商品所属供应商已停用,无法下单');
+		zjmf_json('该商品已暂停销售,无法下单');
 	}
 	$cycles = zjmf_product_cycles($product);
 	if (!isset($cycles[$cycle])) {
@@ -519,12 +591,84 @@ mnbt_register_route('POST', '/reserve/api/create_order', function ($params, $ctx
 		zjmf_json('支付方式不可用,请检查支付插件是否已启用');
 	}
 
-	zjmf_json('正在跳转到支付页面', [
+	zjmf_json('ok', [
 		'html'     => $html,
 		'order_no' => $order_no,
+		'msg'      => '正在跳转到支付页面',
 	]);
 });
 
+/* ============================================================
+ *  用户端 API 路由(SPA 数据接口)
+ * ============================================================ */
+
+// 上架商品列表(含各周期价格,游客可访问,供 account SPA 商城页)
+mnbt_register_route('GET', '/reserve/api/products', function ($params, $ctx) {
+	$list = [];
+	foreach (zjmf_product_list_active() as $p) {
+		$cycles = [];
+		foreach (zjmf_product_cycles($p) as $key => $cfg) {
+			$cycles[] = [
+				'key'         => $key,
+				'name'        => $cfg['name'],
+				'price_cents' => $cfg['price_cents'],
+			];
+		}
+		$list[] = [
+			'id'          => (int)$p['id'],
+			'name'        => (string)$p['name'],
+			'description' => zjmf_render_description((string)($p['description'] ?? '')),
+			'currency'    => (string)($p['currency'] ?? ''),
+			'cycles'      => $cycles,
+		];
+	}
+	zjmf_json('ok', ['list' => $list]);
+});
+
+// 可用支付方式(account SPA 下单页用)
+mnbt_register_route('GET', '/reserve/api/methods', function ($params, $ctx) {
+	$methods = function_exists('mnbt_get_enabled_payment_methods')
+		? mnbt_get_enabled_payment_methods() : [];
+	zjmf_json('ok', ['methods' => $methods]);
+});
+
+// 我的主机(account SPA,脱敏后返回)
+mnbt_register_route('GET', '/reserve/api/hosts', function ($params, $ctx) {
+	$user = function_exists('user_info_auth_current') ? user_info_auth_current() : null;
+	if (!$user) {
+		zjmf_json('not_login', ['logged_in' => false]);
+		return;
+	}
+	$hosts = [];
+	foreach (zjmf_host_list_by_user((int)$user['id']) as $h) {
+		$hosts[] = [
+			'id'            => (int)$h['id'],
+			'up_host_id'    => (int)$h['up_host_id'],
+			'name'          => (string)$h['name'],
+			'username'      => (string)$h['username'],
+			'cycle'         => (string)$h['cycle'],
+			'status'        => (string)$h['status'],
+			'renew_date'    => (string)$h['renew_date'],
+			'created_at'    => (string)$h['created_at'],
+			'updated_at'    => (string)$h['updated_at'],
+		];
+	}
+	zjmf_json('ok', ['logged_in' => true, 'hosts' => $hosts]);
+});
+
+// 我的订单(account SPA,分页)
+mnbt_register_route('GET', '/reserve/api/orders', function ($params, $ctx) {
+	$user = function_exists('user_info_auth_current') ? user_info_auth_current() : null;
+	if (!$user) {
+		zjmf_json('not_login', ['logged_in' => false]);
+		return;
+	}
+	$page = isset($_GET['page']) ? max(1, (int)$_GET['page']) : 1;
+	$per  = isset($_GET['per_page']) ? max(1, min(100, (int)$_GET['per_page'])) : 15;
+	$orders = zjmf_order_list_by_user((int)$user['id'], $page, $per);
+	zjmf_json('ok', ['logged_in' => true, 'orders' => $orders]);
+});
+
 // 我的订单
 mnbt_register_route('GET', '/reserve/orders', function ($params, $ctx) {
 	$user = zjmf_require_user();
@@ -594,7 +738,7 @@ mnbt_register_route('POST', '/reserve/api/host_action', function ($params, $ctx)
 		zjmf_json('该主机缺少上游主机 ID,无法执行操作');
 	}
 	if (!zjmf_supplier_usable((int)$host['supplier_id'])) {
-		zjmf_json('供应商已停用,无法执行操作');
+		zjmf_json('服务已停用,无法执行操作');
 	}
 	$supplier = zjmf_supplier_get((int)$host['supplier_id']);
 
@@ -662,7 +806,7 @@ mnbt_register_route('GET', '/reserve/hosts/{host_id}/upgrade', function ($params
 		return;
 	}
 	if (!zjmf_supplier_usable((int)$host['supplier_id'])) {
-		echo '供应商已停用,无法升级';
+		echo '服务已停用,无法升级';
 		return;
 	}
 	$supplier = zjmf_supplier_get((int)$host['supplier_id']);
@@ -693,7 +837,7 @@ mnbt_register_route('POST', '/reserve/api/upgrade', function ($params, $ctx) {
 		zjmf_json('该主机缺少上游主机 ID,无法升级');
 	}
 	if (!zjmf_supplier_usable((int)$host['supplier_id'])) {
-		zjmf_json('供应商已停用,无法升级');
+		zjmf_json('服务已停用,无法升级');
 	}
 	$supplier = zjmf_supplier_get((int)$host['supplier_id']);
 
diff --git a/app_plugins/zjmfmanager_reserve/lib/upstream.php b/app_plugins/zjmfmanager_reserve/lib/upstream.php
index 539e951..ccb825f 100644
--- a/app_plugins/zjmfmanager_reserve/lib/upstream.php
+++ b/app_plugins/zjmfmanager_reserve/lib/upstream.php
@@ -100,14 +100,33 @@ class ZjmfUpstream
 		}
 		try {
 			$upId = (int)$upProductId;
-			$detail = $client->productDetail($upId, 'agent');
-			$prod = $detail['data']['product'] ?? ($detail['data'] ?? []);
-			$item = [
-				'id'          => $upId,
-				'name'        => (string)($prod['name'] ?? ''),
-				'description' => (string)($prod['description'] ?? ''),
-			];
-			$currency = (string)($detail['data']['currency_code'] ?? '');
+			// 优先从列表取真实条目(含 billingcycle/price 等周期信息),失败再退回详情
+			$item = null;
+			$currency = '';
+			try {
+				$res = $client->productList();
+				if (($res['status'] ?? 0) == 200 && !empty($res['data']['list'])) {
+					$currency = (string)($res['data']['currency_code'] ?? '');
+					foreach ($res['data']['list'] as $row) {
+						if ((int)($row['id'] ?? 0) === $upId) {
+							$item = $row;
+							break;
+						}
+					}
+				}
+			} catch (CubeFinanceException $e) {
+				// 列表失败忽略,退回详情
+			}
+			if (!$item) {
+				$detail = $client->productDetail($upId, 'agent');
+				$prod = $detail['data']['product'] ?? ($detail['data'] ?? []);
+				$item = [
+					'id'          => $upId,
+					'name'        => (string)($prod['name'] ?? ''),
+					'description' => (string)($prod['description'] ?? ''),
+				];
+				$currency = (string)($detail['data']['currency_code'] ?? $currency);
+			}
 			$ok = self::syncOneProduct(
 				$client, (int)($supplier['id'] ?? 0), $upId, $item, $currency
 			);
@@ -194,8 +213,9 @@ class ZjmfUpstream
 		global $DB, $date;
 		$now = $date ?: date('Y-m-d H:i:s');
 
-		// 代理价(详情接口)
+		// 代理价(详情接口,失败回退列表项 price)
 		$agentCents = 0;
+		$detail = null;
 		try {
 			$detail = $client->productDetail($upId, 'agent');
 			$prod = $detail['data']['product'] ?? ($detail['data'] ?? []);
@@ -203,27 +223,51 @@ class ZjmfUpstream
 		} catch (CubeFinanceException $e) {
 			// 详情失败不致命,仅代理价缺失
 		}
+		if ($agentCents <= 0) {
+			$agentCents = self::itemPrice($item);
+		}
 
-		// 各周期试算价
-		$cycles = [];
-		foreach (zjmf_cycles() as $cycle => $cfg) {
-			try {
-				$trial = $client->cartSetConfig(['pid' => $upId, 'billingcycle' => $cycle]);
-				$price = self::parseTrialPrice($trial);
-				if ($price > 0) {
-					$cycles[] = [
-						'cycle'             => $cycle,
-						'name'              => $cfg['name'],
-						'agent_price_cents' => $price,
-					];
+		// 各周期价:优先读上游返回的 cycle 字段(列表项 → 详情),均无则回退购物车试算
+		$cycles = self::parseItemCycles($item);
+		if ($cycles === []) {
+			$cycles = $detail ? self::parseDetailCycles($detail) : [];
+		}
+		if ($cycles === []) {
+			foreach (zjmf_cycles() as $cycle => $cfg) {
+				try {
+					$trial = $client->cartSetConfig(['pid' => $upId, 'billingcycle' => $cycle]);
+					$price = self::parseTrialPrice($trial);
+					if ($price > 0) {
+						$cycles[$cycle] = [
+							'cycle'             => $cycle,
+							'name'              => $cfg['name'],
+							'agent_price_cents' => $price,
+						];
+					}
+				} catch (CubeFinanceException $e) {
+					continue;
 				}
-			} catch (CubeFinanceException $e) {
-				continue;
 			}
 		}
-		$cyclesJson = json_encode($cycles, JSON_UNESCAPED_UNICODE);
+		$cyclesJson = json_encode(array_values($cycles), JSON_UNESCAPED_UNICODE);
 
 		$existing = zjmf_product_get_by_up($supplierId, $upId);
+		if ($existing) {
+			// 同步不覆盖管理员手动设置的售价(override)
+			$oldCycles = zjmf_product_cycles($existing);
+			$changed = false;
+			foreach ($cycles as $k => &$cfg) {
+				if (isset($oldCycles[$k]['override']) && (int)$oldCycles[$k]['override'] > 0) {
+					$cfg['override'] = (int)$oldCycles[$k]['override'];
+					$cfg['price_cents'] = (int)$oldCycles[$k]['override'];
+					$changed = true;
+				}
+			}
+			unset($cfg);
+			if ($changed) {
+				$cyclesJson = json_encode(array_values($cycles), JSON_UNESCAPED_UNICODE);
+			}
+		}
 		if ($existing) {
 			// 重复同步仅刷新价格/周期,不覆盖本地已修改的名称与简介
 			$ok = $DB->query_prepare(
@@ -250,7 +294,7 @@ class ZjmfUpstream
 					$supplierId,
 					$upId,
 					(string)($item['name'] ?? ''),
-					(string)($item['description'] ?? ''),
+					zjmf_render_description((string)($item['description'] ?? '')),
 					$currency,
 					$agentCents,
 					$cyclesJson,
@@ -576,6 +620,125 @@ class ZjmfUpstream
 		return 0;
 	}
 
+	/**
+	 * 从列表项提取各周期价格(分)。
+	 * 魔方财务列表项为「单周期」结构:billingcycle + price + billingcycle_zh(cycle 只是标签字符串)。
+	 * 兼容完整周期结构(cycle/pricing 为数组或映射),解析失败返回空数组。
+	 */
+	protected static function parseItemCycles($item)
+	{
+		if (!is_array($item)) {
+			return [];
+		}
+		// 单周期结构:billingcycle + price + billingcycle_zh
+		$key = (string)($item['billingcycle'] ?? '');
+		if ($key !== '') {
+			$cents = self::toCents(self::pickPrice($item));
+			if ($cents > 0) {
+				$entry = self::cycleEntry($key, $cents, (string)($item['billingcycle_zh'] ?? ''));
+				return [$entry['cycle'] => $entry];
+			}
+		}
+		// 兼容完整周期结构:cycle/pricing 为数组或映射
+		$raw = $item['cycle'] ?? $item['pricing'] ?? null;
+		if (is_string($raw)) {
+			$decoded = json_decode($raw, true);
+			$raw = is_array($decoded) ? $decoded : null;
+		}
+		if (is_array($raw) && $raw !== []) {
+			return self::parseCycleMap($raw);
+		}
+		return [];
+	}
+
+	/**
+	 * 从商品详情响应提取各周期价格(分)。
+	 * 魔方财务详情接口将各周期价格放在 data.product.cycle(结构与列表项一致)。
+	 */
+	protected static function parseDetailCycles($detail)
+	{
+		$data = $detail['data'] ?? [];
+		if (!is_array($data)) {
+			return [];
+		}
+		$prod = is_array($data['product'] ?? null) ? $data['product'] : $data;
+		$raw = $prod['cycle'] ?? $data['cycle'] ?? null;
+		if (is_string($raw)) {
+			$decoded = json_decode($raw, true);
+			$raw = is_array($decoded) ? $decoded : null;
+		}
+		if (!is_array($raw) || $raw === []) {
+			return [];
+		}
+		return self::parseCycleMap($raw);
+	}
+
+	/** 构建单个周期条目(键归一化 + 名称兜底)。 */
+	protected static function cycleEntry($key, $cents, $name = '')
+	{
+		$norm = self::normalizeCycleKey($key);
+		$known = zjmf_cycles();
+		return [
+			'cycle'             => $norm,
+			'name'              => $name !== '' ? $name : ($known[$norm]['name'] ?? $norm),
+			'agent_price_cents' => max(0, (int)$cents),
+		];
+	}
+
+	/** 周期键归一化:忽略大小写匹配内置周期(monthly → Monthly)。 */
+	protected static function normalizeCycleKey($key)
+	{
+		$key = (string)$key;
+		if ($key === '') {
+			return '';
+		}
+		foreach (array_keys(zjmf_cycles()) as $k) {
+			if (strcasecmp($k, $key) === 0) {
+				return $k;
+			}
+		}
+		return $key;
+	}
+
+	/** 解析周期映射/数组,返回 ['Monthly' => ['cycle','name','agent_price_cents']]。 */
+	protected static function parseCycleMap($raw)
+	{
+		$out = [];
+		// 数组形式:[{billingcycle: 'monthly', billingcycle_zh: '月', price: '25.00'}, ...]
+		if (isset($raw[0]) && is_array($raw[0])) {
+			foreach ($raw as $item) {
+				if (!is_array($item)) {
+					continue;
+				}
+				$key = (string)($item['billingcycle'] ?? $item['cycle'] ?? '');
+				if ($key === '') {
+					continue;
+				}
+				$cents = self::toCents(self::pickPrice($item));
+				if ($cents <= 0) {
+					continue;
+				}
+				$name = (string)($item['name'] ?? $item['billingcycle_zh'] ?? '');
+				$out[self::normalizeCycleKey($key)] = self::cycleEntry($key, $cents, $name);
+			}
+		} else {
+			// 映射形式:{'monthly': '25.00' 或 {price: '25.00'}}
+			foreach ($raw as $key => $v) {
+				$key = (string)$key;
+				if ($key === '') {
+					continue;
+				}
+				$cents = self::toCents(is_array($v) ? self::pickPrice($v) : $v);
+				if ($cents <= 0) {
+					continue;
+				}
+				$name = is_array($v) ? (string)($v['name'] ?? $v['billingcycle_zh'] ?? '') : '';
+				$out[self::normalizeCycleKey($key)] = self::cycleEntry($key, $cents, $name);
+			}
+		}
+		return $out;
+	}
+
 	/** 金额(元)→ 分。 */
 	protected static function toCents($val)
 	{
diff --git a/app_plugins/zjmfmanager_reserve/lib/zjmf.php b/app_plugins/zjmfmanager_reserve/lib/zjmf.php
index e4c89bc..1694c7b 100644
--- a/app_plugins/zjmfmanager_reserve/lib/zjmf.php
+++ b/app_plugins/zjmfmanager_reserve/lib/zjmf.php
@@ -201,6 +201,35 @@ function zjmf_cycles()
 	];
 }
 
+/**
+ * 渲染商品简介为规范的展示 HTML。
+ * 上游常见 `&lt;li&gt;CPU:4核&lt;/li&gt; &lt;li&gt;内存:4G&lt;/li&gt;...` 格式:
+ *   解码实体 → 压缩标签间空白 → 外层包裹 <ul> 渲染成列表。
+ * 非 <li> 内容(含管理员手写 HTML)仅解码实体后原样输出。
+ */
+function zjmf_render_description($raw)
+{
+	$html = (string)$raw;
+	if ($html === '') {
+		return '';
+	}
+	// 解码实体直到稳定(兼容单/双重编码)
+	$i = 0;
+	do {
+		$prev = $html;
+		$html = html_entity_decode($html, ENT_QUOTES | ENT_HTML5, 'UTF-8');
+		$i++;
+	} while ($html !== $prev && $i < 3);
+	if (stripos($html, '<li') === false) {
+		return $html;
+	}
+	$html = preg_replace('/>\s+</', '><', $html);
+	if (stripos($html, '<ul') === false) {
+		$html = '<ul>' . $html . '</ul>';
+	}
+	return $html;
+}
+
 /* ============================================================
  *  供应商管理
  * ============================================================ */
@@ -301,8 +330,7 @@ function zjmf_product_list_active()
 {
 	global $DB;
 	return $DB->get_all_prepare(
-		"SELECT p.*, s.name AS supplier_name
-		 FROM MN_plugin_zjmf_product p
+		"SELECT p.* FROM MN_plugin_zjmf_product p
 		 LEFT JOIN MN_plugin_zjmf_supplier s ON s.id = p.supplier_id
 		 WHERE p.status=1 AND s.status=1
 		 ORDER BY s.sort ASC, p.sort ASC, p.id ASC"
@@ -321,7 +349,7 @@ function zjmf_product_list_all()
 	) ?: [];
 }
 
-/** 解析商品周期 JSON,返回 ['cycle' => ['name'=>, 'price_cents'=>]]。 */
+/** 解析商品周期 JSON,返回 ['cycle' => ['name'=>, 'price_cents'=>, 'override'=>]]。 */
 function zjmf_product_cycles($product)
 {
 	$raw = isset($product['cycles']) ? json_decode($product['cycles'], true) : null;
@@ -329,14 +357,31 @@ function zjmf_product_cycles($product)
 		return [];
 	}
 	$map = [];
+	// 旧版扁平格式:["Monthly","月",2500]
+	if (isset($raw[0]) && !is_array($raw[0])) {
+		if ($raw[0] !== '' && isset($raw[2])) {
+			$cycle = (string)$raw[0];
+			$map[$cycle] = [
+				'cycle'             => $cycle,
+				'name'              => (string)$raw[1],
+				'price_cents'       => (int)$raw[2],
+				'agent_price_cents' => (int)$raw[2],
+				'override'          => 0,
+			];
+		}
+		return $map;
+	}
 	foreach ($raw as $item) {
 		$cycle = (string)($item['cycle'] ?? '');
 		if ($cycle === '') {
 			continue;
 		}
 		$map[$cycle] = [
-			'name'        => (string)($item['name'] ?? $cycle),
-			'price_cents' => (int)($item['price_cents'] ?? 0),
+			'cycle'             => $cycle,
+			'name'              => (string)($item['name'] ?? $cycle),
+			'price_cents'       => (int)($item['price_cents'] ?? 0),
+			'agent_price_cents' => (int)($item['agent_price_cents'] ?? 0),
+			'override'          => (int)($item['override'] ?? 0),
 		];
 	}
 	return $map;
@@ -388,12 +433,16 @@ function zjmf_product_recalc_price($product_id)
 		return;
 	}
 	foreach ($cycles as $cycle => &$cfg) {
-		// agent_price 保存的是该周期对应的上游价(分)
-		$cfg['price_cents'] = zjmf_calc_price(
-			$cfg['agent_price_cents'] ?? 0,
-			$markupType,
-			$markupValue
-		);
+		// 管理员手动设置过售价(override>0)时保持不动,否则按加价规则重算
+		if ((int)($cfg['override'] ?? 0) > 0) {
+			$cfg['price_cents'] = (int)$cfg['override'];
+		} else {
+			$cfg['price_cents'] = zjmf_calc_price(
+				$cfg['agent_price_cents'] ?? 0,
+				$markupType,
+				$markupValue
+			);
+		}
 	}
 	unset($cfg);
 	$now = $date ?: date('Y-m-d H:i:s');
@@ -441,9 +490,7 @@ function zjmf_order_list_by_user($user_id, $page = 1, $per_page = 20)
 	);
 	$total = $count_row ? (int)$count_row['cnt'] : 0;
 	$list = $DB->get_all_prepare(
-		"SELECT o.*, s.name AS supplier_name
-		 FROM MN_plugin_zjmf_order o
-		 LEFT JOIN MN_plugin_zjmf_supplier s ON s.id = o.supplier_id
+		"SELECT o.* FROM MN_plugin_zjmf_order o
 		 WHERE o.user_id=? ORDER BY o.id DESC LIMIT {$offset},{$per_page}",
 		[$user_id]
 	) ?: [];
@@ -674,9 +721,7 @@ function zjmf_host_get_by_user($user_id, $host_id)
 {
 	global $DB;
 	return $DB->get_row_prepare(
-		"SELECT h.*, s.name AS supplier_name
-		 FROM MN_plugin_zjmf_host h
-		 LEFT JOIN MN_plugin_zjmf_supplier s ON s.id = h.supplier_id
+		"SELECT h.* FROM MN_plugin_zjmf_host h
 		 WHERE h.id=? AND h.user_id=? LIMIT 1",
 		[(int)$host_id, (int)$user_id]
 	) ?: null;
@@ -697,9 +742,7 @@ function zjmf_host_list_by_user($user_id)
 {
 	global $DB;
 	return $DB->get_all_prepare(
-		"SELECT h.*, s.name AS supplier_name
-		 FROM MN_plugin_zjmf_host h
-		 LEFT JOIN MN_plugin_zjmf_supplier s ON s.id = h.supplier_id
+		"SELECT h.* FROM MN_plugin_zjmf_host h
 		 WHERE h.user_id=? ORDER BY h.id DESC",
 		[(int)$user_id]
 	) ?: [];
diff --git a/app_plugins/zjmfmanager_reserve/views/admin/products.php b/app_plugins/zjmfmanager_reserve/views/admin/products.php
index b3be28b..b82f5a8 100644
--- a/app_plugins/zjmfmanager_reserve/views/admin/products.php
+++ b/app_plugins/zjmfmanager_reserve/views/admin/products.php
@@ -174,6 +174,14 @@ function zjmf_admin_markup_label($product)
               <option value="0">下架</option>
             </select>
           </div>
+          <div class="col-md-12 mb-2">
+            <label class="small text-muted">周期售价(元)</label>
+            <div id="zjf-edit-cycles" class="border rounded p-2 bg-light"></div>
+            <div class="small text-muted mt-1">
+              留空表示按加价规则计算;填写后该周期按填写值定价,重新同步不覆盖手动定价。
+              无周期数据时可直接填写售价生成周期。
+            </div>
+          </div>
           <div class="col-md-12 mb-2">
             <label class="small text-muted">商品简介(支持 HTML)</label>
             <textarea id="zjf-edit-desc" class="form-control form-control-sm" rows="4"
@@ -184,6 +192,7 @@ function zjmf_admin_markup_label($product)
           留空加价数值表示使用所属供应商的加价规则;重新同步上游仅更新价格,不覆盖本地名称与简介。
         </div>
         <button type="button" class="btn btn-sm btn-primary" id="zjf-edit-save">保存</button>
+        <button type="button" class="btn btn-sm btn-outline-primary" id="zjf-edit-save-cycles">保存售价</button>
         <button type="button" class="btn btn-sm btn-secondary" id="zjf-edit-cancel">取消</button>
       </div>
 
@@ -235,6 +244,8 @@ function zjmf_admin_markup_label($product)
                             data-id="<?= (int)$p['id'] ?>"
                             data-name="<?= htmlspecialchars($p['name'], ENT_QUOTES) ?>"
                             data-desc="<?= htmlspecialchars($p['description'] ?? '', ENT_QUOTES) ?>"
+                            data-cycles="<?= htmlspecialchars((string)($p['cycles'] ?? ''), ENT_QUOTES) ?>"
+                            data-agent="<?= (int)$p['agent_price_cents'] ?>"
                             data-type="<?= (int)$p['markup_type'] ?>"
                             data-value="<?= (int)$p['markup_value'] ?>"
                             data-sort="<?= (int)$p['sort'] ?>"
@@ -422,6 +433,33 @@ function zjmf_admin_markup_label($product)
   var editSort = document.getElementById('zjf-edit-sort');
   var editStatus = document.getElementById('zjf-edit-status');
 
+  function renderCyclesEditor(raw, agent) {
+    var box = document.getElementById('zjf-edit-cycles');
+    box.innerHTML = '';
+    var cycles = [];
+    try { cycles = JSON.parse(raw || '[]'); } catch (e) { cycles = []; }
+    cycles = (Array.isArray(cycles) ? cycles : []).filter(function (c) {
+      return c && typeof c === 'object' && c.cycle;
+    });
+    if (!cycles.length) {
+      // 无周期数据(如旧版坏数据/未同步),给默认行便于直接定价
+      cycles = [{cycle: 'Monthly', name: '月付', price_cents: 0, agent_price_cents: agent || 0}];
+    }
+    cycles.forEach(function (c) {
+      var row = document.createElement('div');
+      row.className = 'd-flex align-items-center mb-1';
+      row.style.maxWidth = '420px';
+      row.innerHTML =
+        '<span class="mr-2 small" style="min-width:56px">' + esc(c.name || c.cycle) + '</span>'
+        + '<input type="number" step="0.01" min="0" class="form-control form-control-sm zjf-cycle-input"'
+        + ' data-cycle="' + esc(c.cycle) + '" value="'
+        + (c.price_cents > 0 ? (c.price_cents / 100).toFixed(2) : '') + '">'
+        + '<span class="ml-2 small text-muted" style="min-width:110px">上游 '
+        + (c.agent_price_cents > 0 ? '¥' + (c.agent_price_cents / 100).toFixed(2) : '-') + '</span>';
+      box.appendChild(row);
+    });
+  }
+
   document.querySelectorAll('.zjf-edit').forEach(function (btn) {
     btn.addEventListener('click', function () {
       syncWrap.style.display = 'none';
@@ -433,6 +471,8 @@ function zjmf_admin_markup_label($product)
       editValue.value = btn.getAttribute('data-value');
       editSort.value = btn.getAttribute('data-sort');
       editStatus.value = btn.getAttribute('data-status');
+      renderCyclesEditor(btn.getAttribute('data-cycles'),
+        parseInt(btn.getAttribute('data-agent'), 10) || 0);
       editWrap.style.display = 'block';
       editWrap.scrollIntoView({behavior: 'smooth', block: 'start'});
     });
@@ -458,6 +498,24 @@ function zjmf_admin_markup_label($product)
     }, true);
   });
 
+  document.getElementById('zjf-edit-save-cycles').addEventListener('click', function () {
+    var id = parseInt(editId.value, 10);
+    if (!id) return;
+    var overrides = {};
+    document.querySelectorAll('.zjf-cycle-input').forEach(function (inp) {
+      var v = inp.value;
+      if (v !== '' && !isNaN(parseFloat(v))) {
+        overrides[inp.getAttribute('data-cycle')] = parseFloat(v);
+      }
+    });
+    if (!Object.keys(overrides).length) { alert('请至少填写一个周期的售价'); return; }
+    $.post('ajax.php', {
+      gn: 'p_zjmf_admin_save_cycles',
+      id: id,
+      overrides: overrides
+    }, function (r) { notify(res(r), true); });
+  });
+
   document.querySelectorAll('.zjf-toggle').forEach(function (btn) {
     btn.addEventListener('click', function () {
       post({gn: 'p_zjmf_admin_toggle_product', id: btn.getAttribute('data-id')}, true);
diff --git a/app_plugins/zjmfmanager_reserve/views/order.php b/app_plugins/zjmfmanager_reserve/views/order.php
index 3f7a2ea..8a2381f 100644
--- a/app_plugins/zjmfmanager_reserve/views/order.php
+++ b/app_plugins/zjmfmanager_reserve/views/order.php
@@ -19,7 +19,7 @@ ob_start();
       <a class="layui-btn layui-btn-xs layui-btn-primary" href="<?= zjmf_url('reserve/shop') ?>">返回商品列表</a>
     </div>
 
-    <div class="zj-desc"><?= $product['description'] ?></div>
+    <div class="zj-desc"><?= zjmf_render_description($product['description']) ?></div>
 
     <form class="zj-order-form" id="zjf-order-form">
       <div class="layui-form-item">
diff --git a/app_plugins/zjmfmanager_reserve/views/shop.php b/app_plugins/zjmfmanager_reserve/views/shop.php
index 4d8b588..8200bd8 100644
--- a/app_plugins/zjmfmanager_reserve/views/shop.php
+++ b/app_plugins/zjmfmanager_reserve/views/shop.php
@@ -49,7 +49,7 @@ ob_start();
                 <span class="zj-tag"><?= htmlspecialchars($product['currency']) ?></span>
               <?php endif; ?>
             </div>
-            <div class="zj-card-desc"><?= $product['description'] ?></div>
+            <div class="zj-card-desc"><?= zjmf_render_description($product['description']) ?></div>
             <div class="zj-price">
               <?php $cycles = zjmf_product_cycles($product); ?>
               <?php foreach ($cycles as $cycle => $cfg): ?>
diff --git a/templates/tdesign/account/_spa_boot.php b/templates/tdesign/account/_spa_boot.php
index 2d9db1a..62504c7 100644
--- a/templates/tdesign/account/_spa_boot.php
+++ b/templates/tdesign/account/_spa_boot.php
@@ -47,6 +47,7 @@ $boot = [
 		'hosting_shop' => function_exists('mnbt_plugin_enabled') ? mnbt_plugin_enabled('hosting_shop') : false,
 		'docker_shop'  => function_exists('mnbt_plugin_enabled') ? mnbt_plugin_enabled('docker_shop') : false,
 		'realname'     => function_exists('mnbt_plugin_enabled') ? mnbt_plugin_enabled('realname') : false,
+		'zjmf'         => function_exists('mnbt_plugin_enabled') ? mnbt_plugin_enabled('zjmfmanager_reserve') : false,
 	],
 	// 主机管理面板入口(核心 user scope)
 	'panelUrl'    => $td_base . '/user/',
diff --git a/templates/tdesign/home/index.php b/templates/tdesign/home/index.php
index 92143b3..3d07845 100644
--- a/templates/tdesign/home/index.php
+++ b/templates/tdesign/home/index.php
@@ -61,6 +61,7 @@ $boot = [
 	'hasUser'      => !empty($has_user),
 	'hasSite'      => !empty($has_site),
 	'hasDocker'    => !empty($has_docker),
+	'hasZjmf'      => function_exists('mnbt_plugin_enabled') ? mnbt_plugin_enabled('zjmfmanager_reserve') : false,
 	'plans'        => $plans ?? [],
 	'blocks'       => $blocks ?? [],
 	'base'         => function_exists('mnbt_home_base') ? mnbt_home_base() : '',
diff --git a/templates/tdesign/spa/src/account/api/plugins.js b/templates/tdesign/spa/src/account/api/plugins.js
index a2f5f58..7b9a215 100644
--- a/templates/tdesign/spa/src/account/api/plugins.js
+++ b/templates/tdesign/spa/src/account/api/plugins.js
@@ -150,3 +150,37 @@ export function dockerConsoleUrl() {
   const boot = window.__TD_BOOT__ || {}
   return boot.dockerUrl || ''
 }
+
+/* ============================================================
+ *  zjmfmanager_reserve 插件(云服务器/魔方财务分销)
+ * ============================================================ */
+
+/** 上架商品列表(含各周期价格,游客可访问) */
+export function getZjmfProducts() {
+  return apiGet('/reserve/api/products')
+}
+
+/** 我的主机(已脱敏,不含密码) */
+export function getZjmfHosts() {
+  return apiGet('/reserve/api/hosts')
+}
+
+/** 我的订单(分页) */
+export function getZjmfOrders(page = 1, perPage = 15) {
+  return apiGet('/reserve/api/orders', { page, per_page: perPage })
+}
+
+/** 可用支付方式(zjmf 下单用,含余额支付等全部方式) */
+export function getZjmfMethods() {
+  return apiGet('/reserve/api/methods')
+}
+
+/** 创建购买订单:product_id + cycle + type → 返回支付 HTML */
+export function createZjmfOrder(productId, cycle, type) {
+  return apiPost('/reserve/api/create_order', { product_id: productId, cycle, type })
+}
+
+/** 主机操作:开机/关机/重启/重置密码/重装(重置密码需传 password) */
+export function zjmfHostAction(hostId, action, password = '') {
+  return apiPost('/reserve/api/host_action', { host_id: hostId, action, password })
+}
diff --git a/templates/tdesign/spa/src/account/layouts/AccountLayout.vue b/templates/tdesign/spa/src/account/layouts/AccountLayout.vue
index bda2476..4f589ec 100644
--- a/templates/tdesign/spa/src/account/layouts/AccountLayout.vue
+++ b/templates/tdesign/spa/src/account/layouts/AccountLayout.vue
@@ -154,6 +154,39 @@
             </li>
           </ul>
         </template>
+
+        <!-- 云服务器(zjmfmanager_reserve 插件) -->
+        <template v-if="hasZjmf">
+          <div class="td-side-group" v-show="!sidebarCollapsed || isMobile">
+            <div class="td-side-group-label">
+              <i class="mdi mdi-cloud-outline"></i>
+              <span>云服务器</span>
+            </div>
+          </div>
+          <ul class="td-side-menu">
+            <li class="td-side-item">
+              <router-link to="/zjmf-shop" custom v-slot="{ navigate, isActive }">
+                <a href="javascript:;" :class="{ active: isActive }" @click="navigate">
+                  <i class="mdi mdi-cart"></i><span>商品选购</span>
+                </a>
+              </router-link>
+            </li>
+            <li class="td-side-item">
+              <router-link to="/zjmf-assets" custom v-slot="{ navigate, isActive }">
+                <a href="javascript:;" :class="{ active: isActive }" @click="navigate">
+                  <i class="mdi mdi-server"></i><span>我的主机</span>
+                </a>
+              </router-link>
+            </li>
+            <li class="td-side-item">
+              <router-link to="/zjmf-orders" custom v-slot="{ navigate, isActive }">
+                <a href="javascript:;" :class="{ active: isActive }" @click="navigate">
+                  <i class="mdi mdi-receipt"></i><span>我的订单</span>
+                </a>
+              </router-link>
+            </li>
+          </ul>
+        </template>
       </div>
 
       <!-- 底部用户卡 -->
@@ -242,6 +275,7 @@ const hasBalance = pluginEnabled('balance')
 const hasShop = pluginEnabled('hosting_shop')
 const hasDocker = pluginEnabled('docker_shop')
 const hasRealname = pluginEnabled('realname')
+const hasZjmf = pluginEnabled('zjmf')
 
 const sidebarCollapsed = ref(false)
 const mobileOpen = ref(false)
@@ -261,6 +295,7 @@ const userMenuOptions = computed(() => {
   if (hasBalance) list.push({ content: '余额中心', value: 'balance' })
   if (hasShop) list.push({ content: '主机商城', value: 'shop' })
   if (hasDocker) list.push({ content: 'Docker 商城', value: 'docker-shop' })
+  if (hasZjmf) list.push({ content: '云服务器', value: 'zjmf-shop' })
   list.push({ content: '退出登录', value: 'logout', theme: 'error' })
   return list
 })
@@ -299,12 +334,22 @@ function onLogout() {
   })
 }
 
+/** zjmfmanager_reserve 插件独立页面 URL(主机详情等仍走 P2 路由) */
+function reserveUrl(path) {
+  const base = boot.routeBase || '/index.php?_r='
+  return base + 'reserve/' + String(path).replace(/^\/+/, '')
+}
+
 function onUserMenuClick(item) {
   const v = item?.value
   if (v === 'logout') {
     onLogout()
     return
   }
+  if (v === 'zjmf-shop') {
+    router.push('/zjmf-shop')
+    return
+  }
   const targets = {
     dashboard: '/dashboard',
     profile: '/profile',
diff --git a/templates/tdesign/spa/src/account/router/index.js b/templates/tdesign/spa/src/account/router/index.js
index 2a79ef9..05fcbbd 100644
--- a/templates/tdesign/spa/src/account/router/index.js
+++ b/templates/tdesign/spa/src/account/router/index.js
@@ -95,6 +95,24 @@ const routes = [
         component: () => import('@/account/views/DockerOrdersView.vue'),
         meta: { title: 'Docker 订单', requiresAuth: true },
       },
+      {
+        path: 'zjmf-shop',
+        name: 'zjmf-shop',
+        component: () => import('@/account/views/ZjmfShopView.vue'),
+        meta: { title: '云服务器选购', requiresAuth: true },
+      },
+      {
+        path: 'zjmf-assets',
+        name: 'zjmf-assets',
+        component: () => import('@/account/views/ZjmfAssetsView.vue'),
+        meta: { title: '我的云服务器', requiresAuth: true },
+      },
+      {
+        path: 'zjmf-orders',
+        name: 'zjmf-orders',
+        component: () => import('@/account/views/ZjmfOrdersView.vue'),
+        meta: { title: '云服务器订单', requiresAuth: true },
+      },
     ],
   },
   {
diff --git a/templates/tdesign/spa/src/account/views/ZjmfAssetsView.vue b/templates/tdesign/spa/src/account/views/ZjmfAssetsView.vue
new file mode 100644
index 0000000..f194e68
--- /dev/null
+++ b/templates/tdesign/spa/src/account/views/ZjmfAssetsView.vue
@@ -0,0 +1,309 @@
+<template>
+  <div class="td-page">
+    <div class="td-page-head">
+      <div>
+        <h3 class="td-page-title"><i class="mdi mdi-server"></i> 我的云服务器</h3>
+        <p class="td-page-subtitle">查看已开通的主机,可执行开机/关机/重启/重置密码,管理详情跳转独立页面</p>
+      </div>
+      <t-button variant="outline" @click="router.push('/zjmf-shop')">
+        <i class="mdi mdi-cart"></i> 前往选购
+      </t-button>
+    </div>
+
+    <div v-if="hosts.length" class="zjmf-asset-grid">
+      <div class="zjmf-asset td-card" v-for="h in hosts" :key="h.id">
+        <div class="zjmf-asset-head">
+          <div class="zjmf-asset-icon"><i class="mdi mdi-cloud-outline"></i></div>
+          <div class="zjmf-asset-info">
+            <strong>{{ h.name }}</strong>
+            <span class="zjmf-asset-node">{{ cycleText(h.cycle) }}</span>
+          </div>
+          <t-tag :theme="statusTheme(h.status)" variant="light">
+            {{ statusText(h.status) }}
+          </t-tag>
+        </div>
+
+        <div class="zjmf-asset-meta">
+          <div class="meta-item">
+            <span>主机账号</span>
+            <b class="mono">{{ h.username || '—' }}</b>
+          </div>
+          <div class="meta-item">
+            <span>上游主机 ID</span>
+            <b>{{ h.up_host_id > 0 ? h.up_host_id : '—' }}</b>
+          </div>
+          <div class="meta-item">
+            <span>到期时间</span>
+            <b :class="{ expired: isExpired(h) }">{{ h.renew_date || '—' }}</b>
+          </div>
+          <div class="meta-item">
+            <span>开通时间</span>
+            <b>{{ h.created_at || '—' }}</b>
+          </div>
+        </div>
+
+        <div class="zjmf-asset-actions">
+          <t-button
+            theme="primary"
+            variant="outline"
+            size="small"
+            :href="reserveUrl('hosts/' + h.id)"
+            target="_blank"
+            rel="noopener"
+          >
+            <i class="mdi mdi-cog-outline"></i> 管理详情
+          </t-button>
+          <t-button
+            variant="outline"
+            size="small"
+            theme="success"
+            :disabled="!canAction(h, 'on')"
+            :loading="h._acting === 'on'"
+            @click="onAction(h, 'on')"
+          >
+            <i class="mdi mdi-play"></i> 开机
+          </t-button>
+          <t-button
+            variant="outline"
+            size="small"
+            theme="warning"
+            :disabled="!canAction(h, 'off')"
+            :loading="h._acting === 'off'"
+            @click="onAction(h, 'off')"
+          >
+            <i class="mdi mdi-stop"></i> 关机
+          </t-button>
+          <t-button
+            variant="outline"
+            size="small"
+            :disabled="!canAction(h, 'reboot')"
+            :loading="h._acting === 'reboot'"
+            @click="onAction(h, 'reboot')"
+          >
+            <i class="mdi mdi-restart"></i> 重启
+          </t-button>
+          <t-button
+            variant="outline"
+            size="small"
+            :loading="h._acting === 'reset_password'"
+            @click="onResetPassword(h)"
+          >
+            <i class="mdi mdi-key-change"></i> 重置密码
+          </t-button>
+        </div>
+      </div>
+    </div>
+
+    <t-empty v-else description="暂无云服务器,前往商城选购" style="padding: 60px 0">
+      <t-button theme="primary" @click="router.push('/zjmf-shop')">去选购商品</t-button>
+    </t-empty>
+
+    <!-- 重置密码对话框 -->
+    <t-dialog
+      v-model:visible="pwdVisible"
+      :header="'重置密码:' + (currentHost?.name || '')"
+      :confirm-btn="{ content: '确认重置', theme: 'danger', loading: pwdSubmitting }"
+      :cancel-btn="'取消'"
+      :on-confirm="onConfirmPwd"
+    >
+      <p style="font-size: 13px; color: var(--td-text-secondary); margin: 0 0 12px">
+        新密码将立即生效,请妥善保存。
+      </p>
+      <t-input v-model="newPassword" type="password" placeholder="输入新密码" clearable />
+    </t-dialog>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue'
+import { useRouter } from 'vue-router'
+import { MessagePlugin, DialogPlugin } from 'tdesign-vue-next'
+import { getZjmfHosts, zjmfHostAction } from '@/account/api/plugins'
+
+const router = useRouter()
+const boot = window.__TD_BOOT__ || {}
+
+const hosts = ref([])
+
+// 重置密码
+const pwdVisible = ref(false)
+const currentHost = ref(null)
+const newPassword = ref('')
+const pwdSubmitting = ref(false)
+
+function statusText(status) {
+  return { active: '运行中', suspend: '已暂停', unknown: '未知' }[status] || status
+}
+
+function cycleText(cycle) {
+  const map = {
+    Monthly: '月付',
+    Quarterly: '季付',
+    SemiAnnually: '半年付',
+    Annually: '年付',
+    Biennially: '两年付',
+    Triennially: '三年付',
+  }
+  return map[cycle] || cycle || ''
+}
+
+function statusTheme(status) {
+  if (status === 'active') return 'success'
+  if (status === 'suspend') return 'warning'
+  return 'default'
+}
+
+function isExpired(h) {
+  return !!h.renew_date && h.renew_date < new Date().toISOString().slice(0, 10)
+}
+
+/** 操作可用性:运行中才能关机/重启,已暂停/未知才能开机 */
+function canAction(h, action) {
+  if (h.up_host_id <= 0) return false
+  const active = h.status === 'active'
+  if (action === 'on') return !active
+  if (action === 'off') return active
+  if (action === 'reboot') return active
+  return true
+}
+
+function reserveUrl(path) {
+  const base = boot.routeBase || '/index.php?_r='
+  return base + 'reserve/' + String(path).replace(/^\/+/, '')
+}
+
+function onAction(h, action) {
+  const labels = { on: '开机', off: '关机', reboot: '重启' }
+  const dialog = DialogPlugin.confirm({
+    header: labels[action],
+    body: `确定要执行「${labels[action]}」操作吗?`,
+    confirmBtn: { content: '确认', theme: action === 'off' ? 'danger' : 'primary' },
+    onConfirm: async () => {
+      dialog.destroy()
+      h._acting = action
+      const res = await zjmfHostAction(h.id, action)
+      h._acting = ''
+      if (!res.ok) {
+        MessagePlugin.error(res.message || '操作失败')
+        return
+      }
+      MessagePlugin.success('操作成功')
+    },
+    onClose: () => dialog.destroy(),
+  })
+}
+
+function onResetPassword(h) {
+  currentHost.value = h
+  newPassword.value = ''
+  pwdVisible.value = true
+}
+
+async function onConfirmPwd() {
+  const pwd = newPassword.value.trim()
+  if (!pwd) {
+    MessagePlugin.warning('请输入新密码')
+    return false
+  }
+  pwdSubmitting.value = true
+  const res = await zjmfHostAction(currentHost.value.id, 'reset_password', pwd)
+  pwdSubmitting.value = false
+  if (!res.ok) {
+    MessagePlugin.error(res.message || '重置失败')
+    return false
+  }
+  pwdVisible.value = false
+  MessagePlugin.success('密码已重置,请及时保存')
+  return true
+}
+
+async function load() {
+  const res = await getZjmfHosts()
+  if (res.ok) hosts.value = res.data.hosts || []
+}
+
+onMounted(load)
+</script>
+
+<style scoped>
+.zjmf-asset-grid {
+  display: grid;
+  grid-template-columns: repeat(2, 1fr);
+  gap: 16px;
+}
+.zjmf-asset {
+  padding: 18px;
+}
+.zjmf-asset-head {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+.zjmf-asset-icon {
+  width: 42px;
+  height: 42px;
+  border-radius: 10px;
+  background: var(--td-brand-light);
+  color: var(--td-brand);
+  display: grid;
+  place-items: center;
+  font-size: 22px;
+  flex-shrink: 0;
+}
+.zjmf-asset-info {
+  flex: 1;
+  min-width: 0;
+  display: flex;
+  flex-direction: column;
+}
+.zjmf-asset-info strong {
+  font-size: 15px;
+  color: var(--td-text);
+  font-weight: 600;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+.zjmf-asset-node {
+  font-size: 12px;
+  color: var(--td-text-secondary);
+  margin-top: 2px;
+}
+.zjmf-asset-meta {
+  margin-top: 14px;
+  border-top: 1px dashed var(--td-border);
+  padding-top: 12px;
+}
+.meta-item {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  font-size: 13px;
+  padding: 4px 0;
+}
+.meta-item span {
+  color: var(--td-text-placeholder);
+}
+.meta-item b {
+  color: var(--td-text);
+  font-weight: 500;
+}
+.meta-item b.expired {
+  color: var(--td-error);
+}
+.meta-item .mono {
+  font-family: Consolas, Monaco, monospace;
+}
+.zjmf-asset-actions {
+  display: flex;
+  gap: 8px;
+  margin-top: 14px;
+  flex-wrap: wrap;
+}
+
+@media (max-width: 860px) {
+  .zjmf-asset-grid {
+    grid-template-columns: 1fr;
+  }
+}
+</style>
diff --git a/templates/tdesign/spa/src/account/views/ZjmfOrdersView.vue b/templates/tdesign/spa/src/account/views/ZjmfOrdersView.vue
new file mode 100644
index 0000000..6e7acfc
--- /dev/null
+++ b/templates/tdesign/spa/src/account/views/ZjmfOrdersView.vue
@@ -0,0 +1,189 @@
+<template>
+  <div class="td-page">
+    <div class="td-page-head">
+      <div>
+        <h3 class="td-page-title"><i class="mdi mdi-receipt"></i> 云服务器订单</h3>
+        <p class="td-page-subtitle">查看购买与升级订单记录</p>
+      </div>
+      <t-button variant="outline" @click="router.push('/zjmf-shop')">
+        <i class="mdi mdi-cart"></i> 继续购买
+      </t-button>
+    </div>
+
+    <div class="td-card" v-if="orders.length">
+      <div class="order-item" v-for="order in orders" :key="order.id">
+        <div class="order-icon" :class="'order-icon-' + orderStatus(order.status)">
+          <i class="mdi mdi-cloud-outline"></i>
+        </div>
+        <div class="order-main">
+          <div class="order-title">
+            <strong>{{ order.product_name }}</strong>
+            <span class="order-period">{{ actionLabel(order.action) }} · {{ order.cycle_name || order.cycle || '—' }}</span>
+            <t-tag :theme="statusTheme(order.status)" variant="light" size="small">
+              {{ statusText(order.status) }}
+            </t-tag>
+          </div>
+          <div class="order-meta">
+            <span>单号 {{ order.order_no || '—' }}</span>
+            <span>下单 {{ order.created_at || '—' }}</span>
+            <span v-if="order.remark" class="order-remark">备注:{{ order.remark }}</span>
+          </div>
+        </div>
+        <div class="order-amount">¥{{ centsToYuan(order.amount_cents) }}</div>
+      </div>
+
+      <div class="pager" v-if="total > perPage">
+        <t-pagination
+          v-model:current="page"
+          :page-size="perPage"
+          :total="total"
+          :show-jumper="true"
+          @change="load"
+        />
+      </div>
+    </div>
+
+    <t-empty v-else description="暂无云服务器订单" style="padding: 60px 0">
+      <t-button theme="primary" @click="router.push('/zjmf-shop')">去选购商品</t-button>
+    </t-empty>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted } from 'vue'
+import { useRouter } from 'vue-router'
+import { getZjmfOrders, centsToYuan } from '@/account/api/plugins'
+
+const router = useRouter()
+
+const ACTION_LABELS = {
+  buy: '购买',
+  upgrade_config: '配置升级',
+  upgrade_product: '产品升级',
+}
+
+const orders = ref([])
+const page = ref(1)
+const perPage = 15
+const total = ref(0)
+
+function actionLabel(action) {
+  return ACTION_LABELS[action] || action
+}
+
+function orderStatus(status) {
+  return { pending: 'pending', paid: 'paid', opened: 'opened', failed: 'failed', cancelled: 'cancelled' }[status] || 'pending'
+}
+
+function statusText(status) {
+  return { pending: '待处理', paid: '已支付', opened: '已完成', failed: '失败', cancelled: '已取消' }[status] || status
+}
+
+function statusTheme(status) {
+  if (status === 'opened') return 'success'
+  if (status === 'pending') return 'warning'
+  if (status === 'failed' || status === 'cancelled') return 'default'
+  return 'primary'
+}
+
+async function load() {
+  const res = await getZjmfOrders(page.value, perPage)
+  if (!res.ok) return
+  const data = res.data.orders || {}
+  orders.value = data.list || []
+  total.value = data.total || 0
+}
+
+onMounted(load)
+</script>
+
+<style scoped>
+.order-item {
+  display: flex;
+  align-items: center;
+  gap: 14px;
+  padding: 16px 18px;
+  border-bottom: 1px solid var(--td-border);
+}
+.order-item:last-child {
+  border-bottom: none;
+}
+.order-icon {
+  width: 40px;
+  height: 40px;
+  border-radius: 10px;
+  display: grid;
+  place-items: center;
+  font-size: 20px;
+  flex-shrink: 0;
+}
+.order-icon-pending {
+  background: #fff3e0;
+  color: #e37318;
+}
+.order-icon-opened {
+  background: #e8f8f0;
+  color: #2ba471;
+}
+.order-icon-paid {
+  background: #e8f3ff;
+  color: #0052d9;
+}
+.order-icon-failed,
+.order-icon-cancelled {
+  background: #f0f0f0;
+  color: #8c8c8c;
+}
+.order-main {
+  flex: 1;
+  min-width: 0;
+}
+.order-title {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  flex-wrap: wrap;
+}
+.order-title strong {
+  font-size: 14px;
+  color: var(--td-text);
+  font-weight: 600;
+}
+.order-period {
+  font-size: 12px;
+  color: var(--td-text-secondary);
+  background: var(--td-bg);
+  padding: 1px 8px;
+  border-radius: 10px;
+}
+.order-meta {
+  display: flex;
+  align-items: center;
+  gap: 14px;
+  font-size: 12px;
+  color: var(--td-text-placeholder);
+  margin-top: 4px;
+  flex-wrap: wrap;
+}
+.order-remark {
+  color: var(--td-warning);
+}
+.order-amount {
+  font-size: 16px;
+  font-weight: 700;
+  color: var(--td-text);
+  white-space: nowrap;
+}
+.pager {
+  display: flex;
+  justify-content: flex-end;
+  padding: 16px 18px;
+  border-top: 1px solid var(--td-border);
+}
+
+@media (max-width: 560px) {
+  .order-meta {
+    gap: 8px;
+  }
+}
+</style>
diff --git a/templates/tdesign/spa/src/account/views/ZjmfShopView.vue b/templates/tdesign/spa/src/account/views/ZjmfShopView.vue
new file mode 100644
index 0000000..9635e38
--- /dev/null
+++ b/templates/tdesign/spa/src/account/views/ZjmfShopView.vue
@@ -0,0 +1,279 @@
+<template>
+  <div class="td-page">
+    <div class="td-page-head">
+      <div>
+        <h3 class="td-page-title"><i class="mdi mdi-cloud-outline"></i> 云服务器选购</h3>
+        <p class="td-page-subtitle">魔方财务代理分销,即买即用,支付完成自动开通</p>
+      </div>
+      <t-button variant="outline" @click="router.push('/zjmf-assets')">
+        <i class="mdi mdi-server"></i> 我的主机
+      </t-button>
+    </div>
+
+    <div v-if="products.length" class="zjmf-grid">
+      <div class="zjmf-card td-card" v-for="p in products" :key="p.id">
+        <div class="zjmf-head">
+          <div class="zjmf-name">{{ p.name }}</div>
+        </div>
+        <div class="zjmf-desc" v-if="p.description" v-html="p.description"></div>
+        <p v-else class="zjmf-desc zjmf-desc-empty">暂无介绍</p>
+
+        <div class="zjmf-foot">
+          <div class="zjmf-price">
+            <template v-if="p.cycles.length">
+              <span class="price-num">¥{{ centsToYuan(minPrice(p)) }}</span>
+              <span class="price-unit">起</span>
+            </template>
+            <span v-else class="price-empty">未设置价格</span>
+          </div>
+          <t-button theme="primary" size="small" :disabled="!p.cycles.length" @click="openOrder(p)">
+            {{ p.cycles.length ? '立即购买' : '暂不可购' }}
+          </t-button>
+        </div>
+      </div>
+    </div>
+
+    <t-empty v-else description="暂无可购买的云服务器" style="padding: 60px 0" />
+
+    <!-- 下单对话框 -->
+    <t-dialog
+      v-model:visible="dialogVisible"
+      :header="'购买:' + (currentProduct?.name || '')"
+      :confirm-btn="{ content: '确认购买', theme: 'primary', loading: submitting }"
+      :cancel-btn="'取消'"
+      :on-confirm="onConfirmOrder"
+    >
+      <div class="order-body">
+        <div class="order-label">购买周期</div>
+        <div class="period-grid">
+          <label
+            v-for="c in currentCycles"
+            :key="c.key"
+            class="period-item"
+            :class="{ active: cycle === c.key }"
+          >
+            <input type="radio" name="cycle" :value="c.key" v-model="cycle" />
+            <span class="period-name">{{ c.name }}</span>
+            <span class="period-price">¥{{ centsToYuan(c.price_cents) }}</span>
+          </label>
+        </div>
+
+        <div class="order-label" style="margin-top: 16px">支付方式</div>
+        <div class="period-grid">
+          <label
+            v-for="m in methods"
+            :key="m.plugin + '__' + m.method"
+            class="period-item"
+            :class="{ active: payType === m.plugin + '__' + m.method }"
+          >
+            <input type="radio" name="paytype" :value="m.plugin + '__' + m.method" v-model="payType" />
+            <span class="period-name">{{ m.display_name || m.plugin + ' / ' + m.method }}</span>
+          </label>
+        </div>
+        <p v-if="!methods.length" class="no-methods">暂无可用的支付方式</p>
+      </div>
+    </t-dialog>
+  </div>
+</template>
+
+<script setup>
+import { ref, computed, onMounted } from 'vue'
+import { useRouter } from 'vue-router'
+import { MessagePlugin } from 'tdesign-vue-next'
+import {
+  getZjmfProducts, getZjmfMethods, createZjmfOrder, centsToYuan, goPay,
+} from '@/account/api/plugins'
+
+const router = useRouter()
+
+const products = ref([])
+const methods = ref([])
+
+// 下单状态
+const dialogVisible = ref(false)
+const currentProduct = ref(null)
+const cycle = ref('')
+const payType = ref('')
+const submitting = ref(false)
+
+const currentCycles = computed(() => currentProduct.value?.cycles || [])
+
+function minPrice(p) {
+  const prices = (p.cycles || []).map((c) => c.price_cents || 0).filter((v) => v > 0)
+  return prices.length ? Math.min(...prices) : 0
+}
+
+function openOrder(p) {
+  currentProduct.value = p
+  cycle.value = p.cycles?.[0]?.key || ''
+  payType.value = methods.value[0] ? methods.value[0].plugin + '__' + methods.value[0].method : ''
+  dialogVisible.value = true
+}
+
+async function onConfirmOrder() {
+  if (!currentProduct.value) return
+  if (!payType.value) {
+    MessagePlugin.warning('请选择支付方式')
+    return false
+  }
+  submitting.value = true
+  const res = await createZjmfOrder(currentProduct.value.id, cycle.value, payType.value)
+  submitting.value = false
+  if (!res.ok) {
+    MessagePlugin.error(res.message || '创建订单失败')
+    return false
+  }
+  if (!goPay(res)) {
+    MessagePlugin.success('订单创建成功')
+    dialogVisible.value = false
+  }
+  return true
+}
+
+async function load() {
+  const [productsRes, methodsRes] = await Promise.all([getZjmfProducts(), getZjmfMethods()])
+  if (productsRes.ok) products.value = productsRes.data.list || []
+  if (methodsRes.ok) methods.value = methodsRes.data.methods || []
+}
+
+onMounted(load)
+</script>
+
+<style scoped>
+.zjmf-grid {
+  display: grid;
+  grid-template-columns: repeat(3, 1fr);
+  gap: 16px;
+}
+.zjmf-card {
+  padding: 20px;
+  display: flex;
+  flex-direction: column;
+  transition: transform var(--td-dur) var(--td-ease),
+              box-shadow var(--td-dur) var(--td-ease);
+}
+.zjmf-card:hover {
+  transform: translateY(-3px);
+  box-shadow: var(--td-shadow-md);
+}
+.zjmf-head {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 8px;
+}
+.zjmf-name {
+  font-size: 16px;
+  font-weight: 700;
+  color: var(--td-text);
+}
+.zjmf-desc {
+  margin: 8px 0 16px;
+  font-size: 13px;
+  color: var(--td-text-secondary);
+  line-height: 1.6;
+  flex: 1;
+}
+/* 简介 li 列表:每项独占一行、带圆点 */
+.zjmf-desc ul {
+  margin: 0;
+  padding-left: 16px;
+}
+.zjmf-desc li {
+  display: list-item;
+  list-style: disc;
+  margin: 2px 0;
+}
+.zjmf-desc p {
+  margin: 2px 0;
+}
+.zjmf-desc-empty {
+  min-height: auto;
+}
+.zjmf-foot {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  border-top: 1px solid var(--td-border);
+  padding-top: 14px;
+}
+.zjmf-price {
+  display: flex;
+  align-items: baseline;
+  gap: 2px;
+}
+.price-num {
+  font-size: 22px;
+  font-weight: 700;
+  color: var(--td-error);
+}
+.price-unit {
+  font-size: 12px;
+  color: var(--td-text-placeholder);
+}
+.price-empty {
+  font-size: 13px;
+  color: var(--td-text-placeholder);
+}
+
+/* 下单对话框 */
+.order-body {
+  padding: 4px 0 0;
+}
+.order-label {
+  font-size: 13px;
+  font-weight: 600;
+  color: var(--td-text);
+  margin-bottom: 10px;
+}
+.period-grid {
+  display: grid;
+  grid-template-columns: repeat(2, 1fr);
+  gap: 8px;
+}
+.period-item {
+  display: flex;
+  flex-direction: column;
+  gap: 2px;
+  padding: 10px 12px;
+  border: 1px solid var(--td-border);
+  border-radius: 10px;
+  cursor: pointer;
+  font-size: 13px;
+  color: var(--td-text);
+  transition: border-color var(--td-dur) var(--td-ease),
+              background var(--td-dur) var(--td-ease);
+}
+.period-item input {
+  position: absolute;
+  opacity: 0;
+  pointer-events: none;
+}
+.period-item.active {
+  border-color: var(--td-brand);
+  background: var(--td-brand-light);
+}
+.period-name {
+  font-weight: 600;
+}
+.period-price {
+  font-size: 12px;
+  color: var(--td-brand);
+}
+.no-methods {
+  margin: 12px 0 0;
+  font-size: 13px;
+  color: var(--td-text-placeholder);
+}
+
+@media (max-width: 1080px) {
+  .zjmf-grid {
+    grid-template-columns: repeat(2, 1fr);
+  }
+}
+@media (max-width: 680px) {
+  .zjmf-grid {
+    grid-template-columns: 1fr;
+  }
+}
+</style>
diff --git a/templates/tdesign/spa/src/home/api/zjmf.js b/templates/tdesign/spa/src/home/api/zjmf.js
new file mode 100644
index 0000000..2f424f9
--- /dev/null
+++ b/templates/tdesign/spa/src/home/api/zjmf.js
@@ -0,0 +1,13 @@
+import { apiGet } from './http'
+
+/**
+ * zjmfmanager_reserve 插件 API(魔方财务代理分销 - 云服务器)
+ * 依赖插件启用:zjmfmanager_reserve
+ * 注意:购买/主机/订单等用户中心能力统一在 account SPA 或插件独立页面,
+ * home 仅展示公开商品列表
+ */
+
+/** 公开商品列表(游客可访问,含最低价) */
+export function getZjmfProducts() {
+  return apiGet('/reserve/api/public_products')
+}
diff --git a/templates/tdesign/spa/src/home/styles/home.scss b/templates/tdesign/spa/src/home/styles/home.scss
index 6d3de3d..6cefde9 100644
--- a/templates/tdesign/spa/src/home/styles/home.scss
+++ b/templates/tdesign/spa/src/home/styles/home.scss
@@ -691,6 +691,19 @@
   font-size: 13px;
   min-height: 40px;
   line-height: 1.6;
+  /* 简介 li 列表:每项独占一行、带圆点 */
+  ul {
+    margin: 4px 0;
+    padding-left: 16px;
+  }
+  li {
+    display: list-item;
+    list-style: disc;
+    margin: 2px 0;
+  }
+  p {
+    margin: 2px 0;
+  }
 }
 
 .hd-plan-body {
diff --git a/templates/tdesign/spa/src/home/views/LandingView.vue b/templates/tdesign/spa/src/home/views/LandingView.vue
index 64b4870..cf02fd1 100644
--- a/templates/tdesign/spa/src/home/views/LandingView.vue
+++ b/templates/tdesign/spa/src/home/views/LandingView.vue
@@ -129,6 +129,43 @@
       </div>
     </section>
 
+    <!-- 云服务器区(zjmfmanager_reserve 插件) -->
+    <section v-if="boot.hasZjmf" class="plans-section section">
+      <div class="container">
+        <div class="section-header">
+          <h2 class="section-title">云服务器</h2>
+          <p class="section-subtitle">魔方财务代理分销,即买即用,支付完成自动开通</p>
+          <a :href="accountUrl('zjmf-shop')" class="more-news">查看全部云服务器 ›</a>
+        </div>
+        <div v-if="zjmfProducts.length === 0" class="hd-empty">
+          <i class="mdi mdi-cloud-outline"></i>
+          <p>暂无可购买的云服务器,请联系管理员</p>
+        </div>
+        <div v-else class="hd-plans">
+          <div
+            v-for="(p, i) in zjmfProducts.slice(0, 3)"
+            :key="p.id"
+            class="hd-plan-card"
+            :class="{ pop: i === 1 }"
+          >
+            <div class="hd-plan-top">
+              <span v-if="i === 1" class="hd-plan-chip">推荐</span>
+              <h3>{{ p.name }}</h3>
+              <div class="hd-plan-desc" v-if="p.description" v-html="p.description"></div>
+              <div class="hd-plan-desc" v-else>云服务器服务,即买即用</div>
+            </div>
+            <div class="hd-plan-body">
+              <div class="hd-plan-price">
+                <div class="num">{{ zjmfPriceText(p) }}</div>
+                <div class="sub">即买即用</div>
+              </div>
+              <t-button block theme="primary" size="large" @click="goZjmfBuy">立即购买</t-button>
+            </div>
+          </div>
+        </div>
+      </div>
+    </section>
+
     <!-- 核心优势 -->
     <section class="features section">
       <div class="container">
@@ -222,6 +259,7 @@ import { ref, computed, onMounted, onUnmounted } from 'vue'
 import authState from '@/home/store/auth'
 import { getSiteNews } from '@/home/api/site'
 import { getDockerPlans } from '@/home/api/docker'
+import { getZjmfProducts } from '@/home/api/zjmf'
 import { formatDate } from '@/home/utils/format'
 import { accountUrl } from '@/home/utils/account'
 import bg1 from '@/shared/assets/bg1.jpg'
@@ -260,6 +298,22 @@ function dockerSpecText(base, key) {
   return '—'
 }
 
+// 云服务器(zjmfmanager_reserve 插件)
+const zjmfProducts = ref([])
+/** 最低价展示:有价显示 ¥x.xx 起,无价显示 咨询价格 */
+function zjmfPriceText(p) {
+  const cents = Number(p.min_price_cents || 0)
+  return cents > 0 ? '¥' + (cents / 100).toFixed(2) + ' 起' : '咨询价格'
+}
+/** 立即购买:与主机/Docker 一致,跳转 account SPA 商城 */
+function goZjmfBuy() {
+  if (!authState.loggedIn) {
+    window.location.href = accountUrl()
+    return
+  }
+  window.location.href = accountUrl('zjmf-shop')
+}
+
 // hero 按钮路由:优先官网产品页,无官网插件则回退套餐/登录
 const productsUrl = computed(() =>
   boot.hasSite ? '/site/products' : (boot.hasShop ? '/shop' : '/')
@@ -365,6 +419,12 @@ onMounted(async () => {
       dockerPlans.value = res.data.plans
     }
   }
+  if (boot.hasZjmf) {
+    const res = await getZjmfProducts()
+    if (res.ok && res.data?.list) {
+      zjmfProducts.value = res.data.list
+    }
+  }
 })
 onUnmounted(stopAutoPlay)
 </script>