@adep/cli 0.1.6 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.js +2894 -569
  2. package/dist/vite.js +11 -0
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1931,6 +1931,17 @@ var init_sql_engine = __esm({
1931
1931
  }
1932
1932
  return { changes: 0 };
1933
1933
  }
1934
+ if (/^CREATE\s+(?:UNIQUE\s+)?INDEX\s+(?:IF\s+NOT\s+EXISTS\s+)?/i.test(stmt)) {
1935
+ const idx = /^CREATE\s+(?:UNIQUE\s+)?INDEX\s+(?:IF\s+NOT\s+EXISTS\s+)?((?:"[^"]+")|([A-Za-z_][A-Za-z0-9_]*))\s+ON\s+((?:"[^"]+")|([A-Za-z_][A-Za-z0-9_]*))/i.exec(
1936
+ stmt
1937
+ );
1938
+ if (idx === null) throw new SimDbError("DB_UNSAFE_OP", `\u65E0\u6CD5\u89E3\u6790 CREATE INDEX \u8BED\u53E5`);
1939
+ const tableName = ident(idx[3]);
1940
+ if (this.tables[tableName] === void 0) {
1941
+ throw new SimDbError("DB_UNSAFE_OP", `CREATE INDEX \u76EE\u6807\u8868\u4E0D\u5B58\u5728\uFF1A${tableName}`);
1942
+ }
1943
+ return { changes: 0 };
1944
+ }
1934
1945
  if (/^INSERT\s+INTO/i.test(stmt)) return this.execInsert(stmt, params);
1935
1946
  if (/^UPDATE\s+/i.test(stmt)) return this.execUpdate(stmt, params);
1936
1947
  if (/^DELETE\s+FROM/i.test(stmt)) return this.execDelete(stmt, params);
@@ -4706,6 +4717,260 @@ var init_dev = __esm({
4706
4717
  }
4707
4718
  });
4708
4719
 
4720
+ // packages/cli/src/client.ts
4721
+ var client_exports = {};
4722
+ __export(client_exports, {
4723
+ createClient: () => createClient,
4724
+ resolveSlug: () => resolveSlug
4725
+ });
4726
+ async function envelopeError(response, fallbackCode, parsed) {
4727
+ const payload = parsed ?? null;
4728
+ return new CliError(
4729
+ payload?.error?.code ?? fallbackCode,
4730
+ payload?.error?.message ?? `\u5E73\u53F0\u8FD4\u56DE HTTP ${response.status}`
4731
+ );
4732
+ }
4733
+ async function parseBody(response) {
4734
+ const text = await response.text();
4735
+ if (text.length === 0) return null;
4736
+ try {
4737
+ return JSON.parse(text);
4738
+ } catch {
4739
+ return null;
4740
+ }
4741
+ }
4742
+ async function createClient(paths) {
4743
+ const credentials = await loadCredentials(paths);
4744
+ if (credentials === null) {
4745
+ throw new CliError("NOT_LOGGED_IN", "\u672A\u767B\u5F55\uFF1A\u8BF7\u5148\u6267\u884C adep login");
4746
+ }
4747
+ const server = credentials.server;
4748
+ const cookie = `better-auth.session_token=${decodeToken(credentials.encodedToken)}`;
4749
+ const request = async (path, init = {}, fallbackCode = "API_REQUEST_FAILED") => {
4750
+ const method = init.method ?? (init.body === void 0 ? "GET" : "POST");
4751
+ let response;
4752
+ try {
4753
+ response = await fetch(`${server}${path}`, {
4754
+ method,
4755
+ headers: { "content-type": "application/json", cookie },
4756
+ ...init.body === void 0 ? {} : { body: JSON.stringify(init.body) }
4757
+ });
4758
+ } catch (error) {
4759
+ throw new CliError(
4760
+ "SERVER_UNREACHABLE",
4761
+ `\u65E0\u6CD5\u8FDE\u63A5\u5E73\u53F0 ${server}\uFF1A${error instanceof Error ? error.message : String(error)}`
4762
+ );
4763
+ }
4764
+ const parsed = await parseBody(response);
4765
+ if (response.status < 200 || response.status >= 300) {
4766
+ throw await envelopeError(response, fallbackCode, parsed);
4767
+ }
4768
+ return parsed;
4769
+ };
4770
+ const upload = async (path, form, fallbackCode = "UPLOAD_FAILED") => {
4771
+ let response;
4772
+ try {
4773
+ response = await fetch(`${server}${path}`, {
4774
+ method: "POST",
4775
+ headers: { cookie },
4776
+ body: form
4777
+ });
4778
+ } catch (error) {
4779
+ throw new CliError(
4780
+ "SERVER_UNREACHABLE",
4781
+ `\u65E0\u6CD5\u8FDE\u63A5\u5E73\u53F0 ${server}\uFF1A${error instanceof Error ? error.message : String(error)}`
4782
+ );
4783
+ }
4784
+ if (response.status < 200 || response.status >= 300) {
4785
+ throw await envelopeError(response, fallbackCode, await parseBody(response));
4786
+ }
4787
+ return response;
4788
+ };
4789
+ const download = async (url, fallbackCode = "DOWNLOAD_FAILED") => {
4790
+ const target = url.startsWith("http") ? url : `${server}${url}`;
4791
+ let response;
4792
+ try {
4793
+ response = await fetch(target, { headers: { cookie } });
4794
+ } catch (error) {
4795
+ throw new CliError(
4796
+ "SERVER_UNREACHABLE",
4797
+ `\u65E0\u6CD5\u8FDE\u63A5\u5E73\u53F0 ${server}\uFF1A${error instanceof Error ? error.message : String(error)}`
4798
+ );
4799
+ }
4800
+ if (response.status < 200 || response.status >= 300) {
4801
+ throw await envelopeError(response, fallbackCode, await parseBody(response));
4802
+ }
4803
+ return response;
4804
+ };
4805
+ return { server, cookie, request, upload, download };
4806
+ }
4807
+ async function resolveSlug(cwd, slug) {
4808
+ if (slug !== void 0 && slug.length > 0) return slug;
4809
+ const config = await loadConfig(cwd);
4810
+ return config.name;
4811
+ }
4812
+ var init_client = __esm({
4813
+ "packages/cli/src/client.ts"() {
4814
+ "use strict";
4815
+ init_auth();
4816
+ init_credentials();
4817
+ init_dev();
4818
+ }
4819
+ });
4820
+
4821
+ // packages/cli/src/db.ts
4822
+ var db_exports = {};
4823
+ __export(db_exports, {
4824
+ dbExec: () => dbExec,
4825
+ dbMigrate: () => dbMigrate,
4826
+ dbRollback: () => dbRollback,
4827
+ dbSnapshotCreate: () => dbSnapshotCreate,
4828
+ dbSnapshotList: () => dbSnapshotList,
4829
+ dbSnapshotRestore: () => dbSnapshotRestore,
4830
+ dbStart: () => dbStart,
4831
+ dbStatus: () => dbStatus,
4832
+ dbStop: () => dbStop,
4833
+ splitSqlStatements: () => splitSqlStatements
4834
+ });
4835
+ import { readFile as readFile8 } from "node:fs/promises";
4836
+ import { resolve as resolve5 } from "node:path";
4837
+ async function open2(paths, options) {
4838
+ const client = await createClient(paths);
4839
+ const project2 = await resolveSlug(resolve5(options.cwd), options.slug);
4840
+ return { client, project: project2 };
4841
+ }
4842
+ async function dbStart(paths, options) {
4843
+ const { client, project: project2 } = await open2(paths, options);
4844
+ return client.request(`/api/v1/projects/${project2}/database`, { method: "POST" });
4845
+ }
4846
+ async function dbStatus(paths, options) {
4847
+ const { client, project: project2 } = await open2(paths, options);
4848
+ return client.request(`/api/v1/projects/${project2}/database`);
4849
+ }
4850
+ async function dbStop(paths, options) {
4851
+ const { client, project: project2 } = await open2(paths, options);
4852
+ return client.request(`/api/v1/projects/${project2}/database`, {
4853
+ method: "DELETE"
4854
+ });
4855
+ }
4856
+ async function dbExec(paths, options) {
4857
+ const { client, project: project2 } = await open2(paths, options);
4858
+ return client.request(
4859
+ `/api/v1/projects/${project2}/database/console/sql`,
4860
+ {
4861
+ method: "POST",
4862
+ body: {
4863
+ sql: options.sql,
4864
+ ...options.params === void 0 ? {} : { params: options.params },
4865
+ ...options.confirmTable === void 0 ? {} : { confirmTable: options.confirmTable }
4866
+ }
4867
+ },
4868
+ "SQL_EXEC_FAILED"
4869
+ );
4870
+ }
4871
+ async function dbSnapshotList(paths, options) {
4872
+ const { client, project: project2 } = await open2(paths, options);
4873
+ return client.request(
4874
+ `/api/v1/projects/${project2}/database/snapshots`
4875
+ );
4876
+ }
4877
+ async function dbSnapshotCreate(paths, options) {
4878
+ const { client, project: project2 } = await open2(paths, options);
4879
+ return client.request(`/api/v1/projects/${project2}/database/snapshots`, {
4880
+ method: "POST"
4881
+ });
4882
+ }
4883
+ async function dbSnapshotRestore(paths, options) {
4884
+ const { client, project: project2 } = await open2(paths, options);
4885
+ return client.request(
4886
+ `/api/v1/projects/${project2}/database/snapshots/${options.snapshotId}/restore`,
4887
+ { method: "POST" }
4888
+ );
4889
+ }
4890
+ async function dbRollback(paths, options) {
4891
+ const { client, project: project2 } = await open2(paths, options);
4892
+ return client.request(
4893
+ `/api/v1/projects/${project2}/database/rollback`,
4894
+ { method: "POST", body: { to: options.to } }
4895
+ );
4896
+ }
4897
+ function splitSqlStatements(source) {
4898
+ const statements = [];
4899
+ let buffer = "";
4900
+ let inQuote = false;
4901
+ const lines = source.split("\n");
4902
+ for (const rawLine of lines) {
4903
+ const line = rawLine.trim();
4904
+ if (line.length === 0) continue;
4905
+ if (line.startsWith("--")) continue;
4906
+ for (let i = 0; i < line.length; i++) {
4907
+ const ch = line[i];
4908
+ if (ch === "'") {
4909
+ if (inQuote && line[i + 1] === "'") {
4910
+ buffer += ch + line[i + 1];
4911
+ i++;
4912
+ continue;
4913
+ }
4914
+ inQuote = !inQuote;
4915
+ buffer += ch;
4916
+ continue;
4917
+ }
4918
+ if (ch === ";" && !inQuote) {
4919
+ const statement = buffer.trim();
4920
+ if (statement.length > 0) statements.push(statement);
4921
+ buffer = "";
4922
+ continue;
4923
+ }
4924
+ buffer += ch;
4925
+ }
4926
+ buffer += " ";
4927
+ }
4928
+ if (inQuote) {
4929
+ throw new Error(`SQL \u8FC1\u79FB\u6587\u4EF6\u5F15\u53F7\u672A\u95ED\u5408\uFF1A${buffer.trim().slice(0, 80)}\u2026`);
4930
+ }
4931
+ const tail = buffer.trim();
4932
+ if (tail.length > 0) statements.push(tail);
4933
+ return statements;
4934
+ }
4935
+ async function dbMigrate(paths, options) {
4936
+ const source = await readFile8(resolve5(options.file), "utf8");
4937
+ const statements = splitSqlStatements(source);
4938
+ if (statements.length === 0) {
4939
+ throw new Error(`\u8FC1\u79FB\u6587\u4EF6\u6CA1\u6709\u53EF\u6267\u884C\u8BED\u53E5\uFF1A${options.file}`);
4940
+ }
4941
+ const { project: project2 } = await open2(paths, options);
4942
+ const applied = [];
4943
+ const changes = [];
4944
+ for (const statement of statements) {
4945
+ try {
4946
+ const result = await dbExec(paths, {
4947
+ cwd: options.cwd,
4948
+ ...options.slug === void 0 ? {} : { slug: options.slug },
4949
+ sql: statement
4950
+ });
4951
+ applied.push(statement);
4952
+ changes.push(result.changes);
4953
+ } catch (error) {
4954
+ const code = error instanceof CliError ? error.code : "MIGRATE_FAILED";
4955
+ const message = error instanceof Error ? error.message : String(error);
4956
+ return {
4957
+ projectId: project2,
4958
+ applied,
4959
+ changes,
4960
+ failed: { statement, code, message }
4961
+ };
4962
+ }
4963
+ }
4964
+ return { projectId: project2, applied, changes };
4965
+ }
4966
+ var init_db2 = __esm({
4967
+ "packages/cli/src/db.ts"() {
4968
+ "use strict";
4969
+ init_auth();
4970
+ init_client();
4971
+ }
4972
+ });
4973
+
4709
4974
  // packages/cli/src/serve/banner.ts
