1633 lines
67 KiB
Vue
1633 lines
67 KiB
Vue
<script>
|
|
/**
|
|
* 商品目录:两级分类、搜索、分页和配置摘要;摘要缓存按账户令牌与 API origin 分区,切换会话后清理,不能跨账号复用。
|
|
*/
|
|
import "~/assets/styles/ProductGroupLabel.B1kWpFA-.css";
|
|
import "~/assets/styles/ProductDescription.Ctb7jDuO.css";
|
|
import "~/assets/styles/ProductConfigurationSummary.BqZns1LB.css";
|
|
import "~/assets/styles/ProductCategoryNotice.CwaR3eXN.css";
|
|
import "~/assets/styles/products.CrccEo2m.css";
|
|
import { i as rolldown_runtime_i } from "~/lib/rolldown-runtime.js";
|
|
import { useRuntimeConfig } from "#app";
|
|
import { toDisplayString } from "vue";
|
|
import { renderList } from "vue";
|
|
import { onMounted } from "vue";
|
|
import { normalizeClass } from "vue";
|
|
import { watch } from "vue";
|
|
import { onScopeDispose } from "vue";
|
|
import { createBlock } from "vue";
|
|
import { ref } from "vue";
|
|
import { withCtx } from "vue";
|
|
import { createCommentVNode } from "vue";
|
|
import { computed } from "vue";
|
|
import { effectScope } from "vue";
|
|
import { isRef } from "vue";
|
|
import { createElementBlock } from "vue";
|
|
import { Fragment } from "vue";
|
|
import { createElementVNode } from "vue";
|
|
import { createTextVNode } from "vue";
|
|
import { unref } from "vue";
|
|
import { defineComponent } from "vue";
|
|
import { createVNode } from "vue";
|
|
import { openBlock } from "vue";
|
|
import { withModifiers } from "vue";
|
|
import { useRoute } from "#app";
|
|
import { useRouter } from "#app";
|
|
import { NuxtLink } from "#components";
|
|
import { useApi } from "~/lib/auth.js";
|
|
import { getCrypto } from "~/utils/crypto.js";
|
|
import { useAuth } from "~/lib/auth.js";
|
|
import button from "primevue/button";
|
|
import { t as _plugin_vue_export_helper_t } from "~/lib/_plugin-vue_export-helper.js";
|
|
import message from "primevue/message";
|
|
import inputtext from "primevue/inputtext";
|
|
import { n as product_name_n } from "~/lib/product-name.js";
|
|
import { t as product_name_t } from "~/lib/product-name.js";
|
|
import paginator from "primevue/paginator";
|
|
import ListEmpty from "~/components/ListEmpty.vue";
|
|
import skeleton from "primevue/skeleton";
|
|
import ProductCategoryNotice from "~/components/ProductCategoryNotice.vue";
|
|
import { n as ProductConfigurationSummary_n } from "~/components/ProductConfigurationSummary.vue";
|
|
import ProductConfigurationSummary from "~/components/ProductConfigurationSummary.vue";
|
|
import ProductDescription from "~/components/ProductDescription.vue";
|
|
import ProductGroupLabel from "~/components/ProductGroupLabel.vue";
|
|
import { productId } from "~/lib/product-id.js";
|
|
var j = rolldown_runtime_i(getCrypto(), 1),
|
|
M = 2592e6,
|
|
N = 100,
|
|
P = 512e3,
|
|
F = 3;
|
|
// 校验本地摘要缓存键的结构、字段长度和规范序列化,拒绝不符合当前格式的存储条目。
|
|
function I(e) {
|
|
if (typeof e != `string` || e.length > 4096) return !1;
|
|
try {
|
|
let t = JSON.parse(e);
|
|
return (
|
|
Array.isArray(t) &&
|
|
t.length === 3 &&
|
|
Number.isSafeInteger(t[0]) &&
|
|
t[0] > 0 &&
|
|
typeof t[1] == `string` &&
|
|
t[1].length <= 2048 &&
|
|
product_name_t(t[1]) &&
|
|
typeof t[2] == `string` &&
|
|
t[2].length <= 128 &&
|
|
JSON.stringify(t) === e
|
|
);
|
|
} catch {
|
|
return !1;
|
|
}
|
|
}
|
|
// 摘要仅接受有界、非空的字符串数组,不从本地存储信任任意结构。
|
|
function L(e) {
|
|
return (
|
|
Array.isArray(e) &&
|
|
e.length > 0 &&
|
|
e.length <= 5 &&
|
|
e.every(
|
|
(e) => typeof e == `string` && e.trim().length > 0 && e.length <= 1024,
|
|
)
|
|
);
|
|
}
|
|
// 管理摘要缓存、过期清理和同键在途请求复用;持久化容量有界,清空操作会使旧异步任务失效。
|
|
function R(e, t = Date.now, n) {
|
|
let r = new Map(),
|
|
i = new Map(),
|
|
a = 0,
|
|
o = (e) => JSON.stringify([e.id, e.name, e.module || e.res_module || ``]);
|
|
function s() {
|
|
try {
|
|
n?.storage.removeItem(n.key);
|
|
} catch {}
|
|
}
|
|
function c() {
|
|
if (n)
|
|
try {
|
|
let e = t(),
|
|
i = [...r]
|
|
.filter(
|
|
([t, n]) =>
|
|
I(t) && L(n.summary) && n.expiresAt > e && n.expiresAt <= e + M,
|
|
)
|
|
.map(([e, t]) => ({
|
|
key: e,
|
|
summary: t.summary,
|
|
expiresAt: t.expiresAt,
|
|
})),
|
|
a = JSON.stringify({
|
|
version: F,
|
|
entries: i,
|
|
});
|
|
for (; a.length > P && i.length;)
|
|
(i.shift(),
|
|
(a = JSON.stringify({
|
|
version: F,
|
|
entries: i,
|
|
})));
|
|
i.length ? n.storage.setItem(n.key, a) : s();
|
|
} catch {
|
|
s();
|
|
}
|
|
}
|
|
if (n)
|
|
try {
|
|
let e = n.storage.getItem(n.key);
|
|
if (e !== null) {
|
|
if (e.length > P) throw Error(`Summary cache is too large`);
|
|
let n = JSON.parse(e);
|
|
if (
|
|
!n ||
|
|
typeof n != `object` ||
|
|
!(`version` in n) ||
|
|
n.version !== F ||
|
|
!(`entries` in n) ||
|
|
!Array.isArray(n.entries)
|
|
)
|
|
throw Error(`Invalid summary cache`);
|
|
let i = t();
|
|
for (let e of n.entries)
|
|
if (!(
|
|
!e ||
|
|
typeof e != `object` ||
|
|
!I(e.key) ||
|
|
!L(e.summary) ||
|
|
typeof e.expiresAt != `number` ||
|
|
!Number.isFinite(e.expiresAt) ||
|
|
e.expiresAt <= i ||
|
|
e.expiresAt > i + M
|
|
))
|
|
for (
|
|
r.delete(e.key),
|
|
r.set(e.key, {
|
|
summary: [...e.summary],
|
|
expiresAt: e.expiresAt,
|
|
});
|
|
r.size > N;
|
|
)
|
|
r.delete(r.keys().next().value);
|
|
c();
|
|
}
|
|
} catch {
|
|
s();
|
|
}
|
|
function l(e) {
|
|
if (!product_name_t(e.name)) return;
|
|
let n = o(e),
|
|
i = r.get(n);
|
|
if (i) {
|
|
if (i.expiresAt <= t()) {
|
|
(r.delete(n), c());
|
|
return;
|
|
}
|
|
return (r.delete(n), r.set(n, i), c(), [...i.summary]);
|
|
}
|
|
}
|
|
async function u(n, s) {
|
|
if (!product_name_t(n.name)) return [];
|
|
let u = o(n);
|
|
if (s?.force) (r.delete(u), c());
|
|
else {
|
|
let e = l(n);
|
|
if (e) return e;
|
|
}
|
|
let d = i.get(u);
|
|
if (!d) {
|
|
let o = a,
|
|
s = {
|
|
id: n.id,
|
|
name: n.name,
|
|
module: n.module,
|
|
res_module: n.res_module,
|
|
};
|
|
((d = Promise.resolve()
|
|
.then(() => (o === a ? ProductConfigurationSummary_n(e, s) : []))
|
|
.then((e) => {
|
|
if (o !== a) return [];
|
|
if (e.length) {
|
|
for (
|
|
r.set(u, {
|
|
summary: [...e],
|
|
expiresAt: t() + M,
|
|
});
|
|
r.size > N;
|
|
)
|
|
r.delete(r.keys().next().value);
|
|
c();
|
|
}
|
|
return e;
|
|
})
|
|
.catch(() => [])
|
|
.finally(() => {
|
|
i.get(u) === d && i.delete(u);
|
|
})),
|
|
i.set(u, d));
|
|
}
|
|
return [...(await d)];
|
|
}
|
|
return {
|
|
load: u,
|
|
peek: l,
|
|
clear() {
|
|
(a++, r.clear(), i.clear(), s());
|
|
},
|
|
};
|
|
}
|
|
var z = new WeakMap();
|
|
// 为当前 Pinia 会话建立缓存管理;每次读取确认 localStorage 令牌一致,并按后端 origin 隔离数据。
|
|
function pe() {
|
|
let e = useAuth(),
|
|
n = useRuntimeConfig(),
|
|
r = useApi(),
|
|
i = z.get(e);
|
|
if (!i) {
|
|
((i = {
|
|
caches: new Map(),
|
|
version: 0,
|
|
}),
|
|
z.set(e, i));
|
|
let t = i;
|
|
effectScope(!0).run(() =>
|
|
watch(
|
|
() => e.token,
|
|
() => B(t),
|
|
{
|
|
flush: `sync`,
|
|
},
|
|
),
|
|
);
|
|
}
|
|
let a = i;
|
|
function s() {
|
|
if (localStorage.getItem(`jwt`) !== e.token) {
|
|
B(a);
|
|
return;
|
|
}
|
|
let t = String(n.public.apiBase ?? ``),
|
|
i = a.caches.get(t);
|
|
if (!i) {
|
|
let o = e.token,
|
|
s = a.version,
|
|
c = () =>
|
|
s === a.version &&
|
|
e.token === o &&
|
|
localStorage.getItem(`jwt`) === o &&
|
|
String(n.public.apiBase ?? ``) === t;
|
|
async function l(e) {
|
|
if (!c()) throw Error(`登录状态已变化`);
|
|
let t = await e();
|
|
if (!c()) throw Error(`登录状态已变化`);
|
|
return t;
|
|
}
|
|
((i = R(
|
|
{
|
|
get: (...e) => l(() => r.get(...e)),
|
|
getRaw: (...e) => l(() => r.getRaw(...e)),
|
|
},
|
|
Date.now,
|
|
{
|
|
storage: localStorage,
|
|
key: `console:product-summary:v3:${(0, j.default)(JSON.stringify([t, o])).toString()}`,
|
|
},
|
|
)),
|
|
a.caches.set(t, i));
|
|
}
|
|
return i;
|
|
}
|
|
return {
|
|
load: (e, t) => s()?.load(e, t) ?? Promise.resolve([]),
|
|
peek: (e) => s()?.peek(e),
|
|
clear: () => B(a),
|
|
};
|
|
}
|
|
// 提升缓存版本并清除各分区及持久化内容,使旧账号的在途摘要结果不能写回。
|
|
function B(e) {
|
|
e.version++;
|
|
for (let t of e.caches.values()) t.clear();
|
|
e.caches.clear();
|
|
}
|
|
var me = {
|
|
class: `catalog-page`,
|
|
},
|
|
he = {
|
|
class: `catalog-header`,
|
|
},
|
|
ge = [`aria-busy`],
|
|
_e = {
|
|
key: 1,
|
|
class: `category-error`,
|
|
role: `alert`,
|
|
},
|
|
ve = {
|
|
class: `category-options`,
|
|
role: `group`,
|
|
"aria-label": `一级产品分类`,
|
|
},
|
|
ye = [`aria-pressed`, `disabled`, `onClick`],
|
|
be = {
|
|
key: 0,
|
|
class: `category-options secondary-categories`,
|
|
role: `group`,
|
|
"aria-label": `二级产品分类`,
|
|
},
|
|
xe = [`aria-pressed`, `disabled`, `onClick`],
|
|
Se = {
|
|
key: 1,
|
|
class: `category-empty`,
|
|
},
|
|
Ce = {
|
|
key: 3,
|
|
class: `category-empty`,
|
|
},
|
|
we = [`aria-busy`],
|
|
Te = {
|
|
class: `catalog-toolbar`,
|
|
},
|
|
Ee = {
|
|
class: `search-input`,
|
|
},
|
|
De = {
|
|
class: `catalog-count`,
|
|
role: `status`,
|
|
"aria-live": `polite`,
|
|
},
|
|
Oe = {
|
|
key: 0,
|
|
class: `search-result`,
|
|
},
|
|
ke = {
|
|
key: 2,
|
|
class: `product-list`,
|
|
role: `status`,
|
|
"aria-label": `正在加载产品`,
|
|
},
|
|
Ae = [`aria-busy`, `inert`],
|
|
je = [`aria-busy`, `aria-label`],
|
|
Me = {
|
|
class: `product-heading`,
|
|
},
|
|
Ne = {
|
|
class: `product-purchase`,
|
|
},
|
|
Pe = {
|
|
class: `product-price`,
|
|
},
|
|
Fe = {
|
|
key: 0,
|
|
},
|
|
Ie = [`inert`, `aria-disabled`],
|
|
Le = {
|
|
class: `catalog-help`,
|
|
},
|
|
V = _plugin_vue_export_helper_t(
|
|
defineComponent({
|
|
__name: `index`,
|
|
setup(e) {
|
|
let t = useApi(),
|
|
o = useRoute(),
|
|
p = useRouter(),
|
|
y = pe(),
|
|
T = ref([]),
|
|
E = ref(0),
|
|
D = ref(!0),
|
|
A = ref(!1),
|
|
j = ref(``),
|
|
M = ref(1),
|
|
N = ref(12),
|
|
P = ref(``),
|
|
F = ref(``),
|
|
I = ref([]),
|
|
L = ref([]),
|
|
R = ref(null),
|
|
z = ref(null),
|
|
B = ref(!0),
|
|
V = ref(``),
|
|
H = computed(() =>
|
|
L.value.filter((e) => Number(e.parent_id) === R.value),
|
|
),
|
|
U = computed(() => {
|
|
let e = H.value.find((e) => Number(e.id) === z.value)?.description;
|
|
return typeof e == `string` ? e : ``;
|
|
}),
|
|
W = 0,
|
|
G = 0,
|
|
K = ``;
|
|
onScopeDispose(() => {
|
|
(W++, G++);
|
|
});
|
|
// 部分物理模块列表无可靠起价,需要后续从套餐配置补充,不直接把无效零价当报价。
|
|
function q(e) {
|
|
let t = Number(e.price);
|
|
return (
|
|
e.module === `jy_dcim` &&
|
|
e.pay_type !== `free` &&
|
|
(!Number.isFinite(t) || t <= 0)
|
|
);
|
|
}
|
|
// 列出有效父分类与子分类配对,供未指定分类时逐一寻找可展示商品。
|
|
function Re() {
|
|
return I.value.flatMap((e) =>
|
|
L.value
|
|
.filter((t) => Number(t.parent_id) === Number(e.id))
|
|
.map((t) => ({
|
|
first: Number(e.id),
|
|
second: Number(t.id),
|
|
})),
|
|
);
|
|
}
|
|
// 分类查询参数只接受正的安全整数,避免错误编号进入目录状态。
|
|
function J(e) {
|
|
if (typeof e != `string` || !/^[1-9]\d*$/.test(e)) return null;
|
|
let t = Number(e);
|
|
return Number.isSafeInteger(t) ? t : null;
|
|
}
|
|
// 依据实际分类树恢复查询选择,子分类必须属于有效父分类;无效组合不直接套用。
|
|
function ze() {
|
|
let e = J(o.query.g) ?? J(o.query.first_group_id),
|
|
t = J(o.query.sg) ?? J(o.query.second_group_id),
|
|
n = L.value.find((e) => Number(e.id) === t),
|
|
r = I.value.find((t) => Number(t.id) === e);
|
|
if (n) {
|
|
let e = Number(n.parent_id);
|
|
if (!r || Number(r.id) !== e) {
|
|
if (I.value.find((t) => Number(t.id) === e))
|
|
return {
|
|
first: e,
|
|
second: Number(n.id),
|
|
};
|
|
} else
|
|
return {
|
|
first: Number(r.id),
|
|
second: Number(n.id),
|
|
};
|
|
}
|
|
if (r) {
|
|
let e = L.value.find((e) => Number(e.parent_id) === Number(r.id));
|
|
return {
|
|
first: Number(r.id),
|
|
second: Number(e?.id) || null,
|
|
};
|
|
}
|
|
return null;
|
|
}
|
|
// 把分类选择规范为 g/sg 查询参数,去掉旧参数别名并保留其他查询。
|
|
function Y(e, t) {
|
|
let n = {
|
|
...o.query,
|
|
};
|
|
(delete n.first_group_id,
|
|
delete n.second_group_id,
|
|
e ? (n.g = String(e)) : delete n.g,
|
|
t ? (n.sg = String(t)) : delete n.sg,
|
|
(o.query.g !== n.g ||
|
|
o.query.sg !== n.sg ||
|
|
o.query.first_group_id !== void 0 ||
|
|
o.query.second_group_id !== void 0) &&
|
|
p.replace({
|
|
path: o.path,
|
|
query: n,
|
|
}));
|
|
}
|
|
// 未指定有效分类时依次查找非空分类;全部为空则回到首个分类展示真实空态。
|
|
async function Be(e) {
|
|
let t = Re(),
|
|
n = t[0];
|
|
if (!n) {
|
|
((R.value = Number(I.value[0]?.id) || null),
|
|
(z.value = null),
|
|
Y(R.value, z.value),
|
|
await Z());
|
|
return;
|
|
}
|
|
for (let n of t) {
|
|
if (e !== G) return;
|
|
((R.value = n.first), (z.value = n.second));
|
|
let t = await Z();
|
|
if (e !== G) return;
|
|
if (t !== `empty`) {
|
|
Y(R.value, z.value);
|
|
return;
|
|
}
|
|
}
|
|
((R.value = n.first),
|
|
(z.value = n.second),
|
|
Y(R.value, z.value),
|
|
(T.value = []),
|
|
(E.value = 0),
|
|
(A.value = !0),
|
|
(j.value = ``),
|
|
(D.value = !1));
|
|
}
|
|
// 重新加载两级分类后恢复有效选择与页码;分类已变更时重新选择,不沿用过期商品列表。
|
|
async function X() {
|
|
let e = ++G;
|
|
(W++, (B.value = !0), (D.value = !0), (V.value = ``), (j.value = ``));
|
|
let n = {
|
|
first: R.value,
|
|
second: z.value,
|
|
page: M.value,
|
|
};
|
|
try {
|
|
let [r, i] = await Promise.all([
|
|
t.get(`/product/group/first`),
|
|
t.get(`/product/group/second`),
|
|
]);
|
|
if (e !== G) return;
|
|
((I.value = r.list ?? []), (L.value = i.list ?? []));
|
|
let a = L.value.find(
|
|
(e) =>
|
|
Number(e.id) === n.second && Number(e.parent_id) === n.first,
|
|
),
|
|
o =
|
|
ze() ??
|
|
(a
|
|
? {
|
|
first: n.first,
|
|
second: n.second,
|
|
}
|
|
: null),
|
|
s = !!o;
|
|
((M.value =
|
|
o?.first === n.first && o?.second === n.second ? n.page : 1),
|
|
(B.value = !1),
|
|
s
|
|
? ((R.value = o.first),
|
|
(z.value = o.second),
|
|
Y(R.value, z.value),
|
|
await Z())
|
|
: await Be(e));
|
|
} catch (t) {
|
|
if (e !== G) return;
|
|
((V.value = t instanceof Error ? t.message : `加载产品分类失败`),
|
|
(D.value = !1));
|
|
} finally {
|
|
e === G && (B.value = !1);
|
|
}
|
|
}
|
|
// 切换一级分类时选择其首个子分类,重置分页并同步 URL。
|
|
function Ve(e) {
|
|
if (
|
|
!B.value &&
|
|
R.value !== e &&
|
|
I.value.some((t) => Number(t.id) === e)
|
|
)
|
|
return (
|
|
(R.value = e),
|
|
(z.value = Number(H.value[0]?.id) || null),
|
|
(M.value = 1),
|
|
Y(R.value, z.value),
|
|
Z()
|
|
);
|
|
}
|
|
// 只允许切换当前一级分类下存在的二级分类。
|
|
function He(e) {
|
|
if (
|
|
!B.value &&
|
|
z.value !== e &&
|
|
H.value.some((t) => Number(t.id) === e)
|
|
)
|
|
return ((z.value = e), (M.value = 1), Y(R.value, z.value), Z());
|
|
}
|
|
// 按当前分类/页码/搜索请求列表;仅同一查询复用现有摘要与起价,异步补全仍需匹配列表代次。
|
|
async function Z(e = !1) {
|
|
if (B.value || V.value) return;
|
|
let n = ++W;
|
|
if (!z.value) {
|
|
((T.value = []),
|
|
(E.value = 0),
|
|
(A.value = !0),
|
|
(K = ``),
|
|
(j.value = ``),
|
|
(D.value = !1));
|
|
return;
|
|
}
|
|
((D.value = !0), (j.value = ``));
|
|
try {
|
|
let r = {
|
|
id: z.value,
|
|
page: M.value,
|
|
limit: N.value,
|
|
};
|
|
F.value && (r.keywords = F.value);
|
|
let i = JSON.stringify(r),
|
|
a = K === i,
|
|
o = new Map(a ? T.value.map((e) => [e.id, e]) : []);
|
|
a || ((T.value = []), (E.value = 0), (A.value = !1));
|
|
let s = await t.get(`/product`, r);
|
|
if (n !== W) return;
|
|
((T.value = (s.list ?? [])
|
|
.filter((t) => productId(t.id) !== null)
|
|
.map((t) => ({
|
|
...t,
|
|
startingPrice: q(t)
|
|
? (o.get(t.id)?.startingPrice ?? null)
|
|
: void 0,
|
|
configurationSummary: product_name_t(t.name)
|
|
? e
|
|
? o.get(t.id)?.configurationSummary
|
|
: y.peek(t)
|
|
: [],
|
|
}))),
|
|
(E.value = Number(s.count) || 0),
|
|
(K = i),
|
|
(A.value = !0));
|
|
for (let t of T.value)
|
|
product_name_t(t.name) &&
|
|
(e || t.configurationSummary === void 0) &&
|
|
y
|
|
.load(t, {
|
|
force: e,
|
|
})
|
|
.catch(() => [])
|
|
.then((e) => {
|
|
n === W && (t.configurationSummary = e);
|
|
});
|
|
for (let e of T.value)
|
|
q(e) &&
|
|
t
|
|
.get(`/product/${e.id}/jy_dcim/order_page`)
|
|
.then((t) => {
|
|
if (n !== W) return;
|
|
let r = (t.package ?? [])
|
|
.map((e) => Number(e.price))
|
|
.filter((e) => Number.isFinite(e) && e > 0);
|
|
e.startingPrice = r.length ? Math.min(...r).toFixed(2) : ``;
|
|
})
|
|
.catch(() => {
|
|
n === W && (e.startingPrice = ``);
|
|
});
|
|
return T.value.length ? `loaded` : `empty`;
|
|
} catch (e) {
|
|
return n === W
|
|
? ((j.value = e instanceof Error ? e.message : `加载产品失败`),
|
|
`failed`)
|
|
: void 0;
|
|
} finally {
|
|
n === W && (D.value = !1);
|
|
}
|
|
}
|
|
// 搜索确认后应用关键词并回到第一页。
|
|
function Ue() {
|
|
return ((F.value = P.value.trim()), (M.value = 1), Z());
|
|
}
|
|
// 清空草稿关键词,再复用统一搜索提交逻辑。
|
|
function We() {
|
|
return ((P.value = ``), Ue());
|
|
}
|
|
// 加载中或当前查询有错误时不接受分页,防止叠加新的列表状态。
|
|
function Ge(e) {
|
|
if (!(D.value || B.value || j.value || V.value))
|
|
return ((M.value = e.page + 1), (N.value = e.rows), Z());
|
|
}
|
|
// 把已知计费周期转换为中文单位,未知英文枚举不直接作为用户文案。
|
|
function Ke(e) {
|
|
return e
|
|
? {
|
|
monthly: `月`,
|
|
quarterly: `季`,
|
|
semiannually: `半年`,
|
|
annually: `年`,
|
|
biennially: `两年`,
|
|
triennially: `三年`,
|
|
onetime: `次`,
|
|
}[e] || (/[\u4e00-\u9fff]/.test(e) ? e : ``)
|
|
: ``;
|
|
}
|
|
// 仅在库存控制已开启且数量明确不大于零时判定售罄。
|
|
function Q(e) {
|
|
return (
|
|
Number(e.stock_control) === 1 && e.qty != null && Number(e.qty) <= 0
|
|
);
|
|
}
|
|
// 不允许订购标记或未知模块会阻止进入配置流程,不能单靠列表有价格就开放下单。
|
|
function $(e) {
|
|
let t = e.module?.trim();
|
|
return (
|
|
e.order_allowed === !1 ||
|
|
(!!t &&
|
|
![
|
|
`mf_cloud`,
|
|
`remf_cloud`,
|
|
`mf_dcim`,
|
|
`remf_dcim`,
|
|
`idcsmart_common`,
|
|
`remf_finance`,
|
|
`remf_finance_dcim`,
|
|
`remf_finance_common`,
|
|
`kky_ecs`,
|
|
`jy_cloud`,
|
|
`jy_dcim`,
|
|
].includes(t))
|
|
);
|
|
}
|
|
// 区别免费、有效起价、补价中、缺货及配置后报价,不把未知金额显示成零元。
|
|
function qe(e) {
|
|
if (e.pay_type === `free`) return `免费`;
|
|
let t = Number(e.price);
|
|
return Number.isFinite(t) && t > 0
|
|
? `¥ ${t.toFixed(2)}`
|
|
: e.startingPrice === null
|
|
? `正在获取价格…`
|
|
: e.startingPrice
|
|
? `¥ ${e.startingPrice} 起`
|
|
: Q(e)
|
|
? `暂无价格`
|
|
: `配置后报价`;
|
|
}
|
|
// 详情链接保留当前有效分类选择,返回目录时可恢复上下文。
|
|
function Je(e) {
|
|
return {
|
|
path: `/products/${e}`,
|
|
query: {
|
|
...(R.value
|
|
? {
|
|
g: String(R.value),
|
|
}
|
|
: {}),
|
|
...(z.value
|
|
? {
|
|
sg: String(z.value),
|
|
}
|
|
: {}),
|
|
},
|
|
};
|
|
}
|
|
return (
|
|
onMounted(X),
|
|
// 分类、商品列表、起价/摘要及库存状态分别呈现;配置摘要是辅助信息,最终报价仍在配置器中确认。
|
|
(e, t) => {
|
|
let i = NuxtLink,
|
|
o = skeleton,
|
|
s = button,
|
|
l = ProductGroupLabel,
|
|
f = ProductCategoryNotice,
|
|
p = inputtext,
|
|
y = message,
|
|
S = ListEmpty,
|
|
C = ProductConfigurationSummary,
|
|
w = ProductDescription,
|
|
k = paginator;
|
|
return (
|
|
openBlock(),
|
|
createElementBlock(`div`, me, [
|
|
createElementVNode(`header`, he, [
|
|
(t[6] ||= createElementVNode(
|
|
`div`,
|
|
null,
|
|
[
|
|
createElementVNode(`h1`, null, `订购产品`),
|
|
createElementVNode(
|
|
`p`,
|
|
null,
|
|
`选择产品,按需配置计算、存储与网络资源。`,
|
|
),
|
|
],
|
|
-1,
|
|
)),
|
|
createVNode(
|
|
i,
|
|
{
|
|
to: `/cart`,
|
|
class: `cart-link`,
|
|
},
|
|
{
|
|
default: withCtx(() => [
|
|
...(t[5] ||= [
|
|
createElementVNode(
|
|
`i`,
|
|
{
|
|
class: `pi pi-shopping-cart`,
|
|
"aria-hidden": `true`,
|
|
},
|
|
null,
|
|
-1,
|
|
),
|
|
createTextVNode(`购物车`, -1),
|
|
]),
|
|
]),
|
|
_: 1,
|
|
},
|
|
),
|
|
]),
|
|
createElementVNode(
|
|
`section`,
|
|
{
|
|
class: `catalog-categories`,
|
|
"aria-label": `产品分类`,
|
|
"aria-busy": unref(B),
|
|
},
|
|
[
|
|
(t[7] ||= createElementVNode(`h2`, null, `产品分类`, -1)),
|
|
unref(B) && !unref(I).length
|
|
? (openBlock(),
|
|
createBlock(o, {
|
|
key: 0,
|
|
height: `86px`,
|
|
}))
|
|
: createCommentVNode(``, !0),
|
|
unref(V)
|
|
? (openBlock(),
|
|
createElementBlock(`div`, _e, [
|
|
createElementVNode(
|
|
`span`,
|
|
null,
|
|
`产品分类加载失败:` + toDisplayString(unref(V)),
|
|
1,
|
|
),
|
|
createVNode(s, {
|
|
label: `重新加载分类`,
|
|
severity: `secondary`,
|
|
outlined: ``,
|
|
onClick: X,
|
|
}),
|
|
]))
|
|
: createCommentVNode(``, !0),
|
|
unref(I).length
|
|
? (openBlock(),
|
|
createElementBlock(
|
|
Fragment,
|
|
{
|
|
key: 2,
|
|
},
|
|
[
|
|
createElementVNode(`div`, ve, [
|
|
(openBlock(!0),
|
|
createElementBlock(
|
|
Fragment,
|
|
null,
|
|
renderList(
|
|
unref(I),
|
|
(e) => (
|
|
openBlock(),
|
|
createElementBlock(
|
|
`button`,
|
|
{
|
|
key: e.id,
|
|
type: `button`,
|
|
class: normalizeClass({
|
|
active: unref(R) === Number(e.id),
|
|
}),
|
|
"aria-pressed":
|
|
unref(R) === Number(e.id),
|
|
disabled: unref(B) || !!unref(V),
|
|
onClick: (t) => Ve(Number(e.id)),
|
|
},
|
|
[
|
|
createVNode(
|
|
l,
|
|
{
|
|
name: e.name,
|
|
},
|
|
null,
|
|
8,
|
|
[`name`],
|
|
),
|
|
],
|
|
10,
|
|
ye,
|
|
)
|
|
),
|
|
),
|
|
128,
|
|
)),
|
|
]),
|
|
unref(H).length
|
|
? (openBlock(),
|
|
createElementBlock(`div`, be, [
|
|
(openBlock(!0),
|
|
createElementBlock(
|
|
Fragment,
|
|
null,
|
|
renderList(
|
|
unref(H),
|
|
(e) => (
|
|
openBlock(),
|
|
createElementBlock(
|
|
`button`,
|
|
{
|
|
key: e.id,
|
|
type: `button`,
|
|
class: normalizeClass({
|
|
active: unref(z) === Number(e.id),
|
|
}),
|
|
"aria-pressed":
|
|
unref(z) === Number(e.id),
|
|
disabled: unref(B) || !!unref(V),
|
|
onClick: (t) => He(Number(e.id)),
|
|
},
|
|
[
|
|
createVNode(
|
|
l,
|
|
{
|
|
name: e.name,
|
|
},
|
|
null,
|
|
8,
|
|
[`name`],
|
|
),
|
|
],
|
|
10,
|
|
xe,
|
|
)
|
|
),
|
|
),
|
|
128,
|
|
)),
|
|
]))
|
|
: (openBlock(),
|
|
createElementBlock(
|
|
`p`,
|
|
Se,
|
|
`该分类暂无可选产品。`,
|
|
)),
|
|
],
|
|
64,
|
|
))
|
|
: !unref(B) && !unref(V)
|
|
? (openBlock(),
|
|
createElementBlock(`p`, Ce, `暂无可选产品分类。`))
|
|
: createCommentVNode(``, !0),
|
|
],
|
|
8,
|
|
ge,
|
|
),
|
|
unref(U)
|
|
? (openBlock(),
|
|
createBlock(
|
|
f,
|
|
{
|
|
key: 0,
|
|
description: unref(U),
|
|
},
|
|
null,
|
|
8,
|
|
[`description`],
|
|
))
|
|
: createCommentVNode(``, !0),
|
|
createElementVNode(
|
|
`section`,
|
|
{
|
|
class: `catalog-panel`,
|
|
"aria-label": `可订购产品`,
|
|
"aria-busy": unref(D),
|
|
},
|
|
[
|
|
createElementVNode(`div`, Te, [
|
|
createElementVNode(
|
|
`form`,
|
|
{
|
|
class: `catalog-search`,
|
|
role: `search`,
|
|
onSubmit: withModifiers(Ue, [`prevent`]),
|
|
},
|
|
[
|
|
createElementVNode(`div`, Ee, [
|
|
(t[8] ||= createElementVNode(
|
|
`i`,
|
|
{
|
|
class: `pi pi-search`,
|
|
"aria-hidden": `true`,
|
|
},
|
|
null,
|
|
-1,
|
|
)),
|
|
createVNode(
|
|
p,
|
|
{
|
|
modelValue: unref(P),
|
|
"onUpdate:modelValue": (t[0] ||= (e) =>
|
|
isRef(P) ? (P.value = e) : null),
|
|
placeholder: `搜索产品名称或配置`,
|
|
"aria-label": `搜索产品名称或配置`,
|
|
},
|
|
null,
|
|
8,
|
|
[`modelValue`],
|
|
),
|
|
]),
|
|
createVNode(
|
|
s,
|
|
{
|
|
type: `submit`,
|
|
label: `搜索`,
|
|
disabled: unref(D) || !!unref(V) || !unref(z),
|
|
},
|
|
null,
|
|
8,
|
|
[`disabled`],
|
|
),
|
|
],
|
|
32,
|
|
),
|
|
createElementVNode(`div`, De, [
|
|
createElementVNode(
|
|
`span`,
|
|
null,
|
|
toDisplayString(
|
|
unref(D)
|
|
? `正在加载产品…`
|
|
: `共 ${unref(E)} 款产品`,
|
|
),
|
|
1,
|
|
),
|
|
createVNode(
|
|
s,
|
|
{
|
|
icon: `pi pi-refresh`,
|
|
"aria-label": `刷新产品`,
|
|
title: `刷新产品`,
|
|
text: ``,
|
|
severity: `secondary`,
|
|
disabled: unref(D),
|
|
onClick: (t[1] ||= (e) =>
|
|
unref(V) || !unref(z) ? X() : Z(!0)),
|
|
},
|
|
null,
|
|
8,
|
|
[`disabled`],
|
|
),
|
|
]),
|
|
]),
|
|
unref(F)
|
|
? (openBlock(),
|
|
createElementBlock(`div`, Oe, [
|
|
createElementVNode(
|
|
`span`,
|
|
null,
|
|
`“` + toDisplayString(unref(F)) + `” 的搜索结果`,
|
|
1,
|
|
),
|
|
createElementVNode(
|
|
`button`,
|
|
{
|
|
type: `button`,
|
|
onClick: We,
|
|
},
|
|
[
|
|
...(t[9] ||= [
|
|
createTextVNode(`清除搜索`, -1),
|
|
createElementVNode(
|
|
`i`,
|
|
{
|
|
class: `pi pi-times`,
|
|
"aria-hidden": `true`,
|
|
},
|
|
null,
|
|
-1,
|
|
),
|
|
]),
|
|
],
|
|
),
|
|
]))
|
|
: createCommentVNode(``, !0),
|
|
(t[13] ||= createElementVNode(
|
|
`div`,
|
|
{
|
|
class: `product-columns`,
|
|
"aria-hidden": `true`,
|
|
},
|
|
[
|
|
createElementVNode(`span`, null, `产品`),
|
|
createElementVNode(`span`, null, `产品介绍`),
|
|
createElementVNode(`span`, null, `价格与订购`),
|
|
],
|
|
-1,
|
|
)),
|
|
(unref(j) || unref(V)) && unref(A)
|
|
? (openBlock(),
|
|
createBlock(
|
|
y,
|
|
{
|
|
key: 1,
|
|
severity: `error`,
|
|
closable: !1,
|
|
role: `alert`,
|
|
},
|
|
{
|
|
default: withCtx(() => [
|
|
createTextVNode(
|
|
toDisplayString(unref(j) || unref(V)),
|
|
1,
|
|
),
|
|
createVNode(
|
|
s,
|
|
{
|
|
label: `重新加载`,
|
|
severity: `secondary`,
|
|
text: ``,
|
|
disabled: unref(D) || unref(B),
|
|
onClick: (t[2] ||= (e) =>
|
|
unref(V) ? X() : Z()),
|
|
},
|
|
null,
|
|
8,
|
|
[`disabled`],
|
|
),
|
|
]),
|
|
_: 1,
|
|
},
|
|
))
|
|
: createCommentVNode(``, !0),
|
|
unref(D) && !unref(A)
|
|
? (openBlock(),
|
|
createElementBlock(`div`, ke, [
|
|
(openBlock(!0),
|
|
createElementBlock(
|
|
Fragment,
|
|
null,
|
|
renderList(
|
|
unref(N),
|
|
(e) => (
|
|
openBlock(),
|
|
createElementBlock(
|
|
`div`,
|
|
{
|
|
key: e,
|
|
class: `product-row product-skeleton`,
|
|
},
|
|
[
|
|
createVNode(o, {
|
|
height: `22px`,
|
|
width: `70%`,
|
|
}),
|
|
createVNode(o, {
|
|
height: `42px`,
|
|
}),
|
|
createVNode(o, {
|
|
height: `38px`,
|
|
}),
|
|
],
|
|
)
|
|
),
|
|
),
|
|
128,
|
|
)),
|
|
]))
|
|
: unref(V) && !unref(A)
|
|
? (openBlock(),
|
|
createBlock(S, {
|
|
key: 3,
|
|
icon: `pi pi-exclamation-circle`,
|
|
title: `暂时无法展示产品`,
|
|
description: `重新加载上方产品分类后查看可订购产品。`,
|
|
}))
|
|
: unref(j) && !unref(A)
|
|
? (openBlock(),
|
|
createBlock(
|
|
S,
|
|
{
|
|
key: 4,
|
|
icon: `pi pi-exclamation-circle`,
|
|
title: `产品加载失败`,
|
|
description: unref(j),
|
|
},
|
|
{
|
|
default: withCtx(() => [
|
|
createVNode(s, {
|
|
label: `重新加载`,
|
|
severity: `secondary`,
|
|
outlined: ``,
|
|
onClick: (t[3] ||= (e) => Z()),
|
|
}),
|
|
]),
|
|
_: 1,
|
|
},
|
|
8,
|
|
[`description`],
|
|
))
|
|
: unref(T).length
|
|
? (openBlock(),
|
|
createElementBlock(
|
|
`div`,
|
|
{
|
|
key: 5,
|
|
class: `product-list`,
|
|
"aria-busy": unref(D),
|
|
inert:
|
|
unref(D) || unref(B) || unref(j) || unref(V)
|
|
? !0
|
|
: void 0,
|
|
},
|
|
[
|
|
(openBlock(!0),
|
|
createElementBlock(
|
|
Fragment,
|
|
null,
|
|
renderList(
|
|
unref(T),
|
|
(e) => (
|
|
openBlock(),
|
|
createElementBlock(
|
|
`article`,
|
|
{
|
|
key: e.id,
|
|
class: normalizeClass([
|
|
`product-row`,
|
|
{
|
|
unavailable: Q(e) || $(e),
|
|
"product-skeleton":
|
|
e.configurationSummary ===
|
|
void 0,
|
|
},
|
|
]),
|
|
"aria-busy":
|
|
e.configurationSummary === void 0,
|
|
"aria-label":
|
|
e.configurationSummary === void 0
|
|
? `正在加载产品配置`
|
|
: void 0,
|
|
},
|
|
[
|
|
e.configurationSummary === void 0
|
|
? (openBlock(),
|
|
createElementBlock(
|
|
Fragment,
|
|
{
|
|
key: 0,
|
|
},
|
|
[
|
|
createVNode(o, {
|
|
height: `64px`,
|
|
width: `85%`,
|
|
}),
|
|
createVNode(o, {
|
|
height: `42px`,
|
|
}),
|
|
createVNode(o, {
|
|
height: `38px`,
|
|
}),
|
|
],
|
|
64,
|
|
))
|
|
: (openBlock(),
|
|
createElementBlock(
|
|
Fragment,
|
|
{
|
|
key: 1,
|
|
},
|
|
[
|
|
createElementVNode(
|
|
`div`,
|
|
Me,
|
|
[
|
|
(t[10] ||=
|
|
createElementVNode(
|
|
`span`,
|
|
{
|
|
class: `product-icon`,
|
|
},
|
|
[
|
|
createElementVNode(
|
|
`i`,
|
|
{
|
|
class: `pi pi-server`,
|
|
"aria-hidden": `true`,
|
|
},
|
|
),
|
|
],
|
|
-1,
|
|
)),
|
|
createElementVNode(
|
|
`div`,
|
|
null,
|
|
[
|
|
createElementVNode(
|
|
`h2`,
|
|
null,
|
|
toDisplayString(
|
|
unref(
|
|
product_name_n,
|
|
)(e.name),
|
|
),
|
|
1,
|
|
),
|
|
createVNode(
|
|
C,
|
|
{
|
|
product: e,
|
|
summary:
|
|
e.configurationSummary,
|
|
},
|
|
null,
|
|
8,
|
|
[
|
|
`product`,
|
|
`summary`,
|
|
],
|
|
),
|
|
createElementVNode(
|
|
`span`,
|
|
{
|
|
class:
|
|
normalizeClass(
|
|
[
|
|
`product-status`,
|
|
{
|
|
"in-stock":
|
|
!Q(
|
|
e,
|
|
) &&
|
|
!$(e),
|
|
},
|
|
],
|
|
),
|
|
},
|
|
[
|
|
createElementVNode(
|
|
`i`,
|
|
{
|
|
class:
|
|
normalizeClass(
|
|
Q(e) ||
|
|
$(e)
|
|
? `pi pi-minus-circle`
|
|
: `pi pi-check-circle`,
|
|
),
|
|
"aria-hidden": `true`,
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
createTextVNode(
|
|
toDisplayString(
|
|
$(e)
|
|
? `暂未上线`
|
|
: Q(e)
|
|
? `暂时缺货`
|
|
: `可订购`,
|
|
),
|
|
1,
|
|
),
|
|
],
|
|
2,
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
createVNode(
|
|
w,
|
|
{
|
|
description:
|
|
e.description,
|
|
"product-name": e.name,
|
|
compact: ``,
|
|
},
|
|
null,
|
|
8,
|
|
[
|
|
`description`,
|
|
`product-name`,
|
|
],
|
|
),
|
|
createElementVNode(
|
|
`div`,
|
|
Ne,
|
|
[
|
|
createElementVNode(
|
|
`div`,
|
|
Pe,
|
|
[
|
|
createElementVNode(
|
|
`strong`,
|
|
null,
|
|
toDisplayString(
|
|
qe(e),
|
|
),
|
|
1,
|
|
),
|
|
e.pay_type !==
|
|
`free` &&
|
|
Number(e.price) >
|
|
0 &&
|
|
Ke(e.cycle)
|
|
? (openBlock(),
|
|
createElementBlock(
|
|
`span`,
|
|
Fe,
|
|
` / ` +
|
|
toDisplayString(
|
|
Ke(
|
|
e.cycle,
|
|
),
|
|
) +
|
|
`起`,
|
|
1,
|
|
))
|
|
: createCommentVNode(
|
|
``,
|
|
!0,
|
|
),
|
|
],
|
|
),
|
|
$(e) || Q(e)
|
|
? (openBlock(),
|
|
createBlock(
|
|
s,
|
|
{
|
|
key: 0,
|
|
label: $(e)
|
|
? `暂未上线`
|
|
: `暂时缺货`,
|
|
severity: `secondary`,
|
|
outlined: ``,
|
|
disabled: ``,
|
|
},
|
|
null,
|
|
8,
|
|
[`label`],
|
|
))
|
|
: (openBlock(),
|
|
createBlock(
|
|
i,
|
|
{
|
|
key: 1,
|
|
to: Je(e.id),
|
|
class: `configure-link`,
|
|
"aria-disabled":
|
|
unref(D) ||
|
|
unref(B) ||
|
|
!!unref(j) ||
|
|
!!unref(V),
|
|
tabindex:
|
|
unref(D) ||
|
|
unref(B) ||
|
|
unref(j) ||
|
|
unref(V)
|
|
? -1
|
|
: void 0,
|
|
"aria-label": `配置 ${unref(product_name_n)(e.name)}`,
|
|
},
|
|
{
|
|
default:
|
|
withCtx(
|
|
() => [
|
|
...(t[11] ||=
|
|
[
|
|
createTextVNode(
|
|
`选择配置`,
|
|
-1,
|
|
),
|
|
createElementVNode(
|
|
`i`,
|
|
{
|
|
class: `pi pi-arrow-right`,
|
|
"aria-hidden": `true`,
|
|
},
|
|
null,
|
|
-1,
|
|
),
|
|
]),
|
|
],
|
|
),
|
|
_: 1,
|
|
},
|
|
8,
|
|
[
|
|
`to`,
|
|
`aria-disabled`,
|
|
`tabindex`,
|
|
`aria-label`,
|
|
],
|
|
)),
|
|
],
|
|
),
|
|
],
|
|
64,
|
|
)),
|
|
],
|
|
10,
|
|
je,
|
|
)
|
|
),
|
|
),
|
|
128,
|
|
)),
|
|
],
|
|
8,
|
|
Ae,
|
|
))
|
|
: (openBlock(),
|
|
createBlock(
|
|
S,
|
|
{
|
|
key: 6,
|
|
icon: `pi pi-box`,
|
|
title: unref(F)
|
|
? `未找到匹配的产品`
|
|
: `暂无可订购产品`,
|
|
description: unref(F)
|
|
? `尝试其他名称或配置关键字,或切换产品分类。`
|
|
: `当前分类没有可供订购的产品,请切换分类或稍后刷新查看。`,
|
|
},
|
|
{
|
|
default: withCtx(() => [
|
|
unref(F)
|
|
? (openBlock(),
|
|
createBlock(s, {
|
|
key: 0,
|
|
label: `清除搜索`,
|
|
severity: `secondary`,
|
|
outlined: ``,
|
|
onClick: We,
|
|
}))
|
|
: (openBlock(),
|
|
createBlock(s, {
|
|
key: 1,
|
|
label: `刷新产品`,
|
|
icon: `pi pi-refresh`,
|
|
severity: `secondary`,
|
|
outlined: ``,
|
|
onClick: (t[4] ||= (e) =>
|
|
unref(z) ? Z() : X()),
|
|
})),
|
|
]),
|
|
_: 1,
|
|
},
|
|
8,
|
|
[`title`, `description`],
|
|
)),
|
|
unref(T).length
|
|
? (openBlock(),
|
|
createElementBlock(
|
|
`footer`,
|
|
{
|
|
key: 7,
|
|
class: `catalog-footer`,
|
|
inert:
|
|
unref(D) || unref(B) || unref(j) || unref(V)
|
|
? !0
|
|
: void 0,
|
|
"aria-disabled":
|
|
unref(D) || unref(B) || !!unref(j) || !!unref(V),
|
|
},
|
|
[
|
|
(t[12] ||= createElementVNode(
|
|
`p`,
|
|
null,
|
|
`展示价格为起价,具体费用以所选配置的报价为准。`,
|
|
-1,
|
|
)),
|
|
unref(E) > unref(N)
|
|
? (openBlock(),
|
|
createBlock(
|
|
k,
|
|
{
|
|
key: 0,
|
|
rows: unref(N),
|
|
"total-records": unref(E),
|
|
first: (unref(M) - 1) * unref(N),
|
|
"rows-per-page-options": [12, 24, 48],
|
|
onPage: Ge,
|
|
},
|
|
null,
|
|
8,
|
|
[`rows`, `total-records`, `first`],
|
|
))
|
|
: createCommentVNode(``, !0),
|
|
],
|
|
8,
|
|
Ie,
|
|
))
|
|
: createCommentVNode(``, !0),
|
|
],
|
|
8,
|
|
we,
|
|
),
|
|
createElementVNode(`p`, Le, [
|
|
(t[15] ||= createTextVNode(`需要选型建议?`, -1)),
|
|
createVNode(
|
|
i,
|
|
{
|
|
to: `/tickets/create`,
|
|
},
|
|
{
|
|
default: withCtx(() => [
|
|
...(t[14] ||= [
|
|
createTextVNode(`联系技术支持`, -1),
|
|
createElementVNode(
|
|
`i`,
|
|
{
|
|
class: `pi pi-arrow-up-right`,
|
|
"aria-hidden": `true`,
|
|
},
|
|
null,
|
|
-1,
|
|
),
|
|
]),
|
|
]),
|
|
_: 1,
|
|
},
|
|
),
|
|
]),
|
|
])
|
|
);
|
|
}
|
|
);
|
|
},
|
|
}),
|
|
[[`__scopeId`, `data-v-a6c63d86`]],
|
|
);
|
|
export {};
|
|
export default V;
|
|
</script>
|