4710
4975
  function isLoopback(host) {
4711
4976
  return host === "127.0.0.1" || host === "localhost" || host === "::1" || host === "[::1]";
@@ -4727,7 +4992,7 @@ function renderBanner(options) {
4727
4992
  lines.push("");
4728
4993
  lines.push(" \u80FD\u529B:");
4729
4994
  if (functions.length > 0) {
4730
- const route = options.functionsPrefix === void 0 || options.functionsPrefix.length === 0 ? `${baseUrl}/api/{fn}` : `${baseUrl}/api/${options.functionsPrefix.replace(/^\/+|\/+$/g, "")}/{fn}`;
4995
+ const route = options.functionsPrefix === void 0 || options.functionsPrefix.length === 0 ? `${baseUrl}/api/{fn}` : `${baseUrl}/${options.functionsPrefix.replace(/^\/+|\/+$/g, "")}/{fn}`;
4731
4996
  lines.push(` - \u51FD\u6570\u8DEF\u7531: ${route}\uFF08${functions.length} \u4E2A\u51FD\u6570: ${functions.join(", ")}\uFF09`);
4732
4997
  } else {
4733
4998
  lines.push(" - \u51FD\u6570\u8DEF\u7531: \u65E0\uFF08functions/ \u76EE\u5F55\u4E3A\u7A7A\uFF09");
@@ -4778,8 +5043,8 @@ __export(server_exports, {
4778
5043
  });
4779
5044
  import { createServer as createServer2 } from "node:http";
4780
5045
  import { watch as watch2 } from "node:fs";
4781
- import { mkdir as mkdir7, readdir as readdir4, readFile as readFile8, stat as stat6 } from "node:fs/promises";
4782
- import { join as join11, relative as relative2, resolve as resolve5 } from "node:path";
5046
+ import { mkdir as mkdir7, readdir as readdir4, readFile as readFile9, stat as stat6 } from "node:fs/promises";
5047
+ import { extname, join as join11, relative as relative2, resolve as resolve6 } from "node:path";
4783
5048
  function envNumber(name) {
4784
5049
  const raw = process.env[name];
4785
5050
  if (raw === void 0 || raw.trim() === "") return void 0;
@@ -4805,7 +5070,7 @@ async function collectFunctions2(dir) {
4805
5070
  if (entry.isDirectory()) {
4806
5071
  await walk(full, rel);
4807
5072
  } else if (entry.isFile() && entry.name.endsWith(".ts")) {
4808
- files[rel] = await readFile8(full, "utf8");
5073
+ files[rel] = await readFile9(full, "utf8");
4809
5074
  }
4810
5075
  }
4811
5076
  };
@@ -4840,12 +5105,12 @@ function contentTypeFor(path) {
4840
5105
  return MIME_TYPES[ext] ?? "application/octet-stream";
4841
5106
  }
4842
5107
  async function startServeServer(options) {
4843
- const cwd = resolve5(options.cwd);
5108
+ const cwd = resolve6(options.cwd);
4844
5109
  const log = options.log ?? ((line) => process.stdout.write(`${line}
4845
5110
  `));
4846
5111
  const host = options.host ?? envString("HOST") ?? "127.0.0.1";
4847
5112
  const port = options.port ?? envNumber("PORT") ?? 8787;
4848
- const staticDir = resolve5(
5113
+ const staticDir = resolve6(
4849
5114
  cwd,
4850
5115
  options.staticDir ?? envString("ADEP_SERVE_STATIC_DIR") ?? "public"
4851
5116
  );
@@ -4860,7 +5125,7 @@ async function startServeServer(options) {
4860
5125
  }
4861
5126
  let files = await collectFunctions2(functionsDir);
4862
5127
  let functionNames = listFunctionNames(files);
4863
- const envText = await readFile8(join11(cwd, ".env.local"), "utf8").catch(() => "");
5128
+ const envText = await readFile9(join11(cwd, ".env.local"), "utf8").catch(() => "");
4864
5129
  let env = parseEnvFile(envText);
4865
5130
  let simEnv = await loadSimEnv(cwd).catch(() => ({}));
4866
5131
  const runtime = await createSimRuntime({
@@ -4869,7 +5134,35 @@ async function startServeServer(options) {
4869
5134
  baseUrl: `http://${host}:${port}`,
4870
5135
  log
4871
5136
  });
4872
- const executor = new WorkerFunctionExecutor({
5137
+ if (options.schemaFile !== void 0) {
5138
+ const schemaPath = resolve6(cwd, options.schemaFile);
5139
+ const schemaSource = await readFile9(schemaPath, "utf8").catch((error) => {
5140
+ if (error.code === "ENOENT") {
5141
+ throw new Error(`schema \u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${schemaPath}`);
5142
+ }
5143
+ throw error;
5144
+ });
5145
+ const statements = splitSqlStatements(schemaSource);
5146
+ let applied = 0;
5147
+ let skipped = 0;
5148
+ for (const statement of statements) {
5149
+ try {
5150
+ await runtime.db.engine.run(statement);
5151
+ applied += 1;
5152
+ } catch (error) {
5153
+ const isUnsupported = error instanceof Error && "code" in error && error.code === "DB_UNSAFE_OP";
5154
+ if (isUnsupported) {
5155
+ skipped += 1;
5156
+ log(`[serve] \u8DF3\u8FC7 schema \u8BED\u53E5\uFF08sim \u5F15\u64CE\u4E0D\u652F\u6301\uFF09\uFF1A${statement.slice(0, 60)}\u2026`);
5157
+ } else {
5158
+ throw error;
5159
+ }
5160
+ }
5161
+ }
5162
+ const skipNote = skipped > 0 ? `\uFF0C\u8DF3\u8FC7 ${skipped} \u6761` : "";
5163
+ log(`[serve] \u5DF2\u5E94\u7528\u6570\u636E\u5E93\u7ED3\u6784\uFF08--schema ${options.schemaFile}\uFF09\uFF1A${applied} \u6761\u8BED\u53E5${skipNote}`);
5164
+ }
5165
+ const executor = new WorkerFunctionExecutor({
4873
5166
  deps: { rootDir: cwd, builtin: LOCAL_BUILTIN_DEPS }
4874
5167
  });
4875
5168
  const dbBundle = runtime.bundle;
@@ -4879,7 +5172,7 @@ async function startServeServer(options) {
4879
5172
  const reload = async () => {
4880
5173
  const [nextFiles, envLocalText] = await Promise.all([
4881
5174
  collectFunctions2(functionsDir),
4882
- readFile8(join11(cwd, ".env.local"), "utf8").catch(() => "")
5175
+ readFile9(join11(cwd, ".env.local"), "utf8").catch(() => "")
4883
5176
  ]);
4884
5177
  files = nextFiles;
4885
5178
  env = parseEnvFile(envLocalText);
@@ -4957,7 +5250,7 @@ async function startServeServer(options) {
4957
5250
  try {
4958
5251
  const indexStat = await stat6(indexPath);
4959
5252
  if (indexStat.isFile()) {
4960
- const content2 = await readFile8(indexPath);
5253
+ const content2 = await readFile9(indexPath);
4961
5254
  res.writeHead(200, { "content-type": contentTypeFor(indexPath) });
4962
5255
  res.end(content2);
4963
5256
  return true;
@@ -4966,20 +5259,33 @@ async function startServeServer(options) {
4966
5259
  }
4967
5260
  return false;
4968
5261
  }
4969
- const content = await readFile8(resolvedPath);
5262
+ const content = await readFile9(resolvedPath);
4970
5263
  res.writeHead(200, { "content-type": contentTypeFor(resolvedPath) });
4971
5264
  res.end(content);
4972
5265
  return true;
4973
5266
  } catch {
5267
+ if (options.spaFallback === true) {
5268
+ const ext = extname(pathname);
5269
+ if (ext === "" || ext === ".html") {
5270
+ try {
5271
+ const indexContent = await readFile9(join11(staticDir, "index.html"));
5272
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
5273
+ res.end(indexContent);
5274
+ return true;
5275
+ } catch {
5276
+ }
5277
+ }
5278
+ }
4974
5279
  return false;
4975
5280
  }
4976
5281
  };
4977
5282
  const handle = async (req, res) => {
4978
5283
  const url = new URL(req.url ?? "/", `http://${host}:${port}`);
4979
5284
  const pathname = url.pathname;
4980
- if (pathname === "/api" || pathname.startsWith("/api/")) {
4981
- const rest = pathname === "/api" ? "" : pathname.slice("/api/".length);
4982
- const { fnName, error: routeError } = resolveRouteFnName(rest, config.functionsPrefix);
5285
+ const normalizedPrefix = config.functionsPrefix.replace(/^\/+|\/+$/g, "");
5286
+ const fnRest = normalizedPrefix.length > 0 ? pathname === `/${normalizedPrefix}` || pathname.startsWith(`/${normalizedPrefix}/`) ? pathname.slice(normalizedPrefix.length + 1) : null : pathname === "/api" ? "" : pathname.startsWith("/api/") ? pathname.slice("/api/".length) : null;
5287
+ if (fnRest !== null) {
5288
+ const { fnName, error: routeError } = resolveRouteFnName(fnRest, "");
4983
5289
  if (routeError !== void 0) {
4984
5290
  writeJson2(res, 400, { error: routeError });
4985
5291
  return;
@@ -5113,6 +5419,7 @@ var init_server = __esm({
5113
5419
  init_http_envelope();
5114
5420
  init_worker_executor();
5115
5421
  init_builtin_deps();
5422
+ init_db2();
5116
5423
  init_runtime();
5117
5424
  init_env();
5118
5425
  init_invoke();
@@ -5142,107 +5449,6 @@ var init_server = __esm({
5142
5449
  }
5143
5450
  });
5144
5451
 
5145
- // packages/cli/src/client.ts
5146
- var client_exports = {};
5147
- __export(client_exports, {
5148
- createClient: () => createClient,
5149
- resolveSlug: () => resolveSlug
5150
- });
5151
- async function envelopeError(response, fallbackCode, parsed) {
5152
- const payload = parsed ?? null;
5153
- return new CliError(
5154
- payload?.error?.code ?? fallbackCode,
5155
- payload?.error?.message ?? `\u5E73\u53F0\u8FD4\u56DE HTTP ${response.status}`
5156
- );
5157
- }
5158
- async function parseBody(response) {
5159
- const text = await response.text();
5160
- if (text.length === 0) return null;
5161
- try {
5162
- return JSON.parse(text);
5163
- } catch {
5164
- return null;
5165
- }
5166
- }
5167
- async function createClient(paths) {
5168
- const credentials = await loadCredentials(paths);
5169
- if (credentials === null) {
5170
- throw new CliError("NOT_LOGGED_IN", "\u672A\u767B\u5F55\uFF1A\u8BF7\u5148\u6267\u884C adep login");
5171
- }
5172
- const server = credentials.server;
5173
- const cookie = `better-auth.session_token=${decodeToken(credentials.encodedToken)}`;
5174
- const request = async (path, init = {}, fallbackCode = "API_REQUEST_FAILED") => {
5175
- const method = init.method ?? (init.body === void 0 ? "GET" : "POST");
5176
- let response;
5177
- try {
5178
- response = await fetch(`${server}${path}`, {
5179
- method,
5180
- headers: { "content-type": "application/json", cookie },
5181
- ...init.body === void 0 ? {} : { body: JSON.stringify(init.body) }
5182
- });
5183
- } catch (error) {
5184
- throw new CliError(
5185
- "SERVER_UNREACHABLE",
5186
- `\u65E0\u6CD5\u8FDE\u63A5\u5E73\u53F0 ${server}\uFF1A${error instanceof Error ? error.message : String(error)}`
5187
- );
5188
- }
5189
- const parsed = await parseBody(response);
5190
- if (response.status < 200 || response.status >= 300) {
5191
- throw await envelopeError(response, fallbackCode, parsed);
5192
- }
5193
- return parsed;
5194
- };
5195
- const upload = async (path, form, fallbackCode = "UPLOAD_FAILED") => {
5196
- let response;
5197
- try {
5198
- response = await fetch(`${server}${path}`, {
5199
- method: "POST",
5200
- headers: { cookie },
5201
- body: form
5202
- });
5203
- } catch (error) {
5204
- throw new CliError(
5205
- "SERVER_UNREACHABLE",
5206
- `\u65E0\u6CD5\u8FDE\u63A5\u5E73\u53F0 ${server}\uFF1A${error instanceof Error ? error.message : String(error)}`
5207
- );
5208
- }
5209
- if (response.status < 200 || response.status >= 300) {
5210
- throw await envelopeError(response, fallbackCode, await parseBody(response));
5211
- }
5212
- return response;
5213
- };
5214
- const download = async (url, fallbackCode = "DOWNLOAD_FAILED") => {
5215
- const target = url.startsWith("http") ? url : `${server}${url}`;
5216
- let response;
5217
- try {
5218
- response = await fetch(target, { headers: { cookie } });
5219
- } catch (error) {
5220
- throw new CliError(
5221
- "SERVER_UNREACHABLE",
5222
- `\u65E0\u6CD5\u8FDE\u63A5\u5E73\u53F0 ${server}\uFF1A${error instanceof Error ? error.message : String(error)}`
5223
- );
5224
- }
5225
- if (response.status < 200 || response.status >= 300) {
5226
- throw await envelopeError(response, fallbackCode, await parseBody(response));
5227
- }
5228
- return response;
5229
- };
5230
- return { server, cookie, request, upload, download };
5231
- }
5232
- async function resolveSlug(cwd, slug) {
5233
- if (slug !== void 0 && slug.length > 0) return slug;
5234
- const config = await loadConfig(cwd);
5235
- return config.name;
5236
- }
5237
- var init_client = __esm({
5238
- "packages/cli/src/client.ts"() {
5239
- "use strict";
5240
- init_auth();
5241
- init_credentials();
5242
- init_dev();
5243
- }
5244
- });
5245
-
5246
5452
  // packages/cli/src/deploy.ts
5247
5453
  var deploy_exports = {};
5248
5454
  __export(deploy_exports, {
@@ -5251,8 +5457,8 @@ __export(deploy_exports, {
5251
5457
  deploy: () => deploy
5252
5458
  });
5253
5459
  import { createHash as createHash2 } from "node:crypto";
5254
- import { readdir as readdir5, readFile as readFile9 } from "node:fs/promises";
5255
- import { join as join12, resolve as resolve6, basename as basename3, relative as relative3, sep } from "node:path";
5460
+ import { readdir as readdir5, readFile as readFile10 } from "node:fs/promises";
5461
+ import { join as join12, resolve as resolve7, basename as basename3, relative as relative3, sep } from "node:path";
5256
5462
  function functionUrlBase(prefix) {
5257
5463
  const raw = (prefix ?? "/api").trim();
5258
5464
  const normalized = raw === "/" ? "/" : raw.replace(/\/+$/, "");
@@ -5268,7 +5474,7 @@ async function collectEntries(functionsDir) {
5268
5474
  return entries;
5269
5475
  }
5270
5476
  for (const name of names) {
5271
- entries[name] = await readFile9(join12(functionsDir, `${name}.ts`), "utf8");
5477
+ entries[name] = await readFile10(join12(functionsDir, `${name}.ts`), "utf8");
5272
5478
  }
5273
5479
  return entries;
5274
5480
  }
@@ -5294,7 +5500,7 @@ async function collectShared(functionsDir) {
5294
5500
  if (!entry.name.endsWith(".ts") || entry.name.endsWith(".test.ts")) continue;
5295
5501
  const full = join12(dir, entry.name);
5296
5502
  const rel = relative3(sharedDir, full).split(sep).join("/");
5297
- files[`_shared/${rel}`] = await readFile9(full, "utf8");
5503
+ files[`_shared/${rel}`] = await readFile10(full, "utf8");
5298
5504
  }
5299
5505
  };
5300
5506
  await walk(sharedDir);
@@ -5305,10 +5511,10 @@ async function deploy(paths, options) {
5305
5511
  const log = options.silent === true ? () => void 0 : options.log ?? ((line) => process.stdout.write(`${line}
5306
5512
  `));
5307
5513
  const client = await createClient(paths);
5308
- const cwd = resolve6(options.cwd);
5514
+ const cwd = resolve7(options.cwd);
5309
5515
  const config = await loadConfig(cwd);
5310
5516
  const slug = options.slug ?? config.name;
5311
- const functionsDir = options.functionsDir === void 0 ? join12(cwd, config.functionsDir) : resolve6(cwd, options.functionsDir);
5517
+ const functionsDir = options.functionsDir === void 0 ? join12(cwd, config.functionsDir) : resolve7(cwd, options.functionsDir);
5312
5518
  const local = await collectEntries(functionsDir);
5313
5519
  if (Object.keys(local).length === 0) {
5314
5520
  throw new CliError("NO_FUNCTIONS", `${functionsDir} \u4E0B\u6CA1\u6709\u51FD\u6570\u6587\u4EF6`);
@@ -5401,7 +5607,7 @@ var frontend_exports = {};
5401
5607
  __export(frontend_exports, {
5402
5608
  frontendSync: () => frontendSync
5403
5609
  });
5404
- import { readdir as readdir6, readFile as readFile10, stat as stat7 } from "node:fs/promises";
5610
+ import { readdir as readdir6, readFile as readFile11, stat as stat7 } from "node:fs/promises";
5405
5611
  import { join as join13, relative as relative4, sep as sep2 } from "node:path";
5406
5612
  async function resolveProjectId(client, cwd, slug) {
5407
5613
  const resolvedSlug = await resolveSlug(cwd, slug);
@@ -5435,7 +5641,7 @@ async function collectFiles(root, dir) {
5435
5641
  continue;
5436
5642
  }
5437
5643
  try {
5438
- const content = await readFile10(full, "utf8");
5644
+ const content = await readFile11(full, "utf8");
5439
5645
  if (content.includes("\0")) {
5440
5646
  skipped.push(rel);
5441
5647
  continue;
@@ -5663,11 +5869,11 @@ var fs_exports = {};
5663
5869
  __export(fs_exports, {
5664
5870
  createExportFileSystem: () => createExportFileSystem
5665
5871
  });
5666
- import { mkdir as mkdir8, readFile as readFile11, rm as rm3, stat as stat9, writeFile as writeFile7 } from "node:fs/promises";
5872
+ import { mkdir as mkdir8, readFile as readFile12, rm as rm3, stat as stat9, writeFile as writeFile7 } from "node:fs/promises";
5667
5873
  import { dirname as dirname7, join as join15 } from "node:path";
5668
5874
  function createExportFileSystem() {
5669
5875
  return {
5670
- readFile: (path) => readFile11(path),
5876
+ readFile: (path) => readFile12(path),
5671
5877
  writeFile: (path, content) => writeFile7(path, content),
5672
5878
  // 同时用于清理解压临时目录(${zip}.tmp 是个目录)——递归强制删除,缺失不报错。
5673
5879
  deleteFile: async (path) => {
@@ -5685,7 +5891,7 @@ function createExportFileSystem() {
5685
5891
  await mkdir8(path, { recursive: true });
5686
5892
  },
5687
5893
  unzip: async (zipPath, targetDir) => {
5688
- const entries = zipRead(await readFile11(zipPath));
5894
+ const entries = zipRead(await readFile12(zipPath));
5689
5895
  for (const [name, data] of entries) {
5690
5896
  const dest = join15(targetDir, name);
5691
5897
  await mkdir8(dirname7(dest), { recursive: true });
@@ -6196,246 +6402,93 @@ ${detail}`);
6196
6402
  }
6197
6403
  });
6198
6404
 
6199
- // packages/cli/src/db.ts
6200
- var db_exports = {};
6201
- __export(db_exports, {
6202
- dbExec: () => dbExec,
6203
- dbMigrate: () => dbMigrate,
6204
- dbRollback: () => dbRollback,
6205
- dbSnapshotCreate: () => dbSnapshotCreate,
6206
- dbSnapshotList: () => dbSnapshotList,
6207
- dbSnapshotRestore: () => dbSnapshotRestore,
6208
- dbStart: () => dbStart,
6209
- dbStatus: () => dbStatus,
6210
- dbStop: () => dbStop,
6211
- splitSqlStatements: () => splitSqlStatements
6405
+ // packages/cli/src/storage.ts
6406
+ var storage_exports = {};
6407
+ __export(storage_exports, {
6408
+ contentTypeOf: () => contentTypeOf,
6409
+ storageDownload: () => storageDownload,
6410
+ storageList: () => storageList,
6411
+ storageRemove: () => storageRemove,
6412
+ storageUpload: () => storageUpload
6212
6413
  });
6213
- import { readFile as readFile12 } from "node:fs/promises";
6214
- import { resolve as resolve8 } from "node:path";
6215
- async function open2(paths, options) {
6414
+ import { mkdir as mkdir9, readFile as readFile13, stat as stat10, writeFile as writeFile8 } from "node:fs/promises";
6415
+ import { basename as basename4, dirname as dirname8, extname as extname2, join as join16, resolve as resolve9 } from "node:path";
6416
+ async function open3(paths, options) {
6216
6417
  const client = await createClient(paths);
6217
- const project2 = await resolveSlug(resolve8(options.cwd), options.slug);
6418
+ const project2 = await resolveSlug(resolve9(options.cwd), options.slug);
6218
6419
  return { client, project: project2 };
6219
6420
  }
6220
- async function dbStart(paths, options) {
6221
- const { client, project: project2 } = await open2(paths, options);
6222
- return client.request(`/api/v1/projects/${project2}/database`, { method: "POST" });
6223
- }
6224
- async function dbStatus(paths, options) {
6225
- const { client, project: project2 } = await open2(paths, options);
6226
- return client.request(`/api/v1/projects/${project2}/database`);
6227
- }
6228
- async function dbStop(paths, options) {
6229
- const { client, project: project2 } = await open2(paths, options);
6230
- return client.request(`/api/v1/projects/${project2}/database`, {
6231
- method: "DELETE"
6232
- });
6421
+ function contentTypeOf(path) {
6422
+ const map = {
6423
+ ".html": "text/html; charset=utf-8",
6424
+ ".htm": "text/html; charset=utf-8",
6425
+ ".css": "text/css; charset=utf-8",
6426
+ ".js": "text/javascript; charset=utf-8",
6427
+ ".mjs": "text/javascript; charset=utf-8",
6428
+ ".json": "application/json; charset=utf-8",
6429
+ ".png": "image/png",
6430
+ ".jpg": "image/jpeg",
6431
+ ".jpeg": "image/jpeg",
6432
+ ".gif": "image/gif",
6433
+ ".svg": "image/svg+xml",
6434
+ ".webp": "image/webp",
6435
+ ".ico": "image/x-icon",
6436
+ ".txt": "text/plain; charset=utf-8",
6437
+ ".md": "text/markdown; charset=utf-8",
6438
+ ".xml": "application/xml",
6439
+ ".wasm": "application/wasm",
6440
+ ".pdf": "application/pdf",
6441
+ ".zip": "application/zip",
6442
+ ".woff": "font/woff",
6443
+ ".woff2": "font/woff2",
6444
+ ".ttf": "font/ttf",
6445
+ ".otf": "font/otf"
6446
+ };
6447
+ return map[extname2(path).toLowerCase()] ?? "application/octet-stream";
6233
6448
  }
6234
- async function dbExec(paths, options) {
6235
- const { client, project: project2 } = await open2(paths, options);
6236
- return client.request(
6237
- `/api/v1/projects/${project2}/database/console/sql`,
6238
- {
6239
- method: "POST",
6240
- body: {
6241
- sql: options.sql,
6242
- ...options.params === void 0 ? {} : { params: options.params },
6243
- ...options.confirmTable === void 0 ? {} : { confirmTable: options.confirmTable }
6244
- }
6245
- },
6246
- "SQL_EXEC_FAILED"
6449
+ async function storageUpload(paths, options) {
6450
+ const { client, project: project2 } = await open3(paths, options);
6451
+ const local = resolve9(options.file);
6452
+ const info = await stat10(local).catch(() => null);
6453
+ if (info === null || !info.isFile()) {
6454
+ throw new CliError("FILE_NOT_FOUND", `\u672C\u5730\u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${local}`);
6455
+ }
6456
+ const visibility = options.visibility ?? "private";
6457
+ const bytes = await readFile13(local);
6458
+ const form = new FormData();
6459
+ form.set("path", options.path);
6460
+ form.set("visibility", visibility);
6461
+ form.set(
6462
+ "file",
6463
+ new Blob([bytes], { type: contentTypeOf(options.path) }),
6464
+ basename4(local)
6247
6465
  );
6248
- }
6249
- async function dbSnapshotList(paths, options) {
6250
- const { client, project: project2 } = await open2(paths, options);
6251
- return client.request(
6252
- `/api/v1/projects/${project2}/database/snapshots`
6466
+ const response = await client.upload(
6467
+ `/api/v1/projects/${project2}/files`,
6468
+ form,
6469
+ "STORAGE_UPLOAD_FAILED"
6253
6470
  );
6471
+ const body = await response.json();
6472
+ return { ...body.file, ...body.signedUrl === void 0 ? {} : { signedUrl: body.signedUrl } };
6254
6473
  }
6255
- async function dbSnapshotCreate(paths, options) {
6256
- const { client, project: project2 } = await open2(paths, options);
6257
- return client.request(`/api/v1/projects/${project2}/database/snapshots`, {
6258
- method: "POST"
6259
- });
6260
- }
6261
- async function dbSnapshotRestore(paths, options) {
6262
- const { client, project: project2 } = await open2(paths, options);
6263
- return client.request(
6264
- `/api/v1/projects/${project2}/database/snapshots/${options.snapshotId}/restore`,
6265
- { method: "POST" }
6474
+ async function storageList(paths, options) {
6475
+ const { client, project: project2 } = await open3(paths, options);
6476
+ const query = options.prefix === void 0 ? "" : `?prefix=${encodeURIComponent(options.prefix)}`;
6477
+ const body = await client.request(
6478
+ `/api/v1/projects/${project2}/files${query}`
6266
6479
  );
6480
+ return { project: project2, files: body.files };
6267
6481
  }
6268
- async function dbRollback(paths, options) {
6269
- const { client, project: project2 } = await open2(paths, options);
6270
- return client.request(
6271
- `/api/v1/projects/${project2}/database/rollback`,
6272
- { method: "POST", body: { to: options.to } }
6482
+ async function resolveDownloadUrl(client, project2, path) {
6483
+ const body = await client.request(
6484
+ `/api/v1/projects/${project2}/files?prefix=${encodeURIComponent(path)}`
6273
6485
  );
6274
- }
6275
- function splitSqlStatements(source) {
6276
- const statements = [];
6277
- let buffer = "";
6278
- let inQuote = false;
6279
- const lines = source.split("\n");
6280
- for (const rawLine of lines) {
6281
- const line = rawLine.trim();
6282
- if (line.length === 0) continue;
6283
- if (line.startsWith("--")) continue;
6284
- for (let i = 0; i < line.length; i++) {
6285
- const ch = line[i];
6286
- if (ch === "'") {
6287
- if (inQuote && line[i + 1] === "'") {
6288
- buffer += ch + line[i + 1];
6289
- i++;
6290
- continue;
6291
- }
6292
- inQuote = !inQuote;
6293
- buffer += ch;
6294
- continue;
6295
- }
6296
- if (ch === ";" && !inQuote) {
6297
- const statement = buffer.trim();
6298
- if (statement.length > 0) statements.push(statement);
6299
- buffer = "";
6300
- continue;
6301
- }
6302
- buffer += ch;
6303
- }
6304
- buffer += " ";
6305
- }
6306
- if (inQuote) {
6307
- throw new Error(`SQL \u8FC1\u79FB\u6587\u4EF6\u5F15\u53F7\u672A\u95ED\u5408\uFF1A${buffer.trim().slice(0, 80)}\u2026`);
6308
- }
6309
- const tail = buffer.trim();
6310
- if (tail.length > 0) statements.push(tail);
6311
- return statements;
6312
- }
6313
- async function dbMigrate(paths, options) {
6314
- const source = await readFile12(resolve8(options.file), "utf8");
6315
- const statements = splitSqlStatements(source);
6316
- if (statements.length === 0) {
6317
- throw new Error(`\u8FC1\u79FB\u6587\u4EF6\u6CA1\u6709\u53EF\u6267\u884C\u8BED\u53E5\uFF1A${options.file}`);
6318
- }
6319
- const { project: project2 } = await open2(paths, options);
6320
- const applied = [];
6321
- const changes = [];
6322
- for (const statement of statements) {
6323
- try {
6324
- const result = await dbExec(paths, {
6325
- cwd: options.cwd,
6326
- ...options.slug === void 0 ? {} : { slug: options.slug },
6327
- sql: statement
6328
- });
6329
- applied.push(statement);
6330
- changes.push(result.changes);
6331
- } catch (error) {
6332
- const code = error instanceof CliError ? error.code : "MIGRATE_FAILED";
6333
- const message = error instanceof Error ? error.message : String(error);
6334
- return {
6335
- projectId: project2,
6336
- applied,
6337
- changes,
6338
- failed: { statement, code, message }
6339
- };
6340
- }
6341
- }
6342
- return { projectId: project2, applied, changes };
6343
- }
6344
- var init_db2 = __esm({
6345
- "packages/cli/src/db.ts"() {
6346
- "use strict";
6347
- init_auth();
6348
- init_client();
6349
- }
6350
- });
6351
-
6352
- // packages/cli/src/storage.ts
6353
- var storage_exports = {};
6354
- __export(storage_exports, {
6355
- contentTypeOf: () => contentTypeOf,
6356
- storageDownload: () => storageDownload,
6357
- storageList: () => storageList,
6358
- storageRemove: () => storageRemove,
6359
- storageUpload: () => storageUpload
6360
- });
6361
- import { mkdir as mkdir9, readFile as readFile13, stat as stat10, writeFile as writeFile8 } from "node:fs/promises";
6362
- import { basename as basename4, dirname as dirname8, extname, join as join16, resolve as resolve9 } from "node:path";
6363
- async function open3(paths, options) {
6364
- const client = await createClient(paths);
6365
- const project2 = await resolveSlug(resolve9(options.cwd), options.slug);
6366
- return { client, project: project2 };
6367
- }
6368
- function contentTypeOf(path) {
6369
- const map = {
6370
- ".html": "text/html; charset=utf-8",
6371
- ".htm": "text/html; charset=utf-8",
6372
- ".css": "text/css; charset=utf-8",
6373
- ".js": "text/javascript; charset=utf-8",
6374
- ".mjs": "text/javascript; charset=utf-8",
6375
- ".json": "application/json; charset=utf-8",
6376
- ".png": "image/png",
6377
- ".jpg": "image/jpeg",
6378
- ".jpeg": "image/jpeg",
6379
- ".gif": "image/gif",
6380
- ".svg": "image/svg+xml",
6381
- ".webp": "image/webp",
6382
- ".ico": "image/x-icon",
6383
- ".txt": "text/plain; charset=utf-8",
6384
- ".md": "text/markdown; charset=utf-8",
6385
- ".xml": "application/xml",
6386
- ".wasm": "application/wasm",
6387
- ".pdf": "application/pdf",
6388
- ".zip": "application/zip",
6389
- ".woff": "font/woff",
6390
- ".woff2": "font/woff2",
6391
- ".ttf": "font/ttf",
6392
- ".otf": "font/otf"
6393
- };
6394
- return map[extname(path).toLowerCase()] ?? "application/octet-stream";
6395
- }
6396
- async function storageUpload(paths, options) {
6397
- const { client, project: project2 } = await open3(paths, options);
6398
- const local = resolve9(options.file);
6399
- const info = await stat10(local).catch(() => null);
6400
- if (info === null || !info.isFile()) {
6401
- throw new CliError("FILE_NOT_FOUND", `\u672C\u5730\u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${local}`);
6402
- }
6403
- const visibility = options.visibility ?? "private";
6404
- const bytes = await readFile13(local);
6405
- const form = new FormData();
6406
- form.set("path", options.path);
6407
- form.set("visibility", visibility);
6408
- form.set(
6409
- "file",
6410
- new Blob([bytes], { type: contentTypeOf(options.path) }),
6411
- basename4(local)
6412
- );
6413
- const response = await client.upload(
6414
- `/api/v1/projects/${project2}/files`,
6415
- form,
6416
- "STORAGE_UPLOAD_FAILED"
6417
- );
6418
- const body = await response.json();
6419
- return { ...body.file, ...body.signedUrl === void 0 ? {} : { signedUrl: body.signedUrl } };
6420
- }
6421
- async function storageList(paths, options) {
6422
- const { client, project: project2 } = await open3(paths, options);
6423
- const query = options.prefix === void 0 ? "" : `?prefix=${encodeURIComponent(options.prefix)}`;
6424
- const body = await client.request(
6425
- `/api/v1/projects/${project2}/files${query}`
6426
- );
6427
- return { project: project2, files: body.files };
6428
- }
6429
- async function resolveDownloadUrl(client, project2, path) {
6430
- const body = await client.request(
6431
- `/api/v1/projects/${project2}/files?prefix=${encodeURIComponent(path)}`
6432
- );
6433
- const entry = body.files.find((file) => file.path === path);
6434
- const url = entry?.url ?? entry?.signedUrl;
6435
- if (entry === void 0 || url === void 0 || url.length === 0) {
6436
- throw new CliError("STORAGE_NOT_FOUND", `\u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${path}`);
6437
- }
6438
- return url;
6486
+ const entry = body.files.find((file) => file.path === path);
6487
+ const url = entry?.url ?? entry?.signedUrl;
6488
+ if (entry === void 0 || url === void 0 || url.length === 0) {
6489
+ throw new CliError("STORAGE_NOT_FOUND", `\u6587\u4EF6\u4E0D\u5B58\u5728\uFF1A${path}`);
6490
+ }
6491
+ return url;
6439
6492
  }
6440
6493
  async function storageDownload(paths, options) {
6441
6494
  const { client, project: project2 } = await open3(paths, options);
@@ -6708,6 +6761,15 @@ async function projectsCreate(paths, options) {
6708
6761
  const name = options.name ?? slug;
6709
6762
  const body = { slug, name };
6710
6763
  if (options.spaceId !== void 0) body.spaceId = options.spaceId;
6764
+ if (options.gitRepoUrl !== void 0 && options.gitBranch !== void 0) {
6765
+ body.git = {
6766
+ repoUrl: options.gitRepoUrl,
6767
+ branch: options.gitBranch,
6768
+ ...options.gitFunctionRoot === void 0 ? {} : { functionRoot: options.gitFunctionRoot },
6769
+ ...options.gitUsername === void 0 ? {} : { authUsername: options.gitUsername },
6770
+ ...options.gitToken === void 0 ? {} : { authToken: options.gitToken }
6771
+ };
6772
+ }
6711
6773
  const created = await client.request(
6712
6774
  "/api/v1/projects",
6713
6775
  { body },
@@ -7294,196 +7356,2335 @@ interface AdepContext {
7294
7356
  readonly user: AdepExecutorUser | null
7295
7357
  }
7296
7358
 
7297
- /** \u5168\u5C40\u5F00\u53D1\u671F\u4E0A\u4E0B\u6587\uFF08\u7F16\u8F91\u5668\u8865\u5168\u7528\uFF1B\u51FD\u6570\u8FD0\u884C\u671F\u7ECF\u53C2\u6570\u6CE8\u5165\uFF0C\u4E0D\u8BFB\u53D6\u5168\u5C40\uFF09\u3002 */
7298
- declare const ctx: AdepContext
7299
- `;
7300
- var ADEP_TSCONFIG_JSON = `{
7301
- "compilerOptions": {
7302
- "target": "ES2021",
7303
- "module": "ESNext",
7304
- "moduleResolution": "Bundler",
7305
- "strict": true,
7306
- "noEmit": true,
7307
- "skipLibCheck": true,
7308
- "types": [],
7309
- "lib": ["ES2021", "DOM"]
7310
- },
7311
- "include": ["**/*.ts", "**/*.d.ts"]
7359
+ /** \u5168\u5C40\u5F00\u53D1\u671F\u4E0A\u4E0B\u6587\uFF08\u7F16\u8F91\u5668\u8865\u5168\u7528\uFF1B\u51FD\u6570\u8FD0\u884C\u671F\u7ECF\u53C2\u6570\u6CE8\u5165\uFF0C\u4E0D\u8BFB\u53D6\u5168\u5C40\uFF09\u3002 */
7360
+ declare const ctx: AdepContext
7361
+ `;
7362
+ var ADEP_TSCONFIG_JSON = `{
7363
+ "compilerOptions": {
7364
+ "target": "ES2021",
7365
+ "module": "ESNext",
7366
+ "moduleResolution": "Bundler",
7367
+ "strict": true,
7368
+ "noEmit": true,
7369
+ "skipLibCheck": true,
7370
+ "types": [],
7371
+ "lib": ["ES2021", "DOM"]
7372
+ },
7373
+ "include": ["**/*.ts", "**/*.d.ts"]
7374
+ }
7375
+ `;
7376
+
7377
+ // packages/cli/src/init.ts
7378
+ init_adep_config();
7379
+
7380
+ // shared/sdk/web-project-template.ts
7381
+ var ADEP_CLIENT_VERSION = "^0.2.0";
7382
+ var ADEP_VITE_PLUGIN_VERSION = "^0.1.0";
7383
+ var WEB_DEPENDENCIES = {
7384
+ vue: "^3.3.4",
7385
+ // @adep/client:浏览器端云函数调用 SDK(IDE-031)。模板默认在 web/src/lib/adep.ts
7386
+ // 封装并由 App.vue 示例调用;运行时 fetch('/api/{name}') 在 dev 态被 @adep/vite-plugin
7387
+ // 拦截(CLI 代理到本地 adep dev / Web IDE 经 postMessage 转发到 IDE 主线程),
7388
+ // 生产态由平台网关路由到已发布云函数。
7389
+ "@adep/client": ADEP_CLIENT_VERSION
7390
+ };
7391
+ var WEB_DEV_DEPENDENCIES = {
7392
+ vite: "^4.4.0",
7393
+ "@vitejs/plugin-vue": "^4.3.0",
7394
+ // vite 内部动态 import('esbuild-wasm'),必须在 package.json 显式声明才能被 Nodebox 解析
7395
+ "esbuild-wasm": "0.18.20",
7396
+ // @adep/vite-plugin:云函数 Vite 插件(IDE-030)。dev 时自动注入 /api/* fetch 拦截器
7397
+ // (Web IDE 预览经 postMessage 转发到 IDE 主线程执行云函数草稿),若注入 createDevServer
7398
+ // (CLI 场景)则启动本地 adep dev server 并按 functions_prefix 代理 /{prefix}/* 到云函数。
7399
+ "@adep/vite-plugin": ADEP_VITE_PLUGIN_VERSION
7400
+ };
7401
+ function webProjectFiles() {
7402
+ return [
7403
+ {
7404
+ path: "package.json",
7405
+ content: JSON.stringify(
7406
+ {
7407
+ name: "adep-web",
7408
+ version: "0.0.0",
7409
+ type: "module",
7410
+ scripts: { dev: "vite" },
7411
+ dependencies: WEB_DEPENDENCIES,
7412
+ devDependencies: WEB_DEV_DEPENDENCIES
7413
+ },
7414
+ null,
7415
+ 2
7416
+ )
7417
+ },
7418
+ {
7419
+ path: "index.html",
7420
+ content: [
7421
+ `<!doctype html>`,
7422
+ `<html lang="zh-CN">`,
7423
+ `<head>`,
7424
+ ` <meta charset="utf-8" />`,
7425
+ ` <meta name="viewport" content="width=device-width, initial-scale=1" />`,
7426
+ ` <title>adep web</title>`,
7427
+ `</head>`,
7428
+ `<body>`,
7429
+ ` <div id="app"></div>`,
7430
+ ` <script type="module" src="/src/main.ts"></script>`,
7431
+ `</body>`,
7432
+ `</html>`,
7433
+ ``
7434
+ ].join("\n")
7435
+ },
7436
+ {
7437
+ path: "vite.config.ts",
7438
+ content: [
7439
+ `// web/vite.config.ts \u2014\u2014 \u6807\u51C6 Vite \u914D\u7F6E\uFF08Vue 3 + @adep/vite-plugin \u4E91\u51FD\u6570\u4EE3\u7406\uFF09\u3002`,
7440
+ `// @adep/vite-plugin \u5728 dev \u65F6\uFF1A`,
7441
+ `// 1. \u6CE8\u5165 /api/* fetch \u62E6\u622A\u5668\uFF08Web IDE \u9884\u89C8\u7ECF postMessage \u8F6C\u53D1\u5230 IDE \u4E3B\u7EBF\u7A0B\u6267\u884C\u4E91\u51FD\u6570\u8349\u7A3F\uFF09\uFF1B`,
7442
+ `// 2. \u82E5\u6CE8\u5165 createDevServer\uFF08CLI \u573A\u666F\uFF09\uFF0C\u542F\u52A8\u672C\u5730 adep dev \u5E76\u4EE3\u7406 /api/* \u5230\u4E91\u51FD\u6570\u3002`,
7443
+ `// \u6D4F\u89C8\u5668\u5185 vite-dev \u9884\u89C8\uFF08@adep/web-container buildDocument\uFF09\u6CE8\u5165\u540C\u4E00\u4EFD\u811A\u672C\uFF0C\u65E0\u9700\u5728\u6B64\u5185\u8054\u3002`,
7444
+ `import { defineConfig } from 'vite'`,
7445
+ `import vue from '@vitejs/plugin-vue'`,
7446
+ `import { adepPlugin } from '@adep/vite-plugin'`,
7447
+ ``,
7448
+ `export default defineConfig({`,
7449
+ ` plugins: [vue(), adepPlugin()],`,
7450
+ `})`,
7451
+ ``
7452
+ ].join("\n")
7453
+ },
7454
+ {
7455
+ path: "src/main.ts",
7456
+ content: [
7457
+ `// web/src/main.ts \u2014\u2014 \u5E94\u7528\u5165\u53E3\uFF08Vue 3 + Vite\uFF09\u3002`,
7458
+ `// \u5F00\u53D1\uFF1A\u5E95\u90E8\u300C\u6D4F\u89C8\u5668\u7EC8\u7AEF\u300D\u8FD0\u884C npm run dev\uFF08\u6216\u7B49 Nodebox \u81EA\u52A8\u62C9\u8D77\uFF09\uFF0C\u4FDD\u5B58\u540E\u9884\u89C8 HMR \u5237\u65B0\u3002`,
7459
+ `import { createApp } from 'vue'`,
7460
+ `import App from './App.vue'`,
7461
+ ``,
7462
+ `createApp(App).mount('#app')`,
7463
+ ``
7464
+ ].join("\n")
7465
+ },
7466
+ {
7467
+ path: "src/lib/adep.ts",
7468
+ content: [
7469
+ `// web/src/lib/adep.ts \u2014\u2014 @adep/client \u6D4F\u89C8\u5668\u7AEF\u5C01\u88C5\uFF08IDE-032\uFF09\u3002`,
7470
+ `// \u5F00\u53D1\u6001\uFF08vite dev + @adep/vite-plugin\uFF09\uFF1AinvokeFunction \u8D70 fetch('/api/*')\uFF0C`,
7471
+ `// \u7531 vite \u63D2\u4EF6\u62E6\u622A\u5230\u672C\u5730\u4E91\u51FD\u6570\u6267\u884C\uFF08CLI\uFF09\u6216\u7ECF postMessage \u8F6C\u53D1\u5230 IDE \u4E3B\u7EBF\u7A0B\uFF08Web IDE\uFF09\u3002`,
7472
+ `// \u751F\u4EA7\u6001\uFF08\u5DF2\u90E8\u7F72\u7AD9\u70B9\uFF09\uFF1Afetch('/api/*') \u7531\u5E73\u53F0\u7F51\u5173\u8DEF\u7531\u5230\u5DF2\u53D1\u5E03\u4E91\u51FD\u6570\u3002`,
7473
+ `import { createAdepClient } from '@adep/client'`,
7474
+ ``,
7475
+ `export const adep = createAdepClient()`,
7476
+ ``,
7477
+ `export const { invokeFunction } = adep`,
7478
+ ``
7479
+ ].join("\n")
7480
+ },
7481
+ {
7482
+ path: "src/App.vue",
7483
+ content: [
7484
+ `<script setup lang="ts">`,
7485
+ `import { ref } from 'vue'`,
7486
+ `import { invokeFunction } from './lib/adep'`,
7487
+ ``,
7488
+ `const title = ref('adep web')`,
7489
+ `const clicks = ref(0)`,
7490
+ `const fnResult = ref<string>('')`,
7491
+ `const fnLoading = ref(false)`,
7492
+ ``,
7493
+ `async function callHello() {`,
7494
+ ` fnLoading.value = true`,
7495
+ ` fnResult.value = ''`,
7496
+ ` try {`,
7497
+ ` const result = await invokeFunction<{ message: string }>('hello')`,
7498
+ ` fnResult.value = result.message`,
7499
+ ` } catch (err) {`,
7500
+ ` fnResult.value = \`\u8C03\u7528\u5931\u8D25\uFF1A\${err instanceof Error ? err.message : String(err)}\``,
7501
+ ` } finally {`,
7502
+ ` fnLoading.value = false`,
7503
+ ` }`,
7504
+ `}`,
7505
+ `</script>`,
7506
+ ``,
7507
+ `<template>`,
7508
+ ` <main>`,
7509
+ ` <h1>{{ title }}</h1>`,
7510
+ ` <p>\u5728 web/src/App.vue \u91CC\u7F16\u8F91\uFF0C\u4FDD\u5B58\u540E\u9884\u89C8\u81EA\u52A8\u5237\u65B0\uFF08HMR\uFF09\u3002</p>`,
7511
+ ` <button @click="clicks++">clicks: {{ clicks }}</button>`,
7512
+ ` <div style="margin-top: 16px;">`,
7513
+ ` <button @click="callHello" :disabled="fnLoading">`,
7514
+ ` {{ fnLoading ? '\u8C03\u7528\u4E2D\u2026' : '\u8C03\u7528 hello \u51FD\u6570' }}`,
7515
+ ` </button>`,
7516
+ ` <p v-if="fnResult" style="margin-top: 8px; color: #059669;">\u7ED3\u679C\uFF1A{{ fnResult }}</p>`,
7517
+ ` </div>`,
7518
+ ` </main>`,
7519
+ `</template>`,
7520
+ ``,
7521
+ `<style scoped>`,
7522
+ `main {`,
7523
+ ` margin: 24px;`,
7524
+ ` font-family: system-ui, -apple-system, sans-serif;`,
7525
+ `}`,
7526
+ `button {`,
7527
+ ` padding: 6px 14px;`,
7528
+ ` border: 1px solid #6366f1;`,
7529
+ ` border-radius: 6px;`,
7530
+ ` background: #eef2ff;`,
7531
+ ` cursor: pointer;`,
7532
+ `}`,
7533
+ `button:disabled {`,
7534
+ ` opacity: 0.6;`,
7535
+ ` cursor: not-allowed;`,
7536
+ `}`,
7537
+ `</style>`,
7538
+ ``
7539
+ ].join("\n")
7540
+ }
7541
+ ];
7542
+ }
7543
+ function webSrcFiles() {
7544
+ return webProjectFiles().filter((file) => file.path.startsWith("src/"));
7545
+ }
7546
+
7547
+ // packages/cli/src/templates/countdown-template.ts
7548
+ var COUNTDOWN_TEMPLATE_FILES = [
7549
+ { path: "functions/_shared/auth.ts", content: "/**\n * \u8BF7\u6C42\u9274\u6743\uFF08\u79FB\u690D\u81EA cf-backend src/middleware/auth.ts + src/lib/jwt.ts \u7684\u8BED\u4E49\uFF09\u3002\n *\n * \u8FC1\u79FB\u70B9\uFF1A\u5FAE\u4FE1 code2Session \u2192 openid \u2192 JWT \u7684\u8EAB\u4EFD\u94FE\u8DEF\uFF0C\u66FF\u6362\u4E3A adep web \u7AEF\n * \u300C\u8BBE\u5907\u8EAB\u4EFD device_id\u300D\u4F5C\u4E3A Bearer \u51ED\u636E\uFF08\u524D\u7AEF\u6301\u4E45\u5316\u4E8E localStorage\uFF0C\u89C1\n * web/src/lib/device.ts\uFF09\u3002device_id \u5373\u7528\u6237\u8868\u552F\u4E00\u952E\uFF0C\u7B49\u4EF7 API Key\uFF1A\u67E5\u5F97\u5230 \u2192 \u8EAB\u4EFD\u6210\u7ACB\u3002\n *\n * \u5931\u8D25\u8FD4\u56DE\u4E0E\u5C0F\u7A0B\u5E8F\u540E\u7AEF\u4E00\u81F4\u7684\u4E1A\u52A1\u7801 40001\uFF08\u524D\u7AEF request.ts \u4F1A\u636E\u6B64\u9759\u9ED8\u91CD\u767B\uFF09\u3002\n */\nimport type { CloudDb, FunctionContext } from '@adep/types'\nimport { fail, CODE } from './response'\nimport { findUserByDeviceId, type UserRow } from './store'\n\n/** \u89E3\u6790 Authorization: Bearer xxx\uFF08\u5E26\u524D\u5BFC\u5927\u5C0F\u5199/\u591A\u4F59\u7A7A\u683C\u5BB9\u9519\uFF09 */\nexport function parseBearer(ctx: FunctionContext): string {\n const header = ctx.headers?.authorization ?? ctx.headers?.Authorization ?? ''\n const match = /^Bearer\\s+(.+)$/i.exec(String(header).trim())\n return match?.[1]?.trim() ?? ''\n}\n\n/** \u4ECE\u8BF7\u6C42\u5934\u89E3\u6790\u51FA\u7528\u6237\uFF1B\u7F3A\u51ED\u636E/\u7528\u6237\u4E0D\u5B58\u5728 \u2192 40001 \u54CD\u5E94\u4FE1\u5C01\u3002 */\nexport async function requireUser(\n db: CloudDb,\n ctx: FunctionContext\n): Promise<{ user: UserRow } | AdepHttpEnvelopeLike> {\n const token = parseBearer(ctx)\n if (!token) return fail(CODE.NO_AUTH, '\u672A\u767B\u5F55')\n const user = await findUserByDeviceId(db, token)\n if (!user) return fail(CODE.NO_AUTH, '\u767B\u5F55\u5DF2\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55')\n return { user }\n}\n\n/** \u5224\u65AD\u8FD4\u56DE\u662F\u5426\u54CD\u5E94\u4FE1\u5C01\uFF08\u7528\u4E8E requireUser \u6536\u7A84\uFF09\u3002 */\nexport function isEnvelope<T>(value: T | AdepHttpEnvelopeLike): value is AdepHttpEnvelopeLike {\n return typeof value === 'object' && value !== null && '__adepHttp' in value\n}\n\nexport interface AdepHttpEnvelopeLike {\n __adepHttp: { status: number; body?: unknown }\n}\n" },
7550
+ { path: "functions/_shared/date.test.ts", content: "/**\n * \u670D\u52A1\u7AEF\u65E5\u671F/\u6392\u5E8F/\u54CD\u5E94\u4FE1\u5C01\u5355\u5143\u6D4B\u8BD5\uFF08\u79FB\u690D\u903B\u8F91\u7684\u56DE\u5F52\u4FDD\u62A4\uFF09\u3002\n * \u8FD0\u884C\uFF1Anpm run test\n */\nimport { describe, it, expect } from 'vitest'\nimport { isValidDate, calcDays, toDTO, sortEvents, type EventRow } from './date'\nimport { ok, fail, CODE } from './response'\n\ndescribe('isValidDate', () => {\n it('\u63A5\u53D7\u5408\u6CD5 YYYY-MM-DD', () => {\n expect(isValidDate('2026-09-20')).toBe(true)\n expect(isValidDate('2024-02-29')).toBe(true)\n })\n it('\u62D2\u7EDD\u975E\u6CD5\u65E5\u671F\u4E0E\u683C\u5F0F', () => {\n expect(isValidDate('2026-13-01')).toBe(false)\n expect(isValidDate('2026-02-30')).toBe(false)\n expect(isValidDate('2026/09/20')).toBe(false)\n expect(isValidDate('')).toBe(false)\n expect(isValidDate(null)).toBe(false)\n })\n})\n\ndescribe('calcDays\uFF08\u5317\u4EAC\u65F6\u95F4\u81EA\u7136\u65E5\uFF09', () => {\n it('\u5012\u8BA1\u65F6\uFF1A\u672A\u6765\u4E3A\u6B63\uFF0C\u5F53\u5929\u4E3A 0', () => {\n const today = new Date()\n const beijing = new Date(today.getTime() + 8 * 3600_000)\n const todayStr = `${beijing.getUTCFullYear()}-${String(beijing.getUTCMonth() + 1).padStart(2, '0')}-${String(\n beijing.getUTCDate()\n ).padStart(2, '0')}`\n expect(calcDays(todayStr)).toBe(0)\n })\n it('\u8FC7\u53BB\u65E5\u671F\u4E3A\u8D1F\uFF1Bcountup \u53D6\u7EDD\u5BF9\u503C', () => {\n expect(calcDays('2020-01-01')).toBeLessThan(0)\n expect(calcDays('2020-01-01', 'countup')).toBeGreaterThan(0)\n })\n it('\u975E\u6CD5\u65E5\u671F\u8FD4\u56DE 0', () => {\n expect(calcDays('bad-date')).toBe(0)\n })\n})\n\ndescribe('toDTO + sortEvents', () => {\n const base = {\n id: '1',\n user_id: 'u1',\n title: '\u4E8B\u4EF6',\n target_date: '2030-01-01',\n note: null,\n category: null,\n direction: 'countdown',\n is_pinned: 0,\n sort_order: 0,\n created_at: '2026-01-01T00:00:00.000Z',\n updated_at: '2026-01-01T00:00:00.000Z',\n } as EventRow\n\n it('toDTO \u8865\u9ED8\u8BA4\u503C\u5E76\u7B97 days', () => {\n const dto = toDTO(base)\n expect(dto.note).toBe('')\n expect(dto.category).toBe('other')\n expect(dto.is_pinned).toBe(false)\n expect(dto.days).toBeGreaterThan(0)\n })\n\n it('\u6392\u5E8F\uFF1A\u7F6E\u9876\u4F18\u5148\uFF0C\u5176\u4F59\u6309\u5269\u4F59\u5929\u6570\u5347\u5E8F\uFF08\u8FC7\u53BB\u6392\u6700\u540E\uFF09', () => {\n const far = { ...base, id: 'far', target_date: '2031-01-01' } as EventRow\n const near = { ...base, id: 'near', target_date: '2026-10-01' } as EventRow\n const past = { ...base, id: 'past', target_date: '2020-01-01' } as EventRow\n const pinnedFar = { ...far, id: 'pinnedFar', is_pinned: 1 } as EventRow\n const sorted = sortEvents([far, near, past, pinnedFar].map((r) => toDTO(r)))\n expect(sorted.map((e) => e.id)).toEqual(['pinnedFar', 'near', 'far', 'past'])\n })\n})\n\ndescribe('\u54CD\u5E94\u4FE1\u5C01', () => {\n it('ok \u8FD4\u56DE 200 + \u4E1A\u52A1\u4FE1\u5C01', () => {\n const env = ok({ a: 1 })\n expect(env.__adepHttp.status).toBe(200)\n expect(env.__adepHttp.body).toEqual({ code: 0, data: { a: 1 }, message: 'ok' })\n })\n it('fail \u6620\u5C04 HTTP \u72B6\u6001', () => {\n expect(fail(CODE.NO_AUTH, 'x').__adepHttp.status).toBe(401)\n expect(fail(CODE.BAD_PARAM, 'x').__adepHttp.status).toBe(400)\n expect(fail(CODE.NOT_FOUND, 'x').__adepHttp.status).toBe(404)\n })\n})\n" },
7551
+ { path: "functions/_shared/date.ts", content: "/**\n * \u670D\u52A1\u7AEF\u65E5\u671F\u5DE5\u5177\uFF08\u79FB\u690D\u81EA cf-backend src/lib/date.ts\uFF09\u3002\n * \u4E0E\u5C0F\u7A0B\u5E8F utils/date.js \u7B97\u6CD5\u5BF9\u9F50\uFF1A\u5929\u6570\u6309\u300C\u5317\u4EAC\u65F6\u95F4\uFF08UTC+8\uFF09\u81EA\u7136\u65E5\u300D\u8BA1\u7B97\u3002\n */\nconst WEEK = ['\u5468\u65E5', '\u5468\u4E00', '\u5468\u4E8C', '\u5468\u4E09', '\u5468\u56DB', '\u5468\u4E94', '\u5468\u516D']\n\n/** \u5317\u4EAC\u65F6\u95F4\u4ECA\u5929 YYYY-MM-DD\uFF08\u81EA\u7136\u65E5\uFF0C\u4E0D\u4EE5 UTC \u65E5\u671F\u4E3A\u51C6\uFF09 */\nexport function todayStr(): string {\n const now = new Date(Date.now() + 8 * 3600_000)\n const y = now.getUTCFullYear()\n const m = now.getUTCMonth() + 1\n const d = now.getUTCDate()\n return fmt(y, m, d)\n}\n\nfunction fmt(y: number, m: number, d: number): string {\n return `${y}-${String(m).padStart(2, '0')}-${String(d).padStart(2, '0')}`\n}\n\nexport function isValidDate(s: unknown): s is string {\n if (typeof s !== 'string' || !/^\\d{4}-\\d{2}-\\d{2}$/.test(s)) return false\n const [y, m, d] = s.split('-').map(Number)\n if (m < 1 || m > 12 || d < 1 || d > 31) return false\n const dt = new Date(y, m - 1, d)\n return dt.getFullYear() === y && dt.getMonth() === m - 1 && dt.getDate() === d\n}\n\n/** \u6309\u5317\u4EAC\u65F6\u95F4\u81EA\u7136\u65E5\u5DEE\uFF08\u6B63\u6570\u672A\u6765 / \u8D1F\u6570\u8FC7\u53BB\uFF1Bcountup \u53D6\u7EDD\u5BF9\u503C\uFF09 */\nexport function calcDays(\n targetDate: string,\n direction: 'countdown' | 'countup' = 'countdown'\n): number {\n if (!isValidDate(targetDate)) return 0\n const target = new Date(`${targetDate}T00:00:00+08:00`).getTime()\n const now = new Date()\n const nowBeijing = new Date(now.getTime() + 8 * 3600_000)\n const todayStart = new Date(\n Date.UTC(nowBeijing.getUTCFullYear(), nowBeijing.getUTCMonth(), nowBeijing.getUTCDate()) -\n 8 * 3600_000\n ).getTime()\n const diff = Math.round((target - todayStart) / 86400000)\n return direction === 'countup' ? Math.abs(diff) : diff\n}\n\nexport function weekdayOf(targetDate: string): string {\n if (!isValidDate(targetDate)) return ''\n const [y, m, d] = targetDate.split('-').map(Number)\n return WEEK[new Date(y, m - 1, d).getDay()]\n}\n\n/** \u670D\u52A1\u5668\u65F6\u95F4 ISO\uFF08UTC\uFF0C\u5B58\u50A8\u7528\uFF09 */\nexport function nowIso(): string {\n return new Date().toISOString()\n}\n\n/** \u4E8B\u4EF6\u884C \u2192 DTO\uFF08\u9644 days / weekday\uFF1Bis_pinned \u5E03\u5C14\u5316\uFF09 */\nexport interface EventRow {\n id: string\n user_id: string\n title: string\n target_date: string\n note: string | null\n category: string | null\n direction: 'countdown' | 'countup' | null\n is_pinned: number | boolean | null\n sort_order: number | null\n created_at: string | null\n updated_at: string | null\n}\n\nexport interface EventDTO {\n id: string\n title: string\n target_date: string\n note: string\n category: string\n direction: 'countdown' | 'countup'\n is_pinned: boolean\n sort_order: number\n created_at: string\n updated_at: string\n days: number\n}\n\nexport function toDTO(row: EventRow): EventDTO {\n const direction = row.direction === 'countup' ? 'countup' : 'countdown'\n const dto: EventDTO = {\n id: row.id,\n title: row.title,\n target_date: row.target_date,\n note: row.note ?? '',\n category: row.category ?? 'other',\n direction,\n is_pinned: !!row.is_pinned,\n sort_order: row.sort_order ?? 0,\n created_at: row.created_at ?? '',\n updated_at: row.updated_at ?? '',\n days: 0,\n }\n dto.days = calcDays(row.target_date, direction)\n return dto\n}\n\n/** \u5217\u8868\u6392\u5E8F\uFF1A\u7F6E\u9876\u5728\u524D\uFF0C\u5176\u4F59\u6309\u5269\u4F59\u5929\u6570\u5347\u5E8F\uFF08\u8FC7\u53BB\u7684\u5929\u6570\u6392\u5728\u6700\u540E\uFF09 */\nexport function sortEvents<T extends EventDTO>(events: T[]): T[] {\n return events.toSorted((a, b) => {\n if (a.is_pinned !== b.is_pinned) return a.is_pinned ? -1 : 1\n const da = a.days >= 0 ? a.days : Number.MAX_SAFE_INTEGER\n const db = b.days >= 0 ? b.days : Number.MAX_SAFE_INTEGER\n return da - db\n })\n}\n" },
7552
+ { path: "functions/_shared/response.ts", content: "/**\n * \u7EDF\u4E00\u54CD\u5E94\u4FE1\u5C01\uFF08\u9010\u884C\u79FB\u690D\u81EA cf-backend src/lib/response.ts\uFF0C\u8F93\u51FA adep `__adepHttp` \u4FE1\u5C01\uFF09\u3002\n *\n * \u5C0F\u7A0B\u5E8F\u540E\u7AEF\u7528 Hono \u8FD4\u56DE `{code, data, message}` + HTTP \u72B6\u6001\uFF1Badep \u4E91\u51FD\u6570\u8FD4\u56DE\n * `__adepHttp` \u4FE1\u5C01\u5373\u53EF\u83B7\u5F97\u540C\u6837\u7684 status/body \u8BED\u4E49\uFF08ADR-0024 / CF-010\uFF0C\n * \u672C\u5730 dev \u4E0E\u5E73\u53F0\u7F51\u5173\u540C\u4E00\u5957\u8BC6\u522B\u903B\u8F91\uFF09\u3002\n */\nimport type { AdepHttpEnvelope } from '@adep/types'\n\nexport const CODE = {\n OK: 0,\n NO_AUTH: 40001,\n BAD_PARAM: 40002,\n NOT_FOUND: 40003,\n LIMIT_EXCEEDED: 40004,\n SERVER_ERROR: 50000,\n} as const\n\nexport type CodeValue = (typeof CODE)[keyof typeof CODE]\n\nconst HTTP_STATUS: Record<number, number> = {\n 0: 200,\n 40001: 401,\n 40002: 400,\n 40003: 404,\n 40004: 409,\n 50000: 500,\n}\n\nexport interface ApiBody<T> {\n code: number\n data: T | null\n message: string\n}\n\nconst JSON_HEADERS = { 'content-type': 'application/json; charset=utf-8' }\n\nfunction envelope<T>(body: ApiBody<T>, status: number): AdepHttpEnvelope {\n return {\n __adepHttp: {\n status,\n headers: JSON_HEADERS,\n body,\n },\n }\n}\n\nexport function ok<T>(data: T, message = 'ok'): AdepHttpEnvelope {\n return envelope({ code: CODE.OK, data, message }, 200)\n}\n\nexport function fail(code: CodeValue, message: string): AdepHttpEnvelope {\n return envelope({ code, data: null, message }, HTTP_STATUS[code] ?? 400)\n}\n" },
7553
+ { path: "functions/_shared/route.ts", content: "/**\n * \u8DEF\u5F84\u5F52\u4E00\u5316\uFF1A\u5E73\u53F0\u7F51\u5173 / CLI dev \u4F20\u7ED9\u51FD\u6570\u7684 ctx.path \u662F\u5B8C\u6574 pathname\uFF08\u542B\n * functions_prefix /api\uFF0C\u89C1 AGENTS.md v1.57 \u8FB9\u754C\u8BF4\u660E\uFF09\u3002\u8FD9\u91CC\u5265\u6389\u524D\u7F00\uFF0C\u51FD\u6570\u5185\n * \u4E00\u5F8B\u6309\u300C\u524D\u7F00\u540E\u8DEF\u5F84\u300D\u5339\u914D\uFF08/events\u3001/auth/login \u7B49\uFF09\u3002\n */\nexport function routePath(ctxPath: string): string {\n if (ctxPath === '/api') return '/'\n if (ctxPath.startsWith('/api/')) return ctxPath.slice(4)\n return ctxPath\n}\n" },
7554
+ { path: "functions/_shared/store.ts", content: "/**\n * \u6570\u636E\u8BBF\u95EE\u5C42\uFF08\u79FB\u690D\u81EA cf-backend src/db/queries.ts + src/db/client.ts\uFF09\u3002\n *\n * \u8FC1\u79FB\u70B9\uFF1ACloudflare D1 \u7684 prepare().bind().run()/all() \u2192 adep `cloud.db` \u94FE\u5F0F\n * builder\uFF08\u7B49\u4EF7 SQLite \u5B50\u96C6\uFF1Bsim \u5F15\u64CE\u4E0E\u5E73\u53F0\u9879\u76EE\u5E93\u540C\u4E00\u5957\u5B9E\u73B0\uFF0C\u89C1\n * `packages/runtime/src/database/builder`\uFF09\u3002\u8868\u7ED3\u6784\u7531 functions/schema.sql \u58F0\u660E\uFF0C\n * \u90E8\u7F72\u524D\u7528 `adep db migrate` \u5EFA\u8868\uFF1B\u672C\u5730 dev\uFF08adep dev / vite \u63D2\u4EF6\uFF09\u7684 sim \u5F15\u64CE\n * \u8BBF\u95EE\u65F6\u4F1A\u81EA\u52A8\u5EFA\u8868\u3002\n */\nimport type { CloudDb, SqlValue } from '@adep/types'\n\n/** users \u8868\u884C\uFF08\u8BFB\u51FA\u7684\u884C\u5B57\u6BB5\u53EF\u7A7A\uFF0C\u5199\u65F6\u8865\u5168\uFF09 */\nexport interface UserRow {\n id: string\n device_id: string\n nickname: string | null\n avatar_url: string | null\n created_at: string | null\n updated_at: string | null\n}\n\n/** events \u8868\u884C\uFF08\u8BFB\u51FA\u7684\u884C\u5B57\u6BB5\u53EF\u7A7A\uFF0C\u5199\u65F6\u8865\u5168\uFF09 */\nexport interface EventRow {\n id: string\n user_id: string\n title: string\n target_date: string\n note: string | null\n category: string | null\n direction: 'countdown' | 'countup' | null\n is_pinned: number | boolean | null\n sort_order: number | null\n created_at: string | null\n updated_at: string | null\n}\n\n/** \u884C\u5BF9\u8C61 \u2192 builder \u53EF\u63A5\u53D7\u7684 SqlValue \u8BB0\u5F55\uFF08\u7ED3\u6784\u517C\u5BB9\u5373\u53EF\uFF0C\u5185\u90E8\u90FD\u662F\u57FA\u672C\u7C7B\u578B\uFF09\u3002 */\nfunction sql(row: Record<string, unknown>): Record<string, SqlValue> {\n return row as unknown as Record<string, SqlValue>\n}\n\n/** \u65B0\u5EFA\u7528\u6237\u884C\u7684\u5B8C\u6574\u5F62\u72B6\uFF08\u5199\u5165\u4E13\u7528\uFF09\u3002 */\nexport interface NewUserRow {\n id: string\n device_id: string\n nickname: string\n avatar_url: string\n created_at: string\n updated_at: string\n}\n\n/** \u65B0\u5EFA\u4E8B\u4EF6\u884C\u7684\u5B8C\u6574\u5F62\u72B6\uFF08\u5199\u5165\u4E13\u7528\uFF09\u3002 */\nexport interface NewEventRow {\n id: string\n user_id: string\n title: string\n target_date: string\n note: string\n category: string\n direction: 'countdown' | 'countup'\n is_pinned: 0 | 1\n sort_order: number\n created_at: string\n updated_at: string\n}\n\n// ---------- users ----------\n\nexport async function findUserByDeviceId(db: CloudDb, deviceId: string): Promise<UserRow | null> {\n const row = await db.table('users').where('device_id', deviceId).first()\n return (row as unknown as UserRow | undefined) ?? null\n}\n\nexport async function findUserById(db: CloudDb, id: string): Promise<UserRow | null> {\n const row = await db.table('users').where('id', id).first()\n return (row as unknown as UserRow | undefined) ?? null\n}\n\nexport async function createUser(db: CloudDb, row: NewUserRow): Promise<void> {\n await db.table('users').insert(sql(row as unknown as Record<string, unknown>))\n}\n\n// ---------- events ----------\n\nexport async function listEvents(db: CloudDb, userId: string): Promise<EventRow[]> {\n const rows = await db.table('events').where('user_id', userId).get()\n return (rows as unknown as EventRow[]) ?? []\n}\n\nexport async function findEventById(\n db: CloudDb,\n id: string,\n userId: string\n): Promise<EventRow | null> {\n const row = await db.table('events').where('id', id).where('user_id', userId).first()\n return (row as unknown as EventRow | undefined) ?? null\n}\n\nexport async function countEvents(db: CloudDb, userId: string): Promise<number> {\n // cloud.db \u7684 count() \u8FD4\u56DE number\uFF08builder \u5185\u90E8\u53D6 SELECT count(*) AS n \u7684 n\uFF09\n const n = await db.table('events').where('user_id', userId).count()\n return typeof n === 'number' ? n : Number(n ?? 0)\n}\n\nexport async function insertEvent(db: CloudDb, row: NewEventRow): Promise<void> {\n await db.table('events').insert(sql(row as unknown as Record<string, unknown>))\n}\n\nexport async function updateEvent(\n db: CloudDb,\n id: string,\n userId: string,\n patch: Partial<NewEventRow>\n): Promise<void> {\n await db\n .table('events')\n .where('id', id)\n .where('user_id', userId)\n .update(sql(patch as unknown as Record<string, unknown>))\n}\n\nexport async function deleteEvent(db: CloudDb, id: string, userId: string): Promise<void> {\n await db.table('events').where('id', id).where('user_id', userId).delete()\n}\n" },
7555
+ { path: "functions/auth.ts", content: "/**\n * \u4E91\u51FD\u6570\uFF1Aauth \u2014\u2014 \u767B\u5F55 / \u5F53\u524D\u7528\u6237\uFF08\u79FB\u690D\u81EA cf-backend src/routes/auth.ts\uFF09\u3002\n *\n * \u8DEF\u7531\uFF08\u5F52\u4E00\u5316\u540E\uFF0C\u524D\u7F00 /api \u7531 _shared/route.ts \u5265\u9664\uFF09\uFF1A\n * POST /auth/login { deviceId } \u2192 \u53D6/\u5EFA\u7528\u6237\uFF0C\u8FD4\u56DE { token, expires_in, user }\n * GET /auth/me \u2192 \u5F53\u524D\u7528\u6237 + \u4E8B\u4EF6\u8BA1\u6570\uFF08Bearer device_id\uFF09\n */\nimport type { FunctionContext } from '@adep/types'\nimport { ok, fail, CODE } from './_shared/response'\nimport {\n findUserByDeviceId,\n findUserById,\n createUser,\n countEvents,\n type NewUserRow,\n} from './_shared/store'\nimport { requireUser, isEnvelope } from './_shared/auth'\nimport { routePath } from './_shared/route'\nimport { nowIso } from './_shared/date'\n\ninterface UserDTO {\n id: string\n nickname: string\n avatar_url: string\n created_at: string\n}\n\nfunction toUserDTO(row: {\n id: string\n nickname: string | null\n avatar_url: string | null\n created_at: string | null\n}): UserDTO {\n return {\n id: row.id,\n nickname: row.nickname ?? '\u672C\u5730\u7528\u6237',\n avatar_url: row.avatar_url ?? '',\n created_at: row.created_at ?? '',\n }\n}\n\nexport default async function handle(ctx: FunctionContext) {\n const db = ctx.cloud.db\n if (!db) return fail(CODE.SERVER_ERROR, '\u6570\u636E\u5E93\u672A\u5C31\u7EEA\uFF0C\u8BF7\u5148\u6267\u884C adep db start \u5E76\u5EFA\u8868')\n\n const path = routePath(ctx.path)\n const method = String(ctx.method ?? 'GET').toUpperCase()\n\n // POST /auth/login \u2014\u2014 \u8BBE\u5907\u8EAB\u4EFD\u6362\u767B\u5F55\u6001\uFF08\u7B49\u4EF7 wx.login \u2192 code2Session\uFF09\n if (path === '/auth/login' && method === 'POST') {\n const body = (ctx.body ?? {}) as Record<string, unknown>\n const deviceId = typeof body.deviceId === 'string' ? body.deviceId.trim() : ''\n if (!deviceId || deviceId.length < 8 || deviceId.length > 128) {\n return fail(CODE.BAD_PARAM, 'deviceId \u53C2\u6570\u4E0D\u5408\u6CD5')\n }\n\n try {\n let user = await findUserByDeviceId(db, deviceId)\n if (!user) {\n const fresh: NewUserRow = {\n id: crypto.randomUUID(),\n device_id: deviceId,\n nickname: '\u672C\u5730\u7528\u6237',\n avatar_url: '',\n created_at: nowIso(),\n updated_at: nowIso(),\n }\n await createUser(db, fresh)\n user = fresh\n }\n return ok({\n token: deviceId, // \u8BBE\u5907\u8EAB\u4EFD\u5373 Bearer \u51ED\u636E\uFF08\u7B49\u4EF7 API Key\uFF1B\u524D\u7AEF\u6301\u4E45\u5316\u4E8E localStorage\uFF09\n expires_in: 7 * 24 * 3600,\n user: toUserDTO(user),\n })\n } catch (err) {\n console.error('[auth/login]', err)\n return fail(CODE.SERVER_ERROR, '\u767B\u5F55\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u91CD\u8BD5')\n }\n }\n\n // GET /auth/me \u2014\u2014 \u5F53\u524D\u7528\u6237 + \u4E8B\u4EF6\u8BA1\u6570\n if (path === '/auth/me' && method === 'GET') {\n const authed = await requireUser(db, ctx)\n if (isEnvelope(authed)) return authed\n const { user } = authed\n const eventCount = await countEvents(db, user.id)\n const fresh = (await findUserById(db, user.id)) ?? user\n return ok({\n user: toUserDTO(fresh),\n event_count: eventCount,\n max_events: 200,\n })\n }\n\n return fail(CODE.NOT_FOUND, '\u63A5\u53E3\u4E0D\u5B58\u5728')\n}\n" },
7556
+ { path: "functions/events.ts", content: "/**\n * \u4E91\u51FD\u6570\uFF1Aevents \u2014\u2014 \u4E8B\u4EF6 CRUD\uFF08\u79FB\u690D\u81EA cf-backend src/routes/events.ts\uFF09\u3002\n *\n * \u8DEF\u7531\uFF08\u5F52\u4E00\u5316\u540E\uFF09\uFF1A\n * GET /events?category=xxx \u2192 { events }\uFF08\u7F6E\u9876\u4F18\u5148\uFF0C\u5176\u4F59\u6309\u5269\u4F59\u5929\u6570\u5347\u5E8F\uFF09\n * POST /events \u2192 \u521B\u5EFA\uFF08\u4E0A\u9650 200 \u4E2A\uFF09\n * GET /events/:id \u2192 { event }\n * PUT /events/:id \u2192 \u5C40\u90E8\u66F4\u65B0\n * DELETE /events/:id \u2192 \u5220\u9664\n * \u9274\u6743\uFF1ABearer device_id\uFF08\u89C1 _shared/auth.ts\uFF09\u3002\n */\nimport type { FunctionContext } from '@adep/types'\nimport { ok, fail, CODE } from './_shared/response'\nimport { requireUser, isEnvelope } from './_shared/auth'\nimport { routePath } from './_shared/route'\nimport { isValidDate, toDTO, sortEvents, nowIso, type EventRow } from './_shared/date'\nimport {\n findEventById,\n insertEvent,\n updateEvent,\n deleteEvent,\n listEvents,\n countEvents,\n type NewEventRow,\n} from './_shared/store'\n\nconst CATEGORIES = new Set(['other', 'birthday', 'anniversary', 'exam', 'holiday'])\nconst MAX_EVENTS = 200\n\nfunction normalizeBody<T>(ctx: FunctionContext): T {\n if (ctx.body && typeof ctx.body === 'object') return ctx.body as T\n return {} as T\n}\n\nexport default async function handle(ctx: FunctionContext) {\n const db = ctx.cloud.db\n if (!db) return fail(CODE.SERVER_ERROR, '\u6570\u636E\u5E93\u672A\u5C31\u7EEA\uFF0C\u8BF7\u5148\u6267\u884C adep db start \u5E76\u5EFA\u8868')\n\n const authed = await requireUser(db, ctx)\n if (isEnvelope(authed)) return authed\n const { user } = authed\n\n const path = routePath(ctx.path)\n const method = String(ctx.method ?? 'GET').toUpperCase()\n\n // GET /events\n if (path === '/events' && method === 'GET') {\n const category = ctx.query?.category\n let events = (await listEvents(db, user.id)).map((row) => toDTO(row as unknown as EventRow))\n if (typeof category === 'string' && category && CATEGORIES.has(category)) {\n events = events.filter((e) => e.category === category)\n }\n return ok({ events: sortEvents(events) })\n }\n\n // POST /events\n if (path === '/events' && method === 'POST') {\n const body = normalizeBody<{\n title?: unknown\n target_date?: unknown\n note?: unknown\n category?: unknown\n direction?: unknown\n is_pinned?: unknown\n }>(ctx)\n\n const title = typeof body.title === 'string' ? body.title.trim() : ''\n const target_date = typeof body.target_date === 'string' ? body.target_date : ''\n const note = typeof body.note === 'string' ? body.note.trim() : ''\n const category = typeof body.category === 'string' ? body.category : 'other'\n const direction = body.direction === 'countup' ? 'countup' : 'countdown'\n const is_pinned = !!body.is_pinned\n\n if (!title) return fail(CODE.BAD_PARAM, '\u8BF7\u586B\u5199\u4E8B\u4EF6\u540D\u79F0')\n if (title.length > 50) return fail(CODE.BAD_PARAM, '\u540D\u79F0\u4E0D\u80FD\u8D85\u8FC7 50 \u4E2A\u5B57')\n if (!isValidDate(target_date)) return fail(CODE.BAD_PARAM, '\u8BF7\u9009\u62E9\u76EE\u6807\u65E5\u671F')\n if (note.length > 200) return fail(CODE.BAD_PARAM, '\u5907\u6CE8\u4E0D\u80FD\u8D85\u8FC7 200 \u4E2A\u5B57')\n if (!CATEGORIES.has(category)) return fail(CODE.BAD_PARAM, '\u5206\u7C7B\u4E0D\u5408\u6CD5')\n\n const count = await countEvents(db, user.id)\n if (count >= MAX_EVENTS) return fail(CODE.LIMIT_EXCEEDED, `\u6700\u591A\u521B\u5EFA ${MAX_EVENTS} \u4E2A\u4E8B\u4EF6`)\n\n const now = nowIso()\n const event: NewEventRow = {\n id: crypto.randomUUID(),\n user_id: user.id,\n title,\n target_date,\n note,\n category,\n direction,\n is_pinned: is_pinned ? 1 : 0,\n sort_order: 0,\n created_at: now,\n updated_at: now,\n }\n await insertEvent(db, event)\n return ok({ event: toDTO(event) }, '\u521B\u5EFA\u6210\u529F')\n }\n\n // GET /events/:id\n const matchId = /^\\/events\\/([^/]+)$/.exec(path)\n if (matchId && method === 'GET') {\n const event = await findEventById(db, matchId[1], user.id)\n if (!event) return fail(CODE.NOT_FOUND, '\u4E8B\u4EF6\u4E0D\u5B58\u5728')\n return ok({ event: toDTO(event as unknown as EventRow) })\n }\n\n // PUT /events/:id\n if (matchId && method === 'PUT') {\n const event = await findEventById(db, matchId[1], user.id)\n if (!event) return fail(CODE.NOT_FOUND, '\u4E8B\u4EF6\u4E0D\u5B58\u5728')\n\n const body = normalizeBody<{\n title?: unknown\n target_date?: unknown\n note?: unknown\n category?: unknown\n direction?: unknown\n is_pinned?: unknown\n }>(ctx)\n\n const patch: Partial<NewEventRow> = {}\n const { title, target_date, note, category, direction, is_pinned } = body\n if (title !== undefined) {\n if (typeof title !== 'string' || !title.trim()) return fail(CODE.BAD_PARAM, '\u8BF7\u586B\u5199\u4E8B\u4EF6\u540D\u79F0')\n if (title.trim().length > 50) return fail(CODE.BAD_PARAM, '\u540D\u79F0\u4E0D\u80FD\u8D85\u8FC7 50 \u4E2A\u5B57')\n patch.title = title.trim()\n }\n if (target_date !== undefined) {\n if (typeof target_date !== 'string' || !isValidDate(target_date)) {\n return fail(CODE.BAD_PARAM, '\u8BF7\u9009\u62E9\u76EE\u6807\u65E5\u671F')\n }\n patch.target_date = target_date\n }\n if (note !== undefined) {\n if (typeof note !== 'string') return fail(CODE.BAD_PARAM, '\u5907\u6CE8\u683C\u5F0F\u4E0D\u5408\u6CD5')\n if (note.trim().length > 200) return fail(CODE.BAD_PARAM, '\u5907\u6CE8\u4E0D\u80FD\u8D85\u8FC7 200 \u4E2A\u5B57')\n patch.note = note.trim()\n }\n if (category !== undefined) {\n if (typeof category !== 'string' || !CATEGORIES.has(category)) {\n return fail(CODE.BAD_PARAM, '\u5206\u7C7B\u4E0D\u5408\u6CD5')\n }\n patch.category = category\n }\n if (direction !== undefined) {\n if (direction !== 'countdown' && direction !== 'countup') {\n return fail(CODE.BAD_PARAM, '\u8BA1\u65F6\u65B9\u5F0F\u4E0D\u5408\u6CD5')\n }\n patch.direction = direction\n }\n if (is_pinned !== undefined) {\n patch.is_pinned = is_pinned ? 1 : 0\n }\n\n patch.updated_at = nowIso()\n await updateEvent(db, event.id, user.id, patch)\n const fresh = await findEventById(db, event.id, user.id)\n return ok({ event: toDTO((fresh ?? event) as unknown as EventRow) }, '\u5DF2\u4FDD\u5B58')\n }\n\n // DELETE /events/:id\n if (matchId && method === 'DELETE') {\n const event = await findEventById(db, matchId[1], user.id)\n if (!event) return fail(CODE.NOT_FOUND, '\u4E8B\u4EF6\u4E0D\u5B58\u5728')\n await deleteEvent(db, event.id, user.id)\n return ok({ id: event.id }, '\u5DF2\u5220\u9664')\n }\n\n return fail(CODE.NOT_FOUND, '\u63A5\u53E3\u4E0D\u5B58\u5728')\n}\n" },
7557
+ { path: "functions/schema.sql", content: "-- \u5012\u6570\u65E5\u5E94\u7528\u6570\u636E\u5E93 Schema\uFF08\u79FB\u690D\u81EA cf-backend migrations/0001_initial.sql\uFF09\u3002\n-- \u7528\u6CD5\uFF1Aadep db migrate functions/schema.sql\uFF08\u5E73\u53F0\u4FA7\u5EFA\u8868\uFF09\uFF1B\n-- \u672C\u5730 dev\uFF08adep dev / vite \u63D2\u4EF6\uFF09\u7684 sim \u5F15\u64CE\u8BBF\u95EE\u65F6\u81EA\u52A8\u5EFA\u8868\uFF0C\u65E0\u9700\u624B\u52A8\u6267\u884C\u3002\n-- \u6BCF\u884C\u4E00\u6761\u8BED\u53E5\uFF0C\u4EE5\u5206\u53F7\u7ED3\u5C3E\uFF08migrate \u6309\u884C\u62C6\u5206\u9010\u6761\u6267\u884C\uFF09\u3002\n\nCREATE TABLE IF NOT EXISTS users (\n id TEXT PRIMARY KEY,\n device_id TEXT NOT NULL UNIQUE,\n nickname TEXT NOT NULL DEFAULT '\u672C\u5730\u7528\u6237',\n avatar_url TEXT NOT NULL DEFAULT '',\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\n\nCREATE TABLE IF NOT EXISTS events (\n id TEXT PRIMARY KEY,\n user_id TEXT NOT NULL,\n title TEXT NOT NULL,\n target_date TEXT NOT NULL,\n note TEXT NOT NULL DEFAULT '',\n category TEXT NOT NULL DEFAULT 'other',\n direction TEXT NOT NULL DEFAULT 'countdown',\n is_pinned INTEGER NOT NULL DEFAULT 0,\n sort_order INTEGER NOT NULL DEFAULT 0,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL\n);\n\nCREATE INDEX IF NOT EXISTS idx_events_user ON events(user_id);\nCREATE INDEX IF NOT EXISTS idx_users_device ON users(device_id);\n" },
7558
+ { path: "web/index.html", content: `<!doctype html>
7559
+ <html lang="zh-CN">
7560
+ <head>
7561
+ <meta charset="UTF-8" />
7562
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
7563
+ <meta name="theme-color" content="#3B6EF6" />
7564
+ <link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Crect width='100' height='100' rx='22' fill='%233B6EF6'/%3E%3Ctext x='50' y='68' font-size='52' font-family='PingFang SC,sans-serif' font-weight='700' fill='%23fff' text-anchor='middle'%3E\u65E5%3C/text%3E%3C/svg%3E" />
7565
+ <title>\u5012\u6570\u65E5</title>
7566
+ </head>
7567
+ <body>
7568
+ <div id="app"></div>
7569
+ <script type="module" src="/src/main.ts"></script>
7570
+ </body>
7571
+ </html>
7572
+ ` },
7573
+ { path: "web/package.json", content: '{\n "name": "countdown-web",\n "version": "0.1.0",\n "private": true,\n "type": "module",\n "description": "\u5012\u6570\u65E5\u524D\u7AEF\uFF08\u5FAE\u4FE1\u5C0F\u7A0B\u5E8F\u79FB\u690D\uFF0CVue 3 + Vite\uFF09",\n "scripts": {\n "dev": "vite",\n "build": "vite build",\n "preview": "vite preview"\n },\n "dependencies": {\n "vue": "^3.5.42",\n "vue-router": "^5.0.0"\n },\n "devDependencies": {\n "@adep/cli": "^0.1.4",\n "@vitejs/plugin-vue": "^6.0.0",\n "esbuild-wasm": "^0.25.12",\n "typescript": "5.9.3",\n "vite": "^7.0.0"\n }\n}\n' },
7574
+ { path: "web/src/App.vue", content: `<script setup lang="ts">
7575
+ // \u5E94\u7528\u6839\uFF1A\u8DEF\u7531\u89C6\u56FE + \u5E95\u90E8 TabBar\uFF08\u9996\u9875/\u6211\u7684\u663E\u793A\uFF0C\u7F16\u8F91\u9875\u9690\u85CF\uFF09+ \u6D6E\u5C42\u5BBF\u4E3B\u3002
7576
+ import { computed } from 'vue'
7577
+ import { useRoute } from 'vue-router'
7578
+ import TabBar from './ui/TabBar.vue'
7579
+ import UiHost from './ui/UiHost.vue'
7580
+
7581
+ const route = useRoute()
7582
+
7583
+ const showTabBar = computed(() => {
7584
+ return !route.path.startsWith('/edit')
7585
+ })
7586
+ </script>
7587
+
7588
+ <template>
7589
+ <div class="app">
7590
+ <router-view />
7591
+ <TabBar v-if="showTabBar" />
7592
+ <UiHost />
7593
+ </div>
7594
+ </template>
7595
+
7596
+ <style>
7597
+ @import './styles/app.css';
7598
+ </style>
7599
+ ` },
7600
+ { path: "web/src/env.d.ts", content: `/// <reference types="vite/client" />
7601
+
7602
+ declare module '*.vue' {
7603
+ import type { DefineComponent } from 'vue'
7604
+ const component: DefineComponent<Record<string, never>, Record<string, never>, unknown>
7605
+ export default component
7606
+ }
7607
+ ` },
7608
+ { path: "web/src/lib/auth.ts", content: "/**\n * \u767B\u5F55\u72B6\u6001\u7BA1\u7406\uFF08\u79FB\u690D\u81EA\u5C0F\u7A0B\u5E8F utils/auth.js\uFF09\u3002\n *\n * \u6D41\u7A0B\uFF1AensureLogin() \u2192 \u6709 token \u76F4\u63A5\u8FD4\u56DE\u7F13\u5B58 user\uFF1B\u5426\u5219\u62FF\u8BBE\u5907\u8EAB\u4EFD device_id\n * POST /auth/login \u6362 token + user\uFF08\u7B49\u4EF7\u5C0F\u7A0B\u5E8F\u7684 wx.login \u2192 code2Session\uFF09\u3002\n * \u5E76\u53D1\u53EA\u53D1\u4E00\u6B21\u767B\u5F55\u8BF7\u6C42\uFF08loginPromise \u590D\u7528\uFF09\u3002\n */\nimport { request } from './request'\nimport { getDeviceId, resetDeviceId } from './device'\nimport { getToken, setToken, clearToken, TOKEN_KEY } from './request'\n\nconst USER_KEY = 'countdown_user'\n\nexport interface AppUser {\n id: string\n nickname: string\n avatar_url: string\n created_at: string\n}\n\nexport interface LoginResult {\n token: string\n expires_in: number\n user: AppUser\n}\n\nlet loginPromise: Promise<AppUser> | null = null\n\nexport function getUser(): AppUser | null {\n try {\n const raw = localStorage.getItem(USER_KEY)\n return raw ? (JSON.parse(raw) as AppUser) : null\n } catch {\n return null\n }\n}\n\nfunction saveUser(user: AppUser): void {\n localStorage.setItem(USER_KEY, JSON.stringify(user))\n}\n\nexport function clearSession(): void {\n clearToken()\n localStorage.removeItem(USER_KEY)\n}\n\n/** \u767B\u5F55\uFF1A\u8BBE\u5907\u8EAB\u4EFD \u2192 \u4E91\u51FD\u6570\u5EFA/\u53D6\u7528\u6237 \u2192 \u843D token + user\u3002 */\nexport async function login(): Promise<AppUser> {\n const deviceId = getDeviceId()\n const data = await request<LoginResult>('/api/auth/login', {\n method: 'POST',\n data: { deviceId },\n retry: false,\n })\n setToken(data.token)\n saveUser(data.user)\n return data.user\n}\n\n/** \u6709 token \u76F4\u63A5\u8FD4\u56DE\uFF0C\u5426\u5219\u767B\u5F55\uFF1B\u5E76\u53D1\u53EA\u53D1\u4E00\u6B21\u8BF7\u6C42\u3002 */\nexport function ensureLogin(): Promise<AppUser | null> {\n if (getToken()) return Promise.resolve(getUser())\n if (loginPromise) return loginPromise\n loginPromise = login().then(\n (user) => {\n loginPromise = null\n return user\n },\n (err: unknown) => {\n loginPromise = null\n throw err\n }\n )\n return loginPromise\n}\n\n/** \u9000\u51FA\u767B\u5F55\uFF1A\u6E05\u9664\u672C\u5730\u767B\u5F55\u6001\u4E0E\u4E8B\u4EF6\u7F13\u5B58\uFF1B\u4E0B\u6B21\u6253\u5F00\u81EA\u52A8\u7528\u65B0\u8EAB\u4EFD\u91CD\u65B0\u767B\u5F55\uFF08\u4E91\u7AEF\u6570\u636E\u4FDD\u7559\uFF09\u3002 */\nexport function logout(): void {\n clearSession()\n resetDeviceId()\n}\n\nexport { getToken, TOKEN_KEY }\n" },
7609
+ { path: "web/src/lib/config.ts", content: "/**\n * \u5168\u5C40\u914D\u7F6E\uFF08\u79FB\u690D\u81EA\u5C0F\u7A0B\u5E8F utils/config.js\uFF09\u3002\n *\n * BASE_URL \u4E3A ''\uFF1A\u4E0E\u9875\u9762\u540C\u6E90\u3002\u672C\u5730\u5F00\u53D1\u7531 @adep/cli/vite \u4EE3\u7406 /api/* \u5230\u6A21\u62DF\u8FD0\u884C\u65F6\uFF1B\n * \u90E8\u7F72\u540E\u7531\u5E73\u53F0\u7F51\u5173\u628A /api/* \u8DEF\u7531\u5230\u4E91\u51FD\u6570\u3002\u4E91\u51FD\u6570\u8DEF\u5F84\u5373 /api/{fn}\uFF08adep.config.ts\n * functions_prefix=/api\uFF09\uFF0C\u6545\u8BF7\u6C42\u7EDF\u4E00\u5199 '/api/...'\u3002\n */\nexport const BASE_URL = ''\n\nexport const MAX_EVENTS = 200\n\nexport const CATEGORIES = [\n { value: 'other', label: '\u65E5\u5E38' },\n { value: 'birthday', label: '\u751F\u65E5' },\n { value: 'anniversary', label: '\u7EAA\u5FF5\u65E5' },\n { value: 'exam', label: '\u8003\u8BD5' },\n { value: 'holiday', label: '\u8282\u65E5' },\n] as const\n\nexport type CategoryValue = (typeof CATEGORIES)[number]['value']\n\nexport const CATEGORY_LABEL: Record<string, string> = {\n other: '\u65E5\u5E38',\n birthday: '\u751F\u65E5',\n anniversary: '\u7EAA\u5FF5\u65E5',\n exam: '\u8003\u8BD5',\n holiday: '\u8282\u65E5',\n}\n\n/** \u4E91\u51FD\u6570\u7EDF\u4E00\u54CD\u5E94\u4FE1\u5C01\uFF08\u79FB\u690D\u81EA cf-backend lib/response.ts\uFF09\u3002 */\nexport interface ApiBody<T> {\n code: number\n data: T | null\n message: string\n}\n\n/** \u4E1A\u52A1\u9519\u8BEF\u7801\uFF08\u4E0E cf-backend \u4E00\u81F4\uFF09\u3002 */\nexport const CODE = {\n OK: 0,\n NO_AUTH: 40001,\n BAD_PARAM: 40002,\n NOT_FOUND: 40003,\n LIMIT_EXCEEDED: 40004,\n SERVER_ERROR: 50000,\n} as const\n" },
7610
+ { path: "web/src/lib/date.ts", content: "/**\n * \u65E5\u671F\u5DE5\u5177\uFF08\u9010\u884C\u79FB\u690D\u81EA\u5C0F\u7A0B\u5E8F utils/date.js\uFF0C\u7B97\u6CD5\u4E0E\u4E91\u51FD\u6570\u4FA7 _shared/date.ts \u4FDD\u6301\u4E00\u81F4\uFF09\u3002\n * \u5929\u6570\u6309\u300C\u672C\u5730\u65F6\u533A\u81EA\u7136\u65E5\u300D\u8BA1\u7B97\uFF1B\u4E91\u51FD\u6570\u4FA7\u6309\u5317\u4EAC\u65F6\u95F4\uFF08UTC+8\uFF09\u8BA1\u7B97\u2014\u2014\u7528\u6237\u5728\u4E2D\u56FD\u5927\u9646\u65F6\u4E24\u8005\u4E00\u81F4\u3002\n */\nimport { CATEGORY_LABEL } from './config'\n\nconst WEEK = ['\u5468\u65E5', '\u5468\u4E00', '\u5468\u4E8C', '\u5468\u4E09', '\u5468\u56DB', '\u5468\u4E94', '\u5468\u516D']\n\n/** \u4ECA\u5929\uFF08\u672C\u5730\u65F6\u533A\uFF09YYYY-MM-DD */\nexport function todayStr(): string {\n const d = new Date()\n return fmt(d.getFullYear(), d.getMonth() + 1, d.getDate())\n}\n\nexport function fmt(y: number, m: number, d: number): string {\n return `${y}-${String(m).padStart(2, '0')}-${String(d).padStart(2, '0')}`\n}\n\nexport function isValidDate(s: unknown): s is string {\n if (typeof s !== 'string' || !/^\\d{4}-\\d{2}-\\d{2}$/.test(s)) return false\n const [y, m, d] = s.split('-').map(Number)\n if (m < 1 || m > 12 || d < 1 || d > 31) return false\n const dt = new Date(y, m - 1, d)\n return dt.getFullYear() === y && dt.getMonth() === m - 1 && dt.getDate() === d\n}\n\n/** \u8DDD\u76EE\u6807\u65E5\u671F\u7684\u81EA\u7136\u65E5\u5DEE\uFF1B\u6B63\u6570\u65E5\u8FD4\u56DE\u7EDD\u5BF9\u503C */\nexport function calcDays(\n targetDate: string,\n direction: 'countdown' | 'countup' = 'countdown'\n): number {\n if (!isValidDate(targetDate)) return 0\n const [y, m, d] = targetDate.split('-').map(Number)\n const target = new Date(y, m - 1, d).getTime()\n const now = new Date()\n const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime()\n const diff = Math.round((target - today) / 86400000)\n return direction === 'countup' ? Math.abs(diff) : diff\n}\n\nexport function weekdayOf(targetDate: string): string {\n if (!isValidDate(targetDate)) return ''\n const [y, m, d] = targetDate.split('-').map(Number)\n return WEEK[new Date(y, m - 1, d).getDay()]\n}\n\n/** 2026-09-13 -> 9\u670813\u65E5 */\nexport function shortLabel(targetDate: string): string {\n if (!isValidDate(targetDate)) return ''\n const [, m, d] = targetDate.split('-').map(Number)\n return `${m}\u6708${d}\u65E5`\n}\n\n/**\n * \u6839\u636E\u5929\u6570\u8FD4\u56DE\u5C55\u793A\u7B49\u7EA7\uFF08\u4E0E\u5C0F\u7A0B\u5E8F\u4E00\u81F4\uFF09\n * normal >30 \u84DD | soon 7-30 \u6A59 | urgent <7 \u7EA2 | today \u5F53\u5929 \u7EA2 | past \u5DF2\u8FC7 \u7070\n */\nexport function levelOf(days: number, direction: 'countdown' | 'countup' = 'countdown'): string {\n if (direction === 'countup') return 'normal'\n if (days < 0) return 'past'\n if (days === 0) return 'today'\n if (days < 7) return 'urgent'\n if (days <= 30) return 'soon'\n return 'normal'\n}\n\n/** \u5929\u6570\u6587\u6848\u4E0E\u524D\u540E\u7F00 */\nexport function daysText(\n days: number,\n direction: 'countdown' | 'countup' = 'countdown'\n): { num: string; prefix: string; suffix: string; tip: string } {\n if (direction === 'countup') {\n return { num: String(days), prefix: '\u5DF2\u7ECF', suffix: '\u5929', tip: '\u5929\u5566' }\n }\n if (days === 0) return { num: '\u4ECA\u5929', prefix: '', suffix: '', tip: '\u5C31\u662F\u4ECA\u5929' }\n if (days > 0) return { num: String(days), prefix: '\u8FD8\u6709', suffix: '\u5929', tip: '' }\n return { num: String(-days), prefix: '\u5DF2\u8FC7', suffix: '\u5929', tip: '' }\n}\n\n/** \u4E8B\u4EF6 DTO\uFF08\u4E91\u51FD\u6570 /events \u8FD4\u56DE\u5E26 days \u7684\u5F62\u72B6\uFF09\u3002 */\nexport interface CountdownEvent {\n id: string\n title: string\n target_date: string\n note: string\n category: string\n direction: 'countdown' | 'countup'\n is_pinned: boolean\n sort_order: number\n created_at: string\n updated_at: string\n days: number\n}\n\n/** \u5217\u8868\u5C55\u793A\u88C5\u9970\uFF1A\u8865\u9F50\u7B49\u7EA7 / \u6587\u6848 / \u526F\u6807\u9898 / \u5206\u7C7B\u540D\uFF08\u79FB\u690D\u81EA index.js decorate\uFF09\u3002 */\nexport function decorateEvent(item: CountdownEvent): DecodedEvent {\n const days =\n typeof item.days === 'number' ? item.days : calcDays(item.target_date, item.direction)\n const t = daysText(days, item.direction)\n return {\n ...item,\n offset: 0,\n days,\n level: levelOf(days, item.direction),\n num: t.num,\n prefix: t.prefix,\n suffix: t.suffix,\n tip: t.tip,\n sub: `${item.target_date} ${weekdayOf(item.target_date)}`,\n categoryLabel: CATEGORY_LABEL[item.category] ?? '\u65E5\u5E38',\n }\n}\n\nexport interface DecodedEvent extends CountdownEvent {\n /** \u5DE6\u6ED1\u504F\u79FB\uFF08px\uFF09\uFF0C\u7528\u4E8E\u5361\u7247\u6ED1\u52A8\u52A8\u753B */\n offset: number\n level: string\n num: string\n prefix: string\n suffix: string\n tip: string\n sub: string\n categoryLabel: string\n}\n" },
7611
+ { path: "web/src/lib/device.ts", content: "/**\n * \u8BBE\u5907\u8EAB\u4EFD\uFF08web \u7248\u7684\u300C\u5FAE\u4FE1 openid\u300D\u7B49\u4EF7\u7269\uFF09\u3002\n *\n * \u5C0F\u7A0B\u5E8F\u9760 wx.login \u2192 openid \u62FF\u5230\u8BBE\u5907\u7EA7\u552F\u4E00\u8EAB\u4EFD\uFF1Bweb \u79FB\u690D\u7528\u6D4F\u89C8\u5668 localStorage \u6301\u4E45\u5316\u7684\n * \u968F\u673A device_id \u627F\u62C5\u540C\u4E00\u89D2\u8272\uFF1A\u9996\u6B21\u8BBF\u95EE\u751F\u6210\u5E76\u843D\u76D8\uFF0C\u4E4B\u540E\u7A33\u5B9A\u590D\u7528\uFF08\u6E05\u7AD9\u70B9\u6570\u636E\u624D\u53D8\uFF09\u3002\n * \u4E91\u51FD\u6570\u628A\u5B83\u5F53 Bearer \u51ED\u636E\uFF08\u89C1 functions/_shared/auth.ts\uFF09\uFF0C\u8BED\u4E49\u7B49\u540C API Key\u3002\n */\nconst DEVICE_KEY = 'countdown_device_id'\n\nexport function getDeviceId(): string {\n let id = localStorage.getItem(DEVICE_KEY)\n if (id) return id\n id = generateDeviceId()\n localStorage.setItem(DEVICE_KEY, id)\n return id\n}\n\n/** \u767B\u51FA\u540E\u91CD\u65B0\u751F\u6210\u8EAB\u4EFD\uFF08\u539F\u8D26\u53F7\u6570\u636E\u4FDD\u7559\u5728\u4E91\u7AEF\uFF0C\u53EA\u662F\u4E0D\u518D\u5173\u8054\u65B0\u8EAB\u4EFD\uFF09\u3002 */\nexport function resetDeviceId(): void {\n localStorage.removeItem(DEVICE_KEY)\n}\n\nfunction generateDeviceId(): string {\n // crypto.randomUUID \u4E0D\u53EF\u7528\u65F6\u515C\u5E95\uFF08\u8001\u6D4F\u89C8\u5668 / \u975E\u5B89\u5168\u4E0A\u4E0B\u6587\uFF09\u3002\n if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {\n return `dev_${crypto.randomUUID()}`\n }\n return `dev_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`\n}\n" },
7612
+ { path: "web/src/lib/request.ts", content: "/**\n * \u7EDF\u4E00\u8BF7\u6C42\u5C01\u88C5\uFF08\u79FB\u690D\u81EA\u5C0F\u7A0B\u5E8F utils/request.js\uFF09\u3002\n *\n * \u5DEE\u5F02\u70B9\uFF1Awx.request \u2192 fetch\uFF1BBASE_URL=''\uFF08\u540C\u6E90 /api/*\uFF09\uFF1Btoken \u4ECE localStorage \u8BFB\u53D6\uFF1B\n * \u9047\u4E1A\u52A1\u7801 40001\uFF08\u672A\u767B\u5F55/\u5931\u6548\uFF09\u2192 \u6E05\u9664 token \u2192 \u9759\u9ED8\u91CD\u65B0\u767B\u5F55\u540E\u91CD\u8BD5\u4E00\u6B21 \u2192 \u4ECD\u5931\u8D25\u629B NEED_LOGIN\u3002\n * \u53EA\u8FD4\u56DE\u4E1A\u52A1 data\uFF08\u54CD\u5E94\u4FE1\u5C01 {code, data, message} \u7684 data\uFF09\uFF0C\u5931\u8D25\u629B Error\u3002\n */\nimport { BASE_URL } from './config'\n\nconst TOKEN_KEY = 'countdown_token'\n\nexport function getToken(): string {\n return localStorage.getItem(TOKEN_KEY) ?? ''\n}\n\nexport function setToken(token: string): void {\n localStorage.setItem(TOKEN_KEY, token)\n}\n\nexport function clearToken(): void {\n localStorage.removeItem(TOKEN_KEY)\n}\n\nexport interface RequestOptions {\n method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'\n data?: unknown\n retry?: boolean\n silent?: boolean\n header?: Record<string, string>\n}\n\ninterface ApiBody<T> {\n code: number\n data: T | null\n message: string\n}\n\nexport class RequestError extends Error {\n code: number\n status: number\n constructor(message: string, code: number, status: number) {\n super(message)\n this.name = 'RequestError'\n this.code = code\n this.status = status\n }\n}\n\n/** \u7279\u6B8A\u9519\u8BEF\uFF1Atoken \u5931\u6548\u4E14\u81EA\u52A8\u91CD\u767B\u5931\u8D25\uFF0C\u8C03\u7528\u65B9\u5E94\u5F15\u5BFC\u91CD\u65B0\u767B\u5F55\u3002 */\nexport const NEED_LOGIN = 'NEED_LOGIN'\n\nasync function doRequest<T>(path: string, options: RequestOptions): Promise<T> {\n const token = getToken()\n const url = BASE_URL + path\n const response = await fetch(url, {\n method: options.method ?? 'GET',\n headers: {\n 'Content-Type': 'application/json',\n ...(token ? { Authorization: `Bearer ${token}` } : {}),\n ...options.header,\n },\n body: options.data === undefined ? undefined : JSON.stringify(options.data),\n })\n\n let body: ApiBody<T>\n try {\n body = (await response.json()) as ApiBody<T>\n } catch {\n throw new RequestError(`\u8BF7\u6C42\u5931\u8D25(${response.status})`, -1, response.status)\n }\n\n if (response.status === 200 && body.code === 0) {\n return body.data as T\n }\n\n if (body.code === 40001) {\n clearToken()\n if (options.retry !== false) {\n // token \u5931\u6548\uFF1A\u9759\u9ED8\u91CD\u65B0\u767B\u5F55\u540E\u91CD\u8BD5\u4E00\u6B21\n const { login } = await import('./auth')\n try {\n await login()\n return doRequest(path, { ...options, retry: false })\n } catch {\n throw new RequestError(NEED_LOGIN, 40001, 401)\n }\n }\n throw new RequestError(NEED_LOGIN, 40001, 401)\n }\n\n const msg = body.message || `\u8BF7\u6C42\u5931\u8D25(${response.status})`\n if (!options.silent) console.warn('[request]', path, msg)\n throw new RequestError(msg, body.code, response.status)\n}\n\nexport function request<T>(path: string, options: RequestOptions = {}): Promise<T> {\n return doRequest<T>(path, options).catch((error: unknown) => {\n // \u7F51\u7EDC\u5F02\u5E38 \u2192 \u4E0E\u5C0F\u7A0B\u5E8F\u4E00\u81F4\u7684\u63D0\u793A\n if (error instanceof TypeError) {\n const e = new RequestError('\u7F51\u7EDC\u5F02\u5E38\uFF0C\u8BF7\u68C0\u67E5\u7F51\u7EDC\u540E\u91CD\u8BD5', -1, 0)\n throw e\n }\n throw error\n })\n}\n\nexport { TOKEN_KEY }\n" },
7613
+ { path: "web/src/main.ts", content: "// web/src/main.ts \u2014\u2014 \u5E94\u7528\u5165\u53E3\uFF08\u79FB\u690D\u81EA\u5C0F\u7A0B\u5E8F app.js\uFF1A\u542F\u52A8\u5373\u9759\u9ED8\u767B\u5F55\uFF09\u3002\nimport { createApp } from 'vue'\nimport App from './App.vue'\nimport router from './router'\nimport { ensureLogin } from './lib/auth'\n\n// \u542F\u52A8\u9759\u9ED8\u767B\u5F55\uFF08\u7B49\u4EF7\u5C0F\u7A0B\u5E8F app.js onLaunch \u7684 ensureLogin()\uFF09\uFF0C\u5931\u8D25\u4E0D\u963B\u585E\u6E32\u67D3\uFF0C\n// \u9875\u9762\u8BF7\u6C42\u65F6\u4F1A\u518D\u6B21 ensureLogin / \u81EA\u52A8\u91CD\u767B\u3002\nvoid ensureLogin().catch((err: unknown) => {\n console.warn('[app] \u81EA\u52A8\u767B\u5F55\u5931\u8D25\uFF0C\u5C06\u5728\u9875\u9762\u91CD\u8BD5', err)\n})\n\ncreateApp(App).use(router).mount('#app')\n" },
7614
+ { path: "web/src/router.ts", content: "import { createRouter, createWebHistory } from 'vue-router'\n\nconst router = createRouter({\n history: createWebHistory(),\n routes: [\n { path: '/', name: 'home', component: () => import('./views/HomeView.vue') },\n { path: '/edit', name: 'edit', component: () => import('./views/EditView.vue') },\n { path: '/edit/:id', name: 'edit-id', component: () => import('./views/EditView.vue') },\n { path: '/profile', name: 'profile', component: () => import('./views/ProfileView.vue') },\n { path: '/:pathMatch(.*)*', redirect: '/' },\n ],\n})\n\nexport default router\n" },
7615
+ { path: "web/src/styles/app.css", content: "/**\n * \u5168\u5C40\u6837\u5F0F\uFF08\u79FB\u690D\u81EA\u5C0F\u7A0B\u5E8F app.wxss + \u5404\u9875 wxss \u7684\u516C\u5171\u57FA\u8C03\uFF09\u3002\n *\n * rpx \u2192 rem \u6362\u7B97\uFF1A\u5C0F\u7A0B\u5E8F\u8BBE\u8BA1\u7A3F\u5BBD 750rpx\u3002web \u7AEF\u628A\u5E94\u7528\u58F3\uFF08.app\uFF09\u9650\u5236\u4E3A 480px \u5BBD\u7684\n * \u624B\u673A\u5F0F\u753B\u5E03\uFF0C\u4EE4 1rem = 100rpx = 64px\uFF08480 / 7.5\uFF09\uFF0C\u5168\u90E8\u539F wxss \u7684 `N rpx` \u6362\u7B97\u4E3A\n * `N/100 rem`\u3002rem \u662F\u76F8\u5BF9\u6839\u5143\u7D20 <html> \u7684\uFF0C\u6545\u57FA\u51C6\u5B57\u53F7\u8BBE\u5728 html \u4E0A\uFF1B\u89C6\u53E3\u7A84\u4E8E\n * 480px \u65F6\u6839\u5B57\u53F7\u968F\u89C6\u53E3\u7F29\u653E\uFF08100rpx = 13.333vw\uFF09\uFF0C\u5C0F\u7A97\u53E3\u4E0B\u6BD4\u4F8B\u4FDD\u6301\u4E00\u81F4\u3002\n */\n:root {\n --primary: #3b6ef6;\n --primary-2: #6a8dff;\n --bg: #f5f7fb;\n --card: #ffffff;\n --text-1: #1f2430;\n --text-2: #6b7385;\n --text-3: #9aa3b2;\n --line: #eef0f5;\n\n --normal: #3b6ef6; /* >30 \u5929 */\n --soon: #ff9f43; /* 7-30 \u5929 */\n --urgent: #ff5a5f; /* <7 \u5929 */\n --past: #a0a8b8; /* \u5DF2\u8FC7\u53BB */\n}\n\nhtml {\n /* 1rem = 100rpx\uFF08480px \u753B\u5E03\u4E0B 1rpx = 0.64px\uFF09\uFF1B\u968F\u89C6\u53E3\u7A84\u4E8E 480px \u7F29\u653E */\n font-size: 64px;\n}\n\n@media (max-width: 479px) {\n html {\n font-size: calc(100vw / 7.5); /* 1rpx = 1vw / 7.5 */\n }\n}\n\n* {\n box-sizing: border-box;\n margin: 0;\n padding: 0;\n}\n\nhtml,\nbody {\n height: 100%;\n}\n\nbody {\n background: #e8ebf1;\n font-family:\n -apple-system,\n BlinkMacSystemFont,\n 'PingFang SC',\n 'Helvetica Neue',\n Helvetica,\n sans-serif;\n -webkit-font-smoothing: antialiased;\n}\n\nbutton,\ninput,\ntextarea {\n font-family: inherit;\n outline: none;\n border: none;\n background: none;\n}\n\n/* \u5E94\u7528\u58F3\uFF1A\u5C45\u4E2D\u624B\u673A\u5F0F\u753B\u5E03\uFF08rem \u57FA\u51C6\u5728 html\uFF0C\u89C1\u6587\u4EF6\u5934\u6CE8\u91CA\uFF09 */\n.app {\n position: relative;\n max-width: 480px;\n height: 100dvh;\n margin: 0 auto;\n overflow: hidden;\n background: var(--bg);\n color: var(--text-1);\n}\n\n/* \u9875\u9762\u6EDA\u52A8\u5BB9\u5668\uFF08\u5404\u89C6\u56FE\u5185\u90E8\u4F7F\u7528\uFF09 */\n.page-scroll {\n position: absolute;\n inset: 0;\n overflow-y: auto;\n -webkit-overflow-scrolling: touch;\n overscroll-behavior: contain;\n}\n\n.page-scroll::-webkit-scrollbar {\n width: 0;\n}\n\n.no-select {\n user-select: none;\n -webkit-user-select: none;\n -webkit-touch-callout: none;\n}\n" },
7616
+ { path: "web/src/ui/TabBar.vue", content: `<script setup lang="ts">
7617
+ // \u81EA\u5B9A\u4E49 TabBar\uFF08\u79FB\u690D\u81EA custom-tab-bar/index.*\uFF09\uFF1A\u9996\u9875 / \uFF0B\uFF08\u65B0\u589E\uFF09/ \u6211\u7684\u3002
7618
+ import { useRoute, useRouter } from 'vue-router'
7619
+
7620
+ const route = useRoute()
7621
+ const router = useRouter()
7622
+
7623
+ const tabs = [
7624
+ { path: '/', text: '\u9996\u9875' },
7625
+ { path: '/profile', text: '\u6211\u7684' },
7626
+ ]
7627
+
7628
+ function selectedIndex(): number {
7629
+ const path = route.path
7630
+ if (path.startsWith('/profile')) return 1
7631
+ return 0
7632
+ }
7633
+
7634
+ function switchTab(path: string): void {
7635
+ if (route.path === path) return
7636
+ router.push(path)
7637
+ }
7638
+
7639
+ function onAdd(): void {
7640
+ router.push('/edit')
7641
+ }
7642
+ </script>
7643
+
7644
+ <template>
7645
+ <div class="tabbar">
7646
+ <div class="tabbar-inner">
7647
+ <div class="tab" :class="{ on: selectedIndex() === 0 }" @click="switchTab('/')">
7648
+ <svg
7649
+ class="icon"
7650
+ viewBox="0 0 24 24"
7651
+ fill="none"
7652
+ :stroke="selectedIndex() === 0 ? '#3B6EF6' : '#9AA3B2'"
7653
+ stroke-width="2"
7654
+ stroke-linecap="round"
7655
+ stroke-linejoin="round"
7656
+ >
7657
+ <path d="M3 10.4 12 3.5l9 6.9" />
7658
+ <path d="M5.5 9.6V20a1 1 0 0 0 1 1H10v-5h4v5h3.5a1 1 0 0 0 1-1V9.6" />
7659
+ </svg>
7660
+ <span class="tab-text">\u9996\u9875</span>
7661
+ </div>
7662
+
7663
+ <div class="plus-wrap" @click="onAdd">
7664
+ <div class="plus">
7665
+ <div class="bar bar-h"></div>
7666
+ <div class="bar bar-v"></div>
7667
+ </div>
7668
+ </div>
7669
+
7670
+ <div class="tab" :class="{ on: selectedIndex() === 1 }" @click="switchTab('/profile')">
7671
+ <svg
7672
+ class="icon"
7673
+ viewBox="0 0 24 24"
7674
+ fill="none"
7675
+ :stroke="selectedIndex() === 1 ? '#3B6EF6' : '#9AA3B2'"
7676
+ stroke-width="2"
7677
+ stroke-linecap="round"
7678
+ stroke-linejoin="round"
7679
+ >
7680
+ <circle cx="12" cy="8" r="3.6" />
7681
+ <path d="M4.8 20a7.2 7.2 0 0 1 14.4 0" />
7682
+ </svg>
7683
+ <span class="tab-text">\u6211\u7684</span>
7684
+ </div>
7685
+ </div>
7686
+ </div>
7687
+ </template>
7688
+
7689
+ <style scoped>
7690
+ .tabbar {
7691
+ position: absolute;
7692
+ left: 0;
7693
+ right: 0;
7694
+ bottom: 0;
7695
+ z-index: 999;
7696
+ background: #ffffff;
7697
+ box-shadow: 0 -0.04rem 0.2rem rgba(31, 36, 48, 0.06);
7698
+ padding-bottom: env(safe-area-inset-bottom);
7699
+ }
7700
+
7701
+ .tabbar-inner {
7702
+ height: 1rem;
7703
+ display: flex;
7704
+ align-items: center;
7705
+ position: relative;
7706
+ }
7707
+
7708
+ .tab {
7709
+ flex: 1;
7710
+ height: 100%;
7711
+ display: flex;
7712
+ flex-direction: column;
7713
+ align-items: center;
7714
+ justify-content: center;
7715
+ cursor: pointer;
7716
+ }
7717
+
7718
+ .tab-text {
7719
+ margin-top: 0.04rem;
7720
+ font-size: 0.22rem;
7721
+ color: #9aa3b2;
7722
+ line-height: 1;
7723
+ }
7724
+
7725
+ .tab.on .tab-text {
7726
+ color: #3b6ef6;
7727
+ font-weight: 500;
7728
+ }
7729
+
7730
+ .icon {
7731
+ width: 0.44rem;
7732
+ height: 0.44rem;
7733
+ }
7734
+
7735
+ .plus-wrap {
7736
+ width: 1.2rem;
7737
+ height: 100%;
7738
+ display: flex;
7739
+ align-items: center;
7740
+ justify-content: center;
7741
+ }
7742
+
7743
+ .plus {
7744
+ width: 0.92rem;
7745
+ height: 0.92rem;
7746
+ margin-top: -0.34rem;
7747
+ border-radius: 50%;
7748
+ background: linear-gradient(135deg, #3b6ef6, #6a8dff);
7749
+ box-shadow: 0 0.08rem 0.2rem rgba(59, 110, 246, 0.35);
7750
+ display: flex;
7751
+ align-items: center;
7752
+ justify-content: center;
7753
+ position: relative;
7754
+ cursor: pointer;
7755
+ transition: transform 0.12s ease;
7756
+ }
7757
+
7758
+ .plus:active {
7759
+ transform: scale(0.94);
7760
+ }
7761
+
7762
+ .bar {
7763
+ position: absolute;
7764
+ background: #ffffff;
7765
+ border-radius: 0.04rem;
7766
+ }
7767
+
7768
+ .bar-h {
7769
+ width: 0.36rem;
7770
+ height: 0.06rem;
7771
+ }
7772
+
7773
+ .bar-v {
7774
+ width: 0.06rem;
7775
+ height: 0.36rem;
7776
+ }
7777
+ </style>
7778
+ ` },
7779
+ { path: "web/src/ui/UiHost.vue", content: `<script setup lang="ts">
7780
+ // \u6D6E\u5C42\u5BBF\u4E3B\uFF1A\u6E32\u67D3 toast / modal / action sheet\uFF08App.vue \u6302\u8F7D\u4E00\u6B21\uFF09\u3002
7781
+ import { useUiState, settleModal, settleSheet, dismissAll } from './ui'
7782
+
7783
+ const state = useUiState()
7784
+ </script>
7785
+
7786
+ <template>
7787
+ <!-- Toast -->
7788
+ <Transition name="toast">
7789
+ <div v-if="state.toast" class="ui-toast">
7790
+ <svg
7791
+ v-if="state.toast.icon === 'success'"
7792
+ class="ui-toast-icon"
7793
+ viewBox="0 0 24 24"
7794
+ fill="none"
7795
+ stroke="currentColor"
7796
+ stroke-width="3"
7797
+ stroke-linecap="round"
7798
+ stroke-linejoin="round"
7799
+ >
7800
+ <path d="M20 6 9 17l-5-5" />
7801
+ </svg>
7802
+ <span>{{ state.toast.title }}</span>
7803
+ </div>
7804
+ </Transition>
7805
+
7806
+ <!-- Modal -->
7807
+ <Transition name="fade">
7808
+ <div v-if="state.modal" class="ui-mask" @click.self="settleModal(false)">
7809
+ <div class="ui-modal">
7810
+ <div class="ui-modal-title">{{ state.modal.title }}</div>
7811
+ <div class="ui-modal-content">{{ state.modal.content }}</div>
7812
+ <div class="ui-modal-actions">
7813
+ <button
7814
+ v-if="state.modal.showCancel"
7815
+ class="ui-modal-btn ui-modal-cancel"
7816
+ @click="settleModal(false)"
7817
+ >
7818
+ {{ state.modal.cancelText }}
7819
+ </button>
7820
+ <button
7821
+ class="ui-modal-btn ui-modal-confirm"
7822
+ :style="{ color: state.modal.confirmColor }"
7823
+ @click="settleModal(true)"
7824
+ >
7825
+ {{ state.modal.confirmText }}
7826
+ </button>
7827
+ </div>
7828
+ </div>
7829
+ </div>
7830
+ </Transition>
7831
+
7832
+ <!-- ActionSheet -->
7833
+ <Transition name="fade">
7834
+ <div v-if="state.sheet" class="ui-mask" @click.self="settleSheet(-1)">
7835
+ <div class="ui-sheet">
7836
+ <div class="ui-sheet-title">\u8BF7\u9009\u62E9</div>
7837
+ <button
7838
+ v-for="(item, index) in state.sheet.itemList"
7839
+ :key="item"
7840
+ class="ui-sheet-item"
7841
+ :style="{ color: state.sheet.itemColor }"
7842
+ @click="settleSheet(index)"
7843
+ >
7844
+ {{ item }}
7845
+ </button>
7846
+ <button class="ui-sheet-cancel" @click="settleSheet(-1)">\u53D6\u6D88</button>
7847
+ </div>
7848
+ </div>
7849
+ </Transition>
7850
+ </template>
7851
+ <style scoped>
7852
+ /* \u4E0E\u5C0F\u7A0B\u5E8F wx.showToast / wx.showModal / wx.showActionSheet \u89C6\u89C9\u5BF9\u9F50 */
7853
+ .ui-toast {
7854
+ position: fixed;
7855
+ left: 50%;
7856
+ top: 45%;
7857
+ transform: translate(-50%, -50%);
7858
+ z-index: 3000;
7859
+ max-width: 70%;
7860
+ padding: 0.24rem 0.36rem;
7861
+ border-radius: 0.16rem;
7862
+ background: rgba(0, 0, 0, 0.78);
7863
+ color: #fff;
7864
+ font-size: 0.28rem;
7865
+ line-height: 1.5;
7866
+ text-align: center;
7867
+ display: flex;
7868
+ flex-direction: column;
7869
+ align-items: center;
7870
+ gap: 0.1rem;
7871
+ pointer-events: none;
7872
+ }
7873
+ .ui-toast-icon {
7874
+ width: 0.4rem;
7875
+ height: 0.4rem;
7876
+ }
7877
+ .ui-mask {
7878
+ position: fixed;
7879
+ inset: 0;
7880
+ z-index: 2000;
7881
+ background: rgba(0, 0, 0, 0.45);
7882
+ display: flex;
7883
+ align-items: center;
7884
+ justify-content: center;
7885
+ }
7886
+ .ui-modal {
7887
+ width: 5.4rem;
7888
+ max-width: 82%;
7889
+ background: #fff;
7890
+ border-radius: 0.24rem;
7891
+ overflow: hidden;
7892
+ padding: 0.4rem 0.32rem 0;
7893
+ }
7894
+ .ui-modal-title {
7895
+ font-size: 0.34rem;
7896
+ font-weight: 600;
7897
+ color: #1f2430;
7898
+ text-align: center;
7899
+ }
7900
+ .ui-modal-content {
7901
+ margin-top: 0.16rem;
7902
+ font-size: 0.28rem;
7903
+ color: #6b7385;
7904
+ line-height: 1.6;
7905
+ text-align: center;
7906
+ word-break: break-word;
7907
+ }
7908
+ .ui-modal-actions {
7909
+ display: flex;
7910
+ margin: 0.32rem -0.32rem 0;
7911
+ border-top: 1px solid #f0f2f7;
7912
+ }
7913
+ .ui-modal-btn {
7914
+ flex: 1;
7915
+ height: 0.92rem;
7916
+ border: none;
7917
+ background: #fff;
7918
+ font-size: 0.3rem;
7919
+ cursor: pointer;
7920
+ }
7921
+ .ui-modal-btn + .ui-modal-btn {
7922
+ border-left: 1px solid #f0f2f7;
7923
+ }
7924
+ .ui-modal-cancel {
7925
+ color: #1f2430;
7926
+ }
7927
+ .ui-modal-confirm {
7928
+ font-weight: 600;
7929
+ }
7930
+ .ui-sheet {
7931
+ position: fixed;
7932
+ left: 0;
7933
+ right: 0;
7934
+ bottom: 0;
7935
+ background: #f5f7fb;
7936
+ padding: 0.08rem 0 calc(0.16rem + env(safe-area-inset-bottom));
7937
+ z-index: 2100;
7938
+ }
7939
+ .ui-sheet-title {
7940
+ text-align: center;
7941
+ font-size: 0.26rem;
7942
+ color: #9aa3b2;
7943
+ padding: 0.16rem 0 0.08rem;
7944
+ }
7945
+ .ui-sheet-item,
7946
+ .ui-sheet-cancel {
7947
+ display: block;
7948
+ width: auto;
7949
+ margin: 0.16rem 0.16rem 0;
7950
+ height: 0.96rem;
7951
+ line-height: 0.96rem;
7952
+ border: none;
7953
+ border-radius: 0.16rem;
7954
+ background: #fff;
7955
+ font-size: 0.3rem;
7956
+ cursor: pointer;
7957
+ text-align: center;
7958
+ }
7959
+ .ui-sheet-cancel {
7960
+ color: #1f2430;
7961
+ }
7962
+ .toast-enter-active,
7963
+ .toast-leave-active,
7964
+ .fade-enter-active,
7965
+ .fade-leave-active {
7966
+ transition: opacity 0.2s ease;
7967
+ }
7968
+ .toast-enter-from,
7969
+ .toast-leave-to,
7970
+ .fade-enter-from,
7971
+ .fade-leave-to {
7972
+ opacity: 0;
7973
+ }
7974
+ </style>
7975
+ ` },
7976
+ { path: "web/src/ui/ui.ts", content: "/**\n * \u8F7B\u91CF UI \u539F\u8BED\uFF08web \u7248 wx.showToast / wx.showModal / wx.showActionSheet\uFF09\u3002\n * \u54CD\u5E94\u5F0F store + App.vue \u91CC\u6302\u4E00\u4E2A UiHost \u7EC4\u4EF6\u6E32\u67D3\u6D6E\u5C42\u3002\n */\nimport { reactive } from 'vue'\n\nexport interface ToastOptions {\n title: string\n /** 'none' = \u65E0\u56FE\u6807\uFF1B'success' = \u5BF9\u52FE\u3002 */\n icon?: 'none' | 'success'\n duration?: number\n}\n\nexport interface ModalOptions {\n title: string\n content: string\n confirmColor?: string\n confirmText?: string\n cancelText?: string\n showCancel?: boolean\n}\n\nlet seq = 0\n\ninterface ToastState {\n id: number\n title: string\n icon: 'none' | 'success'\n}\ninterface ModalState {\n id: number\n title: string\n content: string\n confirmColor: string\n confirmText: string\n cancelText: string\n showCancel: boolean\n resolve: (ok: boolean) => void\n}\ninterface SheetState {\n id: number\n itemList: string[]\n itemColor: string\n resolve: (index: number) => void\n}\n\nconst state = reactive<{\n toast: ToastState | null\n modal: ModalState | null\n sheet: SheetState | null\n}>({\n toast: null,\n modal: null,\n sheet: null,\n})\n\nexport function useUiState() {\n return state\n}\n\n/** \u7C7B wx.showToast\uFF1Bduration \u540E\u81EA\u52A8\u6D88\u5931\u3002 */\nexport function showToast(options: ToastOptions | string): void {\n const opts = typeof options === 'string' ? { title: options } : options\n const id = ++seq\n state.toast = { id, title: opts.title, icon: opts.icon ?? 'none' }\n const duration = opts.duration ?? 2000\n if (duration > 0) {\n setTimeout(() => {\n if (state.toast?.id === id) state.toast = null\n }, duration)\n }\n}\n\n/** \u7C7B wx.showModal\uFF1B\u8FD4\u56DE Promise<boolean>\uFF08confirm \u2192 true\uFF09\u3002 */\nexport function showModal(options: ModalOptions): Promise<boolean> {\n return new Promise((resolve) => {\n state.modal = {\n id: ++seq,\n title: options.title,\n content: options.content,\n confirmColor: options.confirmColor ?? '#3B6EF6',\n confirmText: options.confirmText ?? '\u786E\u5B9A',\n cancelText: options.cancelText ?? '\u53D6\u6D88',\n showCancel: options.showCancel ?? true,\n resolve,\n }\n })\n}\n\n/** \u5173\u95ED\u5F53\u524D modal \u5E76\u56DE\u8C03\u7ED3\u679C\u3002 */\nexport function settleModal(ok: boolean): void {\n const modal = state.modal\n state.modal = null\n modal?.resolve(ok)\n}\n\n/** \u7C7B wx.showActionSheet\uFF1B\u8FD4\u56DE Promise<number>\uFF08\u70B9\u4E2D\u9879\u7684\u4E0B\u6807\uFF09\u3002 */\nexport function showActionSheet(options: {\n itemList: string[]\n itemColor?: string\n}): Promise<number> {\n return new Promise((resolve) => {\n state.sheet = {\n id: ++seq,\n itemList: options.itemList,\n itemColor: options.itemColor ?? '#1F2430',\n resolve,\n }\n })\n}\n\n/** \u5173\u95ED\u5F53\u524D action sheet \u5E76\u56DE\u8C03\u4E0B\u6807\uFF08-1 = \u672A\u9009\u4E2D\uFF09\u3002 */\nexport function settleSheet(index: number): void {\n const sheet = state.sheet\n state.sheet = null\n sheet?.resolve(index)\n}\n\nexport function dismissAll(): void {\n settleModal(false)\n settleSheet(-1)\n state.toast = null\n}\n" },
7977
+ { path: "web/src/views/EditView.vue", content: `<script setup lang="ts">
7978
+ // \u6DFB\u52A0 / \u7F16\u8F91\u4E8B\u4EF6\uFF08\u79FB\u690D\u81EA pages/edit/*\uFF09\u3002
7979
+ // \u5B9E\u65F6\u5929\u6570\u9884\u89C8 + \u8868\u5355\uFF08\u540D\u79F0/\u65E5\u671F/\u5206\u7C7B/\u8BA1\u65F6\u65B9\u5F0F/\u5907\u6CE8/\u7F6E\u9876\uFF09+ \u4FDD\u5B58/\u5220\u9664\u3002
7980
+ import { computed, reactive, ref } from 'vue'
7981
+ import { useRoute, useRouter } from 'vue-router'
7982
+ import { request } from '../lib/request'
7983
+ import { ensureLogin } from '../lib/auth'
7984
+ import { calcDays, daysText, levelOf, isValidDate, todayStr, weekdayOf } from '../lib/date'
7985
+ import { CATEGORIES, CATEGORY_LABEL } from '../lib/config'
7986
+ import { showToast, showModal } from '../ui/ui'
7987
+
7988
+ const route = useRoute()
7989
+ const router = useRouter()
7990
+
7991
+ const id = typeof route.params.id === 'string' ? route.params.id : ''
7992
+ const isEdit = !!id
7993
+
7994
+ const form = reactive({
7995
+ title: '',
7996
+ target_date: todayStr(),
7997
+ note: '',
7998
+ category: 'other',
7999
+ direction: 'countdown' as 'countdown' | 'countup',
8000
+ is_pinned: false,
8001
+ })
8002
+
8003
+ const categoryIndex = ref(0)
8004
+ const dateStart = '1900-01-01'
8005
+ const dateEnd = '2100-12-31'
8006
+ const submitting = ref(false)
8007
+ const deleting = ref(false)
8008
+
8009
+ const preview = computed(() => {
8010
+ if (!isValidDate(form.target_date)) return null
8011
+ const days = calcDays(form.target_date, form.direction)
8012
+ const t = daysText(days, form.direction)
8013
+ return {
8014
+ level: levelOf(days, form.direction),
8015
+ text: \`\${t.prefix}\${t.num}\${t.suffix}\`,
8016
+ sub: \`\${form.target_date} \${weekdayOf(form.target_date)} \xB7 \${CATEGORY_LABEL[form.category] ?? '\u65E5\u5E38'}\`,
8017
+ }
8018
+ })
8019
+
8020
+ async function loadEvent(): Promise<void> {
8021
+ if (!id) return
8022
+ try {
8023
+ await ensureLogin()
8024
+ const res = await request<{
8025
+ event: {
8026
+ title: string
8027
+ target_date: string
8028
+ note: string
8029
+ category: string
8030
+ direction: 'countdown' | 'countup'
8031
+ is_pinned: boolean
8032
+ }
8033
+ }>(\`/api/events/\${id}\`)
8034
+ const e = res.event
8035
+ const idx = Math.max(
8036
+ 0,
8037
+ CATEGORIES.findIndex((c) => c.value === e.category)
8038
+ )
8039
+ form.title = e.title
8040
+ form.target_date = e.target_date
8041
+ form.note = e.note || ''
8042
+ form.category = e.category || 'other'
8043
+ form.direction = e.direction || 'countdown'
8044
+ form.is_pinned = !!e.is_pinned
8045
+ categoryIndex.value = idx
8046
+ } catch (err) {
8047
+ showToast({ title: err instanceof Error ? err.message : '\u52A0\u8F7D\u5931\u8D25', icon: 'none' })
8048
+ setTimeout(() => router.back(), 800)
8049
+ }
8050
+ }
8051
+
8052
+ if (id) void loadEvent()
8053
+
8054
+ async function onSave(): Promise<void> {
8055
+ if (submitting.value) return
8056
+ const title = form.title.trim()
8057
+
8058
+ if (!title) {
8059
+ showToast({ title: '\u8BF7\u586B\u5199\u4E8B\u4EF6\u540D\u79F0', icon: 'none' })
8060
+ return
8061
+ }
8062
+ if (title.length > 50) {
8063
+ showToast({ title: '\u540D\u79F0\u4E0D\u80FD\u8D85\u8FC7 50 \u4E2A\u5B57', icon: 'none' })
8064
+ return
8065
+ }
8066
+ if (!isValidDate(form.target_date)) {
8067
+ showToast({ title: '\u8BF7\u9009\u62E9\u76EE\u6807\u65E5\u671F', icon: 'none' })
8068
+ return
8069
+ }
8070
+ if (form.note.length > 200) {
8071
+ showToast({ title: '\u5907\u6CE8\u4E0D\u80FD\u8D85\u8FC7 200 \u4E2A\u5B57', icon: 'none' })
8072
+ return
8073
+ }
8074
+
8075
+ const payload = {
8076
+ title,
8077
+ target_date: form.target_date,
8078
+ note: form.note.trim(),
8079
+ category: form.category,
8080
+ direction: form.direction,
8081
+ is_pinned: !!form.is_pinned,
8082
+ }
8083
+
8084
+ submitting.value = true
8085
+ try {
8086
+ await ensureLogin()
8087
+ if (isEdit) {
8088
+ await request(\`/api/events/\${id}\`, { method: 'PUT', data: payload })
8089
+ } else {
8090
+ await request('/api/events', { method: 'POST', data: payload })
8091
+ }
8092
+ showToast({ title: isEdit ? '\u5DF2\u4FDD\u5B58' : '\u5DF2\u6DFB\u52A0', icon: 'success' })
8093
+ setTimeout(() => router.back(), 600)
8094
+ } catch (err) {
8095
+ submitting.value = false
8096
+ showToast({ title: err instanceof Error ? err.message : '\u4FDD\u5B58\u5931\u8D25', icon: 'none' })
8097
+ }
8098
+ }
8099
+
8100
+ async function onDelete(): Promise<void> {
8101
+ if (!id || deleting.value) return
8102
+ const ok = await showModal({
8103
+ title: '\u5220\u9664\u4E8B\u4EF6',
8104
+ content: \`\u786E\u5B9A\u5220\u9664\u300C\${form.title}\u300D\u5417\uFF1F\u5220\u9664\u540E\u4E0D\u53EF\u6062\u590D\u3002\`,
8105
+ confirmColor: '#FF5A5F',
8106
+ confirmText: '\u5220\u9664',
8107
+ })
8108
+ if (!ok) return
8109
+ deleting.value = true
8110
+ try {
8111
+ await request(\`/api/events/\${id}\`, { method: 'DELETE' })
8112
+ showToast({ title: '\u5DF2\u5220\u9664', icon: 'success' })
8113
+ setTimeout(() => router.back(), 600)
8114
+ } catch (err) {
8115
+ deleting.value = false
8116
+ showToast({ title: err instanceof Error ? err.message : '\u5220\u9664\u5931\u8D25', icon: 'none' })
8117
+ }
8118
+ }
8119
+
8120
+ function goBack(): void {
8121
+ router.back()
8122
+ }
8123
+ </script>
8124
+
8125
+ <template>
8126
+ <div class="page">
8127
+ <!-- \u9876\u90E8\u5BFC\u822A\uFF08web \u7248\u539F\u751F\u5BFC\u822A\u680F\uFF09 -->
8128
+ <header class="nav">
8129
+ <button class="nav-back" @click="goBack">\u2039</button>
8130
+ <span class="nav-title">{{ isEdit ? '\u7F16\u8F91\u4E8B\u4EF6' : '\u6DFB\u52A0\u4E8B\u4EF6' }}</span>
8131
+ <span class="nav-side"></span>
8132
+ </header>
8133
+
8134
+ <div class="page-scroll">
8135
+ <!-- \u5B9E\u65F6\u9884\u89C8 -->
8136
+ <div v-if="preview" class="preview" :class="'level-' + preview.level">
8137
+ <div class="pv-text">{{ preview.text }}</div>
8138
+ <div class="pv-sub">{{ preview.sub }}</div>
8139
+ </div>
8140
+
8141
+ <div class="form">
8142
+ <div class="group">
8143
+ <div class="label">\u4E8B\u4EF6\u540D\u79F0</div>
8144
+ <input class="input" v-model="form.title" placeholder="\u4F8B\u5982\uFF1A\u5C0F\u660E\u7684\u751F\u65E5" maxlength="50" />
8145
+ </div>
8146
+
8147
+ <div class="group">
8148
+ <div class="label">\u76EE\u6807\u65E5\u671F</div>
8149
+ <input
8150
+ class="input picker-input"
8151
+ type="date"
8152
+ v-model="form.target_date"
8153
+ :min="dateStart"
8154
+ :max="dateEnd"
8155
+ />
8156
+ </div>
8157
+
8158
+ <div class="group">
8159
+ <div class="label">\u5206\u7C7B</div>
8160
+ <div class="picker">
8161
+ <select v-model="form.category" class="picker-select">
8162
+ <option v-for="c in CATEGORIES" :key="c.value" :value="c.value">{{ c.label }}</option>
8163
+ </select>
8164
+ <span class="arrow">\u203A</span>
8165
+ </div>
8166
+ </div>
8167
+
8168
+ <div class="group">
8169
+ <div class="label">\u8BA1\u65F6\u65B9\u5F0F</div>
8170
+ <div class="radio-group">
8171
+ <label
8172
+ class="radio"
8173
+ :class="{ on: form.direction === 'countdown' }"
8174
+ @click="form.direction = 'countdown'"
8175
+ >
8176
+ <span class="radio-dot" :class="{ checked: form.direction === 'countdown' }"></span>
8177
+ <span>\u5012\u6570\u65E5</span>
8178
+ </label>
8179
+ <label
8180
+ class="radio"
8181
+ :class="{ on: form.direction === 'countup' }"
8182
+ @click="form.direction = 'countup'"
8183
+ >
8184
+ <span class="radio-dot" :class="{ checked: form.direction === 'countup' }"></span>
8185
+ <span>\u6B63\u6570\u65E5</span>
8186
+ </label>
8187
+ </div>
8188
+ <div class="hint">\u5012\u6570\u65E5 = \u8DDD\u79BB\u76EE\u6807\u8FD8\u6709\u591A\u5C11\u5929\uFF1B\u6B63\u6570\u65E5 = \u4ECE\u90A3\u5929\u8D77\u5DF2\u7ECF\u8FC7\u4E86\u591A\u5C11\u5929</div>
8189
+ </div>
8190
+
8191
+ <div class="group">
8192
+ <div class="label">\u5907\u6CE8 <span class="opt">\u9009\u586B</span></div>
8193
+ <textarea
8194
+ class="textarea"
8195
+ v-model="form.note"
8196
+ placeholder="\u5199\u70B9\u4EC0\u4E48\u2026"
8197
+ maxlength="200"
8198
+ rows="3"
8199
+ />
8200
+ <div class="counter">{{ form.note.length }}/200</div>
8201
+ </div>
8202
+
8203
+ <div class="group group-row">
8204
+ <div>
8205
+ <div class="label">\u7F6E\u9876\u663E\u793A</div>
8206
+ <div class="hint">\u7F6E\u9876\u7684\u4E8B\u4EF6\u6392\u5728\u5217\u8868\u6700\u524D\u9762</div>
8207
+ </div>
8208
+ <button
8209
+ class="switch"
8210
+ :class="{ on: form.is_pinned }"
8211
+ @click="form.is_pinned = !form.is_pinned"
8212
+ >
8213
+ <span class="switch-knob"></span>
8214
+ </button>
8215
+ </div>
8216
+ </div>
8217
+
8218
+ <div class="footer">
8219
+ <button class="btn btn-save" :disabled="submitting" @click="onSave">
8220
+ {{ submitting ? '\u4FDD\u5B58\u4E2D\u2026' : isEdit ? '\u4FDD\u5B58\u4FEE\u6539' : '\u521B\u5EFA\u4E8B\u4EF6' }}
8221
+ </button>
8222
+ <button v-if="isEdit" class="btn btn-del" :disabled="deleting" @click="onDelete">
8223
+ {{ deleting ? '\u5220\u9664\u4E2D\u2026' : '\u5220\u9664\u4E8B\u4EF6' }}
8224
+ </button>
8225
+ </div>
8226
+ </div>
8227
+ </div>
8228
+ </template>
8229
+
8230
+ <style scoped>
8231
+ .page {
8232
+ height: 100%;
8233
+ padding-bottom: calc(0.6rem + env(safe-area-inset-bottom));
8234
+ }
8235
+
8236
+ .nav {
8237
+ position: absolute;
8238
+ top: 0;
8239
+ left: 0;
8240
+ right: 0;
8241
+ z-index: 10;
8242
+ height: 0.88rem;
8243
+ display: flex;
8244
+ align-items: center;
8245
+ justify-content: space-between;
8246
+ background: linear-gradient(135deg, #3b6ef6, #6a8dff);
8247
+ padding: 0 0.2rem;
8248
+ }
8249
+
8250
+ .nav-back {
8251
+ width: 0.72rem;
8252
+ height: 100%;
8253
+ color: #ffffff;
8254
+ font-size: 0.56rem;
8255
+ line-height: 1;
8256
+ cursor: pointer;
8257
+ display: flex;
8258
+ align-items: center;
8259
+ }
8260
+
8261
+ .nav-title {
8262
+ color: #ffffff;
8263
+ font-size: 0.34rem;
8264
+ font-weight: 600;
8265
+ }
8266
+
8267
+ .nav-side {
8268
+ width: 0.72rem;
8269
+ }
8270
+
8271
+ .page-scroll {
8272
+ padding-top: 0.88rem;
8273
+ }
8274
+
8275
+ /* ---------- \u9884\u89C8 ---------- */
8276
+ .preview {
8277
+ margin: 0.24rem 0.32rem 0.08rem;
8278
+ padding: 0.36rem 0.4rem;
8279
+ border-radius: 0.24rem;
8280
+ background: linear-gradient(135deg, #3b6ef6, #6a8dff);
8281
+ box-shadow: 0 0.12rem 0.28rem rgba(59, 110, 246, 0.24);
8282
+ text-align: center;
8283
+ }
8284
+
8285
+ .preview.level-past {
8286
+ background: linear-gradient(135deg, #a0a8b8, #b9c0cc);
8287
+ box-shadow: 0 0.12rem 0.28rem rgba(160, 168, 184, 0.24);
8288
+ }
8289
+
8290
+ .pv-text {
8291
+ display: block;
8292
+ color: #ffffff;
8293
+ font-size: 0.56rem;
8294
+ font-weight: 700;
8295
+ line-height: 1.2;
8296
+ }
8297
+
8298
+ .pv-sub {
8299
+ display: block;
8300
+ margin-top: 0.12rem;
8301
+ color: rgba(255, 255, 255, 0.85);
8302
+ font-size: 0.24rem;
8303
+ }
8304
+
8305
+ /* ---------- \u8868\u5355 ---------- */
8306
+ .form {
8307
+ margin: 0.24rem 0.32rem 0;
8308
+ background: #ffffff;
8309
+ border-radius: 0.24rem;
8310
+ padding: 0.08rem 0.32rem;
8311
+ box-shadow: 0 0.08rem 0.24rem rgba(31, 36, 48, 0.05);
8312
+ }
8313
+
8314
+ .group {
8315
+ padding: 0.28rem 0;
8316
+ border-bottom: 1px solid #f0f2f7;
8317
+ }
8318
+
8319
+ .group:last-child {
8320
+ border-bottom: none;
8321
+ }
8322
+
8323
+ .group-row {
8324
+ display: flex;
8325
+ align-items: center;
8326
+ justify-content: space-between;
8327
+ }
8328
+
8329
+ .label {
8330
+ font-size: 0.28rem;
8331
+ color: #1f2430;
8332
+ font-weight: 500;
8333
+ margin-bottom: 0.16rem;
8334
+ }
8335
+
8336
+ .group-row .label {
8337
+ margin-bottom: 0.08rem;
8338
+ }
8339
+
8340
+ .opt {
8341
+ font-size: 0.22rem;
8342
+ color: #b4bac6;
8343
+ font-weight: 400;
8344
+ }
8345
+
8346
+ .input,
8347
+ .textarea {
8348
+ width: 100%;
8349
+ font-size: 0.3rem;
8350
+ color: #1f2430;
8351
+ }
8352
+
8353
+ .input::placeholder,
8354
+ .textarea::placeholder {
8355
+ color: #b4bac6;
8356
+ }
8357
+
8358
+ .textarea {
8359
+ min-height: 0.8rem;
8360
+ line-height: 1.6;
8361
+ resize: none;
8362
+ }
8363
+
8364
+ .picker-input {
8365
+ cursor: pointer;
8366
+ }
8367
+
8368
+ .picker {
8369
+ display: flex;
8370
+ align-items: center;
8371
+ justify-content: space-between;
8372
+ font-size: 0.3rem;
8373
+ color: #1f2430;
8374
+ position: relative;
8375
+ }
8376
+
8377
+ .picker-select {
8378
+ width: 100%;
8379
+ font-size: 0.3rem;
8380
+ color: #1f2430;
8381
+ appearance: none;
8382
+ -webkit-appearance: none;
8383
+ background: transparent;
8384
+ cursor: pointer;
8385
+ padding: 0.06rem 0;
8386
+ }
8387
+
8388
+ .arrow {
8389
+ color: #c4c9d4;
8390
+ font-size: 0.36rem;
8391
+ pointer-events: none;
8392
+ position: absolute;
8393
+ right: 0;
8394
+ }
8395
+
8396
+ .radio-group {
8397
+ display: flex;
8398
+ }
8399
+
8400
+ .radio {
8401
+ display: flex;
8402
+ align-items: center;
8403
+ margin-right: 0.48rem;
8404
+ padding: 0.12rem 0.24rem;
8405
+ border-radius: 999rem;
8406
+ background: #f5f7fb;
8407
+ font-size: 0.28rem;
8408
+ color: #6b7385;
8409
+ cursor: pointer;
8410
+ user-select: none;
8411
+ }
8412
+
8413
+ .radio.on {
8414
+ background: rgba(59, 110, 246, 0.1);
8415
+ color: #3b6ef6;
8416
+ }
8417
+
8418
+ .radio-dot {
8419
+ width: 0.32rem;
8420
+ height: 0.32rem;
8421
+ border-radius: 50%;
8422
+ border: 2px solid #c4c9d4;
8423
+ margin-right: 0.08rem;
8424
+ display: inline-flex;
8425
+ align-items: center;
8426
+ justify-content: center;
8427
+ }
8428
+
8429
+ .radio-dot.checked {
8430
+ border-color: #3b6ef6;
8431
+ }
8432
+
8433
+ .radio-dot.checked::after {
8434
+ content: '';
8435
+ width: 0.14rem;
8436
+ height: 0.14rem;
8437
+ border-radius: 50%;
8438
+ background: #3b6ef6;
8439
+ }
8440
+
8441
+ .hint {
8442
+ margin-top: 0.14rem;
8443
+ font-size: 0.22rem;
8444
+ color: #b4bac6;
8445
+ line-height: 1.5;
8446
+ }
8447
+
8448
+ .counter {
8449
+ margin-top: 0.08rem;
8450
+ text-align: right;
8451
+ font-size: 0.22rem;
8452
+ color: #c4c9d4;
8453
+ }
8454
+
8455
+ /* \u5F00\u5173\uFF08\u79FB\u690D wx switch\uFF09 */
8456
+ .switch {
8457
+ width: 0.96rem;
8458
+ height: 0.56rem;
8459
+ border-radius: 999rem;
8460
+ background: #e2e4ea;
8461
+ position: relative;
8462
+ cursor: pointer;
8463
+ transition: background 0.2s ease;
8464
+ flex-shrink: 0;
8465
+ }
8466
+
8467
+ .switch.on {
8468
+ background: #3b6ef6;
8469
+ }
8470
+
8471
+ .switch-knob {
8472
+ position: absolute;
8473
+ top: 0.04rem;
8474
+ left: 0.04rem;
8475
+ width: 0.48rem;
8476
+ height: 0.48rem;
8477
+ border-radius: 50%;
8478
+ background: #ffffff;
8479
+ box-shadow: 0 0.02rem 0.06rem rgba(0, 0, 0, 0.18);
8480
+ transition: transform 0.2s ease;
8481
+ }
8482
+
8483
+ .switch.on .switch-knob {
8484
+ transform: translateX(0.4rem);
8485
+ }
8486
+
8487
+ /* ---------- \u5E95\u90E8\u6309\u94AE ---------- */
8488
+ .footer {
8489
+ padding: 0.48rem 0.32rem 0;
8490
+ }
8491
+
8492
+ .btn {
8493
+ display: flex;
8494
+ align-items: center;
8495
+ justify-content: center;
8496
+ width: 100%;
8497
+ height: 0.92rem;
8498
+ border-radius: 999rem;
8499
+ font-size: 0.3rem;
8500
+ cursor: pointer;
8501
+ }
8502
+
8503
+ .btn-save {
8504
+ background: linear-gradient(135deg, #3b6ef6, #6a8dff);
8505
+ color: #ffffff;
8506
+ box-shadow: 0 0.1rem 0.24rem rgba(59, 110, 246, 0.28);
8507
+ }
8508
+
8509
+ .btn-save:disabled {
8510
+ opacity: 0.7;
8511
+ }
8512
+
8513
+ .btn-del {
8514
+ margin-top: 0.24rem;
8515
+ background: #ffffff;
8516
+ color: #ff5a5f;
8517
+ border: 1px solid #ffd9da;
8518
+ }
8519
+
8520
+ .btn-del:disabled {
8521
+ opacity: 0.7;
8522
+ }
8523
+ </style>
8524
+ ` },
8525
+ { path: "web/src/views/HomeView.vue", content: `<script setup lang="ts">
8526
+ // \u9996\u9875\uFF1A\u4E8B\u4EF6\u5217\u8868\uFF08\u79FB\u690D\u81EA pages/index/*\uFF09\u3002
8527
+ // \u6E10\u53D8\u5934\u90E8 + \u603B\u6570/\u4E0B\u4E00\u4E2A + \u5206\u7C7B\u7B5B\u9009 + \u5DE6\u6ED1\u7F6E\u9876/\u5220\u9664 + \u957F\u6309\u83DC\u5355 + \u4E0B\u62C9\u5237\u65B0 + \u672C\u5730\u7F13\u5B58\u79D2\u5F00\u3002
8528
+ import { computed, onMounted, onUnmounted, reactive, ref } from 'vue'
8529
+ import { useRouter } from 'vue-router'
8530
+ import { request } from '../lib/request'
8531
+ import { ensureLogin } from '../lib/auth'
8532
+ import { decorateEvent, type CountdownEvent, type DecodedEvent } from '../lib/date'
8533
+ import { CATEGORIES } from '../lib/config'
8534
+ import { showToast, showModal, showActionSheet } from '../ui/ui'
8535
+
8536
+ const CACHE_KEY = 'events_cache'
8537
+ const ACTION_RPX = 160 // \u5DE6\u6ED1\u9732\u51FA\u7684\u64CD\u4F5C\u533A\u5BBD\u5EA6\uFF08rpx\uFF09\uFF0C\u8FD0\u884C\u65F6\u6362\u7B97\u6210 px
8538
+
8539
+ const router = useRouter()
8540
+
8541
+ const scrollEl = ref<HTMLElement | null>(null)
8542
+
8543
+ const data = reactive({
8544
+ loaded: false,
8545
+ list: [] as DecodedEvent[],
8546
+ allList: [] as DecodedEvent[],
8547
+ filters: [{ value: '', label: '\u5168\u90E8' }].concat(CATEGORIES),
8548
+ activeFilter: '',
8549
+ total: 0,
8550
+ nextUp: null as null | { title: string; num: string; prefix: string; suffix: string },
8551
+ openIndex: -1,
8552
+ })
8553
+
8554
+ const actionWidth = ref(1.6 * 64) // 1.6rem * \u9ED8\u8BA4 64px\uFF1B\u8FD0\u884C\u65F6\u6309\u5B9E\u9645 font-size \u6821\u6B63
8555
+
8556
+ function applyList(raw: CountdownEvent[]): void {
8557
+ const all = raw.map(decorateEvent)
8558
+ const next = all.find((e) => e.direction !== 'countup' && e.days >= 0)
8559
+ const list = data.activeFilter ? all.filter((e) => e.category === data.activeFilter) : all
8560
+ data.allList = all
8561
+ data.list = list
8562
+ data.total = all.length
8563
+ data.openIndex = -1
8564
+ data.nextUp = next
8565
+ ? { title: next.title, num: next.num, prefix: next.prefix || '', suffix: next.suffix || '' }
8566
+ : null
8567
+ }
8568
+
8569
+ async function loadEvents(force = false): Promise<void> {
8570
+ if (!force && data.loaded) {
8571
+ try {
8572
+ const cached = localStorage.getItem(CACHE_KEY)
8573
+ if (cached) {
8574
+ const parsed = JSON.parse(cached) as CountdownEvent[]
8575
+ if (parsed.length) applyList(parsed)
8576
+ }
8577
+ } catch {
8578
+ /* \u7F13\u5B58\u635F\u574F\u5FFD\u7565 */
8579
+ }
8580
+ }
8581
+
8582
+ try {
8583
+ await ensureLogin()
8584
+ const res = await request<{ events: CountdownEvent[] }>('/api/events')
8585
+ localStorage.setItem(CACHE_KEY, JSON.stringify(res.events ?? []))
8586
+ applyList(res.events ?? [])
8587
+ data.loaded = true
8588
+ } catch (err) {
8589
+ console.warn('[index] \u52A0\u8F7D\u5931\u8D25', err)
8590
+ data.loaded = true
8591
+ if (!data.allList.length) {
8592
+ showToast({ title: err instanceof Error ? err.message : '\u52A0\u8F7D\u5931\u8D25', icon: 'none' })
8593
+ }
8594
+ }
8595
+ }
8596
+
8597
+ function onFilter(value: string): void {
8598
+ data.activeFilter = value
8599
+ applyList(data.allList)
8600
+ }
8601
+
8602
+ // ---------- \u5361\u7247\u4EA4\u4E92 ----------
8603
+
8604
+ /** \u6ED1\u52A8\u72B6\u6001\uFF08\u6309\u5361\u7247\u4E0B\u6807\u8FFD\u8E2A\uFF09\u3002 */
8605
+ const swipe = reactive<{
8606
+ startX: number
8607
+ startY: number
8608
+ lastX: number
8609
+ touchIndex: number | undefined
8610
+ moved: boolean
8611
+ justSwiped: boolean
8612
+ }>({ startX: 0, startY: 0, lastX: 0, touchIndex: undefined, moved: false, justSwiped: false })
8613
+
8614
+ let longPressTimer: ReturnType<typeof setTimeout> | null = null
8615
+
8616
+ function onCardPointerDown(event: PointerEvent, index: number): void {
8617
+ swipe.startX = event.clientX
8618
+ swipe.startY = event.clientY
8619
+ swipe.touchIndex = index
8620
+ swipe.moved = false
8621
+ swipe.lastX = data.list[index]?.offset ?? 0
8622
+ // \u957F\u6309\u83DC\u5355
8623
+ clearLongPress()
8624
+ longPressTimer = setTimeout(() => {
8625
+ const item = data.list[index]
8626
+ if (!item) return
8627
+ void onLongPress(item)
8628
+ }, 600)
8629
+ }
8630
+
8631
+ function onCardPointerMove(event: PointerEvent): void {
8632
+ if (swipe.touchIndex === undefined) return
8633
+ const dx = event.clientX - swipe.startX
8634
+ const dy = event.clientY - swipe.startY
8635
+ if (!swipe.moved && Math.abs(dy) > Math.abs(dx)) {
8636
+ // \u7EB5\u5411\u6EDA\u52A8\u610F\u56FE\uFF1A\u53D6\u6D88\u957F\u6309\uFF0C\u4E0D\u62E6\u622A
8637
+ clearLongPress()
8638
+ return
8639
+ }
8640
+ swipe.moved = true
8641
+ clearLongPress()
8642
+
8643
+ const min = -actionWidth.value
8644
+ let x = (swipe.lastX ?? 0) + dx
8645
+ if (x > 0) x = 0
8646
+ if (x < min) x = min
8647
+ if (Math.abs(x - (swipe.lastX ?? 0)) < 2) return
8648
+
8649
+ const idx = swipe.touchIndex
8650
+ const cur = data.list[idx]?.offset ?? 0
8651
+ if (Math.abs(x - cur) < 3) return
8652
+ data.list[idx] = { ...data.list[idx]!, offset: x }
8653
+ }
8654
+
8655
+ function onCardPointerUp(): void {
8656
+ if (swipe.touchIndex === undefined) return
8657
+ clearLongPress()
8658
+ const idx = swipe.touchIndex
8659
+ const x = data.list[idx]?.offset ?? 0
8660
+ const open = x < -actionWidth.value / 2
8661
+ const target = open ? -actionWidth.value : 0
8662
+
8663
+ // \u540C\u65F6\u53EA\u5141\u8BB8\u4E00\u4E2A\u5361\u7247\u6ED1\u5F00
8664
+ if (open && data.openIndex !== -1 && data.openIndex !== idx) {
8665
+ data.list[data.openIndex] = { ...data.list[data.openIndex]!, offset: 0 }
8666
+ }
8667
+ data.list[idx] = { ...data.list[idx]!, offset: target }
8668
+ data.openIndex = open ? idx : -1
8669
+
8670
+ swipe.justSwiped = swipe.moved
8671
+ swipe.touchIndex = undefined
8672
+ swipe.moved = false
8673
+ }
8674
+
8675
+ function onCardClick(index: number, id: string): void {
8676
+ if (swipe.justSwiped) {
8677
+ // \u521A\u6ED1\u52A8\u5B8C\uFF0C\u8FD9\u4E00\u4E0B click \u4E0D\u5F53\u4F5C\u70B9\u51FB
8678
+ swipe.justSwiped = false
8679
+ return
8680
+ }
8681
+ if (data.openIndex !== -1) {
8682
+ closeAll()
8683
+ return
8684
+ }
8685
+ void router.push(\`/edit/\${id}\`)
8686
+ }
8687
+
8688
+ function closeAll(): void {
8689
+ data.list.forEach((item, i) => {
8690
+ if (item.offset !== 0) data.list[i] = { ...item, offset: 0 }
8691
+ })
8692
+ data.openIndex = -1
8693
+ }
8694
+
8695
+ function clearLongPress(): void {
8696
+ if (longPressTimer !== null) {
8697
+ clearTimeout(longPressTimer)
8698
+ longPressTimer = null
8699
+ }
8700
+ }
8701
+
8702
+ async function onTogglePin(id: string, pinned: boolean): Promise<void> {
8703
+ const next = !pinned
8704
+ try {
8705
+ await request(\`/api/events/\${id}\`, { method: 'PUT', data: { is_pinned: next } })
8706
+ showToast({ title: next ? '\u5DF2\u7F6E\u9876' : '\u5DF2\u53D6\u6D88\u7F6E\u9876', icon: 'none' })
8707
+ await loadEvents(true)
8708
+ } catch (err) {
8709
+ showToast({ title: err instanceof Error ? err.message : '\u64CD\u4F5C\u5931\u8D25', icon: 'none' })
8710
+ }
8711
+ }
8712
+
8713
+ async function onDelete(id: string, title: string): Promise<void> {
8714
+ const ok = await showModal({
8715
+ title: '\u5220\u9664\u4E8B\u4EF6',
8716
+ content: \`\u786E\u5B9A\u5220\u9664\u300C\${title}\u300D\u5417\uFF1F\u5220\u9664\u540E\u4E0D\u53EF\u6062\u590D\u3002\`,
8717
+ confirmColor: '#FF5A5F',
8718
+ confirmText: '\u5220\u9664',
8719
+ })
8720
+ if (!ok) return
8721
+ try {
8722
+ await request(\`/api/events/\${id}\`, { method: 'DELETE' })
8723
+ showToast({ title: '\u5DF2\u5220\u9664', icon: 'success' })
8724
+ await loadEvents(true)
8725
+ } catch (err) {
8726
+ showToast({ title: err instanceof Error ? err.message : '\u5220\u9664\u5931\u8D25', icon: 'none' })
8727
+ }
8728
+ }
8729
+
8730
+ async function onLongPress(item: DecodedEvent): Promise<void> {
8731
+ const index = await showActionSheet({
8732
+ itemList: [item.is_pinned ? '\u53D6\u6D88\u7F6E\u9876' : '\u7F6E\u9876', '\u5220\u9664'],
8733
+ itemColor: '#1F2430',
8734
+ })
8735
+ if (index === 0) {
8736
+ await onTogglePin(item.id, item.is_pinned)
8737
+ } else if (index === 1) {
8738
+ await onDelete(item.id, item.title)
8739
+ }
8740
+ }
8741
+
8742
+ // ---------- \u4E0B\u62C9\u5237\u65B0 ----------
8743
+
8744
+ const pull = reactive({
8745
+ distance: 0, // \u4E0B\u62C9\u504F\u79FB px
8746
+ state: 'idle' as 'idle' | 'pulling' | 'ready' | 'refreshing',
8747
+ })
8748
+
8749
+ let pullStartY = 0
8750
+ let pullTracking = false
8751
+ let pullMoved = false
8752
+
8753
+ function onPullPointerDown(event: PointerEvent): void {
8754
+ if (pull.state === 'refreshing') return
8755
+ if ((scrollEl.value?.scrollTop ?? 0) <= 0) {
8756
+ pullStartY = event.clientY
8757
+ pullTracking = true
8758
+ pullMoved = false
8759
+ }
8760
+ }
8761
+
8762
+ function onPullPointerMove(event: PointerEvent): void {
8763
+ if (!pullTracking || pull.state === 'refreshing') return
8764
+ const dy = event.clientY - pullStartY
8765
+ if (dy <= 0) return
8766
+ pullMoved = true
8767
+ event.preventDefault()
8768
+ pull.distance = Math.min(dy * 0.5, 90)
8769
+ pull.state = pull.distance > 50 ? 'ready' : 'pulling'
8770
+ }
8771
+
8772
+ function onPullPointerUp(): void {
8773
+ if (!pullTracking) return
8774
+ pullTracking = false
8775
+ if (pull.state === 'ready') {
8776
+ pull.state = 'refreshing'
8777
+ pull.distance = 44
8778
+ void loadEvents(true).finally(() => {
8779
+ pull.state = 'idle'
8780
+ pull.distance = 0
8781
+ })
8782
+ } else if (pullMoved) {
8783
+ pull.state = 'idle'
8784
+ pull.distance = 0
8785
+ }
8786
+ pullMoved = false
8787
+ }
8788
+
8789
+ // ---------- \u751F\u547D\u5468\u671F ----------
8790
+
8791
+ onMounted(() => {
8792
+ // \u5DE6\u6ED1\u64CD\u4F5C\u533A\u5BBD\u5EA6\u6309\u5B9E\u9645 font-size \u6362\u7B97\uFF081.6rem\uFF09
8793
+ const root = scrollEl.value?.closest('.app') as HTMLElement | null
8794
+ if (root) {
8795
+ const fs = parseFloat(getComputedStyle(root).fontSize)
8796
+ if (Number.isFinite(fs) && fs > 0) actionWidth.value = (ACTION_RPX / 100) * fs
8797
+ }
8798
+
8799
+ const el = scrollEl.value
8800
+ if (el) {
8801
+ // \u4E0B\u62C9\u5237\u65B0\u76D1\u542C\uFF08pointerdown \u5728 scrollTop=0 \u65F6\u542F\u7528\uFF0Cmove \u9636\u6BB5 preventDefault \u963B\u6B62\u539F\u751F\u6EDA\u52A8\uFF09
8802
+ el.addEventListener('pointerdown', onPullPointerDown, { passive: true })
8803
+ el.addEventListener('pointermove', onPullPointerMove, { passive: false })
8804
+ el.addEventListener('pointerup', onPullPointerUp)
8805
+ el.addEventListener('pointercancel', onPullPointerUp)
8806
+ }
8807
+
8808
+ void loadEvents(false)
8809
+ })
8810
+
8811
+ onUnmounted(() => {
8812
+ clearLongPress()
8813
+ const el = scrollEl.value
8814
+ if (el) {
8815
+ el.removeEventListener('pointerdown', onPullPointerDown)
8816
+ el.removeEventListener('pointermove', onPullPointerMove)
8817
+ el.removeEventListener('pointerup', onPullPointerUp)
8818
+ el.removeEventListener('pointercancel', onPullPointerUp)
8819
+ }
8820
+ })
8821
+
8822
+ const pullLabel = computed(() => {
8823
+ if (pull.state === 'refreshing') return '\u6B63\u5728\u5237\u65B0\u2026'
8824
+ if (pull.state === 'ready') return '\u677E\u5F00\u5237\u65B0'
8825
+ return '\u4E0B\u62C9\u5237\u65B0'
8826
+ })
8827
+ </script>
8828
+
8829
+ <template>
8830
+ <div class="page">
8831
+ <!-- \u9876\u90E8\u84DD\u8272\u6E10\u53D8\u5BFC\u822A -->
8832
+ <header class="header">
8833
+ <div class="nav">
8834
+ <span class="nav-title">\u5012\u6570\u65E5</span>
8835
+ </div>
8836
+ <div class="summary">
8837
+ <div class="summary-left">
8838
+ <div class="summary-num">{{ data.total }}</div>
8839
+ <div class="summary-label">\u4E2A\u91CD\u8981\u65E5\u5B50</div>
8840
+ </div>
8841
+ <div class="summary-right" v-if="data.nextUp">
8842
+ <div class="summary-label">\u4E0B\u4E00\u4E2A</div>
8843
+ <div class="summary-next">
8844
+ <span class="next-title">{{ data.nextUp.title }}</span>
8845
+ <span class="next-days"
8846
+ >{{ data.nextUp.prefix }}{{ data.nextUp.num }}{{ data.nextUp.suffix }}</span
8847
+ >
8848
+ </div>
8849
+ </div>
8850
+ </div>
8851
+ </header>
8852
+
8853
+ <!-- \u5185\u5BB9\u533A -->
8854
+ <div ref="scrollEl" class="page-scroll body">
8855
+ <!-- \u4E0B\u62C9\u5237\u65B0\u6307\u793A -->
8856
+ <div class="pull-indicator" :style="{ height: pull.distance + 'px' }">
8857
+ <span class="pull-spinner" :class="{ spin: pull.state === 'refreshing' }"></span>
8858
+ <span class="pull-text">{{ pullLabel }}</span>
8859
+ </div>
8860
+
8861
+ <!-- \u5206\u7C7B\u7B5B\u9009 -->
8862
+ <div v-if="data.total > 0" class="filters">
8863
+ <span
8864
+ v-for="f in data.filters"
8865
+ :key="f.value"
8866
+ class="chip"
8867
+ :class="{ 'chip-on': data.activeFilter === f.value }"
8868
+ @click="onFilter(f.value)"
8869
+ >
8870
+ {{ f.label }}
8871
+ </span>
8872
+ </div>
8873
+
8874
+ <!-- \u5217\u8868 -->
8875
+ <div v-if="data.list.length > 0" class="list no-select">
8876
+ <div v-for="(item, idx) in data.list" :key="item.id" class="card-row">
8877
+ <div class="card-actions">
8878
+ <span class="act act-pin" @click.stop="onTogglePin(item.id, item.is_pinned)">{{
8879
+ item.is_pinned ? '\u53D6\u6D88\u7F6E\u9876' : '\u7F6E\u9876'
8880
+ }}</span>
8881
+ <span class="act act-del" @click.stop="onDelete(item.id, item.title)">\u5220\u9664</span>
8882
+ </div>
8883
+
8884
+ <div
8885
+ class="card"
8886
+ :style="{ transform: \`translateX(\${item.offset}px)\`, touchAction: 'pan-y' }"
8887
+ @pointerdown="onCardPointerDown($event, idx)"
8888
+ @pointermove="onCardPointerMove($event)"
8889
+ @pointerup="onCardPointerUp"
8890
+ @pointercancel="onCardPointerUp"
8891
+ @click="onCardClick(idx, item.id)"
8892
+ >
8893
+ <div class="card-bar" :class="'level-' + item.level"></div>
8894
+ <div class="card-main">
8895
+ <div class="card-left">
8896
+ <div class="card-title">
8897
+ <span class="title-text">{{ item.title }}</span>
8898
+ <span v-if="item.is_pinned" class="pin-tag">\u7F6E\u9876</span>
8899
+ </div>
8900
+ <div class="card-sub">{{ item.sub }} \xB7 {{ item.categoryLabel }}</div>
8901
+ <div v-if="item.note" class="card-note">{{ item.note }}</div>
8902
+ </div>
8903
+ <div class="card-right">
8904
+ <div class="days">
8905
+ <span v-if="item.prefix" class="days-prefix">{{ item.prefix }}</span>
8906
+ <span class="days-num" :class="'level-' + item.level">{{ item.num }}</span>
8907
+ <span v-if="item.suffix" class="days-unit">{{ item.suffix }}</span>
8908
+ </div>
8909
+ </div>
8910
+ </div>
8911
+ </div>
8912
+ </div>
8913
+ </div>
8914
+
8915
+ <!-- \u7A7A\u6001 -->
8916
+ <div v-if="data.loaded && data.list.length === 0" class="empty">
8917
+ <div class="empty-icon">
8918
+ <div class="empty-bar"></div>
8919
+ <div class="empty-dot"></div>
8920
+ </div>
8921
+ <div class="empty-title">{{ data.total > 0 ? '\u8BE5\u5206\u7C7B\u4E0B\u8FD8\u6CA1\u6709\u4E8B\u4EF6' : '\u8FD8\u6CA1\u6709\u5012\u6570\u65E5' }}</div>
8922
+ <div class="empty-tip">
8923
+ {{ data.total > 0 ? '\u6362\u4E2A\u5206\u7C7B\u770B\u770B\uFF0C\u6216\u6DFB\u52A0\u4E00\u4E2A\u65B0\u7684' : '\u70B9\u51FB\u4E0B\u65B9 + \u8BB0\u5F55\u7B2C\u4E00\u4E2A\u91CD\u8981\u65E5\u5B50' }}
8924
+ </div>
8925
+ <div class="empty-btn" @click="router.push('/edit')">\u7ACB\u5373\u6DFB\u52A0</div>
8926
+ </div>
8927
+
8928
+ <div v-if="data.list.length > 0" class="foot-tip">\u5DE6\u6ED1\u5361\u7247\u53EF\u7F6E\u9876 / \u5220\u9664\uFF0C\u957F\u6309\u4E5F\u6709\u83DC\u5355</div>
8929
+ </div>
8930
+ </div>
8931
+ </template>
8932
+
8933
+ <style scoped>
8934
+ .page {
8935
+ height: 100%;
8936
+ padding-bottom: calc(1.6rem + env(safe-area-inset-bottom));
8937
+ }
8938
+
8939
+ /* ---------- \u9876\u90E8\u6E10\u53D8\u5BFC\u822A ---------- */
8940
+ .header {
8941
+ position: absolute;
8942
+ top: 0;
8943
+ left: 0;
8944
+ right: 0;
8945
+ z-index: 10;
8946
+ background: linear-gradient(135deg, #3b6ef6, #6a8dff);
8947
+ border-bottom-left-radius: 0.4rem;
8948
+ border-bottom-right-radius: 0.4rem;
8949
+ }
8950
+
8951
+ .nav {
8952
+ display: flex;
8953
+ align-items: center;
8954
+ height: 0.88rem;
8955
+ padding-left: 0.4rem;
8956
+ }
8957
+
8958
+ .nav-title {
8959
+ color: #ffffff;
8960
+ font-size: 0.36rem;
8961
+ font-weight: 600;
8962
+ }
8963
+
8964
+ .summary {
8965
+ height: 1.76rem;
8966
+ padding: 0 0.4rem 0.28rem;
8967
+ display: flex;
8968
+ align-items: flex-end;
8969
+ justify-content: space-between;
8970
+ }
8971
+
8972
+ .summary-num {
8973
+ color: #ffffff;
8974
+ font-size: 0.66rem;
8975
+ font-weight: 700;
8976
+ line-height: 1;
8977
+ }
8978
+
8979
+ .summary-label {
8980
+ color: rgba(255, 255, 255, 0.82);
8981
+ font-size: 0.24rem;
8982
+ margin-top: 0.12rem;
8983
+ }
8984
+
8985
+ .summary-right {
8986
+ text-align: right;
8987
+ max-width: 46%;
8988
+ }
8989
+
8990
+ .summary-next {
8991
+ margin-top: 0.08rem;
8992
+ color: #ffffff;
8993
+ font-size: 0.28rem;
8994
+ }
8995
+
8996
+ .next-title {
8997
+ opacity: 0.9;
8998
+ margin-right: 0.12rem;
8999
+ }
9000
+
9001
+ .next-days {
9002
+ font-weight: 600;
9003
+ }
9004
+
9005
+ /* ---------- \u5185\u5BB9\u533A ---------- */
9006
+ .body {
9007
+ padding-top: 2.64rem;
9008
+ padding-bottom: 0.4rem;
9009
+ }
9010
+
9011
+ /* \u4E0B\u62C9\u5237\u65B0 */
9012
+ .pull-indicator {
9013
+ display: flex;
9014
+ align-items: center;
9015
+ justify-content: center;
9016
+ gap: 0.12rem;
9017
+ overflow: hidden;
9018
+ color: #9aa3b2;
9019
+ font-size: 0.24rem;
9020
+ }
9021
+
9022
+ .pull-spinner {
9023
+ width: 0.28rem;
9024
+ height: 0.28rem;
9025
+ border-radius: 50%;
9026
+ border: 0.04rem solid #d7dbe4;
9027
+ border-top-color: #3b6ef6;
9028
+ }
9029
+
9030
+ .pull-spinner.spin {
9031
+ animation: pull-spin 0.8s linear infinite;
9032
+ }
9033
+
9034
+ @keyframes pull-spin {
9035
+ to {
9036
+ transform: rotate(360deg);
9037
+ }
9038
+ }
9039
+
9040
+ /* ---------- \u5206\u7C7B\u7B5B\u9009 ---------- */
9041
+ .filters {
9042
+ white-space: nowrap;
9043
+ padding: 0.24rem 0 0.04rem;
9044
+ overflow-x: auto;
9045
+ scrollbar-width: none;
9046
+ }
9047
+
9048
+ .filters::-webkit-scrollbar {
9049
+ display: none;
9050
+ }
9051
+
9052
+ .chip {
9053
+ display: inline-block;
9054
+ padding: 0.12rem 0.28rem;
9055
+ margin-right: 0.16rem;
9056
+ background: #ffffff;
9057
+ border-radius: 999rem;
9058
+ font-size: 0.26rem;
9059
+ color: #6b7385;
9060
+ cursor: pointer;
9061
+ user-select: none;
9062
+ }
9063
+
9064
+ .chip:first-child {
9065
+ margin-left: 0.32rem;
9066
+ }
9067
+
9068
+ .chip:last-child {
9069
+ margin-right: 0.32rem;
9070
+ }
9071
+
9072
+ .chip-on {
9073
+ background: linear-gradient(135deg, #3b6ef6, #6a8dff);
9074
+ color: #ffffff;
9075
+ }
9076
+
9077
+ /* ---------- \u5361\u7247 ---------- */
9078
+ .list {
9079
+ padding: 0.16rem 0.32rem 0;
9080
+ }
9081
+
9082
+ .card-row {
9083
+ position: relative;
9084
+ margin-bottom: 0.24rem;
9085
+ border-radius: 0.2rem;
9086
+ overflow: hidden;
9087
+ }
9088
+
9089
+ .card-actions {
9090
+ position: absolute;
9091
+ right: 0;
9092
+ top: 0;
9093
+ bottom: 0;
9094
+ width: 1.6rem;
9095
+ display: flex;
9096
+ }
9097
+
9098
+ .act {
9099
+ flex: 1;
9100
+ display: flex;
9101
+ align-items: center;
9102
+ justify-content: center;
9103
+ color: #ffffff;
9104
+ font-size: 0.26rem;
9105
+ cursor: pointer;
9106
+ }
9107
+
9108
+ .act-pin {
9109
+ background: #8a94a6;
9110
+ }
9111
+
9112
+ .act-del {
9113
+ background: #ff5a5f;
9114
+ }
9115
+
9116
+ .card {
9117
+ position: relative;
9118
+ z-index: 2;
9119
+ background: #ffffff;
9120
+ border-radius: 0.2rem;
9121
+ box-shadow: 0 0.08rem 0.24rem rgba(31, 36, 48, 0.06);
9122
+ transition: transform 0.22s ease;
9123
+ cursor: pointer;
9124
+ }
9125
+
9126
+ .card-bar {
9127
+ position: absolute;
9128
+ left: 0;
9129
+ top: 0;
9130
+ bottom: 0;
9131
+ width: 0.08rem;
9132
+ border-radius: 0.2rem 0 0 0.2rem;
9133
+ }
9134
+
9135
+ .card-main {
9136
+ display: flex;
9137
+ align-items: center;
9138
+ padding: 0.32rem 0.28rem 0.32rem 0.36rem;
9139
+ min-height: 1.4rem;
9140
+ }
9141
+
9142
+ .card-left {
9143
+ flex: 1;
9144
+ min-width: 0;
9145
+ }
9146
+
9147
+ .card-title {
9148
+ display: flex;
9149
+ align-items: center;
9150
+ }
9151
+
9152
+ .title-text {
9153
+ font-size: 0.32rem;
9154
+ font-weight: 600;
9155
+ color: #1f2430;
9156
+ overflow: hidden;
9157
+ text-overflow: ellipsis;
9158
+ white-space: nowrap;
9159
+ }
9160
+
9161
+ .pin-tag {
9162
+ margin-left: 0.12rem;
9163
+ font-size: 0.2rem;
9164
+ color: #3b6ef6;
9165
+ background: rgba(59, 110, 246, 0.1);
9166
+ padding: 0.04rem 0.1rem;
9167
+ border-radius: 0.06rem;
9168
+ flex-shrink: 0;
9169
+ }
9170
+
9171
+ .card-sub {
9172
+ margin-top: 0.12rem;
9173
+ font-size: 0.24rem;
9174
+ color: #9aa3b2;
9175
+ }
9176
+
9177
+ .card-note {
9178
+ margin-top: 0.1rem;
9179
+ font-size: 0.24rem;
9180
+ color: #6b7385;
9181
+ overflow: hidden;
9182
+ text-overflow: ellipsis;
9183
+ white-space: nowrap;
9184
+ }
9185
+
9186
+ .card-right {
9187
+ margin-left: 0.2rem;
9188
+ display: flex;
9189
+ align-items: baseline;
9190
+ }
9191
+
9192
+ .days {
9193
+ display: flex;
9194
+ align-items: baseline;
9195
+ }
9196
+
9197
+ .days-num {
9198
+ font-size: 0.58rem;
9199
+ font-weight: 700;
9200
+ line-height: 1;
9201
+ }
9202
+
9203
+ .days-prefix {
9204
+ font-size: 0.22rem;
9205
+ margin-right: 0.06rem;
9206
+ }
9207
+
9208
+ .days-unit {
9209
+ font-size: 0.24rem;
9210
+ margin-left: 0.04rem;
9211
+ }
9212
+
9213
+ .level-normal {
9214
+ color: #3b6ef6;
9215
+ }
9216
+ .level-soon {
9217
+ color: #ff9f43;
9218
+ }
9219
+ .level-urgent {
9220
+ color: #ff5a5f;
9221
+ }
9222
+ .level-today {
9223
+ color: #ff5a5f;
9224
+ }
9225
+ .level-past {
9226
+ color: #a0a8b8;
9227
+ }
9228
+
9229
+ .card-bar.level-normal {
9230
+ background: #3b6ef6;
9231
+ }
9232
+ .card-bar.level-soon {
9233
+ background: #ff9f43;
9234
+ }
9235
+ .card-bar.level-urgent {
9236
+ background: #ff5a5f;
9237
+ }
9238
+ .card-bar.level-today {
9239
+ background: #ff5a5f;
9240
+ }
9241
+ .card-bar.level-past {
9242
+ background: #d7dbe4;
9243
+ }
9244
+
9245
+ /* ---------- \u7A7A\u6001 ---------- */
9246
+ .empty {
9247
+ padding: 1.2rem 0.6rem 0;
9248
+ text-align: center;
9249
+ }
9250
+
9251
+ .empty-icon {
9252
+ width: 1.4rem;
9253
+ height: 1.4rem;
9254
+ border-radius: 50%;
9255
+ background: linear-gradient(135deg, #e8eeff, #f3f6ff);
9256
+ margin: 0 auto 0.32rem;
9257
+ position: relative;
9258
+ }
9259
+
9260
+ .empty-bar {
9261
+ position: absolute;
9262
+ left: 0.4rem;
9263
+ top: 0.66rem;
9264
+ width: 0.6rem;
9265
+ height: 0.08rem;
9266
+ background: #c7d4ff;
9267
+ border-radius: 0.04rem;
9268
+ }
9269
+
9270
+ .empty-dot {
9271
+ position: absolute;
9272
+ right: 0.36rem;
9273
+ top: 0.52rem;
9274
+ width: 0.18rem;
9275
+ height: 0.18rem;
9276
+ border-radius: 50%;
9277
+ background: #ff9f43;
9278
+ }
9279
+
9280
+ .empty-title {
9281
+ font-size: 0.32rem;
9282
+ color: #1f2430;
9283
+ font-weight: 600;
9284
+ }
9285
+
9286
+ .empty-tip {
9287
+ margin-top: 0.12rem;
9288
+ font-size: 0.26rem;
9289
+ color: #9aa3b2;
9290
+ }
9291
+
9292
+ .empty-btn {
9293
+ margin: 0.4rem auto 0;
9294
+ width: 2.8rem;
9295
+ height: 0.8rem;
9296
+ line-height: 0.8rem;
9297
+ border-radius: 999rem;
9298
+ background: linear-gradient(135deg, #3b6ef6, #6a8dff);
9299
+ color: #ffffff;
9300
+ font-size: 0.28rem;
9301
+ box-shadow: 0 0.1rem 0.24rem rgba(59, 110, 246, 0.28);
9302
+ cursor: pointer;
9303
+ }
9304
+
9305
+ .foot-tip {
9306
+ text-align: center;
9307
+ color: #b4bac6;
9308
+ font-size: 0.22rem;
9309
+ padding: 0.2rem 0 0.08rem;
9310
+ }
9311
+ </style>
9312
+ ` },
9313
+ { path: "web/src/views/ProfileView.vue", content: `<script setup lang="ts">
9314
+ // \u6211\u7684\uFF08\u79FB\u690D\u81EA pages/profile/*\uFF09\u3002
9315
+ // \u7528\u6237\u5361\u7247 + \u5206\u7C7B\u7EDF\u8BA1 + \u6E05\u7F13\u5B58/\u670D\u52A1\u5730\u5740/\u7248\u672C + \u6570\u636E\u8BF4\u660E + \u9000\u51FA\u767B\u5F55\u3002
9316
+ import { computed, onMounted, reactive } from 'vue'
9317
+ import { request } from '../lib/request'
9318
+ import { ensureLogin, getUser, logout } from '../lib/auth'
9319
+ import { CATEGORY_LABEL } from '../lib/config'
9320
+ import { showModal, showToast } from '../ui/ui'
9321
+
9322
+ const CACHE_KEY = 'events_cache'
9323
+ const VERSION = '1.0.0'
9324
+
9325
+ const data = reactive({
9326
+ user: null as null | {
9327
+ id: string
9328
+ nickname: string
9329
+ avatar_url: string
9330
+ created_at: string
9331
+ },
9332
+ eventCount: 0,
9333
+ maxEvents: 200,
9334
+ stats: [] as Array<{ label: string; value: number }>,
9335
+ })
9336
+
9337
+ const apiHost = computed(() => {
9338
+ const url = new URL(window.location.href)
9339
+ return url.host || ''
9340
+ })
9341
+
9342
+ function formatCreatedAt(raw: string | undefined): string {
9343
+ if (!raw) return ''
9344
+ const d = new Date(raw)
9345
+ if (Number.isNaN(d.getTime())) return raw
9346
+ return \`\${d.getFullYear()}\u5E74\${d.getMonth() + 1}\u6708\${d.getDate()}\u65E5\`
9347
+ }
9348
+
9349
+ async function loadStats(): Promise<void> {
9350
+ data.user = getUser()
9351
+ try {
9352
+ await ensureLogin()
9353
+ const [me, listData] = await Promise.all([
9354
+ request<{
9355
+ user: { id: string; nickname: string; avatar_url: string; created_at: string }
9356
+ event_count: number
9357
+ max_events: number
9358
+ }>('/api/auth/me'),
9359
+ request<{ events: Array<{ category: string }> }>('/api/events').catch(() => ({ events: [] })),
9360
+ ])
9361
+ const events = listData.events ?? []
9362
+ const counter: Record<string, number> = {}
9363
+ events.forEach((e) => {
9364
+ const key = e.category || 'other'
9365
+ counter[key] = (counter[key] ?? 0) + 1
9366
+ })
9367
+ const stats = Object.keys(counter).map((k) => ({
9368
+ label: CATEGORY_LABEL[k] ?? '\u65E5\u5E38',
9369
+ value: counter[k] as number,
9370
+ }))
9371
+ data.user = me.user
9372
+ data.eventCount = me.event_count
9373
+ data.maxEvents = me.max_events
9374
+ data.stats = stats
9375
+ } catch (err) {
9376
+ console.warn('[profile] \u52A0\u8F7D\u5931\u8D25', err)
9377
+ }
9378
+ }
9379
+
9380
+ async function onClearCache(): Promise<void> {
9381
+ const ok = await showModal({
9382
+ title: '\u6E05\u7A7A\u672C\u5730\u7F13\u5B58',
9383
+ content: '\u53EA\u4F1A\u6E05\u9664\u672C\u673A\u7684\u4E34\u65F6\u7F13\u5B58\uFF0C\u4E91\u7AEF\u6570\u636E\u4E0D\u53D7\u5F71\u54CD\u3002',
9384
+ confirmText: '\u6E05\u7A7A',
9385
+ })
9386
+ if (!ok) return
9387
+ localStorage.removeItem(CACHE_KEY)
9388
+ showToast({ title: '\u5DF2\u6E05\u7A7A', icon: 'success' })
9389
+ }
9390
+
9391
+ async function onLogout(): Promise<void> {
9392
+ const ok = await showModal({
9393
+ title: '\u9000\u51FA\u767B\u5F55',
9394
+ content: '\u9000\u51FA\u540E\u672C\u5730\u767B\u5F55\u72B6\u6001\u5C06\u88AB\u6E05\u9664\uFF0C\u4E0B\u6B21\u6253\u5F00\u4F1A\u81EA\u52A8\u91CD\u65B0\u767B\u5F55\u3002',
9395
+ confirmText: '\u9000\u51FA',
9396
+ })
9397
+ if (!ok) return
9398
+ logout()
9399
+ localStorage.removeItem(CACHE_KEY)
9400
+ showToast({ title: '\u5DF2\u9000\u51FA', icon: 'success' })
9401
+ setTimeout(() => {
9402
+ window.location.href = '/'
9403
+ }, 400)
9404
+ }
9405
+
9406
+ async function onCopyHost(): Promise<void> {
9407
+ try {
9408
+ await navigator.clipboard.writeText(apiHost.value)
9409
+ showToast({ title: '\u5DF2\u590D\u5236', icon: 'success' })
9410
+ } catch {
9411
+ showToast({ title: '\u590D\u5236\u5931\u8D25', icon: 'none' })
9412
+ }
9413
+ }
9414
+
9415
+ onMounted(() => {
9416
+ void loadStats()
9417
+ })
9418
+ </script>
9419
+
9420
+ <template>
9421
+ <div class="page">
9422
+ <header class="header">
9423
+ <div class="nav">
9424
+ <span class="nav-title">\u6211\u7684</span>
9425
+ </div>
9426
+
9427
+ <div class="user-card">
9428
+ <div class="avatar">
9429
+ <span class="avatar-text">\u65E5</span>
9430
+ </div>
9431
+ <div class="user-info">
9432
+ <div class="user-name">{{ data.user?.nickname || '\u672C\u5730\u7528\u6237' }}</div>
9433
+ <div v-if="data.user?.created_at" class="user-meta">
9434
+ \u6CE8\u518C\u4E8E {{ formatCreatedAt(data.user.created_at) }}
9435
+ </div>
9436
+ </div>
9437
+ <div class="user-count">
9438
+ <div class="count-num">{{ data.eventCount }}</div>
9439
+ <div class="count-label">/ {{ data.maxEvents }} \u4E2A</div>
9440
+ </div>
9441
+ </div>
9442
+ </header>
9443
+
9444
+ <div class="page-scroll body">
9445
+ <div v-if="data.stats.length > 0" class="panel">
9446
+ <div class="panel-title">\u5206\u7C7B\u7EDF\u8BA1</div>
9447
+ <div class="stats">
9448
+ <div v-for="stat in data.stats" :key="stat.label" class="stat">
9449
+ <div class="stat-num">{{ stat.value }}</div>
9450
+ <div class="stat-label">{{ stat.label }}</div>
9451
+ </div>
9452
+ </div>
9453
+ </div>
9454
+
9455
+ <div class="panel">
9456
+ <div class="cell" @click="onClearCache">
9457
+ <span class="cell-title">\u6E05\u7A7A\u672C\u5730\u7F13\u5B58</span>
9458
+ <span class="arrow">\u203A</span>
9459
+ </div>
9460
+ <div class="cell" @click="onCopyHost">
9461
+ <span class="cell-title">\u670D\u52A1\u5730\u5740</span>
9462
+ <span class="cell-value">{{ apiHost }}</span>
9463
+ </div>
9464
+ <div class="cell">
9465
+ <span class="cell-title">\u7248\u672C</span>
9466
+ <span class="cell-value">v{{ VERSION }}</span>
9467
+ </div>
9468
+ </div>
9469
+
9470
+ <div class="tips">
9471
+ <div class="tips-title">\u6570\u636E\u5B58\u50A8\u8BF4\u660E</div>
9472
+ <div class="tips-text">
9473
+ \u4F60\u7684\u4E8B\u4EF6\u6570\u636E\u4FDD\u5B58\u5728 adep \u5E73\u53F0\u9879\u76EE\u6570\u636E\u5E93\uFF08\u672C\u5730\u5F00\u53D1\u4E3A\u6A21\u62DF\u8FD0\u884C\u65F6 / \u90E8\u7F72\u540E\u4E3A\u5E73\u53F0\u9879\u76EE\u5E93\uFF09\uFF0C
9474
+ \u6362\u6D4F\u89C8\u5668\u767B\u5F55\u540C\u4E00\u4E2A\u8BBE\u5907\u8EAB\u4EFD\u5373\u53EF\u7EE7\u7EED\u67E5\u770B\uFF0C\u6E05\u7F13\u5B58\u4E0D\u4F1A\u4E22\u5931\u4E91\u7AEF\u6570\u636E\u3002
9475
+ </div>
9476
+ </div>
9477
+
9478
+ <button class="logout" @click="onLogout">\u9000\u51FA\u767B\u5F55</button>
9479
+ </div>
9480
+ </div>
9481
+ </template>
9482
+
9483
+ <style scoped>
9484
+ .page {
9485
+ height: 100%;
9486
+ padding-bottom: calc(1.8rem + env(safe-area-inset-bottom));
9487
+ }
9488
+
9489
+ .header {
9490
+ position: absolute;
9491
+ top: 0;
9492
+ left: 0;
9493
+ right: 0;
9494
+ z-index: 10;
9495
+ background: linear-gradient(135deg, #3b6ef6, #6a8dff);
9496
+ border-bottom-left-radius: 0.4rem;
9497
+ border-bottom-right-radius: 0.4rem;
9498
+ padding-bottom: 0.28rem;
9499
+ }
9500
+
9501
+ .nav {
9502
+ display: flex;
9503
+ align-items: center;
9504
+ height: 0.88rem;
9505
+ padding-left: 0.4rem;
9506
+ }
9507
+
9508
+ .nav-title {
9509
+ color: #ffffff;
9510
+ font-size: 0.36rem;
9511
+ font-weight: 600;
9512
+ }
9513
+
9514
+ .user-card {
9515
+ margin: 0.08rem 0.32rem 0;
9516
+ padding: 0.32rem;
9517
+ background: rgba(255, 255, 255, 0.16);
9518
+ border-radius: 0.24rem;
9519
+ display: flex;
9520
+ align-items: center;
9521
+ }
9522
+
9523
+ .avatar {
9524
+ width: 1.04rem;
9525
+ height: 1.04rem;
9526
+ border-radius: 50%;
9527
+ background: #ffffff;
9528
+ display: flex;
9529
+ align-items: center;
9530
+ justify-content: center;
9531
+ flex-shrink: 0;
9532
+ }
9533
+
9534
+ .avatar-text {
9535
+ color: #3b6ef6;
9536
+ font-size: 0.4rem;
9537
+ font-weight: 600;
9538
+ }
9539
+
9540
+ .user-info {
9541
+ flex: 1;
9542
+ margin-left: 0.24rem;
9543
+ min-width: 0;
9544
+ }
9545
+
9546
+ .user-name {
9547
+ color: #ffffff;
9548
+ font-size: 0.32rem;
9549
+ font-weight: 600;
9550
+ }
9551
+
9552
+ .user-meta {
9553
+ margin-top: 0.1rem;
9554
+ color: rgba(255, 255, 255, 0.8);
9555
+ font-size: 0.24rem;
9556
+ }
9557
+
9558
+ .user-count {
9559
+ text-align: right;
9560
+ flex-shrink: 0;
9561
+ }
9562
+
9563
+ .count-num {
9564
+ color: #ffffff;
9565
+ font-size: 0.44rem;
9566
+ font-weight: 700;
9567
+ line-height: 1;
9568
+ }
9569
+
9570
+ .count-label {
9571
+ margin-top: 0.08rem;
9572
+ color: rgba(255, 255, 255, 0.8);
9573
+ font-size: 0.22rem;
9574
+ }
9575
+
9576
+ .body {
9577
+ padding: 3.2rem 0.32rem 0;
9578
+ }
9579
+
9580
+ .panel {
9581
+ background: #ffffff;
9582
+ border-radius: 0.24rem;
9583
+ padding: 0.08rem 0.32rem;
9584
+ margin-bottom: 0.24rem;
9585
+ box-shadow: 0 0.08rem 0.24rem rgba(31, 36, 48, 0.05);
9586
+ }
9587
+
9588
+ .panel-title {
9589
+ padding: 0.24rem 0 0.08rem;
9590
+ font-size: 0.26rem;
9591
+ color: #9aa3b2;
9592
+ }
9593
+
9594
+ .stats {
9595
+ display: flex;
9596
+ flex-wrap: wrap;
9597
+ padding-bottom: 0.2rem;
9598
+ }
9599
+
9600
+ .stat {
9601
+ width: 25%;
9602
+ text-align: center;
9603
+ padding: 0.16rem 0;
9604
+ }
9605
+
9606
+ .stat-num {
9607
+ font-size: 0.4rem;
9608
+ font-weight: 700;
9609
+ color: #3b6ef6;
9610
+ }
9611
+
9612
+ .stat-label {
9613
+ margin-top: 0.08rem;
9614
+ font-size: 0.22rem;
9615
+ color: #9aa3b2;
9616
+ }
9617
+
9618
+ .cell {
9619
+ display: flex;
9620
+ align-items: center;
9621
+ justify-content: space-between;
9622
+ padding: 0.3rem 0;
9623
+ border-bottom: 1px solid #f0f2f7;
9624
+ cursor: pointer;
7312
9625
  }
7313
- `;
7314
9626
 
7315
- // packages/cli/src/init.ts
7316
- init_adep_config();
9627
+ .cell:last-child {
9628
+ border-bottom: none;
9629
+ }
7317
9630
 
7318
- // shared/sdk/web-project-template.ts
7319
- var ADEP_CLIENT_VERSION = "^0.2.0";
7320
- var ADEP_VITE_PLUGIN_VERSION = "^0.1.0";
7321
- var WEB_DEPENDENCIES = {
7322
- vue: "^3.3.4",
7323
- // @adep/client:浏览器端云函数调用 SDK(IDE-031)。模板默认在 web/src/lib/adep.ts
7324
- // 封装并由 App.vue 示例调用;运行时 fetch('/api/{name}') 在 dev 态被 @adep/vite-plugin
7325
- // 拦截(CLI 代理到本地 adep dev / Web IDE 经 postMessage 转发到 IDE 主线程),
7326
- // 生产态由平台网关路由到已发布云函数。
7327
- "@adep/client": ADEP_CLIENT_VERSION
7328
- };
7329
- var WEB_DEV_DEPENDENCIES = {
7330
- vite: "^4.4.0",
7331
- "@vitejs/plugin-vue": "^4.3.0",
7332
- // vite 内部动态 import('esbuild-wasm'),必须在 package.json 显式声明才能被 Nodebox 解析
7333
- "esbuild-wasm": "0.18.20",
7334
- // @adep/vite-plugin:云函数 Vite 插件(IDE-030)。dev 时自动注入 /api/* fetch 拦截器
7335
- // (Web IDE 预览经 postMessage 转发到 IDE 主线程执行云函数草稿),若注入 createDevServer
7336
- // (CLI 场景)则启动本地 adep dev server 并按 functions_prefix 代理 /{prefix}/* 到云函数。
7337
- "@adep/vite-plugin": ADEP_VITE_PLUGIN_VERSION
7338
- };
7339
- function webProjectFiles() {
7340
- return [
7341
- {
7342
- path: "package.json",
7343
- content: JSON.stringify(
7344
- {
7345
- name: "adep-web",
7346
- version: "0.0.0",
7347
- type: "module",
7348
- scripts: { dev: "vite" },
7349
- dependencies: WEB_DEPENDENCIES,
7350
- devDependencies: WEB_DEV_DEPENDENCIES
7351
- },
7352
- null,
7353
- 2
7354
- )
7355
- },
7356
- {
7357
- path: "index.html",
7358
- content: [
7359
- `<!doctype html>`,
7360
- `<html lang="zh-CN">`,
7361
- `<head>`,
7362
- ` <meta charset="utf-8" />`,
7363
- ` <meta name="viewport" content="width=device-width, initial-scale=1" />`,
7364
- ` <title>adep web</title>`,
7365
- `</head>`,
7366
- `<body>`,
7367
- ` <div id="app"></div>`,
7368
- ` <script type="module" src="/src/main.ts"></script>`,
7369
- `</body>`,
7370
- `</html>`,
7371
- ``
7372
- ].join("\n")
7373
- },
7374
- {
7375
- path: "vite.config.ts",
7376
- content: [
7377
- `// web/vite.config.ts \u2014\u2014 \u6807\u51C6 Vite \u914D\u7F6E\uFF08Vue 3 + @adep/vite-plugin \u4E91\u51FD\u6570\u4EE3\u7406\uFF09\u3002`,
7378
- `// @adep/vite-plugin \u5728 dev \u65F6\uFF1A`,
7379
- `// 1. \u6CE8\u5165 /api/* fetch \u62E6\u622A\u5668\uFF08Web IDE \u9884\u89C8\u7ECF postMessage \u8F6C\u53D1\u5230 IDE \u4E3B\u7EBF\u7A0B\u6267\u884C\u4E91\u51FD\u6570\u8349\u7A3F\uFF09\uFF1B`,
7380
- `// 2. \u82E5\u6CE8\u5165 createDevServer\uFF08CLI \u573A\u666F\uFF09\uFF0C\u542F\u52A8\u672C\u5730 adep dev \u5E76\u4EE3\u7406 /api/* \u5230\u4E91\u51FD\u6570\u3002`,
7381
- `// \u6D4F\u89C8\u5668\u5185 vite-dev \u9884\u89C8\uFF08@adep/web-container buildDocument\uFF09\u6CE8\u5165\u540C\u4E00\u4EFD\u811A\u672C\uFF0C\u65E0\u9700\u5728\u6B64\u5185\u8054\u3002`,
7382
- `import { defineConfig } from 'vite'`,
7383
- `import vue from '@vitejs/plugin-vue'`,
7384
- `import { adepPlugin } from '@adep/vite-plugin'`,
7385
- ``,
7386
- `export default defineConfig({`,
7387
- ` plugins: [vue(), adepPlugin()],`,
7388
- `})`,
7389
- ``
7390
- ].join("\n")
7391
- },
7392
- {
7393
- path: "src/main.ts",
7394
- content: [
7395
- `// web/src/main.ts \u2014\u2014 \u5E94\u7528\u5165\u53E3\uFF08Vue 3 + Vite\uFF09\u3002`,
7396
- `// \u5F00\u53D1\uFF1A\u5E95\u90E8\u300C\u6D4F\u89C8\u5668\u7EC8\u7AEF\u300D\u8FD0\u884C npm run dev\uFF08\u6216\u7B49 Nodebox \u81EA\u52A8\u62C9\u8D77\uFF09\uFF0C\u4FDD\u5B58\u540E\u9884\u89C8 HMR \u5237\u65B0\u3002`,
7397
- `import { createApp } from 'vue'`,
7398
- `import App from './App.vue'`,
7399
- ``,
7400
- `createApp(App).mount('#app')`,
7401
- ``
7402
- ].join("\n")
7403
- },
7404
- {
7405
- path: "src/lib/adep.ts",
7406
- content: [
7407
- `// web/src/lib/adep.ts \u2014\u2014 @adep/client \u6D4F\u89C8\u5668\u7AEF\u5C01\u88C5\uFF08IDE-032\uFF09\u3002`,
7408
- `// \u5F00\u53D1\u6001\uFF08vite dev + @adep/vite-plugin\uFF09\uFF1AinvokeFunction \u8D70 fetch('/api/*')\uFF0C`,
7409
- `// \u7531 vite \u63D2\u4EF6\u62E6\u622A\u5230\u672C\u5730\u4E91\u51FD\u6570\u6267\u884C\uFF08CLI\uFF09\u6216\u7ECF postMessage \u8F6C\u53D1\u5230 IDE \u4E3B\u7EBF\u7A0B\uFF08Web IDE\uFF09\u3002`,
7410
- `// \u751F\u4EA7\u6001\uFF08\u5DF2\u90E8\u7F72\u7AD9\u70B9\uFF09\uFF1Afetch('/api/*') \u7531\u5E73\u53F0\u7F51\u5173\u8DEF\u7531\u5230\u5DF2\u53D1\u5E03\u4E91\u51FD\u6570\u3002`,
7411
- `import { createAdepClient } from '@adep/client'`,
7412
- ``,
7413
- `export const adep = createAdepClient()`,
7414
- ``,
7415
- `export const { invokeFunction } = adep`,
7416
- ``
7417
- ].join("\n")
7418
- },
7419
- {
7420
- path: "src/App.vue",
7421
- content: [
7422
- `<script setup lang="ts">`,
7423
- `import { ref } from 'vue'`,
7424
- `import { invokeFunction } from './lib/adep'`,
7425
- ``,
7426
- `const title = ref('adep web')`,
7427
- `const clicks = ref(0)`,
7428
- `const fnResult = ref<string>('')`,
7429
- `const fnLoading = ref(false)`,
7430
- ``,
7431
- `async function callHello() {`,
7432
- ` fnLoading.value = true`,
7433
- ` fnResult.value = ''`,
7434
- ` try {`,
7435
- ` const result = await invokeFunction<{ message: string }>('hello')`,
7436
- ` fnResult.value = result.message`,
7437
- ` } catch (err) {`,
7438
- ` fnResult.value = \`\u8C03\u7528\u5931\u8D25\uFF1A\${err instanceof Error ? err.message : String(err)}\``,
7439
- ` } finally {`,
7440
- ` fnLoading.value = false`,
7441
- ` }`,
7442
- `}`,
7443
- `</script>`,
7444
- ``,
7445
- `<template>`,
7446
- ` <main>`,
7447
- ` <h1>{{ title }}</h1>`,
7448
- ` <p>\u5728 web/src/App.vue \u91CC\u7F16\u8F91\uFF0C\u4FDD\u5B58\u540E\u9884\u89C8\u81EA\u52A8\u5237\u65B0\uFF08HMR\uFF09\u3002</p>`,
7449
- ` <button @click="clicks++">clicks: {{ clicks }}</button>`,
7450
- ` <div style="margin-top: 16px;">`,
7451
- ` <button @click="callHello" :disabled="fnLoading">`,
7452
- ` {{ fnLoading ? '\u8C03\u7528\u4E2D\u2026' : '\u8C03\u7528 hello \u51FD\u6570' }}`,
7453
- ` </button>`,
7454
- ` <p v-if="fnResult" style="margin-top: 8px; color: #059669;">\u7ED3\u679C\uFF1A{{ fnResult }}</p>`,
7455
- ` </div>`,
7456
- ` </main>`,
7457
- `</template>`,
7458
- ``,
7459
- `<style scoped>`,
7460
- `main {`,
7461
- ` margin: 24px;`,
7462
- ` font-family: system-ui, -apple-system, sans-serif;`,
7463
- `}`,
7464
- `button {`,
7465
- ` padding: 6px 14px;`,
7466
- ` border: 1px solid #6366f1;`,
7467
- ` border-radius: 6px;`,
7468
- ` background: #eef2ff;`,
7469
- ` cursor: pointer;`,
7470
- `}`,
7471
- `button:disabled {`,
7472
- ` opacity: 0.6;`,
7473
- ` cursor: not-allowed;`,
7474
- `}`,
7475
- `</style>`,
7476
- ``
7477
- ].join("\n")
7478
- }
7479
- ];
9631
+ .cell-title {
9632
+ font-size: 0.28rem;
9633
+ color: #1f2430;
7480
9634
  }
7481
- function webSrcFiles() {
7482
- return webProjectFiles().filter((file) => file.path.startsWith("src/"));
9635
+
9636
+ .cell-value {
9637
+ font-size: 0.26rem;
9638
+ color: #9aa3b2;
9639
+ max-width: 52%;
9640
+ overflow: hidden;
9641
+ text-overflow: ellipsis;
9642
+ white-space: nowrap;
9643
+ }
9644
+
9645
+ .arrow {
9646
+ color: #c4c9d4;
9647
+ font-size: 0.36rem;
9648
+ }
9649
+
9650
+ .tips {
9651
+ padding: 0.08rem 0.08rem 0.32rem;
9652
+ }
9653
+
9654
+ .tips-title {
9655
+ font-size: 0.26rem;
9656
+ color: #6b7385;
9657
+ font-weight: 500;
9658
+ }
9659
+
9660
+ .tips-text {
9661
+ margin-top: 0.12rem;
9662
+ font-size: 0.24rem;
9663
+ color: #a0a8b8;
9664
+ line-height: 1.7;
7483
9665
  }
7484
9666
 
9667
+ .logout {
9668
+ display: flex;
9669
+ align-items: center;
9670
+ justify-content: center;
9671
+ width: 100%;
9672
+ height: 0.92rem;
9673
+ background: #ffffff;
9674
+ color: #ff5a5f;
9675
+ font-size: 0.3rem;
9676
+ border-radius: 999rem;
9677
+ border: 1px solid #ffd9da;
9678
+ cursor: pointer;
9679
+ }
9680
+ </style>
9681
+ ` },
9682
+ { path: "web/tsconfig.json", content: '{\n "compilerOptions": {\n "target": "ES2021",\n "module": "ESNext",\n "moduleResolution": "Bundler",\n "strict": true,\n "noEmit": true,\n "skipLibCheck": true,\n "jsx": "preserve",\n "resolveJsonModule": true,\n "esModuleInterop": true,\n "lib": ["ES2021", "DOM", "DOM.Iterable"],\n "types": ["vite/client"]\n },\n "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.vue"]\n}\n' },
9683
+ { path: "web/vite.config.ts", content: "// web/vite.config.ts \u2014\u2014 \u5012\u6570\u65E5\u5E94\u7528\u524D\u7AEF\uFF08Vue 3 + Vite\uFF09\u3002\n//\n// \u672C\u5730\u5F00\u53D1\uFF08npm run dev\uFF0C\u5728 apps/countdown \u6839\u6267\u884C\uFF09\uFF1A\n// - root \u56FA\u5B9A\u4E3A\u672C\u76EE\u5F55\uFF08web/\uFF09\uFF0C\u524D\u7AEF\u6E90\u7801\u4ECE web/src \u52A0\u8F7D\uFF1B\n// - command === 'serve' \u65F6\u52A8\u6001\u52A0\u8F7D @adep/cli/vite\uFF1A\u8BA9 vite \u5728\u8FDB\u7A0B\u5185\u542F\u52A8 `adep dev`\n// \uFF08\u6A21\u62DF\u8FD0\u884C\u65F6\uFF0Ccwd = \u5E94\u7528\u6839 apps/countdown\uFF0C\u8BFB\u53D6 adep.config.ts \u4E0E functions/\uFF09\uFF0C\n// \u5E76\u628A /api/* \u4EE3\u7406\u5230\u5B83\u2014\u2014\u4E00\u6761\u547D\u4EE4\u540C\u65F6\u8C03\u8BD5\u524D\u7AEF + \u4E91\u51FD\u6570\uFF08\u51FD\u6570\u6539\u52A8\u70ED\u91CD\u8F7D\uFF09\u3002\n//\n// \u5E73\u53F0\u6784\u5EFA\uFF08adep publish --only frontend \u7684\u670D\u52A1\u7AEF vite build\uFF09\uFF1A\n// - \u5E73\u53F0\u628A web/ \u8349\u7A3F\u5199\u5165\u4E34\u65F6\u76EE\u5F55\u540E\u6267\u884C bun install + vite build\uFF0Ccommand === 'build'\uFF0C\n// \u672C\u914D\u7F6E**\u4E0D\u4F1A**\u52A0\u8F7D @adep/cli/vite\uFF08\u4E0D\u89E3\u6790\u3001\u4E0D\u5B89\u88C5\u5176\u8FD0\u884C\uFF0C\u5E73\u53F0\u65E0\u9700\u53D1\u5E03\u7248 cli \u7684\n// ./vite \u5B50\u8DEF\u5F84\uFF09\uFF0C\u4FDD\u8BC1\u53D1\u5E03\u4EA7\u7269\u53EF\u72EC\u7ACB\u6784\u5EFA\uFF1BdevDependencies \u91CC\u7684 @adep/cli \u4EC5\u672C\u5730\u7528\u3002\nimport { defineConfig, type Plugin } from 'vite'\nimport { fileURLToPath, URL } from 'node:url'\nimport vue from '@vitejs/plugin-vue'\n\nexport default defineConfig(async ({ command }) => {\n const plugins: Plugin[] = [vue()]\n\n if (command === 'serve') {\n // \u672C\u5730\u5F00\u53D1\uFF1A\u8FDB\u7A0B\u5185 adep dev + /api \u4EE3\u7406\uFF08\u52A8\u6001 import\uFF0C\u6784\u5EFA\u671F\u4E0D\u89E3\u6790\u8BE5\u6A21\u5757\uFF09\u3002\n const adep = (await import('@adep/cli/vite')).default\n plugins.push(adep({ cwd: fileURLToPath(new URL('..', import.meta.url)) }) as unknown as Plugin)\n }\n\n return {\n root: fileURLToPath(new URL('.', import.meta.url)),\n plugins,\n build: {\n outDir: 'dist',\n emptyOutDir: true,\n },\n server: {\n port: 5173,\n host: '127.0.0.1',\n },\n }\n})\n" }
9684
+ ];
9685
+
7485
9686
  // packages/cli/src/init.ts
7486
- var TEMPLATE_NAMES = ["empty", "function", "fullstack"];
9687
+ var TEMPLATE_NAMES = ["empty", "function", "fullstack", "countdown"];
7487
9688
  var InitError = class extends Error {
7488
9689
  constructor(code, message) {
7489
9690
  super(message);
@@ -7495,10 +9696,10 @@ var requireJson = createRequire(import.meta.url);
7495
9696
  var CLI_VERSION = requireJson("../package.json").version;
7496
9697
  var ADEP_CONFIG_HEADER = `// adep \u9879\u76EE\u914D\u7F6E\uFF1ACLI\uFF08dev / serve / deploy\uFF09\u4E0E\u4E91\u51FD\u6570 vite \u63D2\u4EF6\u6309 default export \u8BFB\u53D6\u3002
7497
9698
  // - name\uFF1A\u9879\u76EE\u6807\u8BC6\uFF08deploy / db \u7B49\u547D\u4EE4\u7684\u7F3A\u7701 project slug\uFF09
7498
- // - template\uFF1A\u811A\u624B\u67B6\u6A21\u677F\uFF08empty / function / fullstack\uFF0Cinit \u65F6\u786E\u5B9A\uFF09
9699
+ // - template\uFF1A\u811A\u624B\u67B6\u6A21\u677F\uFF08empty / function / fullstack / countdown\uFF0Cinit \u65F6\u786E\u5B9A\uFF09
7499
9700
  // - functionsDir\uFF1A\u4E91\u51FD\u6570\u76EE\u5F55\uFF08dev / serve / deploy \u8BFB\u53D6\uFF0C\u7F3A\u7701 functions/\uFF09
7500
9701
  // - functions_prefix\uFF1A\u4E91\u51FD\u6570\u8DEF\u7531\u524D\u7F00\uFF0C**\u9ED8\u8BA4 /api**\uFF08\u4E0E\u7EBF\u4E0A\u51FD\u6570\u8DEF\u7531 /api/{fn} \u5BF9\u9F50\uFF09\u3002
7501
- // dev \u8BBF\u95EE\u8DEF\u5F84\u4E3A /{prefix}/{fnName}\uFF08\u5982 /api/hello\uFF09\uFF1Bserve \u4E3A /api/{prefix}/{fnName}\u3002
9702
+ // dev / serve / \u5E73\u53F0\u8BBF\u95EE\u8DEF\u5F84\u4E00\u81F4\u4E3A /{prefix}/{fnName}\uFF08\u5982 /api/hello\uFF09\u3002
7502
9703
  // vite \u63D2\u4EF6\uFF08vite.config.ts \u7684 adepPlugin()\uFF09\u6309\u6B64\u524D\u7F00\u628A\u524D\u7AEF /{prefix}/* \u8BF7\u6C42\u4EE3\u7406\u5230\u4E91\u51FD\u6570\u3002
7503
9704
  // \u8FB9\u754C\uFF1A\u53EA\u5F71\u54CD\u672C\u5730 HTTP \u5165\u53E3\u8DEF\u7531\uFF1B\u51FD\u6570\u4E92\u8C03\uFF08ctx.cloud.invoke\uFF09\u4E0E\u7EBF\u4E0A\u90E8\u7F72\u8DEF\u5F84\u6309\u51FD\u6570\u540D\uFF0C\u4E0D\u53D7\u5F71\u54CD\u3002`;
7504
9705
  var PACKAGE_JSON = (name, template) => {
@@ -7529,6 +9730,106 @@ var PACKAGE_JSON = (name, template) => {
7529
9730
  return `${JSON.stringify(pkg, null, 2)}
7530
9731
  `;
7531
9732
  };
9733
+ var COUNTDOWN_PACKAGE_JSON = (name) => {
9734
+ const pkg = {
9735
+ name,
9736
+ version: "0.1.0",
9737
+ private: true,
9738
+ type: "module",
9739
+ description: "\u5012\u6570\u65E5 / \u7EAA\u5FF5\u65E5\uFF08countdown \u6A21\u677F\uFF09\u2014\u2014\u5FAE\u4FE1\u5C0F\u7A0B\u5E8F\u79FB\u690D\u7684 adep \u5E94\u7528\uFF1Afunctions/ \u4E91\u51FD\u6570 + web/ \u524D\u7AEF\uFF08Vue 3 + Vite\uFF09",
9740
+ scripts: {
9741
+ dev: "vite -c web/vite.config.ts",
9742
+ "dev:api": "adep dev",
9743
+ build: "vite build -c web/vite.config.ts",
9744
+ preview: "vite preview -c web/vite.config.ts",
9745
+ start: "adep serve --host 0.0.0.0 --schema functions/schema.sql --static ./web/dist --spa",
9746
+ test: "vitest run",
9747
+ "test:watch": "vitest",
9748
+ "db:init": "adep db start && adep db migrate functions/schema.sql",
9749
+ deploy: "adep publish",
9750
+ "deploy:functions": "adep publish --only functions",
9751
+ "deploy:frontend": "adep publish --only frontend",
9752
+ "frontend:sync": "adep frontend sync",
9753
+ doctor: "adep doctor"
9754
+ },
9755
+ // 依赖分两处声明:根 = @adep/cli + @adep/types + 前端构建链;web/package.json = 前端运行依赖。
9756
+ // vitest 用 ^3.2.4:vitest 4.x 的 packument 触发 npm 10.9 arborist edgesOut 崩溃(官方源同样),
9757
+ // 与 apps/countdown 保持一致;双目录安装形态(不用 npm workspaces),README 已写明两条 install。
9758
+ dependencies: {
9759
+ "@adep/cli": CLI_VERSION
9760
+ },
9761
+ devDependencies: {
9762
+ "@adep/types": "^0.2.0",
9763
+ "@vitejs/plugin-vue": "^6.0.0",
9764
+ typescript: "5.9.3",
9765
+ vite: "^7.0.0",
9766
+ vitest: "^3.2.4",
9767
+ vue: "^3.5.42"
9768
+ },
9769
+ engines: {
9770
+ node: ">=18",
9771
+ bun: ">=1.0"
9772
+ }
9773
+ };
9774
+ return `${JSON.stringify(pkg, null, 2)}
9775
+ `;
9776
+ };
9777
+ var COUNTDOWN_README = (name) => `# ${name}
9778
+
9779
+ \u5012\u6570\u65E5 / \u7EAA\u5FF5\u65E5\uFF08countdown \u6A21\u677F\uFF09\u2014\u2014\u7531\u5FAE\u4FE1\u5C0F\u7A0B\u5E8F\u300C\u5012\u6570\u65E5\u300D\u79FB\u690D\u7684 adep \u5168\u6808\u5E94\u7528\uFF1A
9780
+ \`functions/\` \u4E91\u51FD\u6570\uFF08auth / events + \`_shared/\` \u5171\u4EAB\u6A21\u5757\uFF09+ \`web/\` \u524D\u7AEF\uFF08Vue 3 + Vite\uFF0C
9781
+ \u4E09\u9875 Home / Edit / Profile + \u81EA\u5B9A\u4E49 TabBar + \u5DE6\u6ED1\u5220\u9664 / \u957F\u6309\u7F6E\u9876 / \u4E0B\u62C9\u5237\u65B0\uFF0C\u754C\u9762\u4E0E\u5C0F\u7A0B\u5E8F\u5BF9\u9F50\uFF09\u3002
9782
+
9783
+ ## \u5FEB\u901F\u5F00\u59CB
9784
+
9785
+ \`\`\`bash
9786
+ npm install # \u6839\uFF1A@adep/cli + \u524D\u7AEF\u6784\u5EFA\u94FE\uFF08vue/vite/plugin-vue/vitest\uFF09
9787
+ npm install --prefix web # web/\uFF1A\u524D\u7AEF\u8FD0\u884C\u4F9D\u8D56\uFF08vue\uFF09
9788
+ npm run dev # \u5168\u6808\u5F00\u53D1\uFF1Avite\uFF08web/\uFF09\u8FDB\u7A0B\u5185\u542F\u52A8 adep dev + /api \u4EE3\u7406\uFF0C\u524D\u7AEF HMR + \u51FD\u6570\u70ED\u91CD\u8F7D
9789
+ npm test # \u51FD\u6570\u5171\u4EAB\u903B\u8F91\u5355\u5143\u6D4B\u8BD5\uFF08vitest\uFF09
9790
+ npm run build # \u6784\u5EFA\u524D\u7AEF \u2192 web/dist
9791
+ npm run start # \u72EC\u7ACB\u8FD0\u884C\uFF08\u672C\u5730\u6A21\u62DF\u8FD0\u884C\u65F6 + \u9759\u6001\u6258\u7BA1 + --schema \u81EA\u52A8\u5EFA\u8868\uFF09\uFF0C\u6D4F\u89C8\u5668\u5F00 http://127.0.0.1:8787
9792
+ \`\`\`
9793
+
9794
+ ## \u90E8\u7F72\u5230 adep \u5E73\u53F0
9795
+
9796
+ \`\`\`bash
9797
+ adep login # \u767B\u5F55\uFF08\u5199 ~/.adep/credentials\uFF09
9798
+ npm run deploy # \u5168\u6808\u53D1\u5E03\uFF1A\u51FD\u6570 deploy\uFF08\u542B _shared/\uFF09+ \u524D\u7AEF sync + publish
9799
+ npm run deploy:functions # \u53EA\u53D1\u5E03\u4E91\u51FD\u6570
9800
+ npm run deploy:frontend # \u53EA\u53D1\u5E03\u524D\u7AEF
9801
+ \`\`\`
9802
+
9803
+ \u53D1\u5E03\u540E\u8BBF\u95EE \`https://{project-slug}.adep.localhost:3001\`\uFF08\u672C\u5730 dev \u5E73\u53F0\uFF09\u6216\u7EBF\u4E0A\u7AD9\u70B9\u5730\u5740\u3002
9804
+
9805
+ ## \u72EC\u7ACB\u90E8\u7F72\uFF08\u4E0D\u4F9D\u8D56 adep \u5E73\u53F0\uFF09
9806
+
9807
+ \`npm run build && npm run start\` \u5373\u53EF\u672C\u5730\u72EC\u7ACB\u8DD1\uFF08adep serve \u4E0E\u5E73\u53F0\u540C\u6E90\u6267\u884C\u5668\uFF09\u3002
9808
+ \u5BB9\u5668\u5316\u90E8\u7F72\u53C2\u8003 \`apps/countdown\` \u7684 \`Dockerfile\` + \`compose.yaml\`\uFF08\u5355\u5BB9\u5668 =
9809
+ serve \u627F\u8F7D /api/* + \u9759\u6001\u6258\u7BA1 web/dist + .adep/sim/ \u6570\u636E\u5377\uFF09\u3002
9810
+
9811
+ ## \u76EE\u5F55\u7ED3\u6784
9812
+
9813
+ - \`adep.config.ts\`\uFF1A\u9879\u76EE\u914D\u7F6E\uFF08name / template / functionsDir / functions_prefix \u9ED8\u8BA4 /api\uFF09
9814
+ - \`functions/\`\uFF1A\u4E91\u51FD\u6570\u76EE\u5F55\u2014\u2014\`auth.ts\`\uFF08\u767B\u5F55/\u4F1A\u8BDD\uFF09\u3001\`events.ts\`\uFF08\u4E8B\u4EF6 CRUD + \u7F6E\u9876\u6392\u5E8F\uFF09\u3001
9815
+ \`schema.sql\`\uFF08users / events \u8868\uFF09\u3001\`_shared/\`\uFF08auth / store / date / response / route \u5171\u4EAB\u6A21\u5757\uFF09
9816
+ - \`web/\`\uFF1A\u524D\u7AEF\u5DE5\u7A0B\u6839\uFF08web/vite.config.ts + web/src/*\uFF09\u2014\u2014\`web/src/views/\` \u4E09\u9875\u3001
9817
+ \`web/src/ui/\`\uFF08TabBar / UiHost \u590D\u523B wx.showToast \u7B49\uFF09\u3001\`web/src/lib/\`\uFF08request / date / device / auth\uFF09
9818
+ - \`web/src/lib/request.ts\`\uFF1Afetch \u5C01\u88C5\uFF08\u4E1A\u52A1\u4FE1\u5C01 + 40001 \u9759\u9ED8\u91CD\u767B\uFF09
9819
+ - \`tsconfig.json\` / \`adep.d.ts\`\uFF1ATypeScript \u914D\u7F6E\u4E0E\u4E91\u51FD\u6570\u4E0A\u4E0B\u6587\u7C7B\u578B\u58F0\u660E
9820
+
9821
+ ## \u7528\u6237\u4F53\u7CFB\u8BF4\u660E\uFF08\u767B\u5F55\u94FE\u8DEF\uFF09
9822
+
9823
+ \u5C0F\u7A0B\u5E8F\u7AEF \`wx.login\` \u2192 openid \u7684\u94FE\u8DEF\uFF0Cweb \u7AEF\u4EE5\u300C\u8BBE\u5907\u8EAB\u4EFD device_id\u300D\u66FF\u4EE3\uFF1A\u9996\u6B21\u8BBF\u95EE\u751F\u6210\u5E76
9824
+ \u6301\u4E45\u5316\u4E8E localStorage\uFF08web/src/lib/device.ts\uFF09\uFF0C\u4F5C\u4E3A Bearer \u51ED\u636E\u8BBF\u95EE\u51FD\u6570\uFF08\u7B49\u4EF7 API Key\uFF09\u3002
9825
+ \u51FD\u6570\u4FA7 \`functions/_shared/auth.ts\` \u67E5 users \u8868\u6821\u9A8C\uFF0C\u672A\u767B\u5F55/\u5931\u6548\u8FD4\u56DE\u4E1A\u52A1\u7801 40001\uFF0C
9826
+ \u524D\u7AEF request.ts \u636E\u6B64\u9759\u9ED8\u91CD\u65B0\u767B\u5F55\u3002
9827
+
9828
+ ## \u66F4\u591A
9829
+
9830
+ - \u79FB\u690D\u7ECF\u9A8C\u4E0E\u9010\u9879\u5BF9\u9F50\u8BF4\u660E\uFF1A\`packages/docs/tutorial-countdown.md\`\uFF08adep \u4ED3\u5E93\u6587\u6863\uFF09
9831
+ - \u72EC\u7ACB\u90E8\u7F72\u5BB9\u5668\u914D\u7F6E\u89C1 adep \u4ED3\u5E93 \`apps/countdown\`\uFF08Dockerfile / compose.yaml\uFF09
9832
+ `;
7532
9833
  var README = (name, template) => `# ${name}
7533
9834
 
7534
9835
  AgentDeploy \u5168\u6808\u9879\u76EE\uFF08\u6A21\u677F\uFF1A${template}\uFF09\u3002
@@ -7688,8 +9989,8 @@ async function initProject(cwd, name, template) {
7688
9989
  throw new InitError("DIR_EXISTS", `\u76EE\u5F55 ${projectPath} \u5DF2\u5B58\u5728\uFF1A\u8BF7\u6362\u4E00\u4E2A\u540D\u5B57\u6216\u5148\u5220\u9664`);
7689
9990
  }
7690
9991
  const files = [
7691
- // 根 package.json:应用定义(@adep/cli + 前端依赖同根声明;与导出的离线部署包同构键集)。
7692
- { path: "package.json", content: PACKAGE_JSON(name, template) },
9992
+ // 根 package.json:应用定义(countdown 模板 = 独立 vite 工程根在 web/,npm workspaces 装齐根 + web/)。
9993
+ template === "countdown" ? { path: "package.json", content: COUNTDOWN_PACKAGE_JSON(name) } : { path: "package.json", content: PACKAGE_JSON(name, template) },
7693
9994
  // adep.config.ts:经 shared/sdk/adep-config 的 renderAdepConfig 渲染(schema 唯一真相源)。
7694
9995
  // template 是 init 元数据(不在 AdepConfig 中),经 extraFields 传入。
7695
9996
  {
@@ -7702,7 +10003,10 @@ async function initProject(cwd, name, template) {
7702
10003
  // 云函数 TypeScript 类型提示:tsconfig 把 adep.d.ts 纳入 include,写 ctx: AdepContext 即有补全。
7703
10004
  { path: "tsconfig.json", content: ADEP_TSCONFIG_JSON },
7704
10005
  { path: "adep.d.ts", content: ADEP_ENV_DTS },
7705
- { path: "README.md", content: README(name, template) },
10006
+ {
10007
+ path: "README.md",
10008
+ content: template === "countdown" ? COUNTDOWN_README(name) : README(name, template)
10009
+ },
7706
10010
  { path: ".gitignore", content: "node_modules/\ndata/\n.adep/\nsite/\n" }
7707
10011
  ];
7708
10012
  if (template === "function") {
@@ -7717,6 +10021,10 @@ async function initProject(cwd, name, template) {
7717
10021
  for (const file of webSrcFiles()) {
7718
10022
  files.push({ path: `web/${file.path}`, content: file.content });
7719
10023
  }
10024
+ } else if (template === "countdown") {
10025
+ for (const file of COUNTDOWN_TEMPLATE_FILES) {
10026
+ files.push({ path: file.path, content: file.content });
10027
+ }
7720
10028
  } else {
7721
10029
  files.push({ path: "README-EMPTY.md", content: EMPTY_INDEX });
7722
10030
  }
@@ -8713,20 +11021,30 @@ function registerDevServers(program2, ctx) {
8713
11021
  ).option(
8714
11022
  "--watch",
8715
11023
  "\u70ED\u91CD\u8F7D\uFF1A\u76D1\u542C functions/ \u4E0E .env* \u53D8\u66F4\u81EA\u52A8\u751F\u6548\uFF0C\u51FD\u6570 console \u8F93\u51FA\u76F4\u8FBE\u7EC8\u7AEF\uFF08\u5F00\u53D1\u6001\u8C03\u8BD5\u7528\uFF09"
8716
- ).action(async (flags) => {
8717
- const command = ctx.io("serve");
8718
- await command.run(async () => {
8719
- const { startServeServer: startServeServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
8720
- await startServeServer2({
8721
- cwd: ctx.cwd,
8722
- ...flags.port === void 0 ? {} : { port: Number(flags.port) },
8723
- ...flags.host === void 0 ? {} : { host: flags.host },
8724
- ...flags.static === void 0 ? {} : { staticDir: flags.static },
8725
- ...flags.watch === void 0 ? {} : { watch: true }
11024
+ ).option(
11025
+ "--schema <file>",
11026
+ "\u6570\u636E\u5E93\u7ED3\u6784\u6587\u4EF6\uFF1A\u542F\u52A8\u65F6\u5BF9\u672C\u5730\u6A21\u62DF\u5E93\u9010\u6761\u5E94\u7528\uFF08\u5EFA\u8868\u8BED\u53E5\u5EFA\u8BAE IF NOT EXISTS\uFF0C\u5E42\u7B49\u53EF\u91CD\u590D\u542F\u52A8\uFF09"
11027
+ ).option(
11028
+ "--spa",
11029
+ "SPA \u56DE\u9000\uFF1A\u9759\u6001\u8D44\u6E90\u672A\u547D\u4E2D\u65F6\u56DE\u9000 index.html\uFF08\u524D\u7AEF\u8DEF\u7531\u76F4\u8BBF /profile \u7B49\u9875\u9762\u9700\u8981\uFF09"
11030
+ ).action(
11031
+ async (flags) => {
11032
+ const command = ctx.io("serve");
11033
+ await command.run(async () => {
11034
+ const { startServeServer: startServeServer2 } = await Promise.resolve().then(() => (init_server(), server_exports));
11035
+ await startServeServer2({
11036
+ cwd: ctx.cwd,
11037
+ ...flags.port === void 0 ? {} : { port: Number(flags.port) },
11038
+ ...flags.host === void 0 ? {} : { host: flags.host },
11039
+ ...flags.static === void 0 ? {} : { staticDir: flags.static },
11040
+ ...flags.watch === void 0 ? {} : { watch: true },
11041
+ ...flags.schema === void 0 ? {} : { schemaFile: flags.schema },
11042
+ ...flags.spa === void 0 ? {} : { spaFallback: true }
11043
+ });
11044
+ await new Promise(() => void 0);
8726
11045
  });
8727
- await new Promise(() => void 0);
8728
- });
8729
- });
11046
+ }
11047
+ );
8730
11048
  }
8731
11049
 
8732
11050
  // packages/cli/src/commands/publish.ts
@@ -8734,9 +11052,9 @@ init_auth();
8734
11052
  init_client();
8735
11053
  init_deploy();
8736
11054
  import { stat as stat8 } from "node:fs/promises";
8737
- import { join as join14, resolve as resolve7 } from "node:path";
11055
+ import { join as join14, resolve as resolve8 } from "node:path";
8738
11056
  async function resolveProjectId2(client, cwd, slug) {
8739
- const resolvedSlug = await resolveSlug(resolve7(cwd), slug);
11057
+ const resolvedSlug = await resolveSlug(resolve8(cwd), slug);
8740
11058
  const listed = await client.request("/api/v1/projects", {}, "PROJECT_LIST_FAILED");
8741
11059
  const match = listed.projects.find((project2) => project2.slug === resolvedSlug);
8742
11060
  if (match === void 0) {
@@ -9263,18 +11581,25 @@ function registerMcp(program2, ctx) {
9263
11581
  // packages/cli/src/commands/projects.ts
9264
11582
  function registerProjects(program2, ctx) {
9265
11583
  const projects = program2.command("projects").description("\u9879\u76EE\uFF1A\u521B\u5EFA / \u5217\u51FA / \u67E5\u770B\uFF08\u5B50\u57DF\u5730\u5740\u7531\u5E73\u53F0\u56DE\u663E\uFF0C\u4E0D\u5728 CLI \u4FA7\u62FC\uFF09");
9266
- projects.command("create").description("\u521B\u5EFA\u9879\u76EE\u5E76\u8F93\u51FA\u5B50\u57DF\u5730\u5740").argument("<slug>", "\u9879\u76EE slug\uFF08\u5168\u5C40\u552F\u4E00\uFF0C\u5373\u5B50\u57DF\u540D\uFF09").option("-n, --name <name>", "\u9879\u76EE\u5C55\u793A\u540D\uFF08\u7F3A\u7701\u53D6 slug\uFF09").option("--space <spaceId>", "\u5F52\u5C5E\u7A7A\u95F4\uFF08\u7F3A\u7701\u4E2A\u4EBA\u7A7A\u95F4\uFF09").action(async (slug, flags) => {
9267
- const command = ctx.io("projects create");
9268
- await command.run(async () => {
9269
- const { projectsCreate: projectsCreate2 } = await Promise.resolve().then(() => (init_projects(), projects_exports));
9270
- const created = await projectsCreate2(ctx.paths, {
9271
- slug,
9272
- ...flags.name === void 0 ? {} : { name: flags.name },
9273
- ...flags.space === void 0 ? {} : { spaceId: flags.space }
11584
+ projects.command("create").description("\u521B\u5EFA\u9879\u76EE\u5E76\u8F93\u51FA\u5B50\u57DF\u5730\u5740").argument("<slug>", "\u9879\u76EE slug\uFF08\u5168\u5C40\u552F\u4E00\uFF0C\u5373\u5B50\u57DF\u540D\uFF09").option("-n, --name <name>", "\u9879\u76EE\u5C55\u793A\u540D\uFF08\u7F3A\u7701\u53D6 slug\uFF09").option("--space <spaceId>", "\u5F52\u5C5E\u7A7A\u95F4\uFF08\u7F3A\u7701\u4E2A\u4EBA\u7A7A\u95F4\uFF09").option("--git <repoUrl>", "\u4ECE Git \u4ED3\u5E93\u521B\u5EFA\uFF08\u521B\u5EFA\u540E\u7ED1\u5B9A + \u9996\u6B21\u540C\u6B65/\u90E8\u7F72\uFF09").option("-b, --branch <branch>", "Git \u5206\u652F\uFF08\u914D\u5408 --git\uFF0C\u7F3A\u7701 main\uFF09").option("--username <username>", "Git \u79C1\u6709\u4ED3\u5E93\u7528\u6237\u540D\uFF08\u914D\u5408 --git\uFF09").option("--token <token>", "Git \u79C1\u6709\u4ED3\u5E93\u8BBF\u95EE\u4EE4\u724C\uFF08\u914D\u5408 --git\uFF0C\u5E73\u53F0\u52A0\u5BC6\u843D\u5E93\uFF09").option("--function-root <root>", "legacy \u5E03\u5C40\u51FD\u6570\u6839\u76EE\u5F55\uFF08\u914D\u5408 --git\uFF0C\u7F3A\u7701\u5E94\u7528\u5E03\u5C40\u81EA\u52A8\u63A2\u6D4B\uFF09").action(
11585
+ async (slug, flags) => {
11586
+ const command = ctx.io("projects create");
11587
+ await command.run(async () => {
11588
+ const { projectsCreate: projectsCreate2 } = await Promise.resolve().then(() => (init_projects(), projects_exports));
11589
+ const created = await projectsCreate2(ctx.paths, {
11590
+ slug,
11591
+ ...flags.name === void 0 ? {} : { name: flags.name },
11592
+ ...flags.space === void 0 ? {} : { spaceId: flags.space },
11593
+ ...flags.git === void 0 ? {} : { gitRepoUrl: flags.git },
11594
+ ...flags.git === void 0 ? {} : { gitBranch: flags.branch ?? "main" },
11595
+ ...flags.username === void 0 ? {} : { gitUsername: flags.username },
11596
+ ...flags.token === void 0 ? {} : { gitToken: flags.token },
11597
+ ...flags.functionRoot === void 0 ? {} : { gitFunctionRoot: flags.functionRoot }
11598
+ });
11599
+ command.ok(created, (data) => `\u2713 Project created \xB7 ${data.url}`);
9274
11600
  });
9275
- command.ok(created, (data) => `\u2713 Project created \xB7 ${data.url}`);
9276
- });
9277
- });
11601
+ }
11602
+ );
9278
11603
  projects.command("list").description("\u5217\u51FA\u5F53\u524D\u7528\u6237\u53EF\u8BBF\u95EE\u7684\u9879\u76EE").action(async () => {
9279
11604
  const command = ctx.io("projects list");
9280
11605
  await command.run(async () => {